177 lines
5.0 KiB
Python
177 lines
5.0 KiB
Python
import argparse
|
|
import json
|
|
import os
|
|
from typing import Any, Optional, Tuple
|
|
|
|
import cv2
|
|
import numpy as np
|
|
|
|
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: Optional[Tuple[int, int]] = None
|
|
|
|
|
|
def resolve_source(source_path: str) -> Optional[np.ndarray]:
|
|
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, target_path):
|
|
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:
|
|
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__":
|
|
parser = argparse.ArgumentParser(
|
|
description="Interactively draw polygons on images or video frames and save "
|
|
"the annotations."
|
|
)
|
|
parser.add_argument(
|
|
"--source_path",
|
|
type=str,
|
|
required=True,
|
|
help="Path to the source image or video file for drawing polygons.",
|
|
)
|
|
parser.add_argument(
|
|
"--zone_configuration_path",
|
|
type=str,
|
|
required=True,
|
|
help="Path where the polygon annotations will be saved as a JSON file.",
|
|
)
|
|
arguments = parser.parse_args()
|
|
main(
|
|
source_path=arguments.source_path,
|
|
zone_configuration_path=arguments.zone_configuration_path,
|
|
)
|