9194ef5abd
Docs/Test Workflow / Test docs build (push) Failing after 0s
Check links & references / links-check (push) Failing after 1s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.10) (push) Failing after 0s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.11) (push) Failing after 0s
PR Conflict Labeler / main (push) Failing after 2s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.12) (push) Failing after 2s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.13) (push) Failing after 0s
Pytest/Test Workflow / Build this Package (push) Failing after 5s
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.10) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.11) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.12) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.13) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.10) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.11) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.12) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.13) (push) Has been cancelled
Pytest/Test Workflow / testing-guardian (push) Has been cancelled
169 lines
4.8 KiB
Python
169 lines
4.8 KiB
Python
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)
|