import json import os from typing import Any import cv2 import numpy as np from jsonargparse import auto_cli import supervision as sv KEY_ENTER = 13 KEY_NEWLINE = 10 KEY_ESCAPE = 27 KEY_QUIT = ord("q") KEY_SAVE = ord("s") THICKNESS = 2 COLORS = sv.ColorPalette.DEFAULT WINDOW_NAME = "Draw Zones" POLYGONS = [[]] current_mouse_position: tuple[int, int] | None = None def resolve_source(source_path: str) -> np.ndarray | None: if not os.path.exists(source_path): return None image = cv2.imread(source_path) if image is not None: return image frame_generator = sv.get_video_frames_generator(source_path=source_path) frame = next(frame_generator) return frame def mouse_event(event: int, x: int, y: int, flags: int, param: Any) -> None: global current_mouse_position if event == cv2.EVENT_MOUSEMOVE: current_mouse_position = (x, y) elif event == cv2.EVENT_LBUTTONDOWN: POLYGONS[-1].append((x, y)) def redraw(image: np.ndarray, original_image: np.ndarray) -> None: global POLYGONS, current_mouse_position image[:] = original_image.copy() for idx, polygon in enumerate(POLYGONS): color = ( COLORS.by_idx(idx).as_bgr() if idx < len(POLYGONS) - 1 else sv.Color.WHITE.as_bgr() ) if len(polygon) > 1: for i in range(1, len(polygon)): cv2.line( img=image, pt1=polygon[i - 1], pt2=polygon[i], color=color, thickness=THICKNESS, ) if idx < len(POLYGONS) - 1: cv2.line( img=image, pt1=polygon[-1], pt2=polygon[0], color=color, thickness=THICKNESS, ) if idx == len(POLYGONS) - 1 and current_mouse_position is not None and polygon: cv2.line( img=image, pt1=polygon[-1], pt2=current_mouse_position, color=color, thickness=THICKNESS, ) cv2.imshow(WINDOW_NAME, image) def close_and_finalize_polygon(image: np.ndarray, original_image: np.ndarray) -> None: if len(POLYGONS[-1]) > 2: cv2.line( img=image, pt1=POLYGONS[-1][-1], pt2=POLYGONS[-1][0], color=COLORS.by_idx(0).as_bgr(), thickness=THICKNESS, ) POLYGONS.append([]) image[:] = original_image.copy() redraw_polygons(image) cv2.imshow(WINDOW_NAME, image) def redraw_polygons(image: np.ndarray) -> None: for idx, polygon in enumerate(POLYGONS[:-1]): if len(polygon) > 1: color = COLORS.by_idx(idx).as_bgr() for i in range(len(polygon) - 1): cv2.line( img=image, pt1=polygon[i], pt2=polygon[i + 1], color=color, thickness=THICKNESS, ) cv2.line( img=image, pt1=polygon[-1], pt2=polygon[0], color=color, thickness=THICKNESS, ) def save_polygons_to_json( polygons: list[list[tuple[int, int]]], target_path: str | os.PathLike[str] ) -> None: data_to_save = polygons if polygons[-1] else polygons[:-1] with open(target_path, "w") as f: json.dump(data_to_save, f) def main(source_path: str, zone_configuration_path: str) -> None: """ Interactively draw polygons on images or video frames and save the annotations. Args: source_path: Path to the source image or video file for drawing polygons. zone_configuration_path: Path where the polygon annotations saved as JSON file. """ global current_mouse_position original_image = resolve_source(source_path=source_path) if original_image is None: print("Failed to load source image.") return image = original_image.copy() cv2.imshow(WINDOW_NAME, image) cv2.setMouseCallback(WINDOW_NAME, mouse_event, image) while True: key = cv2.waitKey(1) & 0xFF if key == KEY_ENTER or key == KEY_NEWLINE: close_and_finalize_polygon(image, original_image) elif key == KEY_ESCAPE: POLYGONS[-1] = [] current_mouse_position = None elif key == KEY_SAVE: save_polygons_to_json(POLYGONS, zone_configuration_path) print(f"Polygons saved to {zone_configuration_path}") break redraw(image, original_image) if key == KEY_QUIT: break cv2.destroyAllWindows() if __name__ == "__main__": from jsonargparse import auto_cli, set_parsing_settings set_parsing_settings(parse_optionals_as_positionals=True) auto_cli(main, as_positional=False)