Skip to content

Commit e0726e1

Browse files
authored
Merge pull request #1011 from luxonis/tof_docs
Updated ToF docs
2 parents 535364d + 11f8a9d commit e0726e1

File tree

7 files changed

+262
-60
lines changed

7 files changed

+262
-60
lines changed
Loading
Loading

docs/source/components/nodes/color_camera.rst

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ It interacts with the 3A algorithms: **auto-focus**, **auto-exposure**, and **au
5757
adjustments such as exposure time, sensitivity (ISO), and lens position (if the camera module has a motorized lens) at runtime.
5858
Click `here <https://en.wikipedia.org/wiki/Image_processor>`__ for more information.
5959

60-
**Image Post-Processing** converts YUV420 planar frames from the **ISP** into :code:`video`/:code:`preview`/:code:`still` frames.
60+
**Image Post-Processing** converts YUV420 planar frames from the **ISP** into ``video``/ ``preview``/ ``still`` frames.
6161

6262
``still`` (when a capture is triggered) and ``isp`` work at the max camera resolution, while ``video`` and ``preview`` are
6363
limited to max 4K (3840 x 2160) resolution, which is cropped from ``isp``.
@@ -69,13 +69,42 @@ For IMX378 (12MP), the **post-processing** works like this:
6969
│ ISP ├────────────────►│ video ├───────────────►│ preview │
7070
└─────┘ max 3840x2160 └─────────┘ and cropping └──────────┘
7171
72-
.. image:: /_static/images/tutorials/isp.jpg
73-
74-
The image above is the ``isp`` output from the ColorCamera (12MP resolution from IMX378). If you aren't downscaling ISP,
75-
the ``video`` output is cropped to 4k (max 3840x2160 due to the limitation of the ``video`` output) as represented by
76-
the blue rectangle. The Yellow rectangle represents a cropped ``preview`` output when the preview size is set to a 1:1 aspect
77-
ratio (eg. when using a 300x300 preview size for the MobileNet-SSD NN model) because the ``preview`` output is derived from
78-
the ``video`` output.
72+
If the resolution is set to 12MP and video mode is used, a 4K frame (3840x2160) will be cropped from the center of the 12MP frame.
73+
74+
Full FOV
75+
########
76+
77+
Some sensors, such as the IXM378, default to a 1080P resolution, which is a crop from the full sensor resolution. You can print sensor features to see how the field of view (FOV) is affected by the selected sensor resolution:
78+
79+
.. code-block:: python
80+
81+
import depthai as dai
82+
83+
with dai.Device() as device:
84+
for cam in dev.getConnectedCameraFeatures():
85+
print(cam)
86+
#continue # uncomment for less verbosity
87+
for cfg in cam.configs:
88+
print(" ", cfg)
89+
90+
# Running on OAK-D-S2 will print:
91+
# {socket: CAM_A, sensorName: IMX378, width: 4056, height: 3040, orientation: AUTO, supportedTypes: [COLOR], hasAutofocus: 1, hasAutofocusIC: 1, name: color}
92+
# {width: 1920, height: 1080, minFps: 2.03, maxFps: 60, type: COLOR, fov: {x:108, y: 440, width: 3840, height: 2160}}
93+
# {width: 3840, height: 2160, minFps: 1.42, maxFps: 42, type: COLOR, fov: {x:108, y: 440, width: 3840, height: 2160}}
94+
# {width: 4056, height: 3040, minFps: 1.42, maxFps: 30, type: COLOR, fov: {x:0, y: 0, width: 4056, height: 3040}}
95+
# {width: 1352, height: 1012, minFps: 1.25, maxFps: 52, type: COLOR, fov: {x:0, y: 0, width: 4056, height: 3036}}
96+
# {width: 2024, height: 1520, minFps: 2.03, maxFps: 85, type: COLOR, fov: {x:4, y: 0, width: 4048, height: 3040}}
97+
# {socket: CAM_B, sensorName: OV9282, width: 1280, height: 800, orientation: AUTO, supportedTypes: [MONO], hasAutofocus: 0, hasAutofocusIC: 0, name: left}
98+
# {width: 1280, height: 720, minFps: 1.687, maxFps: 143.1, type: MONO, fov: {x:0, y: 40, width: 1280, height: 720}}
99+
# {width: 1280, height: 800, minFps: 1.687, maxFps: 129.6, type: MONO, fov: {x:0, y: 0, width: 1280, height: 800}}
100+
# {width: 640, height: 400, minFps: 1.687, maxFps: 255.7, type: MONO, fov: {x:0, y: 0, width: 1280, height: 800}}
101+
# {socket: CAM_C, sensorName: OV9282, width: 1280, height: 800, orientation: AUTO, supportedTypes: [MONO], hasAutofocus: 0, hasAutofocusIC: 0, name: right}
102+
# {width: 1280, height: 720, minFps: 1.687, maxFps: 143.1, type: MONO, fov: {x:0, y: 40, width: 1280, height: 720}}
103+
# {width: 1280, height: 800, minFps: 1.687, maxFps: 129.6, type: MONO, fov: {x:0, y: 0, width: 1280, height: 800}}
104+
# {width: 640, height: 400, minFps: 1.687, maxFps: 255.7, type: MONO, fov: {x:0, y: 0, width: 1280, height: 800}}
105+
106+
For the IMX378 sensor, selecting a 4K or 1080P resolution results in approximately 5% horizontal and 29% vertical FOV cropping.
107+
To utilize the full sensor FOV, you should choose one of the following resolutions: ``THE_12_MP``, ``THE_1352X1012``, or ``THE_2024X1520``.
79108

