Skip to content

Commit 9f11020

Browse files
authored
Merge pull request #822 from luxonis/custom_mesh
Custom mesh example/docs
2 parents e6ac495 + ff10438 commit 9f11020

File tree

6 files changed

+218
-23
lines changed

6 files changed

+218
-23
lines changed

docs/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ sphinx-autopackagesummary==1.3
88
autodocsumm==0.2.2
99
pathlib==1.0.1
1010
jinja2==3.0.3
11+
urllib3==1.26.15 # New urllib version breaks sphinx
1112
-r ./requirements_mkdoc.txt
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
Stereo Depth custom Mesh
2+
========================
3+
4+
This example shows how you can load custom mesh to the device and use it for depth calculation.
5+
In this example, mesh files are generated from camera calibration data, but you can also use
6+
your own mesh files.
7+
8+
By default, :ref:`StereoDepth` will use the same logic as inside the ``def getMesh()`` to calculate
9+
mesh files whenever horizontal FOV is larger than 90°. You could also force calculate the mesh using:
10+
11+
.. code-block:: python
12+
13+
stereo = pipeline.create(dai.node.StereoDepth)
14+
# Enable mesh calculation to correct distortion:
15+
stereo.enableDistortionCorrection(True)
16+
17+
18+
StereoDepth node also allows you to load mesh files directly from a file path:
19+
20+
.. code-block:: python
21+
22+
stereo = pipeline.create(dai.node.StereoDepth)
23+
stereo.loadMeshFiles('path/to/left_mesh', 'path/to/right_mesh')
24+
25+
Demo
26+
####
27+
28+
.. image:: https://github.com/luxonis/depthai-python/assets/18037362/f2031bd4-0748-4a06-abb1-b52e9a17134e
29+
30+
On the image above you can see that the rectified frame isn't as wide FOV as the original one,
31+
that's because the distortion correction is applied (in this case via custom mesh files), so the
32+
disparity matching can be performed correctly.
33+
34+
Setup
35+
#####
36+
37+
.. include:: /includes/install_from_pypi.rst
38+
39+
Source code
40+
###########
41+
42+
.. tabs::
43+
44+
.. tab:: Python
45+
46+
Also `available on GitHub <https://github.com/luxonis/depthai-python/blob/main/examples/StereoDepth/stereo_depth_custom_mesh.py>`__
47+
48+
.. literalinclude:: ../../../../examples/StereoDepth/stereo_depth_custom_mesh.py
49+
:language: python
50+
:linenos:
51+
52+
.. include:: /includes/footer-short.rst

