Photogrammetric Optode Coregistration
[1]:
import logging
logging.basicConfig()
logging.getLogger("cedalion").setLevel(logging.DEBUG)
import cedalion.io
import cedalion.plots
from cedalion.geometry.photogrammetry.processors import ColoredStickerProcessor
from cedalion.datasets import get_photogrammetry_example_scan
import xarray as xr
import pyvista as pv
xr.set_options(display_expand_data=False);
INFO:pysnirf2:Library loaded by process 3130
Choose between interactive and static 3D plots
[2]:
pv.set_jupyter_backend("static") # uncomment for static rendering
#pv.set_jupyter_backend("client") # uncomment for interactive rendering
#pv.set_jupyter_backend("html")
Use cedalion.io.read_einstar_obj
to read the textured triangle mesh produced by the Einstar scanner.
[3]:
example_scan_fname = get_photogrammetry_example_scan()
s = cedalion.io.read_einstar_obj(example_scan_fname)
Downloading file 'photogrammetry_example_scan.zip' from 'https://doc.ml.tu-berlin.de/cedalion/datasets/photogrammetry_example_scan.zip' to '/home/runner/.cache/cedalion'.
Unzipping contents of '/home/runner/.cache/cedalion/photogrammetry_example_scan.zip' to '/home/runner/.cache/cedalion/photogrammetry_example_scan.zip.unzip'
Processors are meant to analyze the textured mesh and extract positions. The ColoredStickerProcessor searches for colored circular areas. The colors must be specified by their ranges in hue and value. These can for example be found by usig a color pipette tool on the texture file.
In the following to classes of stickers are searched: “O(ptodes)” in blue and “L(andmarks” in yellow.
[4]:
processor = ColoredStickerProcessor(
colors={
"O" : ((0.11, 0.21, 0.8, 1)), # (hue_min, hue_max, value_min, value_max)
"L" : ((0.25, 0.37, 0.35, 0.6))
}
)
[5]:
sticker_centers, normals, details = processor.process(s, details=True)
DEBUG:cedalion:skipping non-circuluar cluster 37
[6]:
display(sticker_centers)
<xarray.DataArray (label: 67, digitized: 3)> Size: 2kB [mm] 95.06 4.429 646.8 77.58 -1.072 408.8 ... -60.75 562.5 82.59 80.29 498.7 Coordinates: * label (label) <U4 1kB 'O-01' 'O-02' 'O-03' ... 'L-04' 'L-05' 'L-06' type (label) object 536B PointType.UNKNOWN ... PointType.UNKNOWN group (label) <U1 268B 'O' 'O' 'O' 'O' 'O' 'O' ... 'L' 'L' 'L' 'L' 'L' Dimensions without coordinates: digitized
Visualize the surface and extraced results.
[7]:
plt = pv.Plotter()
cedalion.plots.plot_surface(plt, s, opacity=1.0)
cedalion.plots.plot_labeled_points(plt, sticker_centers, color="r")
cedalion.plots.plot_vector_field(plt, sticker_centers, normals)
plt.show()
![../_images/examples_photogrammetry_11_0.png](../_images/examples_photogrammetry_11_0.png)
The details object is meant as a container for debuging information. It also provides plotting functionality.The following scatter plot shows the vertex colors in the hue-value plane in which the vertex classification operates.
[8]:
details.plot_vertex_colors()
![../_images/examples_photogrammetry_13_0.png](../_images/examples_photogrammetry_13_0.png)
The following plots show for each cluster (tentative group of sticker vertices) The vertex positions perpendicular to the sticker normal as well as the minimum enclosing circle which is used to find the sticker’s center.
[9]:
details.plot_cluster_circles()
![../_images/examples_photogrammetry_15_0.png](../_images/examples_photogrammetry_15_0.png)
![../_images/examples_photogrammetry_15_1.png](../_images/examples_photogrammetry_15_1.png)
![../_images/examples_photogrammetry_15_2.png](../_images/examples_photogrammetry_15_2.png)
Finally, to get from the sticker centers to the scalp coordinates we have to subtract the lenght of the optodes in the direction of the normals:
[10]:
optode_length = 22.6 * cedalion.units.mm
scalp_coords = sticker_centers.copy()
mask_optodes = sticker_centers.group == 'O'
scalp_coords[mask_optodes] = sticker_centers[mask_optodes] - optode_length*normals[mask_optodes]
[11]:
display(scalp_coords)
<xarray.DataArray (label: 67, digitized: 3)> Size: 2kB [mm] 90.52 5.402 624.7 77.72 -4.772 431.1 ... -60.75 562.5 82.59 80.29 498.7 Coordinates: * label (label) <U4 1kB 'O-01' 'O-02' 'O-03' ... 'L-04' 'L-05' 'L-06' type (label) object 536B PointType.UNKNOWN ... PointType.UNKNOWN group (label) <U1 268B 'O' 'O' 'O' 'O' 'O' 'O' ... 'L' 'L' 'L' 'L' 'L' Dimensions without coordinates: digitized
[12]:
plt = pv.Plotter()
cedalion.plots.plot_surface(plt, s, opacity=0.3)
cedalion.plots.plot_labeled_points(plt, sticker_centers, color="r")
cedalion.plots.plot_labeled_points(plt, scalp_coords, color="g")
cedalion.plots.plot_vector_field(plt, sticker_centers, normals)
plt.show()
![../_images/examples_photogrammetry_19_0.png](../_images/examples_photogrammetry_19_0.png)
TBD: The found landmark and optode positions must still be matched to a montage in order to distinguish between sources and detectors and to assign the correct labels.