matrice-analytics 0.1.60__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matrice_analytics/__init__.py +28 -0
- matrice_analytics/boundary_drawing_internal/README.md +305 -0
- matrice_analytics/boundary_drawing_internal/__init__.py +45 -0
- matrice_analytics/boundary_drawing_internal/boundary_drawing_internal.py +1207 -0
- matrice_analytics/boundary_drawing_internal/boundary_drawing_tool.py +429 -0
- matrice_analytics/boundary_drawing_internal/boundary_tool_template.html +1036 -0
- matrice_analytics/boundary_drawing_internal/data/.gitignore +12 -0
- matrice_analytics/boundary_drawing_internal/example_usage.py +206 -0
- matrice_analytics/boundary_drawing_internal/usage/README.md +110 -0
- matrice_analytics/boundary_drawing_internal/usage/boundary_drawer_launcher.py +102 -0
- matrice_analytics/boundary_drawing_internal/usage/simple_boundary_launcher.py +107 -0
- matrice_analytics/post_processing/README.md +455 -0
- matrice_analytics/post_processing/__init__.py +732 -0
- matrice_analytics/post_processing/advanced_tracker/README.md +650 -0
- matrice_analytics/post_processing/advanced_tracker/__init__.py +17 -0
- matrice_analytics/post_processing/advanced_tracker/base.py +99 -0
- matrice_analytics/post_processing/advanced_tracker/config.py +77 -0
- matrice_analytics/post_processing/advanced_tracker/kalman_filter.py +370 -0
- matrice_analytics/post_processing/advanced_tracker/matching.py +195 -0
- matrice_analytics/post_processing/advanced_tracker/strack.py +230 -0
- matrice_analytics/post_processing/advanced_tracker/tracker.py +367 -0
- matrice_analytics/post_processing/config.py +146 -0
- matrice_analytics/post_processing/core/__init__.py +63 -0
- matrice_analytics/post_processing/core/base.py +704 -0
- matrice_analytics/post_processing/core/config.py +3291 -0
- matrice_analytics/post_processing/core/config_utils.py +925 -0
- matrice_analytics/post_processing/face_reg/__init__.py +43 -0
- matrice_analytics/post_processing/face_reg/compare_similarity.py +556 -0
- matrice_analytics/post_processing/face_reg/embedding_manager.py +950 -0
- matrice_analytics/post_processing/face_reg/face_recognition.py +2234 -0
- matrice_analytics/post_processing/face_reg/face_recognition_client.py +606 -0
- matrice_analytics/post_processing/face_reg/people_activity_logging.py +321 -0
- matrice_analytics/post_processing/ocr/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/easyocr_extractor.py +250 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/__init__.py +9 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/__init__.py +4 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/cli.py +33 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/dataset_stats.py +139 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/export.py +398 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/train.py +447 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/utils.py +129 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/valid.py +93 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/validate_dataset.py +240 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_augmentation.py +176 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_predictions.py +96 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/__init__.py +3 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/process.py +246 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/types.py +60 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/utils.py +87 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/__init__.py +3 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/config.py +82 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/hub.py +141 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/plate_recognizer.py +323 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/py.typed +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/augmentation.py +101 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/dataset.py +97 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/config.py +114 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/layers.py +553 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/loss.py +55 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/metric.py +86 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_builders.py +95 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_schema.py +395 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/backend_utils.py +38 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/utils.py +214 -0
- matrice_analytics/post_processing/ocr/postprocessing.py +270 -0
- matrice_analytics/post_processing/ocr/preprocessing.py +52 -0
- matrice_analytics/post_processing/post_processor.py +1175 -0
- matrice_analytics/post_processing/test_cases/__init__.py +1 -0
- matrice_analytics/post_processing/test_cases/run_tests.py +143 -0
- matrice_analytics/post_processing/test_cases/test_advanced_customer_service.py +841 -0
- matrice_analytics/post_processing/test_cases/test_basic_counting_tracking.py +523 -0
- matrice_analytics/post_processing/test_cases/test_comprehensive.py +531 -0
- matrice_analytics/post_processing/test_cases/test_config.py +852 -0
- matrice_analytics/post_processing/test_cases/test_customer_service.py +585 -0
- matrice_analytics/post_processing/test_cases/test_data_generators.py +583 -0
- matrice_analytics/post_processing/test_cases/test_people_counting.py +510 -0
- matrice_analytics/post_processing/test_cases/test_processor.py +524 -0
- matrice_analytics/post_processing/test_cases/test_usecases.py +165 -0
- matrice_analytics/post_processing/test_cases/test_utilities.py +356 -0
- matrice_analytics/post_processing/test_cases/test_utils.py +743 -0
- matrice_analytics/post_processing/usecases/Histopathological_Cancer_Detection_img.py +604 -0
- matrice_analytics/post_processing/usecases/__init__.py +267 -0
- matrice_analytics/post_processing/usecases/abandoned_object_detection.py +797 -0
- matrice_analytics/post_processing/usecases/advanced_customer_service.py +1601 -0
- matrice_analytics/post_processing/usecases/age_detection.py +842 -0
- matrice_analytics/post_processing/usecases/age_gender_detection.py +1085 -0
- matrice_analytics/post_processing/usecases/anti_spoofing_detection.py +656 -0
- matrice_analytics/post_processing/usecases/assembly_line_detection.py +841 -0
- matrice_analytics/post_processing/usecases/banana_defect_detection.py +624 -0
- matrice_analytics/post_processing/usecases/basic_counting_tracking.py +667 -0
- matrice_analytics/post_processing/usecases/blood_cancer_detection_img.py +881 -0
- matrice_analytics/post_processing/usecases/car_damage_detection.py +834 -0
- matrice_analytics/post_processing/usecases/car_part_segmentation.py +946 -0
- matrice_analytics/post_processing/usecases/car_service.py +1601 -0
- matrice_analytics/post_processing/usecases/cardiomegaly_classification.py +864 -0
- matrice_analytics/post_processing/usecases/cell_microscopy_segmentation.py +897 -0
- matrice_analytics/post_processing/usecases/chicken_pose_detection.py +648 -0
- matrice_analytics/post_processing/usecases/child_monitoring.py +814 -0
- matrice_analytics/post_processing/usecases/color/clip.py +660 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/merges.txt +48895 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/preprocessor_config.json +28 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/special_tokens_map.json +30 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer.json +245079 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer_config.json +32 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/vocab.json +1 -0
- matrice_analytics/post_processing/usecases/color/color_map_utils.py +70 -0
- matrice_analytics/post_processing/usecases/color/color_mapper.py +468 -0
- matrice_analytics/post_processing/usecases/color_detection.py +1936 -0
- matrice_analytics/post_processing/usecases/color_map_utils.py +70 -0
- matrice_analytics/post_processing/usecases/concrete_crack_detection.py +827 -0
- matrice_analytics/post_processing/usecases/crop_weed_detection.py +781 -0
- matrice_analytics/post_processing/usecases/customer_service.py +1008 -0
- matrice_analytics/post_processing/usecases/defect_detection_products.py +936 -0
- matrice_analytics/post_processing/usecases/distracted_driver_detection.py +822 -0
- matrice_analytics/post_processing/usecases/drone_traffic_monitoring.py +585 -0
- matrice_analytics/post_processing/usecases/drowsy_driver_detection.py +829 -0
- matrice_analytics/post_processing/usecases/dwell_detection.py +829 -0
- matrice_analytics/post_processing/usecases/emergency_vehicle_detection.py +827 -0
- matrice_analytics/post_processing/usecases/face_emotion.py +813 -0
- matrice_analytics/post_processing/usecases/face_recognition.py +827 -0
- matrice_analytics/post_processing/usecases/fashion_detection.py +835 -0
- matrice_analytics/post_processing/usecases/field_mapping.py +902 -0
- matrice_analytics/post_processing/usecases/fire_detection.py +1146 -0
- matrice_analytics/post_processing/usecases/flare_analysis.py +836 -0
- matrice_analytics/post_processing/usecases/flower_segmentation.py +1006 -0
- matrice_analytics/post_processing/usecases/gas_leak_detection.py +837 -0
- matrice_analytics/post_processing/usecases/gender_detection.py +832 -0
- matrice_analytics/post_processing/usecases/human_activity_recognition.py +871 -0
- matrice_analytics/post_processing/usecases/intrusion_detection.py +1672 -0
- matrice_analytics/post_processing/usecases/leaf.py +821 -0
- matrice_analytics/post_processing/usecases/leaf_disease.py +840 -0
- matrice_analytics/post_processing/usecases/leak_detection.py +837 -0
- matrice_analytics/post_processing/usecases/license_plate_detection.py +1188 -0
- matrice_analytics/post_processing/usecases/license_plate_monitoring.py +1781 -0
- matrice_analytics/post_processing/usecases/litter_monitoring.py +717 -0
- matrice_analytics/post_processing/usecases/mask_detection.py +869 -0
- matrice_analytics/post_processing/usecases/natural_disaster.py +907 -0
- matrice_analytics/post_processing/usecases/parking.py +787 -0
- matrice_analytics/post_processing/usecases/parking_space_detection.py +822 -0
- matrice_analytics/post_processing/usecases/pcb_defect_detection.py +888 -0
- matrice_analytics/post_processing/usecases/pedestrian_detection.py +808 -0
- matrice_analytics/post_processing/usecases/people_counting.py +706 -0
- matrice_analytics/post_processing/usecases/people_counting_bckp.py +1683 -0
- matrice_analytics/post_processing/usecases/people_tracking.py +1842 -0
- matrice_analytics/post_processing/usecases/pipeline_detection.py +605 -0
- matrice_analytics/post_processing/usecases/plaque_segmentation_img.py +874 -0
- matrice_analytics/post_processing/usecases/pothole_segmentation.py +915 -0
- matrice_analytics/post_processing/usecases/ppe_compliance.py +645 -0
- matrice_analytics/post_processing/usecases/price_tag_detection.py +822 -0
- matrice_analytics/post_processing/usecases/proximity_detection.py +1901 -0
- matrice_analytics/post_processing/usecases/road_lane_detection.py +623 -0
- matrice_analytics/post_processing/usecases/road_traffic_density.py +832 -0
- matrice_analytics/post_processing/usecases/road_view_segmentation.py +915 -0
- matrice_analytics/post_processing/usecases/shelf_inventory_detection.py +583 -0
- matrice_analytics/post_processing/usecases/shoplifting_detection.py +822 -0
- matrice_analytics/post_processing/usecases/shopping_cart_analysis.py +899 -0
- matrice_analytics/post_processing/usecases/skin_cancer_classification_img.py +864 -0
- matrice_analytics/post_processing/usecases/smoker_detection.py +833 -0
- matrice_analytics/post_processing/usecases/solar_panel.py +810 -0
- matrice_analytics/post_processing/usecases/suspicious_activity_detection.py +1030 -0
- matrice_analytics/post_processing/usecases/template_usecase.py +380 -0
- matrice_analytics/post_processing/usecases/theft_detection.py +648 -0
- matrice_analytics/post_processing/usecases/traffic_sign_monitoring.py +724 -0
- matrice_analytics/post_processing/usecases/underground_pipeline_defect_detection.py +775 -0
- matrice_analytics/post_processing/usecases/underwater_pollution_detection.py +842 -0
- matrice_analytics/post_processing/usecases/vehicle_monitoring.py +1029 -0
- matrice_analytics/post_processing/usecases/warehouse_object_segmentation.py +899 -0
- matrice_analytics/post_processing/usecases/waterbody_segmentation.py +923 -0
- matrice_analytics/post_processing/usecases/weapon_detection.py +771 -0
- matrice_analytics/post_processing/usecases/weld_defect_detection.py +615 -0
- matrice_analytics/post_processing/usecases/wildlife_monitoring.py +898 -0
- matrice_analytics/post_processing/usecases/windmill_maintenance.py +834 -0
- matrice_analytics/post_processing/usecases/wound_segmentation.py +856 -0
- matrice_analytics/post_processing/utils/__init__.py +150 -0
- matrice_analytics/post_processing/utils/advanced_counting_utils.py +400 -0
- matrice_analytics/post_processing/utils/advanced_helper_utils.py +317 -0
- matrice_analytics/post_processing/utils/advanced_tracking_utils.py +461 -0
- matrice_analytics/post_processing/utils/alerting_utils.py +213 -0
- matrice_analytics/post_processing/utils/category_mapping_utils.py +94 -0
- matrice_analytics/post_processing/utils/color_utils.py +592 -0
- matrice_analytics/post_processing/utils/counting_utils.py +182 -0
- matrice_analytics/post_processing/utils/filter_utils.py +261 -0
- matrice_analytics/post_processing/utils/format_utils.py +293 -0
- matrice_analytics/post_processing/utils/geometry_utils.py +300 -0
- matrice_analytics/post_processing/utils/smoothing_utils.py +358 -0
- matrice_analytics/post_processing/utils/tracking_utils.py +234 -0
- matrice_analytics/py.typed +0 -0
- matrice_analytics-0.1.60.dist-info/METADATA +481 -0
- matrice_analytics-0.1.60.dist-info/RECORD +196 -0
- matrice_analytics-0.1.60.dist-info/WHEEL +5 -0
- matrice_analytics-0.1.60.dist-info/licenses/LICENSE.txt +21 -0
- matrice_analytics-0.1.60.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1207 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import numpy as np
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import webbrowser
|
|
6
|
+
import sys
|
|
7
|
+
import json
|
|
8
|
+
import base64
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Tuple, Dict, Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BoundaryDrawingTool:
|
|
14
|
+
"""
|
|
15
|
+
A comprehensive tool for drawing boundaries, polygons, and lines on video frames or images.
|
|
16
|
+
Supports multiple zones with custom tags like queue, staff, entry, exit, restricted zone, etc.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
"""Initialize the boundary drawing tool."""
|
|
21
|
+
self.supported_formats = {
|
|
22
|
+
'video': ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm'],
|
|
23
|
+
'image': ['.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif']
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def extract_first_frame(self, video_path: str, output_path: str = None) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Extract the first frame from a video file.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
video_path (str): Path to the video file
|
|
32
|
+
output_path (str): Path to save the extracted frame
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
str: Path to the extracted frame
|
|
36
|
+
"""
|
|
37
|
+
if not os.path.exists(video_path):
|
|
38
|
+
raise FileNotFoundError(f"Video file not found: {video_path}")
|
|
39
|
+
|
|
40
|
+
# Open video
|
|
41
|
+
cap = cv2.VideoCapture(video_path)
|
|
42
|
+
if not cap.isOpened():
|
|
43
|
+
raise ValueError(f"Cannot open video file: {video_path}")
|
|
44
|
+
|
|
45
|
+
# Read first frame
|
|
46
|
+
ret, frame = cap.read()
|
|
47
|
+
cap.release()
|
|
48
|
+
|
|
49
|
+
if not ret:
|
|
50
|
+
raise ValueError(f"Cannot read first frame from video: {video_path}")
|
|
51
|
+
|
|
52
|
+
# Generate output path if not provided
|
|
53
|
+
if output_path is None:
|
|
54
|
+
video_name = Path(video_path).stem
|
|
55
|
+
output_path = f"{video_name}_first_frame.jpg"
|
|
56
|
+
|
|
57
|
+
# Save frame
|
|
58
|
+
cv2.imwrite(output_path, frame)
|
|
59
|
+
print(f"First frame extracted and saved to: {output_path}")
|
|
60
|
+
|
|
61
|
+
return output_path
|
|
62
|
+
|
|
63
|
+
def create_grid_reference_image(self, frame_path: str, output_path: str = None, grid_step: int = 50) -> str:
|
|
64
|
+
"""
|
|
65
|
+
Create a grid reference image to help users define coordinates.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
frame_path (str): Path to the input frame/image
|
|
69
|
+
output_path (str): Path to save the grid reference image
|
|
70
|
+
grid_step (int): Grid line spacing in pixels
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
str: Path to the grid reference image
|
|
74
|
+
"""
|
|
75
|
+
# Read the image
|
|
76
|
+
frame = cv2.imread(frame_path)
|
|
77
|
+
if frame is None:
|
|
78
|
+
raise ValueError(f"Cannot read image file: {frame_path}")
|
|
79
|
+
|
|
80
|
+
h, w = frame.shape[:2]
|
|
81
|
+
|
|
82
|
+
# Create a grid overlay
|
|
83
|
+
grid_frame = frame.copy()
|
|
84
|
+
|
|
85
|
+
# Draw vertical lines
|
|
86
|
+
for x in range(0, w, grid_step):
|
|
87
|
+
cv2.line(grid_frame, (x, 0), (x, h), (255, 255, 255), 1)
|
|
88
|
+
# Add x-coordinate labels
|
|
89
|
+
if x % 100 == 0: # Label every 100 pixels
|
|
90
|
+
cv2.putText(grid_frame, str(x), (x + 2, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
|
|
91
|
+
|
|
92
|
+
# Draw horizontal lines
|
|
93
|
+
for y in range(0, h, grid_step):
|
|
94
|
+
cv2.line(grid_frame, (0, y), (w, y), (255, 255, 255), 1)
|
|
95
|
+
# Add y-coordinate labels
|
|
96
|
+
if y % 100 == 0: # Label every 100 pixels
|
|
97
|
+
cv2.putText(grid_frame, str(y), (5, y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
|
|
98
|
+
|
|
99
|
+
# Generate output path if not provided
|
|
100
|
+
if output_path is None:
|
|
101
|
+
frame_name = Path(frame_path).stem
|
|
102
|
+
output_path = f"{frame_name}_grid_reference.jpg"
|
|
103
|
+
|
|
104
|
+
# Save the grid image
|
|
105
|
+
cv2.imwrite(output_path, grid_frame)
|
|
106
|
+
print(f"Grid reference image saved to: {output_path}")
|
|
107
|
+
|
|
108
|
+
return output_path
|
|
109
|
+
|
|
110
|
+
def image_to_base64(self, image_path: str) -> str:
|
|
111
|
+
"""
|
|
112
|
+
Convert image to base64 for embedding in HTML.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
image_path (str): Path to the image file
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
str: Base64 encoded image data
|
|
119
|
+
"""
|
|
120
|
+
with open(image_path, "rb") as image_file:
|
|
121
|
+
encoded_string = base64.b64encode(image_file.read()).decode()
|
|
122
|
+
|
|
123
|
+
# Get file extension for MIME type
|
|
124
|
+
ext = Path(image_path).suffix.lower()
|
|
125
|
+
mime_type = "image/jpeg" if ext in ['.jpg', '.jpeg'] else f"image/{ext[1:]}"
|
|
126
|
+
|
|
127
|
+
return f"data:{mime_type};base64,{encoded_string}"
|
|
128
|
+
|
|
129
|
+
def create_interactive_html(self, image_path: str, output_html: str = None, embed_image: bool = True) -> str:
|
|
130
|
+
"""
|
|
131
|
+
Create an interactive HTML page for drawing boundaries with custom tags.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
image_path (str): Path to the reference image
|
|
135
|
+
output_html (str): Path to save the HTML file
|
|
136
|
+
embed_image (bool): Whether to embed image as base64 or use file path
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
str: Path to the HTML file
|
|
140
|
+
"""
|
|
141
|
+
if not os.path.exists(image_path):
|
|
142
|
+
raise FileNotFoundError(f"Image file not found: {image_path}")
|
|
143
|
+
|
|
144
|
+
# Generate output path if not provided
|
|
145
|
+
if output_html is None:
|
|
146
|
+
image_name = Path(image_path).stem
|
|
147
|
+
output_html = f"{image_name}_boundary_tool.html"
|
|
148
|
+
|
|
149
|
+
# Prepare image source
|
|
150
|
+
if embed_image:
|
|
151
|
+
image_src = self.image_to_base64(image_path)
|
|
152
|
+
else:
|
|
153
|
+
image_src = Path(image_path).name
|
|
154
|
+
|
|
155
|
+
html_content = self._generate_html_template(image_src, Path(image_path).name)
|
|
156
|
+
|
|
157
|
+
# Write HTML file
|
|
158
|
+
with open(output_html, 'w', encoding='utf-8') as f:
|
|
159
|
+
f.write(html_content)
|
|
160
|
+
|
|
161
|
+
print(f"Interactive HTML boundary tool created: {output_html}")
|
|
162
|
+
return output_html
|
|
163
|
+
|
|
164
|
+
def _generate_html_template(self, image_src: str, image_name: str) -> str:
|
|
165
|
+
"""Generate the HTML template for the boundary drawing tool."""
|
|
166
|
+
return f"""<!DOCTYPE html>
|
|
167
|
+
<html lang="en">
|
|
168
|
+
<head>
|
|
169
|
+
<meta charset="UTF-8">
|
|
170
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
171
|
+
<title>Boundary Drawing Tool - {image_name}</title>
|
|
172
|
+
<style>
|
|
173
|
+
body {{
|
|
174
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
175
|
+
margin: 0;
|
|
176
|
+
padding: 20px;
|
|
177
|
+
background-color: #f8f9fa;
|
|
178
|
+
color: #333;
|
|
179
|
+
}}
|
|
180
|
+
.container {{
|
|
181
|
+
max-width: 1400px;
|
|
182
|
+
margin: 0 auto;
|
|
183
|
+
}}
|
|
184
|
+
h1 {{
|
|
185
|
+
color: #2c3e50;
|
|
186
|
+
text-align: center;
|
|
187
|
+
margin-bottom: 30px;
|
|
188
|
+
font-size: 28px;
|
|
189
|
+
}}
|
|
190
|
+
.main-content {{
|
|
191
|
+
display: grid;
|
|
192
|
+
grid-template-columns: 300px 1fr;
|
|
193
|
+
gap: 20px;
|
|
194
|
+
min-height: calc(100vh - 120px);
|
|
195
|
+
}}
|
|
196
|
+
.sidebar {{
|
|
197
|
+
background: white;
|
|
198
|
+
border-radius: 8px;
|
|
199
|
+
padding: 20px;
|
|
200
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
201
|
+
height: fit-content;
|
|
202
|
+
position: sticky;
|
|
203
|
+
top: 20px;
|
|
204
|
+
}}
|
|
205
|
+
.image-container {{
|
|
206
|
+
background: white;
|
|
207
|
+
border-radius: 8px;
|
|
208
|
+
padding: 20px;
|
|
209
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
210
|
+
position: relative;
|
|
211
|
+
overflow: auto;
|
|
212
|
+
}}
|
|
213
|
+
.image-wrapper {{
|
|
214
|
+
position: relative;
|
|
215
|
+
display: inline-block;
|
|
216
|
+
border: 2px solid #ddd;
|
|
217
|
+
border-radius: 4px;
|
|
218
|
+
overflow: hidden;
|
|
219
|
+
}}
|
|
220
|
+
img {{
|
|
221
|
+
display: block;
|
|
222
|
+
max-width: 100%;
|
|
223
|
+
height: auto;
|
|
224
|
+
}}
|
|
225
|
+
canvas {{
|
|
226
|
+
position: absolute;
|
|
227
|
+
top: 0;
|
|
228
|
+
left: 0;
|
|
229
|
+
cursor: crosshair;
|
|
230
|
+
pointer-events: auto;
|
|
231
|
+
}}
|
|
232
|
+
.control-section {{
|
|
233
|
+
margin-bottom: 25px;
|
|
234
|
+
padding-bottom: 20px;
|
|
235
|
+
border-bottom: 1px solid #eee;
|
|
236
|
+
}}
|
|
237
|
+
.control-section:last-child {{
|
|
238
|
+
border-bottom: none;
|
|
239
|
+
}}
|
|
240
|
+
.control-section h3 {{
|
|
241
|
+
margin: 0 0 15px 0;
|
|
242
|
+
color: #34495e;
|
|
243
|
+
font-size: 16px;
|
|
244
|
+
font-weight: 600;
|
|
245
|
+
}}
|
|
246
|
+
.zone-types {{
|
|
247
|
+
display: grid;
|
|
248
|
+
gap: 8px;
|
|
249
|
+
margin-bottom: 15px;
|
|
250
|
+
}}
|
|
251
|
+
.zone-btn {{
|
|
252
|
+
padding: 10px 15px;
|
|
253
|
+
border: 2px solid transparent;
|
|
254
|
+
border-radius: 6px;
|
|
255
|
+
cursor: pointer;
|
|
256
|
+
font-size: 14px;
|
|
257
|
+
font-weight: 500;
|
|
258
|
+
text-align: center;
|
|
259
|
+
transition: all 0.2s ease;
|
|
260
|
+
background: #f8f9fa;
|
|
261
|
+
color: #495057;
|
|
262
|
+
}}
|
|
263
|
+
.zone-btn:hover {{
|
|
264
|
+
transform: translateY(-1px);
|
|
265
|
+
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
|
266
|
+
}}
|
|
267
|
+
.zone-btn.active {{
|
|
268
|
+
background: #007bff;
|
|
269
|
+
color: white;
|
|
270
|
+
border-color: #0056b3;
|
|
271
|
+
}}
|
|
272
|
+
.zone-btn.queue {{ background: #28a745; color: white; }}
|
|
273
|
+
.zone-btn.staff {{ background: #17a2b8; color: white; }}
|
|
274
|
+
.zone-btn.entry {{ background: #ffc107; color: #212529; }}
|
|
275
|
+
.zone-btn.exit {{ background: #dc3545; color: white; }}
|
|
276
|
+
.zone-btn.restricted {{ background: #6f42c1; color: white; }}
|
|
277
|
+
.zone-btn.waiting {{ background: #fd7e14; color: white; }}
|
|
278
|
+
.zone-btn.service {{ background: #20c997; color: white; }}
|
|
279
|
+
.zone-btn.security {{ background: #495057; color: white; }}
|
|
280
|
+
|
|
281
|
+
.drawing-controls {{
|
|
282
|
+
display: grid;
|
|
283
|
+
gap: 8px;
|
|
284
|
+
}}
|
|
285
|
+
.control-btn {{
|
|
286
|
+
padding: 8px 12px;
|
|
287
|
+
border: 1px solid #ddd;
|
|
288
|
+
border-radius: 4px;
|
|
289
|
+
background: white;
|
|
290
|
+
cursor: pointer;
|
|
291
|
+
font-size: 13px;
|
|
292
|
+
transition: all 0.2s ease;
|
|
293
|
+
}}
|
|
294
|
+
.control-btn:hover {{
|
|
295
|
+
background: #f8f9fa;
|
|
296
|
+
border-color: #adb5bd;
|
|
297
|
+
}}
|
|
298
|
+
.control-btn.primary {{
|
|
299
|
+
background: #007bff;
|
|
300
|
+
color: white;
|
|
301
|
+
border-color: #0056b3;
|
|
302
|
+
}}
|
|
303
|
+
.control-btn.danger {{
|
|
304
|
+
background: #dc3545;
|
|
305
|
+
color: white;
|
|
306
|
+
border-color: #c82333;
|
|
307
|
+
}}
|
|
308
|
+
.control-btn.success {{
|
|
309
|
+
background: #28a745;
|
|
310
|
+
color: white;
|
|
311
|
+
border-color: #1e7e34;
|
|
312
|
+
}}
|
|
313
|
+
|
|
314
|
+
.coordinates-display {{
|
|
315
|
+
font-family: 'Courier New', monospace;
|
|
316
|
+
font-size: 12px;
|
|
317
|
+
color: #666;
|
|
318
|
+
background: #f8f9fa;
|
|
319
|
+
padding: 8px;
|
|
320
|
+
border-radius: 4px;
|
|
321
|
+
margin-bottom: 10px;
|
|
322
|
+
min-height: 20px;
|
|
323
|
+
}}
|
|
324
|
+
|
|
325
|
+
.zones-list {{
|
|
326
|
+
max-height: 300px;
|
|
327
|
+
overflow-y: auto;
|
|
328
|
+
border: 1px solid #ddd;
|
|
329
|
+
border-radius: 4px;
|
|
330
|
+
background: #f8f9fa;
|
|
331
|
+
}}
|
|
332
|
+
.zone-item {{
|
|
333
|
+
padding: 10px;
|
|
334
|
+
border-bottom: 1px solid #ddd;
|
|
335
|
+
display: flex;
|
|
336
|
+
justify-content: space-between;
|
|
337
|
+
align-items: center;
|
|
338
|
+
background: white;
|
|
339
|
+
margin-bottom: 1px;
|
|
340
|
+
}}
|
|
341
|
+
.zone-item:last-child {{
|
|
342
|
+
border-bottom: none;
|
|
343
|
+
}}
|
|
344
|
+
.zone-tag {{
|
|
345
|
+
padding: 2px 8px;
|
|
346
|
+
border-radius: 3px;
|
|
347
|
+
font-size: 11px;
|
|
348
|
+
font-weight: 600;
|
|
349
|
+
color: white;
|
|
350
|
+
}}
|
|
351
|
+
.zone-actions {{
|
|
352
|
+
display: flex;
|
|
353
|
+
gap: 5px;
|
|
354
|
+
}}
|
|
355
|
+
.zone-actions button {{
|
|
356
|
+
padding: 2px 6px;
|
|
357
|
+
border: none;
|
|
358
|
+
border-radius: 3px;
|
|
359
|
+
cursor: pointer;
|
|
360
|
+
font-size: 11px;
|
|
361
|
+
}}
|
|
362
|
+
|
|
363
|
+
.code-output {{
|
|
364
|
+
background: #2d3748;
|
|
365
|
+
color: #e2e8f0;
|
|
366
|
+
padding: 15px;
|
|
367
|
+
border-radius: 6px;
|
|
368
|
+
font-family: 'Courier New', monospace;
|
|
369
|
+
font-size: 12px;
|
|
370
|
+
white-space: pre-wrap;
|
|
371
|
+
max-height: 200px;
|
|
372
|
+
overflow-y: auto;
|
|
373
|
+
margin-top: 10px;
|
|
374
|
+
border: 1px solid #4a5568;
|
|
375
|
+
}}
|
|
376
|
+
|
|
377
|
+
.current-mouse {{
|
|
378
|
+
position: fixed;
|
|
379
|
+
bottom: 20px;
|
|
380
|
+
right: 20px;
|
|
381
|
+
background: rgba(0,0,0,0.8);
|
|
382
|
+
color: white;
|
|
383
|
+
padding: 8px 12px;
|
|
384
|
+
border-radius: 4px;
|
|
385
|
+
font-family: monospace;
|
|
386
|
+
font-size: 12px;
|
|
387
|
+
pointer-events: none;
|
|
388
|
+
z-index: 1000;
|
|
389
|
+
}}
|
|
390
|
+
|
|
391
|
+
.mode-indicator {{
|
|
392
|
+
background: #e3f2fd;
|
|
393
|
+
border: 1px solid #2196f3;
|
|
394
|
+
border-radius: 4px;
|
|
395
|
+
padding: 8px 12px;
|
|
396
|
+
margin-bottom: 15px;
|
|
397
|
+
text-align: center;
|
|
398
|
+
font-weight: 500;
|
|
399
|
+
color: #1976d2;
|
|
400
|
+
}}
|
|
401
|
+
|
|
402
|
+
.instructions {{
|
|
403
|
+
background: #f0f8f0;
|
|
404
|
+
border: 1px solid #4caf50;
|
|
405
|
+
border-radius: 4px;
|
|
406
|
+
padding: 12px;
|
|
407
|
+
margin-bottom: 20px;
|
|
408
|
+
font-size: 13px;
|
|
409
|
+
line-height: 1.4;
|
|
410
|
+
}}
|
|
411
|
+
|
|
412
|
+
.custom-tag-input {{
|
|
413
|
+
display: flex;
|
|
414
|
+
gap: 5px;
|
|
415
|
+
margin-top: 10px;
|
|
416
|
+
}}
|
|
417
|
+
.custom-tag-input input {{
|
|
418
|
+
flex: 1;
|
|
419
|
+
padding: 6px 8px;
|
|
420
|
+
border: 1px solid #ddd;
|
|
421
|
+
border-radius: 4px;
|
|
422
|
+
font-size: 12px;
|
|
423
|
+
}}
|
|
424
|
+
.custom-tag-input button {{
|
|
425
|
+
padding: 6px 10px;
|
|
426
|
+
background: #28a745;
|
|
427
|
+
color: white;
|
|
428
|
+
border: none;
|
|
429
|
+
border-radius: 4px;
|
|
430
|
+
cursor: pointer;
|
|
431
|
+
font-size: 12px;
|
|
432
|
+
}}
|
|
433
|
+
</style>
|
|
434
|
+
</head>
|
|
435
|
+
<body>
|
|
436
|
+
<div class="container">
|
|
437
|
+
<h1>šÆ Boundary Drawing Tool</h1>
|
|
438
|
+
|
|
439
|
+
<div class="main-content">
|
|
440
|
+
<div class="sidebar">
|
|
441
|
+
<div class="instructions">
|
|
442
|
+
<strong>Instructions:</strong><br>
|
|
443
|
+
1. Select a zone type<br>
|
|
444
|
+
2. Click on image to add points<br>
|
|
445
|
+
3. Complete polygon/line<br>
|
|
446
|
+
4. Copy generated code
|
|
447
|
+
</div>
|
|
448
|
+
|
|
449
|
+
<div class="mode-indicator" id="modeIndicator">
|
|
450
|
+
Select a zone type to start
|
|
451
|
+
</div>
|
|
452
|
+
|
|
453
|
+
<div class="control-section">
|
|
454
|
+
<h3>Zone Types</h3>
|
|
455
|
+
<div class="zone-types">
|
|
456
|
+
<div class="zone-btn queue" onclick="setZoneType('queue')">š Queue Area</div>
|
|
457
|
+
<div class="zone-btn staff" onclick="setZoneType('staff')">š„ Staff Area</div>
|
|
458
|
+
<div class="zone-btn entry" onclick="setZoneType('entry')">šŖ Entry Zone</div>
|
|
459
|
+
<div class="zone-btn exit" onclick="setZoneType('exit')">š¶ Exit Zone</div>
|
|
460
|
+
<div class="zone-btn restricted" onclick="setZoneType('restricted')">š« Restricted</div>
|
|
461
|
+
<div class="zone-btn waiting" onclick="setZoneType('waiting')">ā° Waiting Area</div>
|
|
462
|
+
<div class="zone-btn service" onclick="setZoneType('service')">šļø Service Area</div>
|
|
463
|
+
<div class="zone-btn security" onclick="setZoneType('security')">š Security Zone</div>
|
|
464
|
+
</div>
|
|
465
|
+
|
|
466
|
+
<div class="custom-tag-input">
|
|
467
|
+
<input type="text" id="customTagInput" placeholder="Custom tag...">
|
|
468
|
+
<button onclick="setCustomZoneType()">Add</button>
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
|
|
472
|
+
<div class="control-section">
|
|
473
|
+
<h3>Drawing Mode</h3>
|
|
474
|
+
<div class="drawing-controls">
|
|
475
|
+
<button class="control-btn" onclick="setDrawingMode('polygon')" id="polygonBtn">š Polygon</button>
|
|
476
|
+
<button class="control-btn" onclick="setDrawingMode('line')" id="lineBtn">š Line</button>
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
|
|
480
|
+
<div class="control-section">
|
|
481
|
+
<h3>Controls</h3>
|
|
482
|
+
<div class="drawing-controls">
|
|
483
|
+
<button class="control-btn primary" onclick="completeCurrentZone()">ā
Complete Zone</button>
|
|
484
|
+
<button class="control-btn" onclick="undoLastPoint()">ā¶ Undo Point</button>
|
|
485
|
+
<button class="control-btn danger" onclick="cancelCurrentZone()">ā Cancel Zone</button>
|
|
486
|
+
<button class="control-btn" onclick="clearAll()">šļø Clear All</button>
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
|
|
490
|
+
<div class="control-section">
|
|
491
|
+
<h3>Current Position</h3>
|
|
492
|
+
<div class="coordinates-display" id="currentCoords">
|
|
493
|
+
Move mouse over image
|
|
494
|
+
</div>
|
|
495
|
+
</div>
|
|
496
|
+
|
|
497
|
+
<div class="control-section">
|
|
498
|
+
<h3>Export</h3>
|
|
499
|
+
<div class="drawing-controls">
|
|
500
|
+
<button class="control-btn success" onclick="generateCode()">š Generate Code</button>
|
|
501
|
+
<button class="control-btn" onclick="saveConfiguration()">š¾ Save Config</button>
|
|
502
|
+
<button class="control-btn" onclick="loadConfiguration()">š Load Config</button>
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
</div>
|
|
506
|
+
|
|
507
|
+
<div class="image-container">
|
|
508
|
+
<div class="image-wrapper">
|
|
509
|
+
<img src="{image_src}" id="referenceImage" alt="Reference Image">
|
|
510
|
+
<canvas id="drawingCanvas"></canvas>
|
|
511
|
+
</div>
|
|
512
|
+
|
|
513
|
+
<div class="control-section">
|
|
514
|
+
<h3>Defined Zones</h3>
|
|
515
|
+
<div class="zones-list" id="zonesList">
|
|
516
|
+
<div style="padding: 20px; text-align: center; color: #666;">
|
|
517
|
+
No zones defined yet
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
|
|
522
|
+
<div class="control-section">
|
|
523
|
+
<h3>Generated Code</h3>
|
|
524
|
+
<div class="code-output" id="codeOutput">
|
|
525
|
+
# No zones defined yet
|
|
526
|
+
zones = {{}}
|
|
527
|
+
</div>
|
|
528
|
+
<button class="control-btn success" onclick="copyCode()" style="margin-top: 10px;">š Copy Code</button>
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
</div>
|
|
533
|
+
|
|
534
|
+
<div class="current-mouse" id="mouseTracker">x: 0, y: 0</div>
|
|
535
|
+
|
|
536
|
+
<input type="file" id="fileInput" accept=".json" style="display: none;" onchange="handleFileLoad(event)">
|
|
537
|
+
|
|
538
|
+
<script>
|
|
539
|
+
// Global state
|
|
540
|
+
let currentZoneType = null;
|
|
541
|
+
let currentDrawingMode = 'polygon';
|
|
542
|
+
let currentPoints = [];
|
|
543
|
+
let completedZones = [];
|
|
544
|
+
let isDrawing = false;
|
|
545
|
+
let mousePos = {{ x: 0, y: 0 }};
|
|
546
|
+
|
|
547
|
+
// Canvas and image elements
|
|
548
|
+
const canvas = document.getElementById('drawingCanvas');
|
|
549
|
+
const ctx = canvas.getContext('2d');
|
|
550
|
+
const img = document.getElementById('referenceImage');
|
|
551
|
+
const modeIndicator = document.getElementById('modeIndicator');
|
|
552
|
+
const currentCoords = document.getElementById('currentCoords');
|
|
553
|
+
const mouseTracker = document.getElementById('mouseTracker');
|
|
554
|
+
const zonesList = document.getElementById('zonesList');
|
|
555
|
+
const codeOutput = document.getElementById('codeOutput');
|
|
556
|
+
|
|
557
|
+
// Zone type colors
|
|
558
|
+
const zoneColors = {{
|
|
559
|
+
'queue': '#28a745',
|
|
560
|
+
'staff': '#17a2b8',
|
|
561
|
+
'entry': '#ffc107',
|
|
562
|
+
'exit': '#dc3545',
|
|
563
|
+
'restricted': '#6f42c1',
|
|
564
|
+
'waiting': '#fd7e14',
|
|
565
|
+
'service': '#20c997',
|
|
566
|
+
'security': '#495057'
|
|
567
|
+
}};
|
|
568
|
+
|
|
569
|
+
// Initialize canvas when image loads
|
|
570
|
+
img.onload = function() {{
|
|
571
|
+
setupCanvas();
|
|
572
|
+
}};
|
|
573
|
+
|
|
574
|
+
if (img.complete) {{
|
|
575
|
+
setupCanvas();
|
|
576
|
+
}}
|
|
577
|
+
|
|
578
|
+
function setupCanvas() {{
|
|
579
|
+
canvas.width = img.naturalWidth;
|
|
580
|
+
canvas.height = img.naturalHeight;
|
|
581
|
+
canvas.style.width = img.clientWidth + 'px';
|
|
582
|
+
canvas.style.height = img.clientHeight + 'px';
|
|
583
|
+
redrawCanvas();
|
|
584
|
+
}}
|
|
585
|
+
|
|
586
|
+
// Mouse event handlers
|
|
587
|
+
canvas.addEventListener('mousemove', function(e) {{
|
|
588
|
+
updateMousePosition(e);
|
|
589
|
+
}});
|
|
590
|
+
|
|
591
|
+
canvas.addEventListener('click', function(e) {{
|
|
592
|
+
if (!currentZoneType) {{
|
|
593
|
+
alert('Please select a zone type first!');
|
|
594
|
+
return;
|
|
595
|
+
}}
|
|
596
|
+
addPoint(e);
|
|
597
|
+
}});
|
|
598
|
+
|
|
599
|
+
canvas.addEventListener('contextmenu', function(e) {{
|
|
600
|
+
e.preventDefault();
|
|
601
|
+
if (currentPoints.length > 0) {{
|
|
602
|
+
completeCurrentZone();
|
|
603
|
+
}}
|
|
604
|
+
}});
|
|
605
|
+
|
|
606
|
+
function updateMousePosition(e) {{
|
|
607
|
+
const rect = canvas.getBoundingClientRect();
|
|
608
|
+
const scaleX = canvas.width / rect.width;
|
|
609
|
+
const scaleY = canvas.height / rect.height;
|
|
610
|
+
|
|
611
|
+
mousePos.x = Math.round((e.clientX - rect.left) * scaleX);
|
|
612
|
+
mousePos.y = Math.round((e.clientY - rect.top) * scaleY);
|
|
613
|
+
|
|
614
|
+
currentCoords.textContent = `x: ${{mousePos.x}}, y: ${{mousePos.y}}`;
|
|
615
|
+
mouseTracker.textContent = `x: ${{mousePos.x}}, y: ${{mousePos.y}}`;
|
|
616
|
+
|
|
617
|
+
redrawCanvas();
|
|
618
|
+
}}
|
|
619
|
+
|
|
620
|
+
function addPoint(e) {{
|
|
621
|
+
const rect = canvas.getBoundingClientRect();
|
|
622
|
+
const scaleX = canvas.width / rect.width;
|
|
623
|
+
const scaleY = canvas.height / rect.height;
|
|
624
|
+
|
|
625
|
+
const x = Math.round((e.clientX - rect.left) * scaleX);
|
|
626
|
+
const y = Math.round((e.clientY - rect.top) * scaleY);
|
|
627
|
+
|
|
628
|
+
currentPoints.push([x, y]);
|
|
629
|
+
isDrawing = true;
|
|
630
|
+
|
|
631
|
+
updateModeIndicator();
|
|
632
|
+
redrawCanvas();
|
|
633
|
+
|
|
634
|
+
// Auto-complete line when 2 points are added
|
|
635
|
+
if (currentDrawingMode === 'line' && currentPoints.length === 2) {{
|
|
636
|
+
completeCurrentZone();
|
|
637
|
+
}}
|
|
638
|
+
}}
|
|
639
|
+
|
|
640
|
+
function setZoneType(type) {{
|
|
641
|
+
currentZoneType = type;
|
|
642
|
+
|
|
643
|
+
// Update UI
|
|
644
|
+
document.querySelectorAll('.zone-btn').forEach(btn => {{
|
|
645
|
+
btn.classList.remove('active');
|
|
646
|
+
}});
|
|
647
|
+
event.target.classList.add('active');
|
|
648
|
+
|
|
649
|
+
updateModeIndicator();
|
|
650
|
+
}}
|
|
651
|
+
|
|
652
|
+
function setCustomZoneType() {{
|
|
653
|
+
const input = document.getElementById('customTagInput');
|
|
654
|
+
const customType = input.value.trim();
|
|
655
|
+
|
|
656
|
+
if (customType) {{
|
|
657
|
+
currentZoneType = customType;
|
|
658
|
+
input.value = '';
|
|
659
|
+
|
|
660
|
+
// Remove active class from preset buttons
|
|
661
|
+
document.querySelectorAll('.zone-btn').forEach(btn => {{
|
|
662
|
+
btn.classList.remove('active');
|
|
663
|
+
}});
|
|
664
|
+
|
|
665
|
+
updateModeIndicator();
|
|
666
|
+
}}
|
|
667
|
+
}}
|
|
668
|
+
|
|
669
|
+
function setDrawingMode(mode) {{
|
|
670
|
+
currentDrawingMode = mode;
|
|
671
|
+
|
|
672
|
+
// Update UI
|
|
673
|
+
document.getElementById('polygonBtn').classList.remove('primary');
|
|
674
|
+
document.getElementById('lineBtn').classList.remove('primary');
|
|
675
|
+
document.getElementById(mode + 'Btn').classList.add('primary');
|
|
676
|
+
|
|
677
|
+
updateModeIndicator();
|
|
678
|
+
}}
|
|
679
|
+
|
|
680
|
+
function updateModeIndicator() {{
|
|
681
|
+
if (!currentZoneType) {{
|
|
682
|
+
modeIndicator.textContent = 'Select a zone type to start';
|
|
683
|
+
modeIndicator.style.background = '#f8f9fa';
|
|
684
|
+
modeIndicator.style.borderColor = '#ddd';
|
|
685
|
+
modeIndicator.style.color = '#666';
|
|
686
|
+
return;
|
|
687
|
+
}}
|
|
688
|
+
|
|
689
|
+
let modeText = `Drawing ${{currentDrawingMode}} for "${{currentZoneType}}" zone`;
|
|
690
|
+
|
|
691
|
+
if (isDrawing) {{
|
|
692
|
+
const pointsNeeded = currentDrawingMode === 'line' ? 2 : 3;
|
|
693
|
+
const remaining = Math.max(0, pointsNeeded - currentPoints.length);
|
|
694
|
+
modeText += ` (${{currentPoints.length}} points, need ${{remaining}} more)`;
|
|
695
|
+
}}
|
|
696
|
+
|
|
697
|
+
modeIndicator.textContent = modeText;
|
|
698
|
+
modeIndicator.style.background = '#e3f2fd';
|
|
699
|
+
modeIndicator.style.borderColor = '#2196f3';
|
|
700
|
+
modeIndicator.style.color = '#1976d2';
|
|
701
|
+
}}
|
|
702
|
+
|
|
703
|
+
function completeCurrentZone() {{
|
|
704
|
+
if (!currentZoneType || currentPoints.length === 0) return;
|
|
705
|
+
|
|
706
|
+
const minPoints = currentDrawingMode === 'line' ? 2 : 3;
|
|
707
|
+
if (currentPoints.length < minPoints) {{
|
|
708
|
+
alert(`Need at least ${{minPoints}} points for a ${{currentDrawingMode}}!`);
|
|
709
|
+
return;
|
|
710
|
+
}}
|
|
711
|
+
|
|
712
|
+
// Create zone object
|
|
713
|
+
const zone = {{
|
|
714
|
+
type: currentZoneType,
|
|
715
|
+
mode: currentDrawingMode,
|
|
716
|
+
points: [...currentPoints],
|
|
717
|
+
color: zoneColors[currentZoneType] || '#333333',
|
|
718
|
+
id: 'zone_' + Date.now()
|
|
719
|
+
}};
|
|
720
|
+
|
|
721
|
+
completedZones.push(zone);
|
|
722
|
+
|
|
723
|
+
// Reset current drawing
|
|
724
|
+
currentPoints = [];
|
|
725
|
+
isDrawing = false;
|
|
726
|
+
|
|
727
|
+
updateModeIndicator();
|
|
728
|
+
updateZonesList();
|
|
729
|
+
generateCode();
|
|
730
|
+
redrawCanvas();
|
|
731
|
+
}}
|
|
732
|
+
|
|
733
|
+
function undoLastPoint() {{
|
|
734
|
+
if (currentPoints.length > 0) {{
|
|
735
|
+
currentPoints.pop();
|
|
736
|
+
updateModeIndicator();
|
|
737
|
+
redrawCanvas();
|
|
738
|
+
}}
|
|
739
|
+
}}
|
|
740
|
+
|
|
741
|
+
function cancelCurrentZone() {{
|
|
742
|
+
currentPoints = [];
|
|
743
|
+
isDrawing = false;
|
|
744
|
+
updateModeIndicator();
|
|
745
|
+
redrawCanvas();
|
|
746
|
+
}}
|
|
747
|
+
|
|
748
|
+
function clearAll() {{
|
|
749
|
+
if (confirm('Clear all zones? This cannot be undone.')) {{
|
|
750
|
+
currentPoints = [];
|
|
751
|
+
completedZones = [];
|
|
752
|
+
isDrawing = false;
|
|
753
|
+
updateModeIndicator();
|
|
754
|
+
updateZonesList();
|
|
755
|
+
generateCode();
|
|
756
|
+
redrawCanvas();
|
|
757
|
+
}}
|
|
758
|
+
}}
|
|
759
|
+
|
|
760
|
+
function deleteZone(zoneId) {{
|
|
761
|
+
completedZones = completedZones.filter(zone => zone.id !== zoneId);
|
|
762
|
+
updateZonesList();
|
|
763
|
+
generateCode();
|
|
764
|
+
redrawCanvas();
|
|
765
|
+
}}
|
|
766
|
+
|
|
767
|
+
function redrawCanvas() {{
|
|
768
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
769
|
+
|
|
770
|
+
// Draw completed zones
|
|
771
|
+
completedZones.forEach(zone => {{
|
|
772
|
+
drawZone(zone);
|
|
773
|
+
}});
|
|
774
|
+
|
|
775
|
+
// Draw current drawing
|
|
776
|
+
if (currentPoints.length > 0) {{
|
|
777
|
+
const color = zoneColors[currentZoneType] || '#333333';
|
|
778
|
+
drawCurrentDrawing(color);
|
|
779
|
+
}}
|
|
780
|
+
|
|
781
|
+
// Draw mouse cursor
|
|
782
|
+
if (mousePos.x > 0 && mousePos.y > 0 && isDrawing) {{
|
|
783
|
+
ctx.beginPath();
|
|
784
|
+
ctx.arc(mousePos.x, mousePos.y, 6, 0, Math.PI * 2);
|
|
785
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
|
786
|
+
ctx.fill();
|
|
787
|
+
ctx.strokeStyle = '#333';
|
|
788
|
+
ctx.lineWidth = 2;
|
|
789
|
+
ctx.stroke();
|
|
790
|
+
}}
|
|
791
|
+
}}
|
|
792
|
+
|
|
793
|
+
function drawZone(zone) {{
|
|
794
|
+
if (zone.points.length === 0) return;
|
|
795
|
+
|
|
796
|
+
ctx.strokeStyle = zone.color;
|
|
797
|
+
ctx.fillStyle = zone.color + '40'; // Add transparency
|
|
798
|
+
ctx.lineWidth = 3;
|
|
799
|
+
|
|
800
|
+
if (zone.mode === 'line') {{
|
|
801
|
+
// Draw line
|
|
802
|
+
ctx.beginPath();
|
|
803
|
+
ctx.moveTo(zone.points[0][0], zone.points[0][1]);
|
|
804
|
+
ctx.lineTo(zone.points[1][0], zone.points[1][1]);
|
|
805
|
+
ctx.stroke();
|
|
806
|
+
}} else {{
|
|
807
|
+
// Draw polygon
|
|
808
|
+
ctx.beginPath();
|
|
809
|
+
ctx.moveTo(zone.points[0][0], zone.points[0][1]);
|
|
810
|
+
for (let i = 1; i < zone.points.length; i++) {{
|
|
811
|
+
ctx.lineTo(zone.points[i][0], zone.points[i][1]);
|
|
812
|
+
}}
|
|
813
|
+
ctx.closePath();
|
|
814
|
+
ctx.fill();
|
|
815
|
+
ctx.stroke();
|
|
816
|
+
}}
|
|
817
|
+
|
|
818
|
+
// Draw points
|
|
819
|
+
zone.points.forEach((point, index) => {{
|
|
820
|
+
ctx.beginPath();
|
|
821
|
+
ctx.arc(point[0], point[1], 5, 0, Math.PI * 2);
|
|
822
|
+
ctx.fillStyle = '#fff';
|
|
823
|
+
ctx.fill();
|
|
824
|
+
ctx.strokeStyle = zone.color;
|
|
825
|
+
ctx.lineWidth = 2;
|
|
826
|
+
ctx.stroke();
|
|
827
|
+
|
|
828
|
+
// Draw point number
|
|
829
|
+
ctx.fillStyle = '#333';
|
|
830
|
+
ctx.font = '12px Arial';
|
|
831
|
+
ctx.textAlign = 'center';
|
|
832
|
+
ctx.fillText(index + 1, point[0], point[1] - 10);
|
|
833
|
+
}});
|
|
834
|
+
|
|
835
|
+
// Draw zone label
|
|
836
|
+
if (zone.points.length > 0) {{
|
|
837
|
+
const centerX = zone.points.reduce((sum, p) => sum + p[0], 0) / zone.points.length;
|
|
838
|
+
const centerY = zone.points.reduce((sum, p) => sum + p[1], 0) / zone.points.length;
|
|
839
|
+
|
|
840
|
+
ctx.fillStyle = zone.color;
|
|
841
|
+
ctx.font = 'bold 14px Arial';
|
|
842
|
+
ctx.textAlign = 'center';
|
|
843
|
+
ctx.fillText(zone.type.toUpperCase(), centerX, centerY);
|
|
844
|
+
}}
|
|
845
|
+
}}
|
|
846
|
+
|
|
847
|
+
function drawCurrentDrawing(color) {{
|
|
848
|
+
if (currentPoints.length === 0) return;
|
|
849
|
+
|
|
850
|
+
ctx.strokeStyle = color;
|
|
851
|
+
ctx.fillStyle = color + '20';
|
|
852
|
+
ctx.lineWidth = 2;
|
|
853
|
+
ctx.setLineDash([5, 5]);
|
|
854
|
+
|
|
855
|
+
if (currentDrawingMode === 'line' && currentPoints.length >= 2) {{
|
|
856
|
+
// Draw line
|
|
857
|
+
ctx.beginPath();
|
|
858
|
+
ctx.moveTo(currentPoints[0][0], currentPoints[0][1]);
|
|
859
|
+
ctx.lineTo(currentPoints[1][0], currentPoints[1][1]);
|
|
860
|
+
ctx.stroke();
|
|
861
|
+
}} else if (currentDrawingMode === 'polygon' && currentPoints.length >= 2) {{
|
|
862
|
+
// Draw polygon outline
|
|
863
|
+
ctx.beginPath();
|
|
864
|
+
ctx.moveTo(currentPoints[0][0], currentPoints[0][1]);
|
|
865
|
+
for (let i = 1; i < currentPoints.length; i++) {{
|
|
866
|
+
ctx.lineTo(currentPoints[i][0], currentPoints[i][1]);
|
|
867
|
+
}}
|
|
868
|
+
ctx.stroke();
|
|
869
|
+
|
|
870
|
+
// Draw line to mouse position
|
|
871
|
+
if (mousePos.x > 0 && mousePos.y > 0) {{
|
|
872
|
+
ctx.lineTo(mousePos.x, mousePos.y);
|
|
873
|
+
ctx.stroke();
|
|
874
|
+
}}
|
|
875
|
+
}}
|
|
876
|
+
|
|
877
|
+
ctx.setLineDash([]);
|
|
878
|
+
|
|
879
|
+
// Draw current points
|
|
880
|
+
currentPoints.forEach((point, index) => {{
|
|
881
|
+
ctx.beginPath();
|
|
882
|
+
ctx.arc(point[0], point[1], 4, 0, Math.PI * 2);
|
|
883
|
+
ctx.fillStyle = '#fff';
|
|
884
|
+
ctx.fill();
|
|
885
|
+
ctx.strokeStyle = color;
|
|
886
|
+
ctx.lineWidth = 2;
|
|
887
|
+
ctx.stroke();
|
|
888
|
+
}});
|
|
889
|
+
}}
|
|
890
|
+
|
|
891
|
+
function updateZonesList() {{
|
|
892
|
+
if (completedZones.length === 0) {{
|
|
893
|
+
zonesList.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">No zones defined yet</div>';
|
|
894
|
+
return;
|
|
895
|
+
}}
|
|
896
|
+
|
|
897
|
+
zonesList.innerHTML = completedZones.map(zone => `
|
|
898
|
+
<div class="zone-item">
|
|
899
|
+
<div>
|
|
900
|
+
<span class="zone-tag" style="background: ${{zone.color}};">${{zone.type}}</span>
|
|
901
|
+
<small style="margin-left: 8px; color: #666;">
|
|
902
|
+
${{zone.mode}} (${{zone.points.length}} points)
|
|
903
|
+
</small>
|
|
904
|
+
</div>
|
|
905
|
+
<div class="zone-actions">
|
|
906
|
+
<button onclick="deleteZone('${{zone.id}}')" style="background: #dc3545; color: white;">Delete</button>
|
|
907
|
+
</div>
|
|
908
|
+
</div>
|
|
909
|
+
`).join('');
|
|
910
|
+
}}
|
|
911
|
+
|
|
912
|
+
function generateCode() {{
|
|
913
|
+
if (completedZones.length === 0) {{
|
|
914
|
+
codeOutput.textContent = '# No zones defined yet\\nzones = {{}}';
|
|
915
|
+
return;
|
|
916
|
+
}}
|
|
917
|
+
|
|
918
|
+
let code = '# Generated boundary definitions\\n';
|
|
919
|
+
code += '# Copy this code for use in your applications\\n\\n';
|
|
920
|
+
|
|
921
|
+
// Group zones by type
|
|
922
|
+
const zonesByType = {{}};
|
|
923
|
+
completedZones.forEach(zone => {{
|
|
924
|
+
if (!zonesByType[zone.type]) {{
|
|
925
|
+
zonesByType[zone.type] = [];
|
|
926
|
+
}}
|
|
927
|
+
zonesByType[zone.type].push(zone);
|
|
928
|
+
}});
|
|
929
|
+
|
|
930
|
+
// Generate Python dictionary
|
|
931
|
+
code += 'zones = {{\\n';
|
|
932
|
+
Object.keys(zonesByType).forEach(type => {{
|
|
933
|
+
const zones = zonesByType[type];
|
|
934
|
+
if (zones.length === 1) {{
|
|
935
|
+
code += ` "${{type}}": ${{JSON.stringify(zones[0].points)}},\\n`;
|
|
936
|
+
}} else {{
|
|
937
|
+
code += ` "${{type}}": {{\\n`;
|
|
938
|
+
zones.forEach((zone, index) => {{
|
|
939
|
+
code += ` "${{type}}_${{index + 1}}": ${{JSON.stringify(zone.points)}},\\n`;
|
|
940
|
+
}});
|
|
941
|
+
code += ` }},\\n`;
|
|
942
|
+
}}
|
|
943
|
+
}});
|
|
944
|
+
code += '}}\\n\\n';
|
|
945
|
+
|
|
946
|
+
// Add usage examples
|
|
947
|
+
code += '# Usage examples:\\n';
|
|
948
|
+
code += '# For post-processing configuration:\\n';
|
|
949
|
+
code += '# config.customer_service.customer_areas = zones["queue"]\\n';
|
|
950
|
+
code += '# config.advanced_tracking.boundary_config = {{ "points": zones["entry"] }}\\n\\n';
|
|
951
|
+
|
|
952
|
+
// Add individual zone coordinates for easy copying
|
|
953
|
+
code += '# Individual zone coordinates:\\n';
|
|
954
|
+
completedZones.forEach((zone, index) => {{
|
|
955
|
+
code += `# ${{zone.type}} ${{zone.mode}} (${{zone.points.length}} points):\\n`;
|
|
956
|
+
code += `${{zone.type}}_${{index + 1}} = ${{JSON.stringify(zone.points)}}\\n`;
|
|
957
|
+
}});
|
|
958
|
+
|
|
959
|
+
codeOutput.textContent = code;
|
|
960
|
+
}}
|
|
961
|
+
|
|
962
|
+
function copyCode() {{
|
|
963
|
+
const text = codeOutput.textContent;
|
|
964
|
+
navigator.clipboard.writeText(text).then(() => {{
|
|
965
|
+
const btn = event.target;
|
|
966
|
+
const originalText = btn.textContent;
|
|
967
|
+
btn.textContent = 'ā
Copied!';
|
|
968
|
+
btn.style.background = '#28a745';
|
|
969
|
+
setTimeout(() => {{
|
|
970
|
+
btn.textContent = originalText;
|
|
971
|
+
btn.style.background = '';
|
|
972
|
+
}}, 2000);
|
|
973
|
+
}}).catch(err => {{
|
|
974
|
+
console.error('Failed to copy: ', err);
|
|
975
|
+
alert('Failed to copy to clipboard. Please copy manually.');
|
|
976
|
+
}});
|
|
977
|
+
}}
|
|
978
|
+
|
|
979
|
+
function saveConfiguration() {{
|
|
980
|
+
const config = {{
|
|
981
|
+
zones: completedZones,
|
|
982
|
+
metadata: {{
|
|
983
|
+
created: new Date().toISOString(),
|
|
984
|
+
image: '{image_name}',
|
|
985
|
+
tool_version: '1.0'
|
|
986
|
+
}}
|
|
987
|
+
}};
|
|
988
|
+
|
|
989
|
+
const blob = new Blob([JSON.stringify(config, null, 2)], {{ type: 'application/json' }});
|
|
990
|
+
const url = URL.createObjectURL(blob);
|
|
991
|
+
const a = document.createElement('a');
|
|
992
|
+
a.href = url;
|
|
993
|
+
a.download = 'boundary_config.json';
|
|
994
|
+
a.click();
|
|
995
|
+
URL.revokeObjectURL(url);
|
|
996
|
+
}}
|
|
997
|
+
|
|
998
|
+
function loadConfiguration() {{
|
|
999
|
+
document.getElementById('fileInput').click();
|
|
1000
|
+
}}
|
|
1001
|
+
|
|
1002
|
+
function handleFileLoad(event) {{
|
|
1003
|
+
const file = event.target.files[0];
|
|
1004
|
+
if (!file) return;
|
|
1005
|
+
|
|
1006
|
+
const reader = new FileReader();
|
|
1007
|
+
reader.onload = function(e) {{
|
|
1008
|
+
try {{
|
|
1009
|
+
const config = JSON.parse(e.target.result);
|
|
1010
|
+
if (config.zones && Array.isArray(config.zones)) {{
|
|
1011
|
+
completedZones = config.zones;
|
|
1012
|
+
updateZonesList();
|
|
1013
|
+
generateCode();
|
|
1014
|
+
redrawCanvas();
|
|
1015
|
+
alert('Configuration loaded successfully!');
|
|
1016
|
+
}} else {{
|
|
1017
|
+
alert('Invalid configuration file format.');
|
|
1018
|
+
}}
|
|
1019
|
+
}} catch (err) {{
|
|
1020
|
+
alert('Error reading configuration file: ' + err.message);
|
|
1021
|
+
}}
|
|
1022
|
+
}};
|
|
1023
|
+
reader.readAsText(file);
|
|
1024
|
+
}}
|
|
1025
|
+
|
|
1026
|
+
// Keyboard shortcuts
|
|
1027
|
+
document.addEventListener('keydown', function(e) {{
|
|
1028
|
+
if (e.key === 'Escape') {{
|
|
1029
|
+
cancelCurrentZone();
|
|
1030
|
+
}} else if (e.key === 'Enter') {{
|
|
1031
|
+
completeCurrentZone();
|
|
1032
|
+
}} else if (e.ctrlKey && e.key === 'z') {{
|
|
1033
|
+
e.preventDefault();
|
|
1034
|
+
undoLastPoint();
|
|
1035
|
+
}}
|
|
1036
|
+
}});
|
|
1037
|
+
|
|
1038
|
+
// Initialize
|
|
1039
|
+
updateModeIndicator();
|
|
1040
|
+
generateCode();
|
|
1041
|
+
</script>
|
|
1042
|
+
</body>
|
|
1043
|
+
</html>"""
|
|
1044
|
+
|
|
1045
|
+
def open_in_browser(self, html_path: str):
|
|
1046
|
+
"""
|
|
1047
|
+
Open the HTML file in the default web browser.
|
|
1048
|
+
|
|
1049
|
+
Args:
|
|
1050
|
+
html_path (str): Path to the HTML file
|
|
1051
|
+
"""
|
|
1052
|
+
try:
|
|
1053
|
+
webbrowser.open(f'file://{os.path.abspath(html_path)}')
|
|
1054
|
+
print(f"Opened boundary tool in browser: {html_path}")
|
|
1055
|
+
except Exception as e:
|
|
1056
|
+
print(f"Could not open browser: {e}")
|
|
1057
|
+
print(f"Please manually open: {html_path}")
|
|
1058
|
+
|
|
1059
|
+
def get_file_type(self, file_path: str) -> str:
|
|
1060
|
+
"""
|
|
1061
|
+
Determine if the file is a video or image.
|
|
1062
|
+
|
|
1063
|
+
Args:
|
|
1064
|
+
file_path (str): Path to the file
|
|
1065
|
+
|
|
1066
|
+
Returns:
|
|
1067
|
+
str: 'video', 'image', or 'unknown'
|
|
1068
|
+
"""
|
|
1069
|
+
ext = Path(file_path).suffix.lower()
|
|
1070
|
+
|
|
1071
|
+
if ext in self.supported_formats['video']:
|
|
1072
|
+
return 'video'
|
|
1073
|
+
elif ext in self.supported_formats['image']:
|
|
1074
|
+
return 'image'
|
|
1075
|
+
else:
|
|
1076
|
+
return 'unknown'
|
|
1077
|
+
|
|
1078
|
+
def process_input_file(self, input_path: str, output_dir: str = None, grid_step: int = 50,
|
|
1079
|
+
open_browser: bool = True, embed_image: bool = True) -> Dict[str, str]:
|
|
1080
|
+
"""
|
|
1081
|
+
Process an input video or image file and create the boundary drawing tool.
|
|
1082
|
+
|
|
1083
|
+
Args:
|
|
1084
|
+
input_path (str): Path to input video or image file
|
|
1085
|
+
output_dir (str): Directory to save output files
|
|
1086
|
+
grid_step (int): Grid line spacing for reference image
|
|
1087
|
+
open_browser (bool): Whether to open the tool in browser
|
|
1088
|
+
embed_image (bool): Whether to embed image as base64 in HTML
|
|
1089
|
+
|
|
1090
|
+
Returns:
|
|
1091
|
+
Dict[str, str]: Dictionary with paths to created files
|
|
1092
|
+
"""
|
|
1093
|
+
if not os.path.exists(input_path):
|
|
1094
|
+
raise FileNotFoundError(f"Input file not found: {input_path}")
|
|
1095
|
+
|
|
1096
|
+
# Determine file type
|
|
1097
|
+
file_type = self.get_file_type(input_path)
|
|
1098
|
+
if file_type == 'unknown':
|
|
1099
|
+
raise ValueError(f"Unsupported file format: {Path(input_path).suffix}")
|
|
1100
|
+
|
|
1101
|
+
# Set up output directory
|
|
1102
|
+
if output_dir is None:
|
|
1103
|
+
output_dir = os.path.dirname(input_path) or '.'
|
|
1104
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
1105
|
+
|
|
1106
|
+
# Get base name for output files
|
|
1107
|
+
base_name = Path(input_path).stem
|
|
1108
|
+
|
|
1109
|
+
results = {}
|
|
1110
|
+
|
|
1111
|
+
# Extract first frame if video
|
|
1112
|
+
if file_type == 'video':
|
|
1113
|
+
frame_path = os.path.join(output_dir, f"{base_name}_first_frame.jpg")
|
|
1114
|
+
frame_path = self.extract_first_frame(input_path, frame_path)
|
|
1115
|
+
results['frame'] = frame_path
|
|
1116
|
+
else:
|
|
1117
|
+
frame_path = input_path
|
|
1118
|
+
results['frame'] = frame_path
|
|
1119
|
+
|
|
1120
|
+
# Create grid reference image
|
|
1121
|
+
grid_path = os.path.join(output_dir, f"{base_name}_grid_reference.jpg")
|
|
1122
|
+
grid_path = self.create_grid_reference_image(frame_path, grid_path, grid_step)
|
|
1123
|
+
results['grid_reference'] = grid_path
|
|
1124
|
+
|
|
1125
|
+
# Create interactive HTML
|
|
1126
|
+
html_path = os.path.join(output_dir, f"{base_name}_boundary_tool.html")
|
|
1127
|
+
html_path = self.create_interactive_html(grid_path, html_path, embed_image)
|
|
1128
|
+
results['html_tool'] = html_path
|
|
1129
|
+
|
|
1130
|
+
# Open in browser
|
|
1131
|
+
if open_browser:
|
|
1132
|
+
self.open_in_browser(html_path)
|
|
1133
|
+
|
|
1134
|
+
return results
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
def main():
|
|
1138
|
+
"""Main function for command line usage."""
|
|
1139
|
+
parser = argparse.ArgumentParser(
|
|
1140
|
+
description="Boundary Drawing Tool - Create interactive tools for defining zones and boundaries",
|
|
1141
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1142
|
+
epilog="""
|
|
1143
|
+
Examples:
|
|
1144
|
+
# Process a video file
|
|
1145
|
+
python boundary_drawing_internal.py --input video.mp4 --output ./boundaries/
|
|
1146
|
+
|
|
1147
|
+
# Process an image file with custom grid spacing
|
|
1148
|
+
python boundary_drawing_internal.py --input frame.jpg --grid-step 25
|
|
1149
|
+
|
|
1150
|
+
# Create tool without opening browser
|
|
1151
|
+
python boundary_drawing_internal.py --input video.mp4 --no-browser
|
|
1152
|
+
|
|
1153
|
+
# Create tool with external image reference (not embedded)
|
|
1154
|
+
python boundary_drawing_internal.py --input video.mp4 --no-embed
|
|
1155
|
+
"""
|
|
1156
|
+
)
|
|
1157
|
+
|
|
1158
|
+
parser.add_argument('--input', '-i', required=True,
|
|
1159
|
+
help='Input video or image file')
|
|
1160
|
+
parser.add_argument('--output', '-o',
|
|
1161
|
+
help='Output directory for generated files')
|
|
1162
|
+
parser.add_argument('--grid-step', type=int, default=50,
|
|
1163
|
+
help='Grid line spacing in pixels (default: 50)')
|
|
1164
|
+
parser.add_argument('--no-browser', action='store_true',
|
|
1165
|
+
help='Do not open the tool in browser automatically')
|
|
1166
|
+
parser.add_argument('--no-embed', action='store_true',
|
|
1167
|
+
help='Do not embed image as base64 in HTML')
|
|
1168
|
+
|
|
1169
|
+
args = parser.parse_args()
|
|
1170
|
+
|
|
1171
|
+
try:
|
|
1172
|
+
tool = BoundaryDrawingTool()
|
|
1173
|
+
|
|
1174
|
+
results = tool.process_input_file(
|
|
1175
|
+
input_path=args.input,
|
|
1176
|
+
output_dir=args.output,
|
|
1177
|
+
grid_step=args.grid_step,
|
|
1178
|
+
open_browser=not args.no_browser,
|
|
1179
|
+
embed_image=not args.no_embed
|
|
1180
|
+
)
|
|
1181
|
+
|
|
1182
|
+
print("\n" + "="*60)
|
|
1183
|
+
print("šÆ Boundary Drawing Tool - Results")
|
|
1184
|
+
print("="*60)
|
|
1185
|
+
|
|
1186
|
+
for file_type, path in results.items():
|
|
1187
|
+
print(f"{file_type.title().replace('_', ' ')}: {path}")
|
|
1188
|
+
|
|
1189
|
+
print("\nš Instructions:")
|
|
1190
|
+
print("1. Use the interactive HTML tool to draw zones")
|
|
1191
|
+
print("2. Select zone types (queue, staff, entry, exit, etc.)")
|
|
1192
|
+
print("3. Click on the image to add points")
|
|
1193
|
+
print("4. Right-click or press Enter to complete a zone")
|
|
1194
|
+
print("5. Copy the generated code for use in your applications")
|
|
1195
|
+
|
|
1196
|
+
if not args.no_browser:
|
|
1197
|
+
print(f"\nš Tool opened in browser: {results['html_tool']}")
|
|
1198
|
+
else:
|
|
1199
|
+
print(f"\nš Open this file in browser: {results['html_tool']}")
|
|
1200
|
+
|
|
1201
|
+
except Exception as e:
|
|
1202
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
1203
|
+
sys.exit(1)
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
if __name__ == "__main__":
|
|
1207
|
+
main()
|