docs/source/tutorials/code_samples.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ are presented with code.
137137
- :ref:`Depth Crop Control` - Demonstrates how to control cropping of depth frames from the host
138138
- :ref:`Depth Post-Processing` - Depth post-processing filters
139139
- :ref:`Depth Preview` - Displays colorized stereo disparity
140+
- :ref:`Stereo Depth custom Mesh` - Calculate and load custom mesh for stereo depth calculation
140141
- :ref:`Stereo Depth from host` - Generates stereo depth frame from a set of mono images from the host
141142
- :ref:`Stereo Depth Video` - An extended version of **Depth Preview**
142143
- :ref:`RGB Depth alignment` - Displays RGB depth aligned frames
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#!/usr/bin/env python3
2+
3+
import cv2
4+
import numpy as np
5+
import depthai as dai
6+
import argparse
7+
8+
parser = argparse.ArgumentParser()
9+
parser.add_argument("-res", "--resolution", type=str, default="720",
10+
help="Sets the resolution on mono cameras. Options: 800 | 720 | 400")
11+
parser.add_argument("-md", "--mesh_dir", type=str, default=None,
12+
help="Output directory for mesh files. If not specified mesh files won't be saved")
13+
parser.add_argument("-lm", "--load_mesh", default=False, action="store_true",
14+
help="Read camera intrinsics, generate mesh files and load them into the stereo node.")
15+
args = parser.parse_args()
16+
17+
meshDirectory = args.mesh_dir # Output dir for mesh files
18+
generateMesh = args.load_mesh # Load mesh files
19+
RES_MAP = {
20+
'800': {'w': 1280, 'h': 800, 'res': dai.MonoCameraProperties.SensorResolution.THE_800_P },
21+
'720': {'w': 1280, 'h': 720, 'res': dai.MonoCameraProperties.SensorResolution.THE_720_P },
22+
'400': {'w': 640, 'h': 400, 'res': dai.MonoCameraProperties.SensorResolution.THE_400_P }
23+
}
24+
if args.resolution not in RES_MAP:
25+
exit("Unsupported resolution!")
26+
27+
resolution = RES_MAP[args.resolution]
28+
29+
def getMesh(calibData):
30+
M1 = np.array(calibData.getCameraIntrinsics(dai.CameraBoardSocket.CAM_B, resolution['w'], resolution['h']))
31+
d1 = np.array(calibData.getDistortionCoefficients(dai.CameraBoardSocket.CAM_B))
32+
R1 = np.array(calibData.getStereoLeftRectificationRotation())
33+
M2 = np.array(calibData.getCameraIntrinsics(dai.CameraBoardSocket.CAM_C, resolution['w'], resolution['h']))
34+
d2 = np.array(calibData.getDistortionCoefficients(dai.CameraBoardSocket.CAM_C))
35+
R2 = np.array(calibData.getStereoRightRectificationRotation())
36+
mapXL, mapYL = cv2.initUndistortRectifyMap(M1, d1, R1, M2, (resolution['w'], resolution['h']), cv2.CV_32FC1)
37+
mapXR, mapYR = cv2.initUndistortRectifyMap(M2, d2, R2, M2, (resolution['w'], resolution['h']), cv2.CV_32FC1)
38+
39+
meshCellSize = 16
40+
meshLeft = []
41+
meshRight = []
42+
43+
for y in range(mapXL.shape[0] + 1):
44+
if y % meshCellSize == 0:
45+
rowLeft = []
46+
rowRight = []
47+
for x in range(mapXL.shape[1] + 1):
48+
if x % meshCellSize == 0:
49+
if y == mapXL.shape[0] and x == mapXL.shape[1]:
50+
rowLeft.append(mapYL[y - 1, x - 1])
51+
rowLeft.append(mapXL[y - 1, x - 1])
52+
rowRight.append(mapYR[y - 1, x - 1])
53+
rowRight.append(mapXR[y - 1, x - 1])
54+
elif y == mapXL.shape[0]:
55+
rowLeft.append(mapYL[y - 1, x])
56+
rowLeft.append(mapXL[y - 1, x])
57+
rowRight.append(mapYR[y - 1, x])
58+
rowRight.append(mapXR[y - 1, x])
59+
elif x == mapXL.shape[1]:
60+
rowLeft.append(mapYL[y, x - 1])
61+
rowLeft.append(mapXL[y, x - 1])
62+
rowRight.append(mapYR[y, x - 1])
63+
rowRight.append(mapXR[y, x - 1])
64+
else:
65+
rowLeft.append(mapYL[y, x])
66+
rowLeft.append(mapXL[y, x])
67+
rowRight.append(mapYR[y, x])
68+
rowRight.append(mapXR[y, x])
69+
if (mapXL.shape[1] % meshCellSize) % 2 != 0:
70+
rowLeft.append(0)
71+
rowLeft.append(0)
72+
rowRight.append(0)
73+
rowRight.append(0)
74+
75+
meshLeft.append(rowLeft)
76+
meshRight.append(rowRight)
77+
78+
meshLeft = np.array(meshLeft)
79+
meshRight = np.array(meshRight)
80+
81+
return meshLeft, meshRight
82+
83+
def saveMeshFiles(meshLeft, meshRight, outputPath):
84+
print("Saving mesh to:", outputPath)
85+
meshLeft.tofile(outputPath + "/left_mesh.calib")
86+
meshRight.tofile(outputPath + "/right_mesh.calib")
87+
88+
89+
def create_pipeline(device: dai.Device) -> dai.Pipeline:
90+
calibData = device.readCalibration()
91+
print("Creating Stereo Depth pipeline")
92+
pipeline = dai.Pipeline()
93+
94+
camLeft = pipeline.create(dai.node.MonoCamera)
95+
camLeft.setBoardSocket(dai.CameraBoardSocket.LEFT)
96+
97+
camRight = pipeline.create(dai.node.MonoCamera)
98+
camRight.setBoardSocket(dai.CameraBoardSocket.RIGHT)
99+
100+
xoutRight = pipeline.create(dai.node.XLinkOut)
101+
xoutRight.setStreamName("right")
102+
camRight.out.link(xoutRight.input)
103+
104+
for monoCam in (camLeft, camRight): # Common config
105+
monoCam.setResolution(resolution['res'])
106+
# monoCam.setFps(20.0)
107+
108+
stereo = pipeline.create(dai.node.StereoDepth)
109+
camLeft.out.link(stereo.left)
110+
camRight.out.link(stereo.right)
111+
stereo.setDefaultProfilePreset(dai.node.StereoDepth.PresetMode.HIGH_DENSITY)
112+
stereo.setRectifyEdgeFillColor(0) # Black, to better see the cutout
113+
stereo.setLeftRightCheck(True)
114+
stereo.setExtendedDisparity(True)
115+
116+
117+
118+
119+
xoutDisparity = pipeline.create(dai.node.XLinkOut)
120+
xoutDisparity.setStreamName("disparity")
121+
stereo.disparity.link(xoutDisparity.input)
122+
123+
xoutRectifRight = pipeline.create(dai.node.XLinkOut)
124+
xoutRectifRight.setStreamName("rectifiedRight")
125+
stereo.rectifiedRight.link(xoutRectifRight.input)
126+
127+
# Create custom meshes from calibration data. Here you could also
128+
# load your own mesh files, or generate them in any other way.
129+
leftMesh, rightMesh = getMesh(calibData)
130+
if generateMesh:
131+
meshLeft = list(leftMesh.tobytes())
132+
meshRight = list(rightMesh.tobytes())
133+
# Load mesh data to the StereoDepth node
134+
stereo.loadMeshData(meshLeft, meshRight)
135+
136+
if meshDirectory is not None:
137+
saveMeshFiles(leftMesh, rightMesh, meshDirectory)
138+
return pipeline
139+
140+
with dai.Device() as device:
141+
device.startPipeline(create_pipeline(device))
142+
143+
# Create a receive queue for each stream
144+
qList = [device.getOutputQueue(stream, 8, blocking=False) for stream in ['right', 'rectifiedRight', 'disparity']]
145+
146+
while True:
147+
for q in qList:
148+
name = q.getName()
149+
frame = q.get().getCvFrame()
150+
cv2.imshow(name, frame)
151+
if cv2.waitKey(1) == ord("q"):
152+
break

