File size: 23,256 Bytes
a37f5d3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
import numpy as np
import cv2
import open3d as o3d
from scipy.spatial.transform import Rotation
import argparse
import os
 
class LiDARCameraCalibrator:
    def __init__(self, point_cloud_path, image_path, camera_matrix, dist_coeffs):
        """
        Initialize the calibrator with point cloud and image data
       
        Args:
            point_cloud_path (str): Path to LiDAR point cloud file (.pcd, .ply, .bin, etc.)
            image_path (str): Path to camera image
            camera_matrix (np.array): 3x3 camera intrinsic matrix
            dist_coeffs (np.array): Camera distortion coefficients
        """
        # Load point cloud
        self.points_3d = self.load_point_cloud(point_cloud_path)
       
        # Load image
        self.image = cv2.imread(image_path)
        self.original_image = self.image.copy()
        self.height, self.width = self.image.shape[:2]
       
        # Camera parameters
        self.camera_matrix = camera_matrix
        self.dist_coeffs = dist_coeffs
 
        # self.fixed_translation = np.array([0.676, 0.0, 1.486])
        # self.fixed_transform = np.eye(4)
        # self.fixed_transform[:3, 3] = self.fixed_translation
       
        # Initial transformation parameters (adjustable via trackbars)
        # tx=1.932+0.676, ty=0.25+0, tz=1.4+1.486, rx=0, ry=-2.0, rz=1.5
        self.tx = -1.16  # Translation X
        self.ty = -0.23  # Translation Y  
        self.tz = 0.42  # Translation Z
        self.rx = 0.0  # Rotation X (degrees)
        self.ry = 2.0  # Rotation Y (degrees)
        self.rz = -0.55  # Rotation Z (degrees)
       
        # Trackbar ranges (scaled by 1000 for precision)
        self.t_range = 50000  # ±50m in mm
        self.r_range = 18000  # ±180 degrees in 0.01 degree units
       
        # Setup OpenCV window and trackbars
        self.setup_gui()
   
    def load_point_cloud(self, file_path):
        """
        Load point cloud from various formats including .bin files
       
        Args:
            file_path (str): Path to point cloud file
           
        Returns:
            np.array: Nx3 array of 3D points
        """
        file_ext = os.path.splitext(file_path)[1].lower()
       
        if file_ext == '.bin':
            # Load binary point cloud (common format for KITTI, nuScenes, etc.)
            # Assumes format: [x, y, z, intensity] or [x, y, z, intensity, ring, time, etc.]
            points = np.fromfile(file_path, dtype=np.float32)
           
            # Determine the number of fields per point
            # Common formats: 4 (x,y,z,intensity), 5 (x,y,z,intensity,ring), 6+ (additional fields)
            if len(points) % 4 == 0:
                points = points.reshape(-1, 4)
                points_3d = points[:, :3]  # Take only x, y, z
                self.intensities = points[:, 3]  # Store intensity for potential use
                # print(self.intensities.max())
                # print(self.intensities.min())
                print(f"Loaded {len(points_3d)} points from .bin file (4 fields per point)")
            elif len(points) % 6 == 0:
                points = points.reshape(-1, 6)
                points_3d = points[:, :3]  # Take only x, y, z
                self.intensities = points[:, 3]  # Store intensity
                print(f"Loaded {len(points_3d)} points from .bin file (6 fields per point)")
            else:
                # Try to infer the structure or default to 3D points only
                # Assume the most common case where points are stored as [x,y,z,...]
                n_fields = 4  # Default assumption
                if len(points) % 3 == 0 and len(points) % 4 != 0:
                    n_fields = 3
               
                points = points.reshape(-1, n_fields)
                points_3d = points[:, :3]
                if n_fields > 3:
                    self.intensities = points[:, 3]
                else:
                    self.intensities = None
                print(f"Loaded {len(points_3d)} points from .bin file ({n_fields} fields per point)")
           
            return points_3d
           
        else:
            # Use Open3D for standard formats (.pcd, .ply, etc.)
            try:
                pcd = o3d.io.read_point_cloud(file_path)
                points_3d = np.asarray(pcd.points)
                self.intensities = None  # Open3D formats don't always have intensity
                print(f"Loaded {len(points_3d)} points using Open3D")
                return points_3d
            except Exception as e:
                raise ValueError(f"Could not load point cloud {file_path}: {str(e)}")
       
       
    def setup_gui(self):
        """Setup OpenCV window and trackbars for real-time adjustment"""
        cv2.namedWindow('LiDAR-Camera Calibration', cv2.WINDOW_NORMAL)
        cv2.resizeWindow('LiDAR-Camera Calibration', 1200, 800)
       
        # Create trackbars for translation (in mm for precision)
        cv2.createTrackbar('TX (mm)', 'LiDAR-Camera Calibration',
                          int(self.tx * 1000) + self.t_range, 2 * self.t_range, self.update_transform)
        cv2.createTrackbar('TY (mm)', 'LiDAR-Camera Calibration',
                          int(self.ty * 1000) + self.t_range, 2 * self.t_range, self.update_transform)
        cv2.createTrackbar('TZ (mm)', 'LiDAR-Camera Calibration',
                          int(self.tz * 1000) + self.t_range, 2 * self.t_range, self.update_transform)
       
        # Create trackbars for rotation (in 0.01 degree units)
        cv2.createTrackbar('RX (0.01°)', 'LiDAR-Camera Calibration',
                          int(self.rx * 100) + self.r_range, 2 * self.r_range, self.update_transform)
        cv2.createTrackbar('RY (0.01°)', 'LiDAR-Camera Calibration',
                          int(self.ry * 100) + self.r_range, 2 * self.r_range, self.update_transform)
        cv2.createTrackbar('RZ (0.01°)', 'LiDAR-Camera Calibration',
                          int(self.rz * 100) + self.r_range, 2 * self.r_range, self.update_transform)
       
        # Additional controls
        cv2.createTrackbar('Point Size', 'LiDAR-Camera Calibration', 2, 10, self.update_transform)
        cv2.createTrackbar('Max Distance (m)', 'LiDAR-Camera Calibration', 100, 200, self.update_transform)
        cv2.createTrackbar('Min Distance (m)', 'LiDAR-Camera Calibration', 0, 50, self.update_transform)
        cv2.createTrackbar('Intensity Filter', 'LiDAR-Camera Calibration', 0, 100, self.update_transform)
   
    def set_initial_values(self, tx=0, ty=0, tz=0, rx=0, ry=0, rz=0):
        """Set initial transformation values"""
        self.tx, self.ty, self.tz = tx, ty, tz
        self.rx, self.ry, self.rz = rx, ry, rz
       
        # Update trackbars
        cv2.setTrackbarPos('TX (mm)', 'LiDAR-Camera Calibration', int(tx * 1000) + self.t_range)
        cv2.setTrackbarPos('TY (mm)', 'LiDAR-Camera Calibration', int(ty * 1000) + self.t_range)
        cv2.setTrackbarPos('TZ (mm)', 'LiDAR-Camera Calibration', int(tz * 1000) + self.t_range)
        cv2.setTrackbarPos('RX (0.01°)', 'LiDAR-Camera Calibration', int(rx * 100) + self.r_range)
        cv2.setTrackbarPos('RY (0.01°)', 'LiDAR-Camera Calibration', int(ry * 100) + self.r_range)
        cv2.setTrackbarPos('RZ (0.01°)', 'LiDAR-Camera Calibration', int(rz * 100) + self.r_range)
   
    def update_transform(self, val):
        """Callback function for trackbar updates"""
        # Get current trackbar values
        self.tx = (cv2.getTrackbarPos('TX (mm)', 'LiDAR-Camera Calibration') - self.t_range) / 1000.0
        self.ty = (cv2.getTrackbarPos('TY (mm)', 'LiDAR-Camera Calibration') - self.t_range) / 1000.0
        self.tz = (cv2.getTrackbarPos('TZ (mm)', 'LiDAR-Camera Calibration') - self.t_range) / 1000.0
        self.rx = (cv2.getTrackbarPos('RX (0.01°)', 'LiDAR-Camera Calibration') - self.r_range) / 100.0
        self.ry = (cv2.getTrackbarPos('RY (0.01°)', 'LiDAR-Camera Calibration') - self.r_range) / 100.0
        self.rz = (cv2.getTrackbarPos('RZ (0.01°)', 'LiDAR-Camera Calibration') - self.r_range) / 100.0
       
        # Update visualization
        self.visualize_projection()
   
    def get_transformation_matrix(self):
        """Calculate 4x4 transformation matrix from current parameters"""
        # Create rotation matrix from Euler angles (XYZ order)
        rotation = Rotation.from_euler('xyz', [self.rx, self.ry, self.rz], degrees=True)
        # R = rotation.as_matrix()
 
        coord_transform = np.array([
            [0, -1, 0],
            [0, 0, -1],
            [1, 0, 0]
        ])
 
        R = rotation.as_matrix() #@ coord_transform
       
        # Create translation vector
        t = np.array([self.tx, self.ty, self.tz])
       
        # Build 4x4 transformation matrix
        variable_T = np.eye(4)
        variable_T[:3, :3] = R
        variable_T[:3, 3] = t
 
        # T = variable_T @ self.fixed_transform
        T = variable_T
 
        # Convert combined transform to camera coordinate frame
        C4 = np.eye(4)
        C4[:3, :3] = coord_transform   # maps robot -> camera
        T_camera = C4 @ T
       
        return T_camera
   
    def project_points(self):
        """Project 3D LiDAR points to image coordinates"""
        if len(self.points_3d) == 0:
            return np.array([]), np.array([])
       
        # Apply transformation
        T = self.get_transformation_matrix()
        points_homo = np.column_stack([self.points_3d, np.ones(len(self.points_3d))])
        transformed_points = (T @ points_homo.T).T[:, :3]
       
        # Filter points that are in front of camera
        valid_mask = transformed_points[:, 2] > 0
        valid_points = transformed_points[valid_mask]
        valid_inten = self.intensities[valid_mask]
       
        if len(valid_points) == 0:
            return np.array([]), np.array([])
       
        # Filter by distance
        max_dist = cv2.getTrackbarPos('Max Distance (m)', 'LiDAR-Camera Calibration')
        min_dist = cv2.getTrackbarPos('Min Distance (m)', 'LiDAR-Camera Calibration')
        distances = np.linalg.norm(valid_points, axis=1)
        dist_mask = (distances >= min_dist) & (distances <= max_dist)
        valid_points = valid_points[dist_mask]
        valid_inten = valid_inten[dist_mask]
        valid_distances = distances[dist_mask]
       
        # Filter by intensity if available
        if hasattr(self, 'intensities') and self.intensities is not None:
            intensity_threshold = cv2.getTrackbarPos('Intensity Filter', 'LiDAR-Camera Calibration') / 100.0
            # Apply original valid_mask and dist_mask to intensities
            original_valid_mask = np.arange(len(self.points_3d))[valid_mask[:len(valid_mask)]][:len(valid_points)]
            if len(original_valid_mask) > 0 and len(original_valid_mask) <= len(self.intensities):
                valid_intensities = self.intensities[original_valid_mask]
                # Normalize intensities to 0-1 range
                if len(valid_intensities) > 0:
                    intensity_norm = (valid_intensities - np.min(valid_intensities)) / (np.max(valid_intensities) - np.min(valid_intensities) + 1e-8)
                    intensity_mask = intensity_norm >= intensity_threshold
                    valid_points = valid_points[intensity_mask]
                    valid_inten = valid_inten[intensity_mask]
                    valid_distances = valid_distances[intensity_mask]
       
        if len(valid_points) == 0:
            return np.array([]), np.array([])
       
        # Project to image coordinates
        rvec = np.zeros(3)  # No additional rotation
        tvec = np.zeros(3)  # No additional translation
       
        image_points, _ = cv2.projectPoints(
            valid_points, rvec, tvec, self.camera_matrix, self.dist_coeffs
        )
       
        image_points = image_points.reshape(-1, 2)
       
        # Filter points within image bounds
        h, w = self.height, self.width
        valid_img_mask = ((image_points[:, 0] >= 0) & (image_points[:, 0] < w) &
                         (image_points[:, 1] >= 0) & (image_points[:, 1] < h))
       
        return image_points[valid_img_mask], valid_distances[valid_img_mask], valid_inten[valid_img_mask]
   
    def visualize_projection(self):
        """Update the visualization with current transformation"""
        # Reset image
        self.image = self.original_image.copy()
       
        # Project points
        projected_points, distances, intensities = self.project_points()
       
        if len(projected_points) == 0:
            cv2.putText(self.image, "No valid points to display", (50, 50),
                       cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
        else:
            # Color points by distance (closer = red, farther = blue)
            max_dist = cv2.getTrackbarPos('Max Distance (m)', 'LiDAR-Camera Calibration')
            normalized_dist = distances.clip(0, 100) / min(max_dist,distances.max())
            normalized_intensity = intensities/255
           
            point_size = cv2.getTrackbarPos('Point Size', 'LiDAR-Camera Calibration')
            if point_size == 0:
                point_size = 1
               
            # for i, (point, dist) in enumerate(zip(projected_points, normalized_dist)):
            for i, (point, dist) in enumerate(zip(projected_points, normalized_intensity)):
                # Color from red (close) to blue (far)
                color = (255 * (1 - dist), 0, 255 * dist)
                cv2.circle(self.image, (int(point[0]), int(point[1])),
                          point_size, color, -1)
       
        # Add parameter information
        info_text = [
            f"TX: {self.tx:.3f}m  TY: {self.ty:.3f}m  TZ: {self.tz:.3f}m",
            f"RX: {self.rx:.2f}°  RY: {self.ry:.2f}°  RZ: {self.rz:.2f}°",
            f"Projected points: {len(projected_points)}",
            "Press 'q' to quit, 's' to save parameters, 'r' to reset"
        ]
       
        for i, text in enumerate(info_text):
            cv2.putText(self.image, text, (10, 30 + i * 25),
                       cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2, cv2.LINE_AA)
            cv2.putText(self.image, text, (10, 30 + i * 25),
                       cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 1, cv2.LINE_AA)
       
        cv2.imshow('LiDAR-Camera Calibration', self.image)
   
    def save_parameters(self, filename="calibration_params.txt"):
        """Save current transformation parameters to file"""
        T = self.get_transformation_matrix()
       
        with open(filename, 'w') as f:
            f.write("LiDAR-Camera Calibration Parameters\n")
            f.write("=" * 40 + "\n\n")
            f.write(f"Translation (meters):\n")
            f.write(f"  TX: {self.tx:.6f}\n")
            f.write(f"  TY: {self.ty:.6f}\n")
            f.write(f"  TZ: {self.tz:.6f}\n\n")
            f.write(f"Rotation (degrees):\n")
            f.write(f"  RX: {self.rx:.6f}\n")
            f.write(f"  RY: {self.ry:.6f}\n")
            f.write(f"  RZ: {self.rz:.6f}\n\n")
            f.write("4x4 Transformation Matrix:\n")
            f.write(str(T) + "\n")
       
        print(f"Parameters saved to {filename}")
   
    def run(self):
        """Start the interactive calibration tool"""
        print("LiDAR-Camera Calibration Tool")
        print("Use trackbars to adjust transformation parameters")
        print("Controls:")
        print("  'q' - Quit")
        print("  's' - Save current parameters")
        print("  'r' - Reset to initial values")
       
        # Initial visualization
        self.visualize_projection()
       
        while True:
            key = cv2.waitKey(30) & 0xFF
           
            if key == ord('q'):
                break
            elif key == ord('s'):
                self.save_parameters()
            elif key == ord('r'):
                self.set_initial_values()
                self.visualize_projection()
       
        cv2.destroyAllWindows()
 
def create_camera_matrix(focal_length_mm, principal_point_x_pixels, principal_point_y_pixels,
                        pixels_per_mm_x, pixels_per_mm_y):
    """
    Create camera matrix from physical camera parameters.
   
    Args:
        focal_length_mm: Focal length in millimeters
        principal_point_x_pixels: Principal point X coordinate in pixels
        principal_point_y_pixels: Principal point Y coordinate in pixels
        pixels_per_mm_x: Pixels per millimeter in X direction
        pixels_per_mm_y: Pixels per millimeter in Y direction
       
    Returns:
        camera_matrix: 3x3 camera intrinsic matrix
    """
    # Debug: Check inputs
    # print(f"Input values:")
    # print(f"  focal_length_mm: {focal_length_mm}")
    # print(f"  principal_point_x_pixels: {principal_point_x_pixels}")
    # print(f"  principal_point_y_pixels: {principal_point_y_pixels}")
    # print(f"  pixels_per_mm_x: {pixels_per_mm_x}")
    # print(f"  pixels_per_mm_y: {pixels_per_mm_y}")
   
    # Check for None or invalid values
    if any(x is None for x in [focal_length_mm, principal_point_x_pixels, principal_point_y_pixels,
                               pixels_per_mm_x, pixels_per_mm_y]):
        print("ERROR: One or more input parameters is None!")
        return None
   
    if pixels_per_mm_x == 0 or pixels_per_mm_y == 0:
        print("ERROR: pixels_per_mm cannot be zero!")
        return None
   
    try:
        # Convert focal length from mm to pixels
        fx = focal_length_mm * pixels_per_mm_x
        fy = focal_length_mm * pixels_per_mm_y
       
        # Principal point is already in pixels
        cx = principal_point_x_pixels
        cy = principal_point_y_pixels
       
        # print(f"DEBUG: Camera matrix calculation:")
        # print(f"  fx = {focal_length_mm} * {pixels_per_mm_x} = {fx}")
        # print(f"  fy = {focal_length_mm} * {pixels_per_mm_y} = {fy}")
        # print(f"  cx = {cx}")
        # print(f"  cy = {cy}")
       
        camera_matrix = np.array([
            [fx,  0,  cx],
            [0,  fy,  cy],
            [0,   0,   1]
        ], dtype=np.float64)
       
        # print(f"DEBUG: Final camera matrix:\n{camera_matrix}")
       
        return camera_matrix
       
    except Exception as e:
        print(f"ERROR in create_camera_matrix: {e}")
        return None
 
def create_sample_data():
    """Create sample point cloud and camera parameters for testing"""
    # Generate sample point cloud (random points in a cube)
    np.random.seed(42)
    n_points = 1000
    points = np.random.uniform(-10, 10, (n_points, 3))
    points[:, 2] += 20  # Move points in front of camera
   
    # Add intensity values
    intensities = np.random.uniform(0, 255, n_points)
   
    # Create .bin file (KITTI format: x, y, z, intensity)
    bin_data = np.column_stack([points, intensities]).astype(np.float32)
    bin_data.tofile("sample_pointcloud.bin")
   
    # Also create .pcd for compatibility
    pcd = o3d.geometry.PointCloud()
    pcd.points = o3d.utility.Vector3dVector(points)
    o3d.io.write_point_cloud("sample_pointcloud.pcd", pcd)
   
    # Create sample camera matrix (typical values)
 
    focal_length_mm = 12.0            # Your focal length in mm
    principal_point_x_pixels = 960    # Principal point X in pixels
    principal_point_y_pixels = 600    # Principal point Y in pixels  
    pixels_per_mm_x = 170.648           # Your pixels per mm in X
    pixels_per_mm_y = 170.648           # Your pixels per mm in Y
   
    # Create camera matrix
    camera_matrix = create_camera_matrix(
        focal_length_mm, principal_point_x_pixels, principal_point_y_pixels,
        pixels_per_mm_x, pixels_per_mm_y
    )
    # camera_matrix = np.array([
    #     [800, 0, 320],
    #     [0, 800, 240],
    #     [0, 0, 1]
    # ], dtype=np.float32)
   
    # Sample distortion coefficients
    # dist_coeffs = np.array([0.1, -0.2, 0, 0, 0.1], dtype=np.float32)
    dist_coeffs = np.array([0.0, 0.0, 0.0, 0.0, 0.0])
   
    # Create a simple test image
    test_image = np.zeros((1200, 1920, 3), dtype=np.uint8)
    cv2.putText(test_image, "Sample Camera Image", (150, 240),
                cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
    cv2.imwrite("sample_image.jpg", test_image)
   
    return camera_matrix, dist_coeffs
 
 
def main():
    parser = argparse.ArgumentParser(description='LiDAR-Camera Calibration Tool')
    parser.add_argument('--pcd', type=str, help='Path to point cloud file (.pcd, .ply, .bin, etc.)')
    parser.add_argument('--image', type=str, help='Path to camera image')
    parser.add_argument('--demo', action='store_true', help='Run with sample data')
   
    args = parser.parse_args()
   
    if args.demo or (not args.pcd or not args.image):
        print("Creating sample data for demonstration...")
        camera_matrix, dist_coeffs = create_sample_data()
        pcd_path = "sample_pointcloud.bin"  # Use .bin file for demo
        img_path = "sample_image.jpg"
    else:
        pcd_path = args.pcd
        img_path = args.image
       
        # You'll need to provide your camera calibration parameters here
        focal_length_mm = 12.0            # Your focal length in mm
        principal_point_x_pixels = 960    # Principal point X in pixels
        principal_point_y_pixels = 600    # Principal point Y in pixels  
        pixels_per_mm_x = 170.648           # Your pixels per mm in X
        pixels_per_mm_y = 170.648           # Your pixels per mm in Y
       
        # Create camera matrix
        camera_matrix = create_camera_matrix(
            focal_length_mm, principal_point_x_pixels, principal_point_y_pixels,
            pixels_per_mm_x, pixels_per_mm_y
        )
        # camera_matrix = np.array([
        #     [800, 0, 320],
        #     [0, 800, 240],
        #     [0, 0, 1]
        # ], dtype=np.float32)
       
        # Sample distortion coefficients
        # dist_coeffs = np.array([0.1, -0.2, 0, 0, 0.1], dtype=np.float32)
        dist_coeffs = np.array([0.0, 0.0, 0.0, 0.0, 0.0])
   
    # Check if files exist
    if not os.path.exists(pcd_path):
        print(f"Error: Point cloud file {pcd_path} not found")
        return
   
    if not os.path.exists(img_path):
        print(f"Error: Image file {img_path} not found")
        return
   
    # Initialize calibrator
    calibrator = LiDARCameraCalibrator(pcd_path, img_path, camera_matrix, dist_coeffs)
   
    # Set initial transformation values (modify these as needed)
    # calibrator.set_initial_values(tx=1.932, ty=0, tz=1.4, rx=0, ry=-2.0, rz=1.5)
    # calibrator.set_initial_values(tx=1.932, ty=0.25, tz=1.4, rx=0.0, ry=-2.0, rz=1.5)
    calibrator.set_initial_values(tx=-1.256, ty=-0.1, tz=0.5, rx=0.0, ry=2.2, rz=-0.55)
    # calibrator.set_initial_values(tx=-1.16, ty=-0.23, tz=0.42, rx=0.0, ry=2.0, rz=-0.55)
    # calibrator.set_initial_values(tx=1.932, ty=0.25, tz=1.486, rx=0, ry=-2.0, rz=1.5)
    # calibrator.set_initial_values(tx=0.23, ty=-0.42, tz=-1.16, rx=-2.5, ry=0.46, rz=-0.7)
    # calibrator.set_initial_values(tx=0.192, ty=-0.641, tz=-1.474, rx=-2.51, ry=0.46, rz=-0.69)
   
    # Run the calibration tool
    calibrator.run()
 
 
if __name__ == "__main__":
    main(