{ "cells": [ { "cell_type": "markdown", "id": "99ba5a82", "metadata": {}, "source": [ "# Survey\n", "\n", "The survey is where everything comes together. It takes a scene, a scanner and a platform and the so-called \"legs\" that define the positions/trajectories of the platform during the survey and the speicifc settings of the scanner." ] }, { "cell_type": "code", "execution_count": 1, "id": "8ee46410", "metadata": {}, "outputs": [], "source": [ "import copy\n", "import pandas as pd\n", "import helios" ] }, { "cell_type": "markdown", "id": "f9328326", "metadata": {}, "source": [ "Let's demonstrate this for a basic ALS survey and prepare the platform, scanner and scene." ] }, { "cell_type": "code", "execution_count": 2, "id": "f6c82717", "metadata": {}, "outputs": [], "source": [ "platform = helios.platform_from_name(\"sr22\")\n", "scanner = helios.scanner_from_name(\"leica_als50\")\n", "sceneparts = helios.ScenePart.from_objs(\"../data/sceneparts/toyblocks/*.obj\")\n", "scene = helios.StaticScene(sceneparts)" ] }, { "cell_type": "markdown", "id": "57ad1e95", "metadata": {}, "source": [ "From this, we can create an \"empty\" survey, which we can then populate with legs and settings:" ] }, { "cell_type": "code", "execution_count": 3, "id": "5da547a2", "metadata": {}, "outputs": [], "source": [ "survey = helios.Survey(scene=scene, scanner=scanner, platform=platform)" ] }, { "cell_type": "code", "execution_count": 4, "id": "2c53a05d", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "()" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# no legs defined yet\n", "survey.legs" ] }, { "cell_type": "markdown", "id": "385cafbd", "metadata": {}, "source": [ "To use surveys created for ealier versions of `helios` (< v3.0.0), we can also load surveys from XML files. To make sure the survey can be loaded correctly, add the directory where the survey XML file is located as an asset directory (here: the root directory of the cloned `helios` repository):" ] }, { "cell_type": "code", "execution_count": 5, "id": "7829e29f", "metadata": {}, "outputs": [], "source": [ "helios.add_asset_directory(\"D:/Software/_helios_versions/helios/\")\n", "tls_toyblocks_survey = helios.Survey.from_xml(\n", " \"data/surveys/toyblocks/tls_toyblocks.xml\"\n", ")" ] }, { "cell_type": "markdown", "id": "7530cbfe", "metadata": {}, "source": [ "## Scanner settings\n", "\n", "Scanner settings are defined per leg. Often, many or all legs of a survey will share the same scanner settings. We can define them once and then pass them to the legs. Depending on the `optics` of the respective scanner, different scan settings are relevant.\n", "\n", "Note that we always specify the unit of the settings, resolving any ambiguity and ensuring that the settings are correctly interpreted. This is done by the help of the package [pint](https://pint.readthedocs.io/en/stable/). We can either specify the units by multiplying with the unit from `helios.units` or by using a string with the unit." ] }, { "cell_type": "code", "execution_count": 6, "id": "5a3336ab", "metadata": {}, "outputs": [], "source": [ "scan_settings = helios.ScannerSettings(\n", " pulse_frequency=100_000 * helios.units.Hz,\n", " scan_angle=20 * helios.units.deg,\n", " scan_frequency=\"70 Hz\",\n", " trajectory_time_interval=0.01 * helios.units.s,\n", ")" ] }, { "cell_type": "markdown", "id": "2771380b", "metadata": {}, "source": [ "## Platform settings\n", "\n", "Likewise, we also have platform settings. We can use `PlatformSettings` for static platforms and `DynamicPlatformSettings` for moving platforms. The latter allows us to specify the speed of the platform, or - if using interpolated trajectory, the trajectory settings. However, since platform settings are usually not shared between legs, we will often just specify the individual parameters directly when constructing or adding legs." ] }, { "cell_type": "markdown", "id": "8f5f6e25", "metadata": {}, "source": [ "## Legs\n", "\n", "Legs are the positions or waypoints of a survey. In trajectory mode, they can also be portions of a given trajectory.\n", "\n", "### Waypoint mode\n", "\n", "In the first example, we use the waypoint mode and define a list of waypoints that the airplane should fly through during the survey. Additionally, we set the speed of the platform." ] }, { "cell_type": "code", "execution_count": 7, "id": "457b4327", "metadata": {}, "outputs": [], "source": [ "waypoints = [[-50, -25, 250], [50, -25, 250], [50, 25, 250], [-50, 25, 250]]\n", "speed = 30 # m/s" ] }, { "cell_type": "markdown", "id": "db240d78", "metadata": {}, "source": [ "Using a simple loop, we can now add the legs at the respective positions and apply the scan settings." ] }, { "cell_type": "code", "execution_count": 8, "id": "cf68190c", "metadata": {}, "outputs": [], "source": [ "for x, y, z in waypoints:\n", " survey.add_leg(x=x, y=y, z=z, speed_m_s=speed, scanner_settings=scan_settings)" ] }, { "cell_type": "markdown", "id": "fb46a0d8", "metadata": {}, "source": [ "### Trajectory mode\n", "\n", "Instead of defining waypoints, we can also define a trajectory that the platform should follow during the survey. This has the advantage, that the platform's attitude can be considered and does not stay constant as in the waypoint mode. The input trajectory can come from real-world flights, which allows to closely replicate them, or be generated synthetically, e.g., by a flight simulator. \n", "\n", "We can load such a trajectory from a CSV file, or directly create it in Python as a structured NumPy array (see [ALS over a DTM with constant height above ground](../example_notebooks/06-als_hd_height_above_ground.ipynb)). The trajectory must contain the columns \"t\" (time), \"roll\", \"pitch\", \"yaw\", \"x\", \"y\", \"z\". `t` is the time, `roll`, `pitch` and `yaw` are the orientation, `x`, `y` and `z` are the position of the platform at a given time." ] }, { "cell_type": "code", "execution_count": 9, "id": "8e2636ff", "metadata": {}, "outputs": [], "source": [ "trajectory = helios.load_traj_csv(\n", " csv=\"../data/trajectories/flyandrotate.trj\",\n", " tIndex=0,\n", " xIndex=4,\n", " yIndex=5,\n", " zIndex=6,\n", " rollIndex=1,\n", " pitchIndex=2,\n", " yawIndex=3,\n", ")" ] }, { "cell_type": "markdown", "id": "0d6b1152", "metadata": {}, "source": [ "The platform is then loaded as \"interpolate platform\", which means that HELIOS++ will interpolate the platform position and orientation at the time of each scan based on the trajectory. Two `interpolation_method` values can be chosen:\n", "- ARINC 705: This definition is according to the aviation norm ARINC 705 ([Bäumker & Heimes, 2001](https://www.researchgate.net/publication/254001148_New_Calibration_and_Computing_Method_for_Direct_Georeferencing_of_Image_and_Scanner_Data_Using_the_Position_and_Angular_Data_of_an_Hybrid_Inertial_Navigation_System#fullTextFileContent)) and interprets the rotations as intrinsic, i.e., body-fixed rotations. This is the default interpolation method.\n", "- CANONICAL: This definition interprets the rotations as extrinsic, i.e., world-fixed rotations.\n", "\n", "In addition, there are two boolean parameters that can be set when loading the platform:\n", "- `sync_gps_time`: If set to `True`, the starting time of the simulation will be synchronized with the ninimum time value from the input trajectory.\n", "- `is_roll_pitch_yaw_in_radians`: If set to `True`, the roll, pitch and yaw values in the trajectory will be interpreted as radians. If set to `False`, they will be interpreted as degrees. Default is `False`.\n" ] }, { "cell_type": "code", "execution_count": 10, "id": "9be3dc54", "metadata": {}, "outputs": [], "source": [ "scanner2 = helios.scanner_from_name(\"riegl_lms_q560\")\n", "platform_interp = helios.Platform.load_interpolate_platform(\n", " trajectory,\n", " \"data/platforms.xml\",\n", " \"sr22\",\n", " sync_gps_time=True,\n", " interpolation_method=\"ARINC 705\", # default interpolation_mode\n", " is_roll_pitch_yaw_in_radians=False, # default is False\n", ")\n", "survey2 = helios.Survey(scene=scene, scanner=scanner2, platform=platform_interp)" ] }, { "cell_type": "markdown", "id": "4f58ed72", "metadata": {}, "source": [ "We can now add the entire trajectory as a single leg to the survey. Alternatively, we can load only a subset of the trajectory, or split the trajectory into multiple legs by defining a `start_time` and `end_time` for each leg. We can also repeat the same trajectory in multiple legs." ] }, { "cell_type": "code", "execution_count": 11, "id": "92b7b5a6", "metadata": {}, "outputs": [], "source": [ "trajectory_settings1 = helios.TrajectorySettings(\n", " start_time=5, end_time=10, teleport_to_start=True\n", ")\n", "\n", "survey2.add_leg(scanner_settings=scan_settings)\n", "survey2.add_leg(\n", " scanner_settings=scan_settings, trajectory_settings=trajectory_settings1\n", ")" ] }, { "cell_type": "markdown", "id": "43e4d33c", "metadata": {}, "source": [ "## Running a survey\n", "\n", "When running a survey, we can provide `ExecutionSettings` and `OutputSettings` to control the execution of the survey and the output format.\n", "\n", "### Execution settings\n", "\n", "For example, we can explicitly change the number of threads, the logging verbosity or the parallelization strategy." ] }, { "cell_type": "code", "execution_count": 12, "id": "008a4a83", "metadata": {}, "outputs": [], "source": [ "execution_settings = helios.ExecutionSettings(\n", " num_threads=4,\n", " verbosity=helios.LogVerbosity.VERY_VERBOSE,\n", " parallelization=helios.ParallelizationStrategy.WAREHOUSE,\n", ")" ] }, { "cell_type": "code", "execution_count": 13, "id": "4655bb17", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "36066d042d2b44d3b77893649ff82e32", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Legs: 0%| | 0/4 [00:00, 43847 points, 1 vlrs)>\n", "\n", "[248574.86001 248574.86002 248574.86003 ... 248580.60511 248580.60512\n", " 248580.60513]\n" ] } ], "source": [ "print(las)\n", "print(las.x)\n", "print(las.gps_time)" ] }, { "cell_type": "code", "execution_count": 20, "id": "9d004ce0", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([(248574. , [-50.0003, -25. , 250. ], 0., 0., 4.71238898),\n", " (248574.01, [-49.7003, -25. , 250. ], 0., 0., 4.71238898),\n", " (248574.02, [-49.4003, -25. , 250. ], 0., 0., 4.71238898),\n", " (248574.03, [-49.1003, -25. , 250. ], 0., 0., 4.71238898),\n", " (248574.04, [-48.8003, -25. , 250. ], 0., 0., 4.71238898),\n", " (248574.05, [-48.5003, -25. , 250. ], 0., 0., 4.71238898),\n", " (248574.06, [-48.2003, -25. , 250. ], 0., 0., 4.71238898),\n", " (248574.07, [-47.9003, -25. , 250. ], 0., 0., 4.71238898),\n", " (248574.08, [-47.6003, -25. , 250. ], 0., 0., 4.71238898),\n", " (248574.09, [-47.3003, -25. , 250. ], 0., 0., 4.71238898)],\n", " dtype=[('gps_time', '" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Display the results with matplotlib\n", "import matplotlib.pyplot as plt\n", "\n", "# 3d figure\n", "fig = plt.figure(figsize=(10, 10))\n", "ax = fig.add_subplot(111, projection=\"3d\")\n", "for i, (angle, pc) in enumerate(results.items()):\n", " ax.scatter(pc[:, 0], pc[:, 1], pc[:, 2], s=1, label=f\"Angle: {angle}°\")\n", "ax.set_xlabel(\"X\")\n", "ax.set_ylabel(\"Y\")\n", "ax.set_zlabel(\"Z\")\n", "plt.legend()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "a193528d", "metadata": {}, "source": [ "### Example 2: Modifications of full waveform settings\n", "\n", "To test different full waveform settings for the same scene, we use the utility function `combine_parameters`. To create the parameter combinations, we change the `bin_size`, `win_size`, and `beam_sample_quality` variables. The `combine_parameters` function is documented in detail in the [Utility functions](utils.ipynb) section, and more information on the full waveform is provided in the [Full waveform and intensity modelling](intensity_fwf.rst) section." ] }, { "cell_type": "code", "execution_count": 24, "id": "850da58d", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "f1c9e07cf675478c98e7d7742c7c8601", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Legs: 0%| | 0/2 [00:00 1).sum()\n", " num_intermediate_returns = (\n", " (pc[\"return_number\"] > 1) & (pc[\"return_number\"] < pc[\"number_of_returns\"])\n", " ).sum()\n", " ratio_intermediate = num_intermediate_returns / num_points\n", " ratio_multiple = num_multiple_returns / num_points\n", " return_metric_results[\n", " params[\"bin_size\"], params[\"win_size\"], params[\"beam_sample_quality\"]\n", " ] = {\n", " \"num_points\": num_points,\n", " \"num_first_returns\": num_first_returns,\n", " \"num_multiple_returns\": num_multiple_returns,\n", " \"ratio_multiple\": ratio_multiple,\n", " \"num_intermediate_returns\": num_intermediate_returns,\n", " \"ratio_intermediate\": ratio_intermediate,\n", " }" ] }, { "cell_type": "markdown", "id": "2e4a2d94", "metadata": {}, "source": [ "Now we create a table to compare the return metrics for the different settings." ] }, { "cell_type": "code", "execution_count": 25, "id": "0a7541f9", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
num_pointsnum_first_returnsnum_multiple_returnsratio_multiplenum_intermediate_returnsratio_intermediate
bin_sizewin_sizebeam_sample_quality
0.252.031807581800017570.419%80.004%
51809751800019740.538%220.012%
1.0318656818000165673.520%6630.355%
518799518000179944.252%9830.523%
0.5318729918000172983.896%9050.483%
518888518000188844.703%12990.688%
0.102.031808201800018190.453%80.004%
518102818000110270.567%200.011%
1.0318668618000166853.581%6780.363%
518810118000181004.306%9880.525%
0.5318737118000173703.933%8800.470%
518894618000189454.734%12620.668%
0.052.031807551800017540.417%80.004%
51809531800019520.526%190.010%
1.0318654418000165433.507%6550.351%
518793918000179384.224%9490.505%
0.5318721118000172103.851%8160.436%
518874618000187454.633%11800.625%
\n", "
" ], "text/plain": [ " num_points num_first_returns \\\n", "bin_size win_size beam_sample_quality \n", "0.25 2.0 3 180758 180001 \n", " 5 180975 180001 \n", " 1.0 3 186568 180001 \n", " 5 187995 180001 \n", " 0.5 3 187299 180001 \n", " 5 188885 180001 \n", "0.10 2.0 3 180820 180001 \n", " 5 181028 180001 \n", " 1.0 3 186686 180001 \n", " 5 188101 180001 \n", " 0.5 3 187371 180001 \n", " 5 188946 180001 \n", "0.05 2.0 3 180755 180001 \n", " 5 180953 180001 \n", " 1.0 3 186544 180001 \n", " 5 187939 180001 \n", " 0.5 3 187211 180001 \n", " 5 188746 180001 \n", "\n", " num_multiple_returns ratio_multiple \\\n", "bin_size win_size beam_sample_quality \n", "0.25 2.0 3 757 0.419% \n", " 5 974 0.538% \n", " 1.0 3 6567 3.520% \n", " 5 7994 4.252% \n", " 0.5 3 7298 3.896% \n", " 5 8884 4.703% \n", "0.10 2.0 3 819 0.453% \n", " 5 1027 0.567% \n", " 1.0 3 6685 3.581% \n", " 5 8100 4.306% \n", " 0.5 3 7370 3.933% \n", " 5 8945 4.734% \n", "0.05 2.0 3 754 0.417% \n", " 5 952 0.526% \n", " 1.0 3 6543 3.507% \n", " 5 7938 4.224% \n", " 0.5 3 7210 3.851% \n", " 5 8745 4.633% \n", "\n", " num_intermediate_returns \\\n", "bin_size win_size beam_sample_quality \n", "0.25 2.0 3 8 \n", " 5 22 \n", " 1.0 3 663 \n", " 5 983 \n", " 0.5 3 905 \n", " 5 1299 \n", "0.10 2.0 3 8 \n", " 5 20 \n", " 1.0 3 678 \n", " 5 988 \n", " 0.5 3 880 \n", " 5 1262 \n", "0.05 2.0 3 8 \n", " 5 19 \n", " 1.0 3 655 \n", " 5 949 \n", " 0.5 3 816 \n", " 5 1180 \n", "\n", " ratio_intermediate \n", "bin_size win_size beam_sample_quality \n", "0.25 2.0 3 0.004% \n", " 5 0.012% \n", " 1.0 3 0.355% \n", " 5 0.523% \n", " 0.5 3 0.483% \n", " 5 0.688% \n", "0.10 2.0 3 0.004% \n", " 5 0.011% \n", " 1.0 3 0.363% \n", " 5 0.525% \n", " 0.5 3 0.470% \n", " 5 0.668% \n", "0.05 2.0 3 0.004% \n", " 5 0.010% \n", " 1.0 3 0.351% \n", " 5 0.505% \n", " 0.5 3 0.436% \n", " 5 0.625% " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "return_df = pd.DataFrame.from_dict(\n", " return_metric_results,\n", " orient=\"index\",\n", " columns=[\n", " \"num_points\",\n", " \"num_first_returns\",\n", " \"num_multiple_returns\",\n", " \"ratio_multiple\",\n", " \"num_intermediate_returns\",\n", " \"ratio_intermediate\",\n", " ],\n", ")\n", "return_df.index.names = [\"bin_size\", \"win_size\", \"beam_sample_quality\"]\n", "# formatting\n", "for k in return_df.keys():\n", " if \"ratio\" in k:\n", " return_df[k] = return_df[k].map(lambda x: f\"{x:.3%}\")\n", " elif \"number\" in k:\n", " return_df[k] = return_df[k].map(lambda x: f\"{int(x):,}\")\n", "\n", "display(return_df)" ] }, { "cell_type": "markdown", "id": "8b8f157d", "metadata": {}, "source": [ "We can observe the following trends in the results:\n", "- We usually get more points and multiple returns with higher beam sample quality since sampling more subrays within the footprint increases the chances of registering multiple returns.\n", "- The number of multiple returns also increases with smaller window sizes. The window size is used in the maximum detection, so if the window size is smaller than the spacing between two returns, they can be registered as separate returns, leading to more multiple returns.\n", "\n", "By comparing these numbers with a real-world reference, we may choose the full waveform settings that are most suitable for our simulation scenario." ] } ], "metadata": { "kernelspec": { "display_name": "helios-dev-alpha", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.14.2" } }, "nbformat": 4, "nbformat_minor": 5 }