examples/StereoDepth/stereo_depth_video.py

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,16 @@
8585
)
8686
args = parser.parse_args()
8787

88-
resolutionMap = {"800": (1280, 800), "720": (1280, 720), "400": (640, 400)}
89-
if args.resolution not in resolutionMap:
88+
RES_MAP = {
89+
'800': {'w': 1280, 'h': 800, 'res': dai.MonoCameraProperties.SensorResolution.THE_800_P },
90+
'720': {'w': 1280, 'h': 720, 'res': dai.MonoCameraProperties.SensorResolution.THE_720_P },
91+
'400': {'w': 640, 'h': 400, 'res': dai.MonoCameraProperties.SensorResolution.THE_400_P }
92+
}
93+
if args.resolution not in RES_MAP:
9094
exit("Unsupported resolution!")
9195

92-
resolution = resolutionMap[args.resolution]
96+
resolution = RES_MAP[args.resolution]
97+
9398
meshDirectory = args.mesh_dir # Output dir for mesh files
9499
generateMesh = args.load_mesh # Load mesh files
95100

@@ -111,7 +116,7 @@
111116
median = medianMap[args.median]
112117

113118
print("StereoDepth config options:")
114-
print(" Resolution: ", resolution)
119+
print(f" Resolution: {resolution['w']}x{resolution['h']}")
115120
print(" Left-Right check: ", lrcheck)
116121
print(" Extended disparity:", extended)
117122
print(" Subpixel: ", subpixel)
@@ -188,7 +193,6 @@ def getDisparityFrame(frame, cvColorMap):
188193

