|
| 1 | +import cv2 |
| 2 | + |
| 3 | +from lerobot.common.robot_devices.cameras.opencv import OpenCVCamera |
| 4 | + |
| 5 | + |
| 6 | +def select_square_roi(img): |
| 7 | + """ |
| 8 | + Allows the user to draw a square ROI on the image. |
| 9 | +
|
| 10 | + The user must click and drag to draw the square. |
| 11 | + - While dragging, the square is dynamically drawn. |
| 12 | + - On mouse button release, the square is fixed. |
| 13 | + - Press 'c' to confirm the selection. |
| 14 | + - Press 'r' to reset the selection. |
| 15 | + - Press ESC to cancel. |
| 16 | +
|
| 17 | + Returns: |
| 18 | + A tuple (top, left, height, width) representing the square ROI, |
| 19 | + or None if no valid ROI is selected. |
| 20 | + """ |
| 21 | + # Create a working copy of the image |
| 22 | + clone = img.copy() |
| 23 | + working_img = clone.copy() |
| 24 | + |
| 25 | + roi = None # Will store the final ROI as (top, left, side, side) |
| 26 | + drawing = False |
| 27 | + ix, iy = -1, -1 # Initial click coordinates |
| 28 | + |
| 29 | + def mouse_callback(event, x, y, flags, param): |
| 30 | + nonlocal ix, iy, drawing, roi, working_img |
| 31 | + |
| 32 | + if event == cv2.EVENT_LBUTTONDOWN: |
| 33 | + # Start drawing: record starting coordinates |
| 34 | + drawing = True |
| 35 | + ix, iy = x, y |
| 36 | + |
| 37 | + elif event == cv2.EVENT_MOUSEMOVE: |
| 38 | + if drawing: |
| 39 | + # Compute side length as the minimum of horizontal/vertical drags |
| 40 | + side = min(abs(x - ix), abs(y - iy)) |
| 41 | + # Determine the direction to draw (in case of dragging to top/left) |
| 42 | + dx = side if x >= ix else -side |
| 43 | + dy = side if y >= iy else -side |
| 44 | + # Show a temporary image with the current square drawn |
| 45 | + temp = working_img.copy() |
| 46 | + cv2.rectangle(temp, (ix, iy), (ix + dx, iy + dy), (0, 255, 0), 2) |
| 47 | + cv2.imshow("Select ROI", temp) |
| 48 | + |
| 49 | + elif event == cv2.EVENT_LBUTTONUP: |
| 50 | + # Finish drawing |
| 51 | + drawing = False |
| 52 | + side = min(abs(x - ix), abs(y - iy)) |
| 53 | + dx = side if x >= ix else -side |
| 54 | + dy = side if y >= iy else -side |
| 55 | + # Normalize coordinates: (top, left) is the minimum of the two points |
| 56 | + x1 = min(ix, ix + dx) |
| 57 | + y1 = min(iy, iy + dy) |
| 58 | + roi = (y1, x1, side, side) # (top, left, height, width) |
| 59 | + # Draw the final square on the working image and display it |
| 60 | + working_img = clone.copy() |
| 61 | + cv2.rectangle(working_img, (ix, iy), (ix + dx, iy + dy), (0, 255, 0), 2) |
| 62 | + cv2.imshow("Select ROI", working_img) |
| 63 | + |
| 64 | + # Create the window and set the callback |
| 65 | + cv2.namedWindow("Select ROI") |
| 66 | + cv2.setMouseCallback("Select ROI", mouse_callback) |
| 67 | + cv2.imshow("Select ROI", working_img) |
| 68 | + |
| 69 | + print("Instructions for ROI selection:") |
| 70 | + print(" - Click and drag to draw a square ROI.") |
| 71 | + print(" - Press 'c' to confirm the selection.") |
| 72 | + print(" - Press 'r' to reset and draw again.") |
| 73 | + print(" - Press ESC to cancel the selection.") |
| 74 | + |
| 75 | + # Wait until the user confirms with 'c', resets with 'r', or cancels with ESC |
| 76 | + while True: |
| 77 | + key = cv2.waitKey(1) & 0xFF |
| 78 | + # Confirm ROI if one has been drawn |
| 79 | + if key == ord("c") and roi is not None: |
| 80 | + break |
| 81 | + # Reset: clear the ROI and restore the original image |
| 82 | + elif key == ord("r"): |
| 83 | + working_img = clone.copy() |
| 84 | + roi = None |
| 85 | + cv2.imshow("Select ROI", working_img) |
| 86 | + # Cancel selection for this image |
| 87 | + elif key == 27: # ESC key |
| 88 | + roi = None |
| 89 | + break |
| 90 | + |
| 91 | + cv2.destroyWindow("Select ROI") |
| 92 | + return roi |
| 93 | + |
| 94 | + |
| 95 | +def select_square_roi_for_images(images: dict) -> dict: |
| 96 | + """ |
| 97 | + For each image in the provided dictionary, open a window to allow the user |
| 98 | + to select a square ROI. Returns a dictionary mapping each key to a tuple |
| 99 | + (top, left, height, width) representing the ROI. |
| 100 | +
|
| 101 | + Parameters: |
| 102 | + images (dict): Dictionary where keys are identifiers and values are OpenCV images. |
| 103 | +
|
| 104 | + Returns: |
| 105 | + dict: Mapping of image keys to the selected square ROI. |
| 106 | + """ |
| 107 | + selected_rois = {} |
| 108 | + |
| 109 | + for key, img in images.items(): |
| 110 | + if img is None: |
| 111 | + print(f"Image for key '{key}' is None, skipping.") |
| 112 | + continue |
| 113 | + |
| 114 | + print(f"\nSelect square ROI for image with key: '{key}'") |
| 115 | + roi = select_square_roi(img) |
| 116 | + |
| 117 | + if roi is None: |
| 118 | + print(f"No valid ROI selected for '{key}'.") |
| 119 | + else: |
| 120 | + selected_rois[key] = roi |
| 121 | + print(f"ROI for '{key}': {roi}") |
| 122 | + |
| 123 | + return selected_rois |
| 124 | + |
| 125 | + |
| 126 | +if __name__ == "__main__": |
| 127 | + # Example usage: |
| 128 | + # Replace 'image1.jpg' and 'image2.jpg' with valid paths to your image files. |
| 129 | + fps = [5, 30] |
| 130 | + cameras = [OpenCVCamera(i, fps=fps[i], width=640, height=480, mock=False) for i in range(2)] |
| 131 | + [camera.connect() for camera in cameras] |
| 132 | + |
| 133 | + image_keys = ["image_" + str(i) for i in range(len(cameras))] |
| 134 | + |
| 135 | + images = {image_keys[i]: cameras[i].read() for i in range(len(cameras))} |
| 136 | + |
| 137 | + # Verify images loaded correctly |
| 138 | + for key, img in images.items(): |
| 139 | + if img is None: |
| 140 | + raise ValueError(f"Failed to load image for key '{key}'. Check the file path.") |
| 141 | + |
| 142 | + # Let the user select a square ROI for each image |
| 143 | + rois = select_square_roi_for_images(images) |
| 144 | + |
| 145 | + # Print the selected square ROIs |
| 146 | + print("\nSelected Square Regions of Interest (top, left, height, width):") |
| 147 | + for key, roi in rois.items(): |
| 148 | + print(f"{key}: {roi}") |
0 commit comments