{ "cells": [ { "cell_type": "markdown", "id": "22c1e45d", "metadata": {}, "source": [ "# Scenes and scene parts" ] }, { "cell_type": "markdown", "id": "3b02323b", "metadata": {}, "source": [ "A scene is a collection of one or more scene parts. These are 3D models that can be loaded as triangle meshes or voxels." ] }, { "cell_type": "code", "execution_count": 1, "id": "a03e6f26", "metadata": {}, "outputs": [], "source": [ "import helios" ] }, { "cell_type": "markdown", "id": "78d78e97", "metadata": {}, "source": [ "## Loading scene parts\n", "\n", "Scene parts can be loaded with various input formats, including:\n", "\n", "1) from OBJ files (for meshes)\n", "2) from GeoTIFF files (for digital elevation models)\n", "3) from XYZ point cloud files (for voxel models from point clouds)\n", "4) from .vox voxel files (for voxel-based vegetation modelling)\n", "5) from open3d geometries (for meshes or voxel models from point cloud)\n", "\n", "### 1) OBJ files\n", "\n", "For OBJ files, you only need to provide the path to the file. If material files are referenced in the OBJ files, the material properties will also be loaded. Alternatively, users can use the `Material` class to specify and modify material properties. Since OBJ files can have different orientations, i.e. different definitions of the \"up axis\", you can specify the up axis when loading the file. By default, `helios` assumes that the up axis is \"z\"." ] }, { "cell_type": "code", "execution_count": 2, "id": "5ac8fbb3", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "helios.scene.ScenePart" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "groundplane = helios.ScenePart.from_obj(\n", " \"../data/sceneparts/basic/groundplane/groundplane.obj\"\n", ")\n", "tree = helios.ScenePart.from_obj(\n", " \"../data/sceneparts/arbaro/sassafras_low.obj\", up_axis=\"y\"\n", ") # Y as up-axis\n", "type(tree)" ] }, { "cell_type": "markdown", "id": "149c595a", "metadata": {}, "source": [ "### 2) TIFF files\n", "\n", "Digital elevation models (DEMs) can be loaded as GeoTIFF files. " ] }, { "cell_type": "code", "execution_count": 3, "id": "4e47f5e8", "metadata": {}, "outputs": [], "source": [ "dem = helios.ScenePart.from_tiff(\"../data/sceneparts/tiff/dem_hd.tif\")" ] }, { "cell_type": "markdown", "id": "cf40de3c", "metadata": {}, "source": [ "HELIOS will use their encoded georeferencing information and resolution and build a triangle mesh. The centers of the pixels are used as data points. If all points are valid (following the *invalid*/*NoData* value definition from the GeoTIFF header), two triangles are built up: Current point - right neighbor - upper right neighbor and current point - upper neighbor - upper right neighbor:\n", "\n", "![TIFF to mesh conversion](img/tiff_loading.png)\n", "\n", "If any of the three points has an invalid value, the triangle is omitted." ] }, { "cell_type": "markdown", "id": "3ec48a3e", "metadata": {}, "source": [ "### 3) XYZ files" ] }, { "cell_type": "markdown", "id": "075dbb3c", "metadata": {}, "source": [ "`helios` can also load xyz point clouds as scene parts, transforming them into grids of cubic cells (\"voxels\"). If a cell contains at least one point, the cell is defined as \"solid\" and an axis-aligned bounding box primitive with the extent of the cell is created, providing a surface to be virtually scanned." ] }, { "cell_type": "code", "execution_count": 4, "id": "a049ec12", "metadata": {}, "outputs": [], "source": [ "sphere = helios.ScenePart.from_xyz(\n", " \"../data/sceneparts/pointclouds/sphere_dens25000.xyz\", voxel_size=0.1\n", ")" ] }, { "cell_type": "markdown", "id": "150caa3e", "metadata": {}, "source": [ "`helios` expects the columns in the file to have the following format: `X Y Z Nx Ny Nz`, where `Nx Ny Nz` are the optional normal vector components. The desired side length of a voxel is specified with the parameter `voxel_size`, controlling the level of detail of the voxel model.\n", "\n", "To obtain ray incidence angles for voxels for the calculation of return intensity, different options are provided by `helios`. If the point cloud does not contain point normals, normals will always be calculated using Singular Value Decomposition (SVD). This is also possible if the point cloud contains normals by explicitly setting `estimate_normals=True`. For voxels containing less than three points, no normal can be estimated. These voxels are either discarded or they are assigned a `default_normal` if it is specified as a vector like this: `default_normal=[0, 0, 1]`.\n", "\n", "The default behaviour for point clouds with normals is to average the normals of each point within the voxel to derive the voxel normal. As an alternative, the normal of the point closest to the voxel center can be assigned as point normal by setting `snap_neighbor_normal=True`." ] }, { "cell_type": "code", "execution_count": 5, "id": "2e359aa7", "metadata": {}, "outputs": [], "source": [ "sphere = helios.ScenePart.from_xyz(\n", " \"../data/sceneparts/pointclouds/sphere_dens25000.xyz\",\n", " separator=\" \",\n", " voxel_size=1.0,\n", " estimate_normals=True,\n", " default_normal=[0, 0, 1],\n", " snap_neighbor_normal=True,\n", ")" ] }, { "cell_type": "markdown", "id": "api_vox", "metadata": {}, "source": [ "### 4) VOX files\n", "\n", "`helios` can read voxel models provided in a text format with .vox extension, inspired by the format used in the software [AMAPVox](https://amap-dev.cirad.fr/projects/amapvox) software ([Vincent et al. 2017](https://doi.org/10.1016/j.rse.2017.05.034)).\n", "The primary purpose of this loader is to model vegetation with given leaf properties.\n", "\n", "The content of a .vox file looks like this: \n", "\n", "```\n", " VOXEL SPACE\n", " #min_corner: 12.464750289916992 -10.332499504089355 243.5574951171875\n", " #max_corner: 21.312000274658203 -1.6010000705718994 272.84124755859375\n", " #split: 36 35 118\n", " #res:0.25 #nsubvoxel:8 #nrecordmax:0 #fraction-digits:7 #lad_type:Spherical #type:TLS #max_pad:5.0 #build-version:1.4.3\n", " i j k PadBVTotal angleMean bsEntering bsIntercepted bsPotential ground_distance lMeanTotal lgTotal nbEchos nbSampling transmittance attenuation attenuationBiasCorrection\n", " 0 0 0 0 86.3137164 0.4989009 0 2.8475497 0.1240837 0.1632028 693.775004 0 4251 1 0 0\n", " 0 0 1 0 86.989336 1.011544 0 2.840831 0.3722511 0.1784628 1292.784752 0 7244 1 0 0\n", " 0 0 2 0 87.2224845 1.1786434 0 2.835464 0.6204185 0.1828753 1434.1079874 0 7842 1 0 0\n", " 0 0 3 0 87.1534196 0.9608315 0 2.8170681 0.8685859 0.1816032 1186.2322972 0 6532 1 0 0\n", " 0 0 4 0 87.2968587 0.8746295 0 2.8187793 1.1167532 0.1841085 1201.8604542 0 6528 1 0 0\n", " 0 0 5 0 87.1479655 0.9425028 0 2.8363264 1.3649206 0.1747828 1195.6891804 0 6841 1 0 0\n", "```\n", "\n", "The file (as of version 1.4.3) uses space as separator and has six header lines.\n", "- Lines 2 and 3 define the axis aligned bounding box with the xyz coordinates of the minimum and maximum corners (`#min_corner` and `#max_corner`).\n", "- Line 4 gives the number of voxels in x, y and z direction (`#split`).\n", "- Line 5 defines the resolution (`#res`), the leaf angle distribution type (`#lad_type`) and the maximum plant area density value (`#max_pad`). It can furthermore contain additional information which are not read by HELIOS++.\n", "- Line 6 contains the column names. The following are relevant for HELIOS++:\n", " - `i`, `j`, `k`: The voxel indices in x, y and z direction.\n", " - `PADBVTotal` is the plant area density (m2/m3). This parameter is used by HELIOS++ in the transmittive mode to calculate the return and in the scaled mode to determine the size of each voxel (see next sections)\n", "\n", "More in-depth explanations are given in the [AMAPVox 1.0.1 user guide](https://amap-dev.cirad.fr/attachments/download/1499/AMAPVox-1.0.1_userguide.pdf) (page 28) and in the AMAPVox GUI tooltips.\n", "\n", "All following lines contain the voxel values with each line representing one voxel. Empty voxels (`PADBVTotal` = 0 and `transmittance` = 1) may or may not be explicitly defined.\n", "\n", "There are three modes available for handling ray intersections with DetailedVoxels.\n", "\n", "**Transmittive mode** (*default*)" ] }, { "cell_type": "code", "execution_count": 6, "id": "88c0aecc", "metadata": {}, "outputs": [], "source": [ "transmittive_canopy = helios.ScenePart.from_vox(\n", " \"../data/sceneparts/syssifoss/F_BR08_08_merged.vox\",\n", " intersection_mode=\"transmittive\",\n", ")" ] }, { "cell_type": "markdown", "id": "9fde66b8", "metadata": {}, "source": [ "In transmittive mode, voxels are considered to be filled with transmittive turbid medium. Each voxel is filled with randomly distributed infinitely small sized leaf scatterers. Voxels are therefore defined by a leaf area density ($\\mu_L$) and a leaf angle distribution ($g_L$), from which the extinction coefficient $\\sigma$ is calculated ([North 1996](https://doi.org/10.1109/36.508411)).\n", "\n", "$$\\sigma = \\frac{\\mu_L}{2\\pi} \\int_0^{2\\pi} g_L \\left| \\Omega' \\cdot \\Omega_L \\right| d\\Omega_L$$\n", "\n", "where $\\Omega'$ is the unit direction vector of the photon path and $\\Omega_L$ is the leaf normal vector.\n", "\n", "If a ray intersects a voxel, the distance before collision $s$ within the voxel is simulated from a random number $R$ with uniform distribution in the range $(0 < R < 1)$ (North 1996):\n", "\n", "$$s = \\frac{-\\ln (R)}{\\sigma}$$\n", "\n", "If the distance $s$ is larger than the $intersectionLength$, the ray continues through the voxel without generating a return (and may create a return when intersecting another geometry later on). If $s$ is smaller than the $intersectionLength$, the return within the voxel is calculated as:\n", "\n", "$$IntersectionPoint = EntryPoint + s \\cdot rayDirection$$" ] }, { "cell_type": "code", "execution_count": 7, "id": "f154a96d", "metadata": {}, "outputs": [], "source": [ "transmittive_canopy = helios.ScenePart.from_vox(\n", " \"../data/sceneparts/syssifoss/F_BR08_08_merged.vox\"\n", ") # since it is the default\n", "# specifying the LAD LUT\n", "transmittive_canopy = helios.ScenePart.from_vox(\n", " \"../data/sceneparts/syssifoss/F_BR08_08_merged.vox\",\n", " intersection_mode=\"transmittive\",\n", " ladlut_path=\"../data/lut/spherical.txt\",\n", ")" ] }, { "cell_type": "markdown", "id": "5e10cf1e", "metadata": {}, "source": [ "**Scaled mode**" ] }, { "cell_type": "code", "execution_count": 8, "id": "774e9620", "metadata": {}, "outputs": [], "source": [ "voxel_canopy_with_gaps = helios.ScenePart.from_vox(\n", " \"../data/sceneparts/syssifoss/F_BR08_08_merged.vox\",\n", " intersection_mode=\"scaled\",\n", " intersection_argument=0.3334,\n", ")" ] }, { "cell_type": "markdown", "id": "260c1e42", "metadata": {}, "source": [ "In scaled mode, each voxel is considered to be solid (non-transmittive) and of a specific size determined by the leaf area density according to the equation (adapted from [AMAPVox](https://amap-dev.cirad.fr/projects/amapvox) OBJ export tool):\n", "\n", "$$voxelSize= voxelResolution \\cdot \\left( \\frac{pad}{pad_{max}} \\right)^\\alpha$$\n", "\n", "\n", "where $voxelResolution$ and $pad_{max}$ are defined in header line 5, $pad$ is the value read from the column `PADBVTotal` and $\\alpha$ is a scaling factor that is defined using the `intersection_argument` (default: 0.5). If a ray intersects a scaled voxel, the intersection point is returned. \n", "\n", "There is also the option to shift voxels randomly within the original voxel resolution to disrupt the regular pattern of the voxels and to achieve more \"natural\" models using the boolean `random_shift` parameter." ] }, { "cell_type": "code", "execution_count": 9, "id": "6a3c56fb", "metadata": {}, "outputs": [], "source": [ "voxel_canopy_with_gaps = helios.ScenePart.from_vox(\n", " \"../data/sceneparts/syssifoss/F_BR08_08_merged.vox\",\n", " intersection_mode=\"scaled\",\n", " intersection_argument=0.3334,\n", " random_shift=True,\n", ")" ] }, { "cell_type": "markdown", "id": "2fb006da", "metadata": {}, "source": [ "**Fixed mode**" ] }, { "cell_type": "code", "execution_count": 10, "id": "b532b12f", "metadata": {}, "outputs": [], "source": [ "voxel_canopy_25cm = helios.ScenePart.from_vox(\n", " \"../data/sceneparts/syssifoss/F_BR08_08_crown_250.vox\", intersection_mode=\"fixed\"\n", ")" ] }, { "cell_type": "markdown", "id": "23adf49c", "metadata": {}, "source": [ "In fixed mode, each voxel is modelled as solid (not light-transmittive) and of the size given by `#res`. Only Voxels with transmittance < 1 are considered to be filled. If a ray intersects a filled voxel, the intersection point on the surface is returned. " ] }, { "cell_type": "markdown", "id": "806318f2", "metadata": {}, "source": [ "### 5) Open3D Geometries\n", "\n", "`helios` provides an interface to `open3d` for loading both meshes and point clouds. If a mesh is provided, it is loaded as a triangle mesh similar to the OBJ loading. If a point cloud is provided, it is loaded as a voxel model similar to the XYZ loading. The same parameters are available for controlling the normal estimation and handling of voxels with less than three points as described in the XYZ section above.\n", "\n", "This interface allows to load, build or manipulate 3D geometries with the powerful `open3d` library and then use them as scene parts in `helios`." ] }, { "cell_type": "code", "execution_count": 12, "id": "edb5c23c", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Jupyter environment detected. Enabling Open3D WebVisualizer.\n", "[Open3D INFO] WebRTC GUI backend enabled.\n", "[Open3D INFO] WebRTCWindowSystem: HTTP handshake server disabled.\n" ] } ], "source": [ "import open3d as o3d\n", "\n", "# Loading an open3d mesh\n", "monkey = o3d.data.MonkeyModel()\n", "model = o3d.io.read_triangle_mesh(monkey.path)\n", "\n", "monkey_scene_part = helios.ScenePart.from_open3d(model, up_axis=\"z\")" ] }, { "cell_type": "code", "execution_count": 13, "id": "ec9b7237", "metadata": {}, "outputs": [], "source": [ "# Loading an open3d point cloud\n", "pcd = o3d.io.read_point_cloud(\n", " \"../data/sceneparts/pointclouds/sphere_dens25000_normals.xyz\", format=\"xyzn\"\n", ")\n", "pcd.crop(\n", " o3d.geometry.AxisAlignedBoundingBox(\n", " min_bound=(57.8, 14.8, 4.0), max_bound=(77.8, 34.8, 44.1)\n", " )\n", ") # cropping the sphere by half\n", "cropped_sphere = helios.ScenePart.from_open3d(\n", " pcd, voxel_size=0.2, estimate_normals=False, snap_neighbor_normal=False\n", ")" ] }, { "cell_type": "markdown", "id": "f5310229", "metadata": {}, "source": [ "### Loading multiple scene parts at one" ] }, { "cell_type": "markdown", "id": "3fd83363", "metadata": {}, "source": [ "Of course, these methods can be put into a loop to load a large number of scene parts at once. However, `helios` also supports loading multiple scene parts by specifying a path with wildcards to the respective `from_objs`, `from_tiffs` and `from_xyzs` methods. For example, if you have a folder with multiple OBJ files, you can load them all at once like this:" ] }, { "cell_type": "code", "execution_count": 11, "id": "66a985fd", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "list" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "toyblocks = helios.ScenePart.from_objs(\"../data/sceneparts/toyblocks/*.obj\")\n", "type(toyblocks)" ] }, { "cell_type": "markdown", "id": "8b8913de", "metadata": {}, "source": [ "## Transforming scene parts\n", "\n", "Coordinate transformations can be applied to scene parts. This includes translations, rotations and scaling." ] }, { "cell_type": "code", "execution_count": 14, "id": "a807425a", "metadata": {}, "outputs": [], "source": [ "toyblock = helios.ScenePart.from_obj(\"../data/sceneparts/toyblocks/cube.obj\")" ] }, { "cell_type": "markdown", "id": "4e644421", "metadata": {}, "source": [ "### 1) Translate\n", "\n", "To translate a scene part, simply provide the translation vector to the `translate` method:" ] }, { "cell_type": "code", "execution_count": 15, "id": "8a228f2c", "metadata": {}, "outputs": [], "source": [ "toyblock.translate([5, 5, 0])\n", "# or directly when loading the scene part:\n", "toyblock = helios.ScenePart.from_obj(\"../data/sceneparts/toyblocks/cube.obj\").translate(\n", " [5, 5, 0]\n", ")" ] }, { "cell_type": "markdown", "id": "d0d2156e", "metadata": {}, "source": [ "### 2) Rotate\n", "\n", "To rotate a scene part, three rotation definitions are supported:\n", "- **Quaternion**: Provide the four components of the quaternion as a list or array in the order [w, x, y, z].\n", "- **Axis-angle**: Provide the rotation axis as a vector and the scalar rotation angle in degrees or radians.\n", "- **Vector-to-vector**: Provide two vectors u and v and obtain the rotation of minimal angle that maps u to v.\n", "\n", "In addition, a `rotation_center` can be specified to define the center of rotation. By default, the origin of the object is used as rotation center." ] }, { "cell_type": "code", "execution_count": 16, "id": "2c57775e", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# same rotation for each definition\n", "toyblock.rotate(\n", " axis=[0, 1, 0], angle=45 * helios.units.degree\n", ") # example for axis-angle rotation\n", "toyblock.rotate(\n", " quaternion=[0.9238795, 0, 0.3826834, 0]\n", ") # example for quaternion rotation\n", "toyblock.rotate(\n", " from_axis=[0, 0, 1], to_axis=[0.7071, 0, 0.7071]\n", ") # example for vector-to-vector rotation" ] }, { "cell_type": "markdown", "id": "df02a8f9", "metadata": {}, "source": [ "### 3) Scale\n", "\n", "A scene part can be scaled by providing a scaling factor. Scaling will be applied uniformly in all directions. Non-uniform scaling is currently not supported. For non-uniform scaling, edit the scene part in external software, e.g. in Blender for mesh models." ] }, { "cell_type": "code", "execution_count": 17, "id": "b4eb3ef8", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "toyblock.scale(2.0)" ] }, { "cell_type": "markdown", "id": "4a37b02f", "metadata": {}, "source": [ "Transformations can also be chained together and will then be applied in the order specified, which is especially relevant when not rotating around the object origin. This means that the following coordinate transformations are not equivalent." ] }, { "cell_type": "code", "execution_count": 18, "id": "b2ce3f7d", "metadata": {}, "outputs": [], "source": [ "toyblock1 = (\n", " helios.ScenePart.from_obj(\"../data/sceneparts/toyblocks/cube.obj\")\n", " .translate([5, 5, 0])\n", " .rotate(\n", " axis=[0, 1, 0], angle=45 * helios.units.degree, rotation_center=[0.0, 0.0, 0.0]\n", " )\n", " .scale(2.0)\n", ")\n", "toyblock2 = (\n", " helios.ScenePart.from_obj(\"../data/sceneparts/toyblocks/cube.obj\")\n", " .scale(2.0)\n", " .rotate(\n", " axis=[0, 1, 0], angle=45 * helios.units.degree, rotation_center=[0.0, 0.0, 0.0]\n", " )\n", " .translate([5, 5, 0])\n", ")" ] }, { "cell_type": "markdown", "id": "32a2f0f0", "metadata": {}, "source": [ "We can see this by investigating their bounding boxes:" ] }, { "cell_type": "code", "execution_count": 19, "id": "1c4496d1", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "((47.807548872992086, -88.400208, -76.74523769795861),\n", " (104.49799233435176, -48.227797999999986, -20.05477868024975))" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "toyblock1.bbox.bounds" ] }, { "cell_type": "code", "execution_count": 20, "id": "8de4eaac", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "((45.73648106112661, -93.400208, -69.67416988609314),\n", " (102.42692452248629, -53.227797999999986, -12.983710868384271))" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "toyblock2.bbox.bounds" ] }, { "cell_type": "markdown", "id": "572a4f38", "metadata": {}, "source": [ "## Creating, saving and loading scenes\n", "\n", "A static scene can simply created from a list of scene parts. Scene parts can also be added to an existing scene via the `add_scene_part` method." ] }, { "cell_type": "code", "execution_count": 21, "id": "d2f186a8", "metadata": {}, "outputs": [], "source": [ "groundplane = helios.ScenePart.from_obj(\n", " \"../data/sceneparts/basic/groundplane/groundplane.obj\"\n", ")\n", "tree1 = helios.ScenePart.from_obj(\n", " \"../data/sceneparts/arbaro/sassafras_low.obj\", up_axis=\"y\"\n", ")\n", "tree2 = helios.ScenePart.from_obj(\n", " \"../data/sceneparts/arbaro/black_tupelo_low.obj\", up_axis=\"y\"\n", ").translate([10, 0, 0])\n", "scene = helios.StaticScene(scene_parts=[groundplane, tree1])\n", "scene.add_scene_part(tree2)" ] }, { "cell_type": "markdown", "id": "0f37e3d2", "metadata": {}, "source": [ "Once created, a scene can be saved to a binary file and later loaded again from the binary file." ] }, { "cell_type": "code", "execution_count": 22, "id": "b81aafb4", "metadata": {}, "outputs": [], "source": [ "scene.to_binary(\"../arbaro_binary.bin\")" ] }, { "cell_type": "code", "execution_count": 23, "id": "04bae6fb", "metadata": {}, "outputs": [], "source": [ "loaded_scene = helios.StaticScene.from_binary(\"../arbaro_binary.bin\")" ] }, { "cell_type": "markdown", "id": "dfb816d7", "metadata": {}, "source": [ "A scene can also be loaded from an XML file. Here, it might be necessary to add an asset directory to the `helios` search path so that relative paths to the scene parts can be resolved." ] }, { "cell_type": "code", "execution_count": 24, "id": "67424a31", "metadata": {}, "outputs": [], "source": [ "helios.add_asset_directory(\"../\")\n", "scene = helios.StaticScene.from_xml(\"../data/scenes/demo/arbaro_demo.xml\")" ] }, { "cell_type": "markdown", "id": "753741ed", "metadata": {}, "source": [ "## Bounding boxes\n", "\n", "It can be useful to retrieve the axis-aligned bounding box of individual scene parts." ] }, { "cell_type": "code", "execution_count": 25, "id": "ffa40f89", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "((-13.885534999999999, -9.00227, -13.945395), (4.061035, 8.3117, 10.405725000000002))\n", "[-4.91225 -0.345285 -1.769835]\n" ] } ], "source": [ "print(tree1.bbox.bounds)\n", "print(tree1.bbox.centroid)" ] }, { "cell_type": "markdown", "id": "3f42c3e9", "metadata": {}, "source": [ "This is also possible for the whole scene:" ] }, { "cell_type": "code", "execution_count": 26, "id": "3bc8a67d", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "((-100.0, -100.0, -6.9726975), (100.0, 100.0, 6.9726975))\n", "[0. 0. 0.]\n" ] } ], "source": [ "print(scene.bbox.bounds)\n", "print(scene.bbox.centroid)" ] }, { "cell_type": "markdown", "id": "e77321a0", "metadata": {}, "source": [ "## Material properties\n", "\n", "### Reading and modifying material properties of scene parts\n", "\n", "Scene parts also have material properties, which we can access and modify.\n", "Let's start with an example where the scene part already has a material." ] }, { "cell_type": "code", "execution_count": 27, "id": "2811dbbe", "metadata": {}, "outputs": [], "source": [ "material_names = list(tree1.materials.keys())" ] }, { "cell_type": "code", "execution_count": 28, "id": "cb3583c0", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['trunk', 'stems_1', 'stems_2', 'stems_3', 'leaves']" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "material_names" ] }, { "cell_type": "code", "execution_count": 29, "id": "c5ee7c3c", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "wood\n", "nan\n", "0\n", "[0.0, 0.0, 0.0, 0.0]\n", "[0.20000000298023224, 0.20000000298023224, 0.05000000074505806, 0.0]\n", "[0.20000000298023224, 0.20000000298023224, 0.05000000074505806, 0.0]\n", "10.0\n", "False\n" ] } ], "source": [ "print(tree1.materials[\"trunk\"].spectra)\n", "print(tree1.materials[\"trunk\"].reflectance)\n", "print(tree1.materials[\"trunk\"].classification)\n", "print(tree1.materials[\"trunk\"].specular_components)\n", "print(tree1.materials[\"trunk\"].diffuse_components)\n", "print(tree1.materials[\"trunk\"].ambient_components)\n", "print(tree1.materials[\"trunk\"].specular_exponent)\n", "print(tree1.materials[\"trunk\"].is_ground)" ] }, { "cell_type": "code", "execution_count": 30, "id": "72e7550c", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ "groundplane.materials[\"None\"].is_ground" ] }, { "cell_type": "markdown", "id": "e466e83b", "metadata": {}, "source": [ "The materials of `tree1` currently do not have any classification assigned. Let's assign class 1 to the trunk, class 2 to the stems, and class 3 to the leaves, so that the classification scheme could be used as reference data for semantic segmentation (incl. leaf-wood classification)." ] }, { "cell_type": "code", "execution_count": 31, "id": "14896cea", "metadata": {}, "outputs": [], "source": [ "tree1.materials[\"trunk\"].classification = 1\n", "tree1.materials[\"stems_1\"].classification = 2\n", "tree1.materials[\"stems_2\"].classification = 2\n", "tree1.materials[\"stems_3\"].classification = 2\n", "tree1.materials[\"leaves\"].classification = 3" ] }, { "cell_type": "code", "execution_count": 32, "id": "f016d977", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "3" ] }, "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ "tree1.materials[\"leaves\"].classification" ] }, { "cell_type": "markdown", "id": "452f0599", "metadata": {}, "source": [ "If an object does not have any material assigned at all, it takes the global HELIOS++ default values." ] }, { "cell_type": "code", "execution_count": 33, "id": "bcb34a5e", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['default']\n" ] } ], "source": [ "sphere = helios.ScenePart.from_xyz(\n", " \"../data/sceneparts/pointclouds/sphere_dens25000.xyz\", voxel_size=1.0\n", ")\n", "print(sphere.materials.keys())" ] }, { "cell_type": "markdown", "id": "b06c2386", "metadata": {}, "source": [ "### Loading and creating materials\n", "\n", "We can also load a material from file or create a completely new material from scratch." ] }, { "cell_type": "code", "execution_count": 34, "id": "9c144209", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "conifer\n" ] } ], "source": [ "# load material from .mtl file by ID\n", "leaf_mat = helios.scene.Material.from_file(\n", " material_file=\"../data/sceneparts/arbaro/tree.mtl\", material_id=\"leaves\"\n", ")\n", "print(leaf_mat.spectra)" ] }, { "cell_type": "code", "execution_count": 35, "id": "5741bbd0", "metadata": {}, "outputs": [], "source": [ "# create a new material from scratch with HELIOS\n", "new_grass_mat = helios.scene.Material(\n", " name=\"grass\",\n", " reflectance=0.2,\n", " classification=0,\n", " spectra=\"grass\",\n", " ambient_components=[0.05, 0.3, 0.05, 0.0],\n", " diffuse_components=[0.2, 0.6, 0.2, 0.0],\n", " specular_components=[0.1, 0.1, 0.1, 0.0],\n", " specular_exponent=10.0,\n", ")" ] }, { "cell_type": "markdown", "id": "26b17368", "metadata": {}, "source": [ "### Updating material properties of scene parts\n", "\n", "We can now assign this material to the ground plane." ] }, { "cell_type": "code", "execution_count": 36, "id": "c328f447", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['None']" ] }, "execution_count": 36, "metadata": {}, "output_type": "execute_result" } ], "source": [ "groundplane.materials.keys()" ] }, { "cell_type": "code", "execution_count": 37, "id": "6b92445f", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['grass']" ] }, "execution_count": 37, "metadata": {}, "output_type": "execute_result" } ], "source": [ "groundplane.update_material(new_grass_mat)\n", "groundplane.materials.keys()" ] }, { "cell_type": "markdown", "id": "13b06883", "metadata": {}, "source": [ "We could also apply the material to selected primitives of the object, like so:" ] }, { "cell_type": "code", "execution_count": 38, "id": "1edbe119", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['Material']\n", "['grass', 'Material']\n" ] } ], "source": [ "box = helios.ScenePart.from_obj(\"../data/sceneparts/basic/box/box100.obj\")\n", "print(box.materials.keys())\n", "box.update_material(new_grass_mat, indices=[0]) # apply only to first primitive\n", "print(box.materials.keys())" ] }, { "cell_type": "code", "execution_count": 39, "id": "3188c786", "metadata": {}, "outputs": [], "source": [ "new_mat = helios.scene.Material(\n", " name=\"walls\",\n", " reflectance=0.1,\n", " classification=1,\n", " ambient_components=[0.1, 0.1, 0.1, 0.0],\n", " diffuse_components=[0.85, 0.85, 0.85, 0.0],\n", " specular_components=[0.05, 0.05, 0.05, 0.0],\n", ")\n", "box.update_material(new_mat, range_start=3, range_stop=10)" ] }, { "cell_type": "code", "execution_count": 40, "id": "6f720511", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['grass', 'Material', 'walls']\n" ] } ], "source": [ "print(box.materials.keys())" ] } ], "metadata": { "kernelspec": { "display_name": "helios-dev-alpha_o3d", "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.12.13" } }, "nbformat": 4, "nbformat_minor": 5 }