189194
return disp
190195

191-
192196
device = dai.Device()
193197
calibData = device.readCalibration()
194198
print("Creating Stereo Depth pipeline")
@@ -211,15 +215,8 @@ def getDisparityFrame(frame, cvColorMap):
211215
camLeft.setBoardSocket(dai.CameraBoardSocket.LEFT)
212216
camRight.setBoardSocket(dai.CameraBoardSocket.RIGHT)
213217

214-
res = (
215-
dai.MonoCameraProperties.SensorResolution.THE_800_P
216-
if resolution[1] == 800
217-
else dai.MonoCameraProperties.SensorResolution.THE_720_P
218-
if resolution[1] == 720
219-
else dai.MonoCameraProperties.SensorResolution.THE_400_P
220-
)
221218
for monoCam in (camLeft, camRight): # Common config
222-
monoCam.setResolution(res)
219+
monoCam.setResolution(resolution['res'])
223220
# monoCam.setFps(20.0)
224221

225222
stereo.setDefaultProfilePreset(dai.node.StereoDepth.PresetMode.HIGH_DENSITY)
@@ -259,15 +256,6 @@ def getDisparityFrame(frame, cvColorMap):
259256
if depth:
260257
streams.append("depth")
261258

262-
leftMesh, rightMesh = getMesh(calibData)
263-
if generateMesh:
264-
meshLeft = list(leftMesh.tobytes())
265-
meshRight = list(rightMesh.tobytes())
266-
stereo.loadMeshData(meshLeft, meshRight)
267-
268-
if meshDirectory is not None:
269-
saveMeshFiles(leftMesh, rightMesh, meshDirectory)
270-
271259
cvColorMap = cv2.applyColorMap(np.arange(256, dtype=np.uint8), cv2.COLORMAP_JET)
272260
cvColorMap[0] = [0, 0, 0]
273261
print("Creating DepthAI device")

utilities/cam_test.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def socket_type_pair(arg):
6464
"E.g: -cams rgb,m right,c . Default: rgb,c left,m right,m camd,c")
6565
parser.add_argument('-mres', '--mono-resolution', type=int, default=800, choices={480, 400, 720, 800},
6666
help="Select mono camera resolution (height). Default: %(default)s")
67-
parser.add_argument('-cres', '--color-resolution', default='1080', choices={'720', '800', '1080', '1200', '4k', '5mp', '12mp', '48mp'},
67+
parser.add_argument('-cres', '--color-resolution', default='1080', choices={'720', '800', '1080', '1200', '4k', '5mp', '12mp', '13mp', '48mp'},
6868
help="Select color camera resolution / height. Default: %(default)s")
6969
parser.add_argument('-rot', '--rotate', const='all', choices={'all', 'rgb', 'mono'}, nargs="?",
7070
help="Which cameras to rotate 180 degrees. All if not filtered")
@@ -145,6 +145,7 @@ def socket_type_pair(arg):
145145
'4k': dai.ColorCameraProperties.SensorResolution.THE_4_K,
146146
'5mp': dai.ColorCameraProperties.SensorResolution.THE_5_MP,
147147
'12mp': dai.ColorCameraProperties.SensorResolution.THE_12_MP,
148+
'13mp': dai.ColorCameraProperties.SensorResolution.THE_13_MP,
148149
'48mp': dai.ColorCameraProperties.SensorResolution.THE_48_MP,
149150
}
150151

0 commit comments

Comments
 (0)