Skip to content

Commit c0febcc

Browse files
add possibility to rectify camera image
1 parent 1b0666d commit c0febcc

File tree

1 file changed

+134
-18
lines changed

1 file changed

+134
-18
lines changed

ros2_calib/calibration_widget.py

Lines changed: 134 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@
2929
from PySide6.QtGui import QBrush, QColor, QImage, QKeyEvent, QPainter, QPen, QPixmap
3030
from PySide6.QtWidgets import (
3131
QApplication,
32+
QCheckBox,
3233
QComboBox,
3334
QDoubleSpinBox,
3435
QFileDialog,
3536
QFormLayout,
3637
QGraphicsEllipseItem,
3738
QGraphicsItem,
39+
QGraphicsPixmapItem,
3840
QGraphicsScene,
3941
QGraphicsView,
4042
QGridLayout,
@@ -122,6 +124,10 @@ def __init__(self, image_msg, pointcloud_msg, camerainfo_msg, ros_utils, initial
122124
self.extrinsics = np.copy(self.initial_extrinsics)
123125
self.occlusion_mask = None
124126

127+
# Image rectification state
128+
self.original_cv_image = None
129+
self.is_rectification_enabled = False
130+
125131
self.selection_mode = None
126132
self.selected_2d_point = None
127133
self.temp_2d_marker = []
@@ -152,6 +158,48 @@ def __init__(self, image_msg, pointcloud_msg, camerainfo_msg, ros_utils, initial
152158
self._update_calibrate_button_highlight()
153159
self.display_camera_intrinsics()
154160

161+
162+
def has_significant_distortion(self):
163+
"""Check if camera has significant distortion coefficients."""
164+
if not hasattr(self.camerainfo_msg, 'd'):
165+
return False
166+
167+
# Convert distortion coefficients to numpy array
168+
dist_coeffs = np.array(self.camerainfo_msg.d)
169+
170+
# Check if the array is empty or all zeros
171+
if dist_coeffs.size == 0:
172+
return False
173+
174+
# Check if any distortion coefficient is significantly non-zero
175+
# Use a threshold to account for numerical precision
176+
threshold = 1e-6
177+
return np.any(np.abs(dist_coeffs) > threshold)
178+
179+
def toggle_rectification(self, enabled):
180+
"""Toggle image rectification on/off."""
181+
self.is_rectification_enabled = enabled
182+
self.display_image() # Refresh the display
183+
184+
def rectify_image(self, image):
185+
"""Apply camera undistortion to the image using cv2.undistort."""
186+
if not self.has_significant_distortion():
187+
return image
188+
189+
# Get camera matrix and distortion coefficients
190+
K = np.array(self.camerainfo_msg.k).reshape(3, 3)
191+
dist_coeffs = np.array(self.camerainfo_msg.d)
192+
193+
# Undistort the image
194+
try:
195+
# Use cv2.undistort with the same camera matrix as newCameraMatrix
196+
# This preserves the same image dimensions and focal length
197+
rectified_image = cv2.undistort(image, K, dist_coeffs, None, K)
198+
return rectified_image
199+
except Exception as e:
200+
print(f"[WARNING] Failed to rectify image: {e}")
201+
return image
202+
155203
def _setup_controls(self):
156204
right_layout = QHBoxLayout()
157205
col1_layout = QVBoxLayout()
@@ -183,6 +231,20 @@ def _setup_controls(self):
183231
self.max_value_spinbox.setRange(-1e9, 1e9)
184232
self.max_value_spinbox.setDecimals(2)
185233
view_controls_layout.addRow("Max Value:", self.max_value_spinbox)
234+
235+
# Image rectification checkbox
236+
self.rectify_checkbox = QCheckBox("Rectify Image")
237+
self.rectify_checkbox.setToolTip("Undistort the image using camera distortion parameters")
238+
# Only enable if distortion coefficients are available
239+
has_distortion = self.has_significant_distortion()
240+
self.rectify_checkbox.setEnabled(has_distortion)
241+
# Enable by default if distortion is detected
242+
if has_distortion:
243+
self.is_rectification_enabled = True
244+
self.rectify_checkbox.setChecked(True)
245+
self.rectify_checkbox.toggled.connect(self.toggle_rectification)
246+
view_controls_layout.addRow(self.rectify_checkbox)
247+
186248
self.apply_view_button = QPushButton("Apply View Changes")
187249
self.apply_view_button.clicked.connect(self.redraw_points)
188250
view_controls_layout.addRow(self.apply_view_button)
@@ -294,7 +356,8 @@ def _setup_controls(self):
294356
intrinsics_layout = QVBoxLayout(intrinsics_group)
295357

296358
self.intrinsics_display = QTextEdit()
297-
self.intrinsics_display.setMaximumHeight(300)
359+
self.intrinsics_display.setMinimumHeight(400)
360+
self.intrinsics_display.setMaximumHeight(600)
298361
self.intrinsics_display.setFont("monospace")
299362
self.intrinsics_display.setFontPointSize(10)
300363
self.intrinsics_display.setReadOnly(True)
@@ -557,42 +620,95 @@ def reset_selection_mode(self):
557620
self.view.setDragMode(QGraphicsView.ScrollHandDrag)
558621

559622
def display_image(self):
560-
if (
561-
hasattr(self.image_msg, "_type")
562-
and self.image_msg._type == "sensor_msgs/msg/CompressedImage"
563-
):
564-
np_arr = np.frombuffer(self.image_msg.data, np.uint8)
565-
self.cv_image = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
623+
# Decode/load the original image if not already done
624+
if self.original_cv_image is None:
625+
if (
626+
hasattr(self.image_msg, "_type")
627+
and self.image_msg._type == "sensor_msgs/msg/CompressedImage"
628+
):
629+
np_arr = np.frombuffer(self.image_msg.data, np.uint8)
630+
self.original_cv_image = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
631+
else:
632+
self.original_cv_image = self.ros_utils.image_to_numpy(self.image_msg)
633+
634+
# Convert BGR to RGB if needed
635+
if "bgr" in self.image_msg.encoding:
636+
self.original_cv_image = cv2.cvtColor(self.original_cv_image, cv2.COLOR_BGR2RGB)
637+
638+
# Apply rectification if enabled
639+
if self.is_rectification_enabled:
640+
# Convert back to BGR for OpenCV undistort function
641+
bgr_image = cv2.cvtColor(self.original_cv_image, cv2.COLOR_RGB2BGR)
642+
rectified_bgr = self.rectify_image(bgr_image)
643+
self.cv_image = cv2.cvtColor(rectified_bgr, cv2.COLOR_BGR2RGB)
566644
else:
567-
self.cv_image = self.ros_utils.image_to_numpy(self.image_msg)
568-
if "bgr" in self.image_msg.encoding:
569-
self.cv_image = cv2.cvtColor(self.cv_image, cv2.COLOR_BGR2RGB)
645+
self.cv_image = self.original_cv_image.copy()
646+
647+
# Update UI and display
570648
h, w, c = self.cv_image.shape
571649
self.image_res_label.setText(f"{w} x {h}")
650+
651+
# Clear existing image from scene but preserve other items
652+
items_to_preserve = []
653+
for item in self.scene.items():
654+
if not isinstance(item, QGraphicsPixmapItem):
655+
items_to_preserve.append(item)
656+
657+
# Clear the scene and add the new image
658+
self.scene.clear()
572659
q_image = QImage(self.cv_image.data, w, h, 3 * w, QImage.Format_RGB888)
573660
self.scene.addPixmap(QPixmap.fromImage(q_image))
574661

662+
# Restore preserved items
663+
for item in items_to_preserve:
664+
self.scene.addItem(item)
665+
666+
# Re-project point cloud with the updated image
667+
self.project_pointcloud()
668+
575669
def display_camera_intrinsics(self):
576670
"""Display the camera intrinsic matrix K."""
671+
# Add camera info
672+
display_text = (
673+
f"\nImage Size: {self.camerainfo_msg.width} x {self.camerainfo_msg.height}\n"
674+
)
675+
577676
K = np.array(self.camerainfo_msg.k).reshape(3, 3)
578677

579-
display_text = "Camera Matrix K:\n"
678+
display_text += "\nCamera Matrix K:\n"
580679
for i in range(3):
581680
row_text = " ".join(f"{K[i, j]:8.2f}" for j in range(3))
582681
display_text += f"[{row_text}]\n"
583682

584-
# Add camera info
585-
display_text += (
586-
f"\nImage Size: {self.camerainfo_msg.width} x {self.camerainfo_msg.height}\n"
587-
)
588-
display_text += f"Distortion: {self.camerainfo_msg.distortion_model}"
589-
590683
# Add focal length and principal point info
591684
fx, fy = K[0, 0], K[1, 1]
592685
cx, cy = K[0, 2], K[1, 2]
593-
display_text += f"\n\nFocal Length: fx={fx:.1f}, fy={fy:.1f}"
686+
display_text += f"\nFocal Length: fx={fx:.1f}, fy={fy:.1f}"
594687
display_text += f"\nPrincipal Point: cx={cx:.1f}, cy={cy:.1f}"
595688

689+
display_text += "\n"
690+
691+
692+
display_text += f"Distortion Model: {self.camerainfo_msg.distortion_model}"
693+
694+
# Add distortion coefficients
695+
if hasattr(self.camerainfo_msg, 'd') and len(self.camerainfo_msg.d) > 0:
696+
dist_coeffs = np.array(self.camerainfo_msg.d)
697+
display_text += "\nDistortion Coeffs: ["
698+
coeffs_str = ", ".join(f"{coeff:.6f}" for coeff in dist_coeffs)
699+
display_text += coeffs_str + "]"
700+
701+
# Add interpretation of common distortion models
702+
if len(dist_coeffs) >= 4:
703+
display_text += f"\n k1={dist_coeffs[0]:.6f}, k2={dist_coeffs[1]:.6f}"
704+
display_text += f"\n p1={dist_coeffs[2]:.6f}, p2={dist_coeffs[3]:.6f}"
705+
if len(dist_coeffs) >= 5:
706+
display_text += f", k3={dist_coeffs[4]:.6f}"
707+
if len(dist_coeffs) >= 8:
708+
display_text += f"\n k4={dist_coeffs[5]:.6f}, k5={dist_coeffs[6]:.6f}, k6={dist_coeffs[7]:.6f}"
709+
else:
710+
display_text += "\nDistortion Coeffs: None"
711+
596712
self.intrinsics_display.setPlainText(display_text)
597713

598714
def redraw_points(self):

0 commit comments

Comments
 (0)