80109
Usage
81110
#####

docs/source/components/nodes/tof.rst

Lines changed: 90 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,88 @@ Inputs and Outputs
3030
.. code-block::
3131
3232
┌───────────┐ depth
33-
inputConfig | ├────────►
34-
───────────►│ | amplitude
35-
input | ToF ├────────►
36-
───────────►│ │ error
33+
│ ├────────►
34+
inputConfig | | amplitude
35+
───────────►│ ├────────►
36+
│ ToF │ phase
37+
input | ├────────►
38+
───────────►│ │ intensity
3739
│ ├────────►
3840
└───────────┘
3941
42+
4043
**Message types**
4144

4245
- ``inputConfig`` - :ref:`ToFConfig`
4346
- ``input`` - :ref:`ImgFrame`
44-
- ``depth`` - :ref:`ImgFrame`
47+
- ``depth`` - :ref:`ImgFrame` - Decoded depth map
4548
- ``amplitude`` - :ref:`ImgFrame`
46-
- ``error`` - :ref:`ImgFrame`
49+
- ``phase`` - :ref:`ImgFrame` Phase image, useful for debugging (FP32)
50+
- ``intensity`` - :ref:`ImgFrame`
51+
52+
ToF Settings
53+
############
54+
55+
In :ref:`ToF depth` example we allow users to quickly configure ToF settings.
56+
57+
Here are the most important settings:
58+
59+
* Optical Correction: It's a process that corrects the optical effect. When enabled, the ToF returns depth map (represented by Green Line on graph below) instead of distance, so it matches :ref:`StereoDepth` depth reporting. It does rectification and distance to depth conversion (Z-map).
60+
* Phase Unwrapping - Process that corrects the phase wrapping effect of the ToF sensor. You can set it to [0..5 are optimized]. The higher the number, the longer the ToF range, but it also increases the noise. Approximate max distance (for exact value, see :ref:`Max distance` below):
61+
62+
* `0` - Disabled, up to ~1.87 meters (utilizing 80MHz modulation frequency)
63+
* `1` - Up to ~3 meters
64+
* `2` - Up to ~4.5 meters
65+
* `3` - Up to ~6 meters
66+
* `4` - Up to ~7.5 meters
67+
68+
* Burst mode: When enabled, ToF node won't reuse frames, as shown on the graph below. It's related to post-processing of the ToF frames, not the actual sensor/projector. It's disabled by default.
69+
* Phase shuffle Temporal filter: Averages shuffled and non-shuffled frames of the same modulation frequency to reduce noise. It's enabled by default. You can disable it to reduce :ref:`ToF motion blur` and system load.
70+
71+
.. image:: /_static/images/components/tof-optical-correction.png
72+
73+
Here's the time diagram which showcases how ToF decoding gets done based on the settings.
74+
75+
.. image:: /_static/images/components/tof-diagram.png
76+
77+
Phase unwrapping
78+
################
79+
80+
If the time it takes for the light to travel from ToF sensor and back exceeds the period of the emitted wave (1.5m or 1.87m), the resulting measurement will "wrap" back to a lower value. This is called phase wrapping.
81+
It's similar to how a clock resets after 12 hours. Phase unwrapping is possible as our ToF has two different modulation frequencies (80Mhz and 100MHz).
82+
83+
Phase unwrapping aims to correct this by allowing the sensor to interpret longer distances without confusion. It uses algorithms to keep track of how many cycles (round trips of the wave) have occurred,
84+
thus correcting the "wrapped" phases. The downside is that the more cycles the sensor has to keep track of, the more noise it introduces into the measurement.
85+
86+
ToF motion blur
87+
###############
88+
89+
To reduce motion blur, we recommend these settings:
90+
91+
- Increase camera FPS. It goes up to 160 FPS, which causes frame capture to be the fastest (6.25ms between frames). This will reduce motion blur as ToF combines multiple frames to get the depth. Note that 160FPS will increase system load significantly (see :ref:`Debugging <Debugging DepthAI pipeline>`). Note also that higher FPS -> lower exposure times, which can increase noise.
92+
- Disable phase shuffle temporal filter. This will introduce more noise.
93+
- Disable phase unwrapping. This will reduce max distance to 1.87 meters (utilizing 80MHz modulation frequency), so about 1 cubic meter of space will be visible (very limited use-cases).
94+
- Enable burst mode. This is irrelevant if shuffle filter and phase unwrapping are disabled (see diagram above). When enabled, ToF node won't reuse frames (lower FPS).
95+
96+
In the diagram above, the less frames are combined (bottom of the diagram), the less motion blur there is. The more frames are combined (top of the diagram), there's more filtering (better accuracy) but it results in more motion blur.
97+
98+
Max distance
99+
############
100+
101+
Maximum ToF distance depends on the modulation frequency and the phase unwrapping level. If phase unwrapping is enabled,
102+
max distance is the shorter of both modulation frequencies (so max distance at 100MHz). Here's the formula:
103+
104+
.. math::
105+
:nowrap:
106+
107+
\begin{align*}
108+
c & = 299792458.0 \quad \text{// speed of light in m/s} \\
109+
MAX\_80MHZ\_M & = \frac{c}{80000000 \times 2} = 1.873 \, \text{m} \\
110+
MAX\_100MHZ\_M & = \frac{c}{100000000 \times 2} = 1.498 \, \text{m} \\
111+
MAX\_DIST\_80MHZ_M & = (\text{phaseUnwrappingLevel} + 1) \times 1.873 + \frac{\text{phaseUnwrapErrorThreshold}}{2} \\
112+
MAX\_DIST\_100MHZ_M & = (\text{phaseUnwrappingLevel} + 1) \times 1.498 + \frac{\text{phaseUnwrapErrorThreshold}}{2} \\
113+
MAX\_DIST\_PHASE\_UNWRAPPING\_M & = MAX\_DIST\_100MHZ\_M
114+
\end{align*}
47115
48116
Usage
49117
#####
@@ -55,10 +123,26 @@ Usage
55123
pipeline = dai.Pipeline()
56124

57125
tof_cam = pipeline.create(dai.node.Camera)
126+
tof_cam.setFps(30)
58127
# We assume the ToF camera sensor is on port CAM_A
59128
tof_cam.setBoardSocket(dai.CameraBoardSocket.CAM_A)
60129

61130
tof = pipeline.create(dai.node.ToF)
131+
132+
# Higher number => faster processing. 1 shave core can do 30FPS.
133+
tof.setNumShaves(1)
134+
135+
# Median filter, kernel size 5x5
136+
tof.initialConfig.setMedianFilter(dai.MedianFilter.KERNEL_5x5)
137+
138+
tofConfig = tof.initialConfig.get()
139+
# Temporal filter averages shuffle/non-shuffle frequencies
140+
tofConfig.enablePhaseShuffleTemporalFilter = True
141+
# Phase unwrapping, for longer range.
142+
tofConfig.phaseUnwrappingLevel = 4 # Up to 7.5 meters
143+
tofConfig.phaseUnwrapErrorThreshold = 300
144+
tof.initialConfig.set(tofConfig)
145+
62146
# ToF node converts raw sensor frames into depth
63147
tof_cam.raw.link(tof.input)
64148

docs/source/samples/StereoDepth/rgb_depth_aligned.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ To align depth with **higher resolution color stream** (eg. 12MP), you need to l
1616
do that with ``stereo.setOutputSize(w,h)``. Code `example here <https://gist.github.com/Erol444/25f374fa18efa7939ec9bb848b39249a>`__.
1717

1818

19+
Host alignment
20+
--------------
21+
22+
StereoDepth node aligns depth map to selected sensor (in this case, color sensor), on the OAK device itself. One can also do the same
23+
on the host side. We have developed a simple `demo script here <https://github.com/luxonis/depthai-python/commit/94b3177f5f4b28e562f637dcf77baa47826ef643>`__.
24+
1925
Demo
2026
####
2127

docs/source/samples/ToF/tof_depth.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,22 @@ on the ToF sensor.
1515
<iframe src="https://www.youtube.com/embed/D2MnnyxdsMA" frameborder="0" allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe>
1616
</div>
1717

18+
With keyboard you can configure ToF settings:
19+
20+
* *FPPN Correction*; Turn on/off with `f`. It's a process that corrects the fixed pattern noise (FPN) of the ToF sensor. Should be enabled.
21+
* *Wiggle Correction*: Turn on/off with `w`. It's a process that corrects the wiggle effect of the ToF sensor. Should be enabled.
22+
* *Temperature Correction*: Turn on/off with `t`. It's a process that corrects the temperature effect of the ToF sensor. Should be enabled.
23+
* *Optical Correction*: Turn on/off with `o`. It's a process that corrects the optical effect (On -> ToF returns distance represented by Green Line), so it matches stereo depth reporting.
24+
* *Phase Unwrapping* - Process that corrects the phase wrapping effect of the ToF sensor. The higher the number, the longer the ToF range, but it also increases the noise.
25+
26+
* `0` - Disabled, up to ~1.87 meters
27+
* `1` - Up to ~3 meters
28+
* `2` - Up to ~4.5 meters
29+
* `3` - Up to ~6 meters
30+
* `4` - Up to ~7.5 meters
31+
32+
.. image:: /_static/images/components/tof-optical-correction.png
33+
1834
Setup
1935
#####
2036

examples/ToF/tof_depth.py

Lines changed: 113 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,119 @@
11
#!/usr/bin/env python3
22

3+
import time
34
import cv2
45
import depthai as dai
56
import numpy as np
67

7-
pipeline = dai.Pipeline()
8-
9-
cam_a = pipeline.create(dai.node.Camera)
10-
# We assume the ToF camera sensor is on port CAM_A
11-
cam_a.setBoardSocket(dai.CameraBoardSocket.CAM_A)
12-
13-
tof = pipeline.create(dai.node.ToF)
14-
15-
# Configure the ToF node
16-
tofConfig = tof.initialConfig.get()
17-
# tofConfig.depthParams.freqModUsed = dai.RawToFConfig.DepthParams.TypeFMod.MIN
18-
tofConfig.depthParams.freqModUsed = dai.RawToFConfig.DepthParams.TypeFMod.MAX
19-
tofConfig.depthParams.avgPhaseShuffle = False
20-
tofConfig.depthParams.minimumAmplitude = 3.0
21-
tof.initialConfig.set(tofConfig)
22-
# Link the ToF sensor to the ToF node
23-
cam_a.raw.link(tof.input)
24-
25-
xout = pipeline.create(dai.node.XLinkOut)
26-
xout.setStreamName("depth")
27-
tof.depth.link(xout.input)
28-
29-
# Connect to device and start pipeline
30-
with dai.Device(pipeline) as device:
31-
print('Connected cameras:', device.getConnectedCameraFeatures())
32-
q = device.getOutputQueue(name="depth")
33-
34-
while True:
35-
imgFrame = q.get() # blocking call, will wait until a new data has arrived
36-
depth_map = imgFrame.getFrame()
37-
38-
# Colorize the depth frame to jet colormap
39-
depth_downscaled = depth_map[::4]
40-
non_zero_depth = depth_downscaled[depth_downscaled != 0] # Remove invalid depth values
41-
if len(non_zero_depth) == 0:
42-
min_depth, max_depth = 0, 0
43-
else:
44-
min_depth = np.percentile(non_zero_depth, 3)
45-
max_depth = np.percentile(non_zero_depth, 97)
46-
depth_colorized = np.interp(depth_map, (min_depth, max_depth), (0, 255)).astype(np.uint8)
47-
depth_colorized = cv2.applyColorMap(depth_colorized, cv2.COLORMAP_JET)
48-
49-
cv2.imshow("Colorized depth", depth_colorized)
50-
51-
if cv2.waitKey(1) == ord('q'):
52-
break
8+
print(dai.__version__)
9+
10+
cvColorMap = cv2.applyColorMap(np.arange(256, dtype=np.uint8), cv2.COLORMAP_JET)
11+
cvColorMap[0] = [0, 0, 0]
12+
13+
def create_pipeline():
14+
pipeline = dai.Pipeline()
15+
16+
tof = pipeline.create(dai.node.ToF)
17+
18+
# Configure the ToF node
19+
tofConfig = tof.initialConfig.get()
20+
21+
# Optional. Best accuracy, but adds motion blur.
22+
# see ToF node docs on how to reduce/eliminate motion blur.
23+
tofConfig.enableOpticalCorrection = True
24+
tofConfig.enablePhaseShuffleTemporalFilter = True
25+
tofConfig.phaseUnwrappingLevel = 4
26+
tofConfig.phaseUnwrapErrorThreshold = 300
27+
28+
tofConfig.enableTemperatureCorrection = False # Not yet supported
29+
30+
xinTofConfig = pipeline.create(dai.node.XLinkIn)
31+
xinTofConfig.setStreamName("tofConfig")
32+
xinTofConfig.out.link(tof.inputConfig)
33+
34+
tof.initialConfig.set(tofConfig)
35+
36+
cam_tof = pipeline.create(dai.node.Camera)
37+
cam_tof.setFps(60) # ToF node will produce depth frames at /2 of this rate
38+
cam_tof.setBoardSocket(dai.CameraBoardSocket.CAM_A)
39+
cam_tof.raw.link(tof.input)
40+
41+
xout = pipeline.create(dai.node.XLinkOut)
42+
xout.setStreamName("depth")
43+
tof.depth.link(xout.input)
44+
45+
tofConfig = tof.initialConfig.get()
46+
47+
return pipeline, tofConfig
48+
49+
50+
if __name__ == '__main__':
51+
pipeline, tofConfig = create_pipeline()
52+
53+
with dai.Device(pipeline) as device:
54+
print('Connected cameras:', device.getConnectedCameraFeatures())
55+
qDepth = device.getOutputQueue(name="depth")
56+
57+
tofConfigInQueue = device.getInputQueue("tofConfig")
58+
59+
counter = 0
60+
while True:
61+
start = time.time()
62+
key = cv2.waitKey(1)
63+
if key == ord('f'):
64+
tofConfig.enableFPPNCorrection = not tofConfig.enableFPPNCorrection
65+
tofConfigInQueue.send(tofConfig)
66+
elif key == ord('o'):
67+
tofConfig.enableOpticalCorrection = not tofConfig.enableOpticalCorrection
68+
tofConfigInQueue.send(tofConfig)
69+
elif key == ord('w'):
70+
tofConfig.enableWiggleCorrection = not tofConfig.enableWiggleCorrection
71+
tofConfigInQueue.send(tofConfig)
72+
elif key == ord('t'):
73+
tofConfig.enableTemperatureCorrection = not tofConfig.enableTemperatureCorrection
74+
tofConfigInQueue.send(tofConfig)
75+
elif key == ord('q'):
76+
break
77+
elif key == ord('0'):
78+
tofConfig.enablePhaseUnwrapping = False
79+
tofConfig.phaseUnwrappingLevel = 0
80+
tofConfigInQueue.send(tofConfig)
81+
elif key == ord('1'):
82+
tofConfig.enablePhaseUnwrapping = True
83+
tofConfig.phaseUnwrappingLevel = 1
84+
tofConfigInQueue.send(tofConfig)
85+
elif key == ord('2'):
86+
tofConfig.enablePhaseUnwrapping = True
87+
tofConfig.phaseUnwrappingLevel = 2
88+
tofConfigInQueue.send(tofConfig)
89+
elif key == ord('3'):
90+
tofConfig.enablePhaseUnwrapping = True
91+
tofConfig.phaseUnwrappingLevel = 3
92+
tofConfigInQueue.send(tofConfig)
93+
elif key == ord('4'):
94+
tofConfig.enablePhaseUnwrapping = True
95+
tofConfig.phaseUnwrappingLevel = 4
96+
tofConfigInQueue.send(tofConfig)
97+
elif key == ord('5'):
98+
tofConfig.enablePhaseUnwrapping = True
99+
tofConfig.phaseUnwrappingLevel = 5
100+
tofConfigInQueue.send(tofConfig)
101+
elif key == ord('m'):
102+
medianSettings = [dai.MedianFilter.MEDIAN_OFF, dai.MedianFilter.KERNEL_3x3, dai.MedianFilter.KERNEL_5x5,
103+
dai.MedianFilter.KERNEL_7x7]
104+
currentMedian = tofConfig.median
105+
nextMedian = medianSettings[(medianSettings.index(currentMedian) + 1) % len(medianSettings)]
106+
print(f"Changing median to {nextMedian.name} from {currentMedian.name}")
107+
tofConfig.median = nextMedian
108+
tofConfigInQueue.send(tofConfig)
109+
110+
imgFrame = qDepth.get() # blocking call, will wait until a new data has arrived
111+
depth_map = imgFrame.getFrame()
112+
max_depth = (tofConfig.phaseUnwrappingLevel + 1) * 1500 # 100MHz modulation freq.
113+
depth_colorized = np.interp(depth_map, (0, max_depth), (0, 255)).astype(np.uint8)
114+
depth_colorized = cv2.applyColorMap(depth_colorized, cvColorMap)
115+
116+
cv2.imshow("Colorized depth", depth_colorized)
117+
counter += 1
118+
119+
device.close()

0 commit comments

Comments
 (0)