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.
Files changed (196) hide show
  1. matrice_analytics/__init__.py +28 -0
  2. matrice_analytics/boundary_drawing_internal/README.md +305 -0
  3. matrice_analytics/boundary_drawing_internal/__init__.py +45 -0
  4. matrice_analytics/boundary_drawing_internal/boundary_drawing_internal.py +1207 -0
  5. matrice_analytics/boundary_drawing_internal/boundary_drawing_tool.py +429 -0
  6. matrice_analytics/boundary_drawing_internal/boundary_tool_template.html +1036 -0
  7. matrice_analytics/boundary_drawing_internal/data/.gitignore +12 -0
  8. matrice_analytics/boundary_drawing_internal/example_usage.py +206 -0
  9. matrice_analytics/boundary_drawing_internal/usage/README.md +110 -0
  10. matrice_analytics/boundary_drawing_internal/usage/boundary_drawer_launcher.py +102 -0
  11. matrice_analytics/boundary_drawing_internal/usage/simple_boundary_launcher.py +107 -0
  12. matrice_analytics/post_processing/README.md +455 -0
  13. matrice_analytics/post_processing/__init__.py +732 -0
  14. matrice_analytics/post_processing/advanced_tracker/README.md +650 -0
  15. matrice_analytics/post_processing/advanced_tracker/__init__.py +17 -0
  16. matrice_analytics/post_processing/advanced_tracker/base.py +99 -0
  17. matrice_analytics/post_processing/advanced_tracker/config.py +77 -0
  18. matrice_analytics/post_processing/advanced_tracker/kalman_filter.py +370 -0
  19. matrice_analytics/post_processing/advanced_tracker/matching.py +195 -0
  20. matrice_analytics/post_processing/advanced_tracker/strack.py +230 -0
  21. matrice_analytics/post_processing/advanced_tracker/tracker.py +367 -0
  22. matrice_analytics/post_processing/config.py +146 -0
  23. matrice_analytics/post_processing/core/__init__.py +63 -0
  24. matrice_analytics/post_processing/core/base.py +704 -0
  25. matrice_analytics/post_processing/core/config.py +3291 -0
  26. matrice_analytics/post_processing/core/config_utils.py +925 -0
  27. matrice_analytics/post_processing/face_reg/__init__.py +43 -0
  28. matrice_analytics/post_processing/face_reg/compare_similarity.py +556 -0
  29. matrice_analytics/post_processing/face_reg/embedding_manager.py +950 -0
  30. matrice_analytics/post_processing/face_reg/face_recognition.py +2234 -0
  31. matrice_analytics/post_processing/face_reg/face_recognition_client.py +606 -0
  32. matrice_analytics/post_processing/face_reg/people_activity_logging.py +321 -0
  33. matrice_analytics/post_processing/ocr/__init__.py +0 -0
  34. matrice_analytics/post_processing/ocr/easyocr_extractor.py +250 -0
  35. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/__init__.py +9 -0
  36. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/__init__.py +4 -0
  37. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/cli.py +33 -0
  38. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/dataset_stats.py +139 -0
  39. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/export.py +398 -0
  40. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/train.py +447 -0
  41. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/utils.py +129 -0
  42. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/valid.py +93 -0
  43. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/validate_dataset.py +240 -0
  44. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_augmentation.py +176 -0
  45. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_predictions.py +96 -0
  46. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/__init__.py +3 -0
  47. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/process.py +246 -0
  48. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/types.py +60 -0
  49. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/utils.py +87 -0
  50. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/__init__.py +3 -0
  51. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/config.py +82 -0
  52. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/hub.py +141 -0
  53. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/plate_recognizer.py +323 -0
  54. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/py.typed +0 -0
  55. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/__init__.py +0 -0
  56. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/__init__.py +0 -0
  57. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/augmentation.py +101 -0
  58. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/dataset.py +97 -0
  59. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/__init__.py +0 -0
  60. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/config.py +114 -0
  61. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/layers.py +553 -0
  62. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/loss.py +55 -0
  63. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/metric.py +86 -0
  64. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_builders.py +95 -0
  65. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_schema.py +395 -0
  66. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/__init__.py +0 -0
  67. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/backend_utils.py +38 -0
  68. matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/utils.py +214 -0
  69. matrice_analytics/post_processing/ocr/postprocessing.py +270 -0
  70. matrice_analytics/post_processing/ocr/preprocessing.py +52 -0
  71. matrice_analytics/post_processing/post_processor.py +1175 -0
  72. matrice_analytics/post_processing/test_cases/__init__.py +1 -0
  73. matrice_analytics/post_processing/test_cases/run_tests.py +143 -0
  74. matrice_analytics/post_processing/test_cases/test_advanced_customer_service.py +841 -0
  75. matrice_analytics/post_processing/test_cases/test_basic_counting_tracking.py +523 -0
  76. matrice_analytics/post_processing/test_cases/test_comprehensive.py +531 -0
  77. matrice_analytics/post_processing/test_cases/test_config.py +852 -0
  78. matrice_analytics/post_processing/test_cases/test_customer_service.py +585 -0
  79. matrice_analytics/post_processing/test_cases/test_data_generators.py +583 -0
  80. matrice_analytics/post_processing/test_cases/test_people_counting.py +510 -0
  81. matrice_analytics/post_processing/test_cases/test_processor.py +524 -0
  82. matrice_analytics/post_processing/test_cases/test_usecases.py +165 -0
  83. matrice_analytics/post_processing/test_cases/test_utilities.py +356 -0
  84. matrice_analytics/post_processing/test_cases/test_utils.py +743 -0
  85. matrice_analytics/post_processing/usecases/Histopathological_Cancer_Detection_img.py +604 -0
  86. matrice_analytics/post_processing/usecases/__init__.py +267 -0
  87. matrice_analytics/post_processing/usecases/abandoned_object_detection.py +797 -0
  88. matrice_analytics/post_processing/usecases/advanced_customer_service.py +1601 -0
  89. matrice_analytics/post_processing/usecases/age_detection.py +842 -0
  90. matrice_analytics/post_processing/usecases/age_gender_detection.py +1085 -0
  91. matrice_analytics/post_processing/usecases/anti_spoofing_detection.py +656 -0
  92. matrice_analytics/post_processing/usecases/assembly_line_detection.py +841 -0
  93. matrice_analytics/post_processing/usecases/banana_defect_detection.py +624 -0
  94. matrice_analytics/post_processing/usecases/basic_counting_tracking.py +667 -0
  95. matrice_analytics/post_processing/usecases/blood_cancer_detection_img.py +881 -0
  96. matrice_analytics/post_processing/usecases/car_damage_detection.py +834 -0
  97. matrice_analytics/post_processing/usecases/car_part_segmentation.py +946 -0
  98. matrice_analytics/post_processing/usecases/car_service.py +1601 -0
  99. matrice_analytics/post_processing/usecases/cardiomegaly_classification.py +864 -0
  100. matrice_analytics/post_processing/usecases/cell_microscopy_segmentation.py +897 -0
  101. matrice_analytics/post_processing/usecases/chicken_pose_detection.py +648 -0
  102. matrice_analytics/post_processing/usecases/child_monitoring.py +814 -0
  103. matrice_analytics/post_processing/usecases/color/clip.py +660 -0
  104. matrice_analytics/post_processing/usecases/color/clip_processor/merges.txt +48895 -0
  105. matrice_analytics/post_processing/usecases/color/clip_processor/preprocessor_config.json +28 -0
  106. matrice_analytics/post_processing/usecases/color/clip_processor/special_tokens_map.json +30 -0
  107. matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer.json +245079 -0
  108. matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer_config.json +32 -0
  109. matrice_analytics/post_processing/usecases/color/clip_processor/vocab.json +1 -0
  110. matrice_analytics/post_processing/usecases/color/color_map_utils.py +70 -0
  111. matrice_analytics/post_processing/usecases/color/color_mapper.py +468 -0
  112. matrice_analytics/post_processing/usecases/color_detection.py +1936 -0
  113. matrice_analytics/post_processing/usecases/color_map_utils.py +70 -0
  114. matrice_analytics/post_processing/usecases/concrete_crack_detection.py +827 -0
  115. matrice_analytics/post_processing/usecases/crop_weed_detection.py +781 -0
  116. matrice_analytics/post_processing/usecases/customer_service.py +1008 -0
  117. matrice_analytics/post_processing/usecases/defect_detection_products.py +936 -0
  118. matrice_analytics/post_processing/usecases/distracted_driver_detection.py +822 -0
  119. matrice_analytics/post_processing/usecases/drone_traffic_monitoring.py +585 -0
  120. matrice_analytics/post_processing/usecases/drowsy_driver_detection.py +829 -0
  121. matrice_analytics/post_processing/usecases/dwell_detection.py +829 -0
  122. matrice_analytics/post_processing/usecases/emergency_vehicle_detection.py +827 -0
  123. matrice_analytics/post_processing/usecases/face_emotion.py +813 -0
  124. matrice_analytics/post_processing/usecases/face_recognition.py +827 -0
  125. matrice_analytics/post_processing/usecases/fashion_detection.py +835 -0
  126. matrice_analytics/post_processing/usecases/field_mapping.py +902 -0
  127. matrice_analytics/post_processing/usecases/fire_detection.py +1146 -0
  128. matrice_analytics/post_processing/usecases/flare_analysis.py +836 -0
  129. matrice_analytics/post_processing/usecases/flower_segmentation.py +1006 -0
  130. matrice_analytics/post_processing/usecases/gas_leak_detection.py +837 -0
  131. matrice_analytics/post_processing/usecases/gender_detection.py +832 -0
  132. matrice_analytics/post_processing/usecases/human_activity_recognition.py +871 -0
  133. matrice_analytics/post_processing/usecases/intrusion_detection.py +1672 -0
  134. matrice_analytics/post_processing/usecases/leaf.py +821 -0
  135. matrice_analytics/post_processing/usecases/leaf_disease.py +840 -0
  136. matrice_analytics/post_processing/usecases/leak_detection.py +837 -0
  137. matrice_analytics/post_processing/usecases/license_plate_detection.py +1188 -0
  138. matrice_analytics/post_processing/usecases/license_plate_monitoring.py +1781 -0
  139. matrice_analytics/post_processing/usecases/litter_monitoring.py +717 -0
  140. matrice_analytics/post_processing/usecases/mask_detection.py +869 -0
  141. matrice_analytics/post_processing/usecases/natural_disaster.py +907 -0
  142. matrice_analytics/post_processing/usecases/parking.py +787 -0
  143. matrice_analytics/post_processing/usecases/parking_space_detection.py +822 -0
  144. matrice_analytics/post_processing/usecases/pcb_defect_detection.py +888 -0
  145. matrice_analytics/post_processing/usecases/pedestrian_detection.py +808 -0
  146. matrice_analytics/post_processing/usecases/people_counting.py +706 -0
  147. matrice_analytics/post_processing/usecases/people_counting_bckp.py +1683 -0
  148. matrice_analytics/post_processing/usecases/people_tracking.py +1842 -0
  149. matrice_analytics/post_processing/usecases/pipeline_detection.py +605 -0
  150. matrice_analytics/post_processing/usecases/plaque_segmentation_img.py +874 -0
  151. matrice_analytics/post_processing/usecases/pothole_segmentation.py +915 -0
  152. matrice_analytics/post_processing/usecases/ppe_compliance.py +645 -0
  153. matrice_analytics/post_processing/usecases/price_tag_detection.py +822 -0
  154. matrice_analytics/post_processing/usecases/proximity_detection.py +1901 -0
  155. matrice_analytics/post_processing/usecases/road_lane_detection.py +623 -0
  156. matrice_analytics/post_processing/usecases/road_traffic_density.py +832 -0
  157. matrice_analytics/post_processing/usecases/road_view_segmentation.py +915 -0
  158. matrice_analytics/post_processing/usecases/shelf_inventory_detection.py +583 -0
  159. matrice_analytics/post_processing/usecases/shoplifting_detection.py +822 -0
  160. matrice_analytics/post_processing/usecases/shopping_cart_analysis.py +899 -0
  161. matrice_analytics/post_processing/usecases/skin_cancer_classification_img.py +864 -0
  162. matrice_analytics/post_processing/usecases/smoker_detection.py +833 -0
  163. matrice_analytics/post_processing/usecases/solar_panel.py +810 -0
  164. matrice_analytics/post_processing/usecases/suspicious_activity_detection.py +1030 -0
  165. matrice_analytics/post_processing/usecases/template_usecase.py +380 -0
  166. matrice_analytics/post_processing/usecases/theft_detection.py +648 -0
  167. matrice_analytics/post_processing/usecases/traffic_sign_monitoring.py +724 -0
  168. matrice_analytics/post_processing/usecases/underground_pipeline_defect_detection.py +775 -0
  169. matrice_analytics/post_processing/usecases/underwater_pollution_detection.py +842 -0
  170. matrice_analytics/post_processing/usecases/vehicle_monitoring.py +1029 -0
  171. matrice_analytics/post_processing/usecases/warehouse_object_segmentation.py +899 -0
  172. matrice_analytics/post_processing/usecases/waterbody_segmentation.py +923 -0
  173. matrice_analytics/post_processing/usecases/weapon_detection.py +771 -0
  174. matrice_analytics/post_processing/usecases/weld_defect_detection.py +615 -0
  175. matrice_analytics/post_processing/usecases/wildlife_monitoring.py +898 -0
  176. matrice_analytics/post_processing/usecases/windmill_maintenance.py +834 -0
  177. matrice_analytics/post_processing/usecases/wound_segmentation.py +856 -0
  178. matrice_analytics/post_processing/utils/__init__.py +150 -0
  179. matrice_analytics/post_processing/utils/advanced_counting_utils.py +400 -0
  180. matrice_analytics/post_processing/utils/advanced_helper_utils.py +317 -0
  181. matrice_analytics/post_processing/utils/advanced_tracking_utils.py +461 -0
  182. matrice_analytics/post_processing/utils/alerting_utils.py +213 -0
  183. matrice_analytics/post_processing/utils/category_mapping_utils.py +94 -0
  184. matrice_analytics/post_processing/utils/color_utils.py +592 -0
  185. matrice_analytics/post_processing/utils/counting_utils.py +182 -0
  186. matrice_analytics/post_processing/utils/filter_utils.py +261 -0
  187. matrice_analytics/post_processing/utils/format_utils.py +293 -0
  188. matrice_analytics/post_processing/utils/geometry_utils.py +300 -0
  189. matrice_analytics/post_processing/utils/smoothing_utils.py +358 -0
  190. matrice_analytics/post_processing/utils/tracking_utils.py +234 -0
  191. matrice_analytics/py.typed +0 -0
  192. matrice_analytics-0.1.60.dist-info/METADATA +481 -0
  193. matrice_analytics-0.1.60.dist-info/RECORD +196 -0
  194. matrice_analytics-0.1.60.dist-info/WHEEL +5 -0
  195. matrice_analytics-0.1.60.dist-info/licenses/LICENSE.txt +21 -0
  196. 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()