Skip to content

Commit 54c9fdf

Browse files
authored
Fix: Perspective distortion numerical stability (#1793)
* Fix numerical instability issue in undistortPerspective point * Add single cam remapping example * Add const to var declaration and use CamelCase in example * Add cpp example
1 parent 29a9bd6 commit 54c9fdf

5 files changed

Lines changed: 402 additions & 31 deletions

File tree

examples/cpp/Remapping/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@ cmake_minimum_required(VERSION 3.10)
55
## function: dai_set_example_test_labels(example_name ...)
66

77
dai_add_example(point_remapping point_remapping.cpp ON OFF)
8+
dai_add_example(single_cam_remapping single_cam_remapping.cpp ON OFF)
9+
810
dai_set_example_test_labels(point_remapping ondevice rvc2_all rvc4 ci)
11+
dai_set_example_test_labels(single_cam_remapping ondevice rvc2_all rvc4 ci)
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#include <atomic>
2+
#include <cmath>
3+
#include <csignal>
4+
#include <iomanip>
5+
#include <iostream>
6+
#include <opencv2/opencv.hpp>
7+
#include <optional>
8+
#include <sstream>
9+
#include <string>
10+
#include <utility>
11+
#include <vector>
12+
13+
#include "depthai/depthai.hpp"
14+
15+
const std::string DISTORTED_WINDOW = "CAM_A distorted 640x480";
16+
const std::string UNDISTORTED_WINDOW = "CAM_A undistorted 1000x400";
17+
const std::pair<uint32_t, uint32_t> DISTORTED_SIZE = {640, 480};
18+
const std::pair<uint32_t, uint32_t> UNDISTORTED_SIZE = {1000, 400};
19+
20+
std::atomic<bool> quitEvent(false);
21+
std::optional<cv::Point> selectedPoint = std::nullopt;
22+
23+
void signalHandler(int) {
24+
quitEvent = true;
25+
}
26+
27+
void onLeftClick(int event, int x, int y, int flags, void* param) {
28+
(void)flags;
29+
(void)param;
30+
31+
if(event == cv::EVENT_LBUTTONDOWN) {
32+
selectedPoint = cv::Point(x, y);
33+
}
34+
}
35+
36+
cv::Mat toColorFrame(const cv::Mat& frame) {
37+
if(frame.channels() == 3) {
38+
return frame;
39+
}
40+
41+
cv::Mat colorFrame;
42+
cv::cvtColor(frame, colorFrame, cv::COLOR_GRAY2BGR);
43+
return colorFrame;
44+
}
45+
46+
void addStatusLines(cv::Mat& frame, const std::vector<std::string>& lines, const cv::Scalar& color = cv::Scalar(255, 0, 255)) {
47+
for(size_t index = 0; index < lines.size(); ++index) {
48+
cv::putText(frame,
49+
lines[index],
50+
cv::Point(10, 28 + static_cast<int>(index) * 24),
51+
cv::FONT_HERSHEY_SIMPLEX,
52+
0.65,
53+
color,
54+
2,
55+
cv::LINE_AA);
56+
}
57+
}
58+
59+
bool isInsideFrame(const dai::Point2f& point, const cv::Mat& frame) {
60+
return 0 <= point.x && point.x < frame.cols && 0 <= point.y && point.y < frame.rows;
61+
}
62+
63+
void drawPoint(cv::Mat& frame, const std::optional<dai::Point2f>& point, const cv::Scalar& color) {
64+
if(!point.has_value() || !isInsideFrame(*point, frame)) {
65+
return;
66+
}
67+
68+
const cv::Point intPoint(static_cast<int>(std::lround(point->x)), static_cast<int>(std::lround(point->y)));
69+
if(intPoint.x < 0 || intPoint.x >= frame.cols || intPoint.y < 0 || intPoint.y >= frame.rows) {
70+
return;
71+
}
72+
73+
cv::drawMarker(frame, intPoint, color, cv::MARKER_CROSS, 16, 2);
74+
cv::circle(frame, intPoint, 6, color, 1);
75+
}
76+
77+
std::string formatPointStatus(const std::string& prefix, const dai::Point2f& point) {
78+
std::ostringstream stream;
79+
stream << std::fixed << std::setprecision(1) << prefix << ": (" << point.x << ", " << point.y << ")";
80+
return stream.str();
81+
}
82+
83+
int main() {
84+
signal(SIGTERM, signalHandler);
85+
signal(SIGINT, signalHandler);
86+
87+
dai::Pipeline pipeline;
88+
89+
auto camera = pipeline.create<dai::node::Camera>()->build(dai::CameraBoardSocket::CAM_A);
90+
auto* distorted = camera->requestOutput(DISTORTED_SIZE, std::nullopt, dai::ImgResizeMode::CROP, std::nullopt, false);
91+
auto* undistorted = camera->requestOutput(UNDISTORTED_SIZE, std::nullopt, dai::ImgResizeMode::CROP, std::nullopt, true);
92+
93+
auto distQueue = distorted->createOutputQueue();
94+
auto undistQueue = undistorted->createOutputQueue();
95+
96+
cv::namedWindow(DISTORTED_WINDOW);
97+
cv::namedWindow(UNDISTORTED_WINDOW);
98+
cv::setMouseCallback(DISTORTED_WINDOW, onLeftClick);
99+
100+
std::cout << "Left click in the distorted CAM_A window to remap a point to the undistorted output." << std::endl;
101+
std::cout << "Press 'c' to clear the point and 'q' to quit." << std::endl;
102+
103+
pipeline.start();
104+
while(pipeline.isRunning() && !quitEvent) {
105+
auto distortedFrame = distQueue->get<dai::ImgFrame>();
106+
auto undistortedFrame = undistQueue->get<dai::ImgFrame>();
107+
108+
if(distortedFrame == nullptr || undistortedFrame == nullptr) {
109+
continue;
110+
}
111+
112+
if(!distortedFrame->validateTransformations() || !undistortedFrame->validateTransformations()) {
113+
std::cerr << "Invalid transformations!" << std::endl;
114+
continue;
115+
}
116+
117+
auto& distortedTransform = distortedFrame->getTransformation();
118+
auto& undistortedTransform = undistortedFrame->getTransformation();
119+
120+
cv::Mat distortedView = toColorFrame(distortedFrame->getCvFrame());
121+
cv::Mat undistortedView = toColorFrame(undistortedFrame->getCvFrame());
122+
123+
std::optional<dai::Point2f> sourcePoint = std::nullopt;
124+
std::optional<dai::Point2f> remappedPoint = std::nullopt;
125+
std::string sourceStatus = "Click in this window to select a point";
126+
std::string targetStatus = "Waiting for a selected point";
127+
128+
if(selectedPoint.has_value()) {
129+
sourcePoint = dai::Point2f(static_cast<float>(selectedPoint->x), static_cast<float>(selectedPoint->y));
130+
remappedPoint = distortedTransform.remapPointTo(undistortedTransform, *sourcePoint);
131+
sourceStatus = formatPointStatus("Source", *sourcePoint);
132+
targetStatus = formatPointStatus("Remapped", *remappedPoint);
133+
if(!isInsideFrame(*remappedPoint, undistortedView)) {
134+
targetStatus += " outside target frame";
135+
}
136+
}
137+
138+
drawPoint(distortedView, sourcePoint, cv::Scalar(0, 255, 0));
139+
drawPoint(undistortedView, remappedPoint, cv::Scalar(0, 255, 255));
140+
141+
addStatusLines(distortedView, {"Distorted CAM_A output", sourceStatus, "Press c to clear, q to quit"});
142+
addStatusLines(undistortedView,
143+
{"Undistorted CAM_A output",
144+
targetStatus,
145+
"Target size: " + std::to_string(UNDISTORTED_SIZE.first) + "x" + std::to_string(UNDISTORTED_SIZE.second)});
146+
147+
cv::imshow(DISTORTED_WINDOW, distortedView);
148+
cv::imshow(UNDISTORTED_WINDOW, undistortedView);
149+
150+
const int key = cv::waitKey(1);
151+
if(key == 'q') {
152+
pipeline.stop();
153+
break;
154+
}
155+
if(key == 'c') {
156+
selectedPoint = std::nullopt;
157+
}
158+
}
159+
160+
pipeline.stop();
161+
pipeline.wait();
162+
return 0;
163+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
#!/usr/bin/env python3
2+
3+
import cv2
4+
import depthai as dai
5+
6+
DISTORTED_WINDOW = "CAM_A distorted 640x480"
7+
UNDISTORTED_WINDOW = "CAM_A undistorted 1000x400"
8+
DISTORTED_SIZE = (640, 480)
9+
UNDISTORTED_SIZE = (1000, 400)
10+
11+
selectedPoint = None
12+
13+
14+
def onLeftClick(event, x, y, flags, param):
15+
del flags, param
16+
global selectedPoint
17+
if event == cv2.EVENT_LBUTTONDOWN:
18+
selectedPoint = (x, y)
19+
20+
21+
def toColorFrame(frame):
22+
if len(frame.shape) == 3 and frame.shape[2] == 3:
23+
return frame
24+
return cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR)
25+
26+
27+
def addStatusLines(frame, lines, color=(255, 0, 255)):
28+
for index, line in enumerate(lines):
29+
cv2.putText(
30+
frame,
31+
line,
32+
(10, 28 + index * 24),
33+
cv2.FONT_HERSHEY_SIMPLEX,
34+
0.65,
35+
color,
36+
2,
37+
cv2.LINE_AA,
38+
)
39+
40+
41+
def isInsideFrame(point, frame):
42+
height, width = frame.shape[:2]
43+
return 0 <= point.x < width and 0 <= point.y < height
44+
45+
46+
def drawPoint(frame, point, color):
47+
if point is None or not isInsideFrame(point, frame):
48+
return
49+
intPoint = (int(round(point.x)), int(round(point.y)))
50+
cv2.drawMarker(frame, intPoint, color, markerType=cv2.MARKER_CROSS, markerSize=16, thickness=2)
51+
cv2.circle(frame, intPoint, 6, color, 1)
52+
53+
54+
if __name__ == "__main__":
55+
with dai.Pipeline() as pipeline:
56+
camera = pipeline.create(dai.node.Camera).build(dai.CameraBoardSocket.CAM_A)
57+
distorted = camera.requestOutput(DISTORTED_SIZE, resizeMode=dai.ImgResizeMode.CROP, enableUndistortion=False)
58+
undistorted = camera.requestOutput(UNDISTORTED_SIZE, resizeMode=dai.ImgResizeMode.CROP, enableUndistortion=True)
59+
60+
distQueue = distorted.createOutputQueue()
61+
undistQueue = undistorted.createOutputQueue()
62+
63+
cv2.namedWindow(DISTORTED_WINDOW)
64+
cv2.namedWindow(UNDISTORTED_WINDOW)
65+
cv2.setMouseCallback(DISTORTED_WINDOW, onLeftClick)
66+
67+
print("Left click in the distorted CAM_A window to remap a point to the undistorted output.")
68+
print("Press 'c' to clear the point and 'q' to quit.")
69+
70+
pipeline.start()
71+
while pipeline.isRunning():
72+
distortedFrame = distQueue.get()
73+
undistortedFrame = undistQueue.get()
74+
assert isinstance(distortedFrame, dai.ImgFrame)
75+
assert isinstance(undistortedFrame, dai.ImgFrame)
76+
assert distortedFrame.validateTransformations()
77+
assert undistortedFrame.validateTransformations()
78+
79+
distortedTransform = distortedFrame.getTransformation()
80+
undistortedTransform = undistortedFrame.getTransformation()
81+
82+
distortedView = toColorFrame(distortedFrame.getCvFrame())
83+
undistortedView = toColorFrame(undistortedFrame.getCvFrame())
84+
85+
sourcePoint = None
86+
remappedPoint = None
87+
sourceStatus = "Click in this window to select a point"
88+
targetStatus = "Waiting for a selected point"
89+
90+
if selectedPoint is not None:
91+
sourcePoint = dai.Point2f(float(selectedPoint[0]), float(selectedPoint[1]))
92+
remappedPoint = distortedTransform.remapPointTo(undistortedTransform, sourcePoint)
93+
sourceStatus = f"Source: ({sourcePoint.x:.1f}, {sourcePoint.y:.1f})"
94+
targetStatus = f"Remapped: ({remappedPoint.x:.1f}, {remappedPoint.y:.1f})"
95+
if not isInsideFrame(remappedPoint, undistortedView):
96+
targetStatus += " outside target frame"
97+
98+
99+
drawPoint(distortedView, sourcePoint, (0, 255, 0))
100+
drawPoint(undistortedView, remappedPoint, (0, 255, 255))
101+
102+
addStatusLines(
103+
distortedView,
104+
[
105+
"Distorted CAM_A output",
106+
sourceStatus,
107+
"Press c to clear, q to quit",
108+
],
109+
)
110+
addStatusLines(
111+
undistortedView,
112+
[
113+
"Undistorted CAM_A output",
114+
targetStatus,
115+
f"Target size: {UNDISTORTED_SIZE[0]}x{UNDISTORTED_SIZE[1]}",
116+
],
117+
)
118+
119+
cv2.imshow(DISTORTED_WINDOW, distortedView)
120+
cv2.imshow(UNDISTORTED_WINDOW, undistortedView)
121+
122+
key = cv2.waitKey(1)
123+
if key == ord("q"):
124+
break
125+
if key == ord("c"):
126+
selectedPoint = None

0 commit comments

Comments
 (0)