openvisionkit 0.4.0__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.
@@ -0,0 +1,679 @@
1
+ import json
2
+ import os
3
+ import sys
4
+ from datetime import datetime
5
+
6
+ import cv2
7
+
8
+ """
9
+ Form ROI Annotator
10
+ - Click two diagonal corners to define a form field ROI
11
+ - Choose field type + label when prompted
12
+ - Keyboard shortcuts for undo, delete, edit, clear, save, list, help
13
+ - Auto-saves after every change + timestamped backups
14
+ - Output format: roi = [[(x1,y1), (x2,y2), "type", "label","category"], ...]
15
+
16
+ Undo last ROI => Press u
17
+ Delete specific ROI => Press d (shows numbered list)
18
+ Edit existing ROI => Press e (change type or label)
19
+ Auto-save with timestamp => Every change + final exit (JSON + overlaid PNG)
20
+ Always saves annotated_rois_latest.json (easy to continue)
21
+ Load previous session => Pass JSON as second argument
22
+ Right-click to cancel current selection
23
+ Clear all => Press c (with confirmation)
24
+ Manual save anytime => Press s
25
+ List ROIs => Press l
26
+ Help menu => Press h
27
+
28
+ Other features to consider:
29
+ Index numbers shown on image for easy identification
30
+ Custom field types supported
31
+ More field types pre-loaded
32
+ Annotated image export with all boxes/labels
33
+
34
+
35
+ Usage:
36
+ python form_roi_annotator.py your_form.jpg
37
+ # or continue from previous:
38
+ python form_roi_annotator.py your_form.jpg annotated_rois_latest.json
39
+
40
+ """
41
+
42
+
43
+ class FormROIAnnotator:
44
+ def __init__(self, image_path: str, load_path: str = None):
45
+ self.image_path = image_path
46
+ self.image = cv2.imread(image_path)
47
+ if self.image is None:
48
+ raise FileNotFoundError(f"Could not load image: {image_path}")
49
+
50
+ self.clone = self.image.copy()
51
+ self.rois: list = [] # Now: [[(x1,y1), (x2,y2), "type", "label", "category"], ...]
52
+ self.current_points: list[tuple[int, int]] = []
53
+
54
+ # Supported field types
55
+ self.field_types = [
56
+ "text",
57
+ "textbox",
58
+ "box",
59
+ "checkbox",
60
+ "radio",
61
+ "daterange",
62
+ "date_range",
63
+ "table",
64
+ "table_cell",
65
+ "signature",
66
+ "dropdown",
67
+ "header",
68
+ "footer",
69
+ "paragraph",
70
+ "logo",
71
+ "line",
72
+ "custom",
73
+ ]
74
+
75
+ # Load existing annotations if provided
76
+ if load_path and os.path.exists(load_path):
77
+ try:
78
+ with open(load_path) as f:
79
+ self.rois = json.load(f)
80
+ print(f"✅ Loaded {len(self.rois)} existing ROIs from {load_path}")
81
+ self._redraw_all_rois()
82
+ except Exception as e:
83
+ print(f"⚠️ Could not load existing ROIs: {e}")
84
+
85
+ def mouse_callback(self, event, x, y, flags, param):
86
+ if event == cv2.EVENT_LBUTTONDOWN:
87
+ self.current_points.append((x, y))
88
+ cv2.circle(self.clone, (x, y), 6, (0, 255, 255), -1)
89
+ cv2.imshow("Form ROI Annotator", self.clone)
90
+
91
+ if len(self.current_points) == 2:
92
+ pt1 = self.current_points[0]
93
+ pt2 = self.current_points[1]
94
+
95
+ x1 = min(pt1[0], pt2[0])
96
+ y1 = min(pt1[1], pt2[1])
97
+ x2 = max(pt1[0], pt2[0])
98
+ y2 = max(pt1[1], pt2[1])
99
+
100
+ self._select_and_add_roi(x1, y1, x2, y2)
101
+
102
+ self.current_points.clear()
103
+ self.clone = self.image.copy()
104
+ self._redraw_all_rois()
105
+
106
+ elif event == cv2.EVENT_RBUTTONDOWN and self.current_points:
107
+ self.current_points.clear()
108
+ self.clone = self.image.copy()
109
+ self._redraw_all_rois()
110
+ print("🗑️ Current selection cancelled.")
111
+
112
+ def _select_and_add_roi(self, x1: int, y1: int, x2: int, y2: int):
113
+ print("\n" + "=" * 75)
114
+ print("Select Form Field Type:")
115
+ for i, t in enumerate(self.field_types, 1):
116
+ print(f" {i:2d}. {t}")
117
+ print(" 0. Custom type")
118
+
119
+ while True:
120
+ try:
121
+ choice = int(input("\nEnter number for field type: "))
122
+ if choice == 0:
123
+ field_type = input("Enter custom field type: ").strip() or "custom"
124
+ break
125
+ if 1 <= choice <= len(self.field_types):
126
+ field_type = self.field_types[choice - 1]
127
+ break
128
+ print("Invalid number.")
129
+ except ValueError:
130
+ print("Please enter a valid number.")
131
+
132
+ # Label (what is written inside / on the field)
133
+ label = input(
134
+ "Enter label/text visible on field (press Enter to skip): "
135
+ ).strip()
136
+
137
+ # NEW: Category (semantic group / section the field belongs to)
138
+ print(
139
+ "\nEnter Category (e.g., Personal Info, Contact, Meal Preference, Satisfaction, etc.)"
140
+ )
141
+ category = input("Category: ").strip()
142
+ if not category:
143
+ category = "Uncategorized"
144
+
145
+ # Store in new 5-element format
146
+ self.rois.append([(x1, y1), (x2, y2), field_type, label, category])
147
+
148
+ print(
149
+ f"✅ ROI Added → Type: {field_type} | Label: '{label}' | Category: '{category}'"
150
+ )
151
+
152
+ self._save_latest()
153
+
154
+ def _redraw_all_rois(self):
155
+ self.clone = self.image.copy()
156
+
157
+ for idx, roi in enumerate(self.rois, 1):
158
+ (x1, y1), (x2, y2), ftype, label, category = roi
159
+ color = self._get_color(ftype)
160
+
161
+ cv2.rectangle(self.clone, (x1, y1), (x2, y2), color, 3)
162
+
163
+ # Main display text: Type + Label
164
+ display_text = f"{ftype}: {label}" if label else ftype
165
+ cv2.putText(
166
+ self.clone,
167
+ display_text,
168
+ (x1, max(15, y1 - 10)),
169
+ cv2.FONT_HERSHEY_SIMPLEX,
170
+ 0.65,
171
+ color,
172
+ 2,
173
+ cv2.LINE_AA,
174
+ )
175
+
176
+ # Show Category below the box
177
+ if category and category != "Uncategorized":
178
+ cv2.putText(
179
+ self.clone,
180
+ f"[{category}]",
181
+ (x1, y2 + 25),
182
+ cv2.FONT_HERSHEY_SIMPLEX,
183
+ 0.55,
184
+ (200, 200, 255),
185
+ 2,
186
+ cv2.LINE_AA,
187
+ )
188
+
189
+ # Index number
190
+ cv2.putText(
191
+ self.clone,
192
+ str(idx),
193
+ (x1 + 8, y1 + 28),
194
+ cv2.FONT_HERSHEY_SIMPLEX,
195
+ 0.85,
196
+ (255, 255, 255),
197
+ 2,
198
+ )
199
+ cv2.putText(
200
+ self.clone,
201
+ str(idx),
202
+ (x1 + 8, y1 + 28),
203
+ cv2.FONT_HERSHEY_SIMPLEX,
204
+ 0.85,
205
+ color,
206
+ 1,
207
+ )
208
+
209
+ cv2.setWindowTitle(
210
+ "Form ROI Annotator", f"Form ROI Annotator — {len(self.rois)} ROIs"
211
+ )
212
+
213
+ def _get_color(self, ftype: str):
214
+ color_map = {
215
+ "text": (0, 255, 0),
216
+ "textbox": (0, 165, 255),
217
+ "box": (255, 0, 0),
218
+ "checkbox": (255, 0, 255),
219
+ "radio": (0, 255, 255),
220
+ "daterange": (255, 140, 0),
221
+ "table": (128, 0, 255),
222
+ "table_cell": (255, 0, 128),
223
+ "signature": (0, 128, 255),
224
+ "dropdown": (100, 200, 50),
225
+ "header": (0, 100, 200),
226
+ "footer": (200, 100, 0),
227
+ "custom": (255, 255, 0),
228
+ }
229
+ return color_map.get(ftype, (255, 255, 0))
230
+
231
+ def _undo_last(self):
232
+ if not self.rois:
233
+ print("⚠️ No ROIs to undo.")
234
+ return
235
+ removed = self.rois.pop()
236
+ print(f"🗑️ Undid last ROI: {removed}")
237
+ self._redraw_all_rois()
238
+ self._save_latest()
239
+
240
+ def _delete_specific(self):
241
+ if not self.rois:
242
+ print("⚠️ No ROIs.")
243
+ return
244
+ print("\nCurrent ROIs:")
245
+ for i, r in enumerate(self.rois, 1):
246
+ print(f" {i:2d}. {r[2]:10} | Label: '{r[3]}' | Category: '{r[4]}'")
247
+ try:
248
+ idx = int(input("\nEnter ROI number to delete (0 to cancel): "))
249
+ if 1 <= idx <= len(self.rois):
250
+ self.rois.pop(idx - 1)
251
+ print(f"🗑️ Deleted ROI #{idx}")
252
+ self._redraw_all_rois()
253
+ self._save_latest()
254
+ except ValueError:
255
+ print("Invalid input.")
256
+
257
+ def _edit_roi(self):
258
+ if not self.rois:
259
+ print("⚠️ No ROIs to edit.")
260
+ return
261
+
262
+ print("\nCurrent ROIs:")
263
+ for i, r in enumerate(self.rois, 1):
264
+ print(f" {i:2d}. {r[2]:10} | Label: '{r[3]}' | Category: '{r[4]}'")
265
+
266
+ try:
267
+ idx = int(input("\nEnter ROI number to edit (0 to cancel): "))
268
+ if idx == 0 or not (1 <= idx <= len(self.rois)):
269
+ return
270
+
271
+ roi = self.rois[idx - 1]
272
+ _, _, ftype, label, category = roi
273
+
274
+ print(
275
+ f"\nEditing ROI #{idx} (Current: Type={ftype}, Label='{label}', Category='{category}')"
276
+ )
277
+
278
+ # Re-select type
279
+ choice_str = input("New type number (Enter to keep): ").strip()
280
+ if choice_str:
281
+ try:
282
+ ch = int(choice_str)
283
+ if ch == 0:
284
+ new_type = input("Custom type: ").strip() or "custom"
285
+ elif 1 <= ch <= len(self.field_types):
286
+ new_type = self.field_types[ch - 1]
287
+ else:
288
+ new_type = ftype
289
+ except (ValueError, IndexError):
290
+ new_type = ftype
291
+ else:
292
+ new_type = ftype
293
+
294
+ new_label = input(f"New label (current: '{label}'): ").strip()
295
+ if not new_label:
296
+ new_label = label
297
+
298
+ new_category = input(f"New category (current: '{category}'): ").strip()
299
+ if not new_category:
300
+ new_category = category
301
+
302
+ self.rois[idx - 1] = [roi[0], roi[1], new_type, new_label, new_category]
303
+ print(f"✅ ROI #{idx} updated successfully.")
304
+ self._redraw_all_rois()
305
+ self._save_latest()
306
+ except Exception as e:
307
+ print(f"Error during edit: {e}")
308
+
309
+ def _save_latest(self):
310
+ with open("annotated_rois_latest.json", "w") as f:
311
+ json.dump(self.rois, f, indent=2)
312
+
313
+ def _save_with_timestamp(self):
314
+ ts = datetime.now().strftime("%Y%m%d_%H%M%S")
315
+ json_file = f"annotated_rois_{ts}.json"
316
+ png_file = f"annotated_image_{ts}.png"
317
+
318
+ with open(json_file, "w") as f:
319
+ json.dump(self.rois, f, indent=2)
320
+
321
+ # Save visual image
322
+ vis = self.image.copy()
323
+ for roi in self.rois:
324
+ (x1, y1), (x2, y2), ftype, label, category = roi
325
+ color = self._get_color(ftype)
326
+ cv2.rectangle(vis, (x1, y1), (x2, y2), color, 3)
327
+ text = f"{ftype}: {label}" if label else ftype
328
+ cv2.putText(
329
+ vis,
330
+ text,
331
+ (x1, max(10, y1 - 8)),
332
+ cv2.FONT_HERSHEY_SIMPLEX,
333
+ 0.65,
334
+ color,
335
+ 2,
336
+ )
337
+
338
+ if category and category != "Uncategorized":
339
+ cv2.putText(
340
+ vis,
341
+ f"[{category}]",
342
+ (x1, y2 + 22),
343
+ cv2.FONT_HERSHEY_SIMPLEX,
344
+ 0.55,
345
+ (200, 200, 255),
346
+ 2,
347
+ )
348
+
349
+ cv2.imwrite(png_file, vis)
350
+ print(f"💾 Timestamped backup saved: {json_file} & {png_file}")
351
+
352
+ def run(self):
353
+ print("\n" + "=" * 95)
354
+ print("🚀 FORM ROI ANNOTATOR with Category Support")
355
+ print("=" * 95)
356
+ print("Now each ROI includes: [ (x1,y1), (x2,y2), type, label, category ]")
357
+ print("\nKeyboard Shortcuts:")
358
+ print(" u = Undo last | d = Delete specific | e = Edit ROI")
359
+ print(" c = Clear all | s = Manual save | l = List ROIs")
360
+ print(" h = Help | ESC = Finish & Exit")
361
+ print("=" * 95)
362
+
363
+ cv2.namedWindow("Form ROI Annotator", cv2.WINDOW_NORMAL)
364
+ cv2.setMouseCallback("Form ROI Annotator", self.mouse_callback)
365
+
366
+ while True:
367
+ cv2.imshow("Form ROI Annotator", self.clone)
368
+ key = cv2.waitKey(1) & 0xFF
369
+
370
+ if key == 27: # ESC
371
+ break
372
+ elif key == ord("u"):
373
+ self._undo_last()
374
+ elif key == ord("d"):
375
+ self._delete_specific()
376
+ elif key == ord("e"):
377
+ self._edit_roi()
378
+ elif key == ord("c"):
379
+ if (
380
+ input("Clear ALL ROIs? Type YES to confirm: ").strip().upper()
381
+ == "YES"
382
+ ):
383
+ self.rois.clear()
384
+ self.clone = self.image.copy()
385
+ self._redraw_all_rois()
386
+ self._save_latest()
387
+ elif key == ord("s"):
388
+ self._save_with_timestamp()
389
+ elif key == ord("l"):
390
+ print("\nCurrent ROIs:")
391
+ for i, r in enumerate(self.rois, 1):
392
+ print(f" {i:2d}. {r}")
393
+ elif key == ord("h"):
394
+ print("Use mouse + shortcuts as shown above.")
395
+
396
+ cv2.destroyAllWindows()
397
+
398
+ self._save_latest()
399
+ self._save_with_timestamp()
400
+
401
+ print("\n" + "=" * 95)
402
+ print("ANNOTATION COMPLETED!")
403
+ print("Final ROI (with category):")
404
+ print("roi =", self.rois)
405
+ print("\nSaved files: annotated_rois_latest.json + timestamped backups")
406
+ print(" • Multiple timestamped backups + annotated images")
407
+
408
+ # ─────────────────────────── NEW METHODS ───────────────────────────
409
+
410
+ def export_rois_to_json(self, path: str = "annotated_rois_export.json") -> str:
411
+ """Save current ROI list to a JSON file (public API wrapper around _save_latest).
412
+
413
+ Args:
414
+ path: Output file path.
415
+ Returns:
416
+ str: Absolute path of the written file.
417
+ """
418
+ import os
419
+
420
+ with open(path, "w") as f:
421
+ json.dump(self.rois, f, indent=2)
422
+ return os.path.abspath(path)
423
+
424
+ def import_from_json(self, path: str):
425
+ """Load ROI annotations from a JSON file and redraw the canvas.
426
+
427
+ Args:
428
+ path: Path to a JSON file previously exported by export_to_json().
429
+ """
430
+ with open(path) as f:
431
+ self.rois = json.load(f)
432
+ self._redraw_all_rois()
433
+ print(f"Loaded {len(self.rois)} ROIs from {path}")
434
+
435
+ def get_rois_by_category(self, category: str):
436
+ """Return all ROIs belonging to a given category (case-insensitive).
437
+
438
+ Args:
439
+ category: Category string (e.g. 'Personal Info').
440
+ Returns:
441
+ List of ROI entries matching the category.
442
+ """
443
+ cat_lower = category.lower()
444
+ return [r for r in self.rois if r[4].lower() == cat_lower]
445
+
446
+ def get_rois_by_type(self, field_type: str):
447
+ """Return all ROIs with the specified field type (case-insensitive).
448
+
449
+ Args:
450
+ field_type: e.g. 'checkbox', 'text', 'signature'.
451
+ Returns:
452
+ List of ROI entries matching the type.
453
+ """
454
+ ft_lower = field_type.lower()
455
+ return [r for r in self.rois if r[2].lower() == ft_lower]
456
+
457
+ def count_by_type(self) -> dict:
458
+ """Return a count of each field type present in the current annotations.
459
+
460
+ Returns:
461
+ dict: {'checkbox': 5, 'text': 3, ...}
462
+ """
463
+ counts: dict = {}
464
+ for roi in self.rois:
465
+ ft = roi[2]
466
+ counts[ft] = counts.get(ft, 0) + 1
467
+ return counts
468
+
469
+ def count_by_category(self) -> dict:
470
+ """Return a count of ROIs per category.
471
+
472
+ Returns:
473
+ dict: {'Personal Info': 4, 'Contact': 2, ...}
474
+ """
475
+ counts: dict = {}
476
+ for roi in self.rois:
477
+ cat = roi[4]
478
+ counts[cat] = counts.get(cat, 0) + 1
479
+ return counts
480
+
481
+ def to_dataframe(self):
482
+ """Convert current annotations to a pandas DataFrame.
483
+ Columns: x1, y1, x2, y2, field_type, label, category.
484
+
485
+ Returns:
486
+ pandas.DataFrame
487
+ """
488
+ import pandas as pd
489
+
490
+ rows = []
491
+ for roi in self.rois:
492
+ (x1, y1), (x2, y2), ftype, label, category = roi
493
+ rows.append(
494
+ {
495
+ "x1": x1,
496
+ "y1": y1,
497
+ "x2": x2,
498
+ "y2": y2,
499
+ "field_type": ftype,
500
+ "label": label,
501
+ "category": category,
502
+ }
503
+ )
504
+ return pd.DataFrame(rows)
505
+
506
+ def validate_rois(self) -> list:
507
+ """Check annotations for common issues: zero-area boxes, missing labels, overlaps.
508
+
509
+ Returns:
510
+ List[str]: Human-readable issue descriptions. Empty list = no issues.
511
+ """
512
+ issues = []
513
+ for i, roi in enumerate(self.rois, 1):
514
+ (x1, y1), (x2, y2), ftype, label, category = roi
515
+ if x2 <= x1 or y2 <= y1:
516
+ issues.append(f"ROI #{i} ({ftype}): zero or negative area.")
517
+ if not label:
518
+ issues.append(f"ROI #{i} ({ftype}): missing label.")
519
+
520
+ # Simple O(n²) overlap check
521
+ for i in range(len(self.rois)):
522
+ for j in range(i + 1, len(self.rois)):
523
+ (ax1, ay1), (ax2, ay2) = self.rois[i][0], self.rois[i][1]
524
+ (bx1, by1), (bx2, by2) = self.rois[j][0], self.rois[j][1]
525
+ ix1, iy1 = max(ax1, bx1), max(ay1, by1)
526
+ ix2, iy2 = min(ax2, bx2), min(ay2, by2)
527
+ if ix2 > ix1 and iy2 > iy1:
528
+ inter = (ix2 - ix1) * (iy2 - iy1)
529
+ area_a = (ax2 - ax1) * (ay2 - ay1)
530
+ iou = inter / max(area_a, 1)
531
+ if iou > 0.5:
532
+ issues.append(
533
+ f"ROI #{i + 1} and #{j + 1} overlap significantly (IoU={iou:.2f})."
534
+ )
535
+ return issues
536
+
537
+ def get_summary(self) -> str:
538
+ """Return a compact text summary of current annotations.
539
+
540
+ Returns:
541
+ str: Multi-line summary string.
542
+ """
543
+ lines = [f"Total ROIs: {len(self.rois)}"]
544
+ for ft, count in self.count_by_type().items():
545
+ lines.append(f" {ft}: {count}")
546
+ lines.append("Categories:")
547
+ for cat, count in self.count_by_category().items():
548
+ lines.append(f" {cat}: {count}")
549
+ return "\n".join(lines)
550
+
551
+ def get_annotations_by_type(self, annotations, field_type) -> list:
552
+ """Filter annotations to those matching a given field type.
553
+
554
+ Args:
555
+ annotations: List of annotation entries in
556
+ [(x1,y1), (x2,y2), type, label, category] format.
557
+ field_type: String field type to filter by (e.g. "checkbox").
558
+ Returns:
559
+ List of matching annotation entries.
560
+ """
561
+ return [a for a in annotations if a[2] == field_type]
562
+
563
+ def export_to_json(self, annotations, path) -> None:
564
+ """Serialize annotations to a JSON file.
565
+
566
+ Args:
567
+ annotations: List of annotation entries in
568
+ [(x1,y1), (x2,y2), type, label, category] format.
569
+ path: Output file path string.
570
+ """
571
+ data = [
572
+ {
573
+ "x1": a[0][0],
574
+ "y1": a[0][1],
575
+ "x2": a[1][0],
576
+ "y2": a[1][1],
577
+ "type": a[2],
578
+ "label": a[3],
579
+ "category": a[4],
580
+ }
581
+ for a in annotations
582
+ ]
583
+ with open(path, "w") as f:
584
+ json.dump(data, f, indent=2)
585
+
586
+ def get_annotation_count(self, annotations) -> dict:
587
+ """Return count of annotations per field type.
588
+
589
+ Args:
590
+ annotations: List of annotation entries.
591
+ Returns:
592
+ dict mapping field type string to integer count.
593
+ """
594
+ from collections import Counter
595
+
596
+ return dict(Counter(a[2] for a in annotations))
597
+
598
+ def merge_annotations(self, ann_list_a, ann_list_b) -> list:
599
+ """Merge two annotation lists, deduplicating by IoU > 0.5.
600
+
601
+ Annotations in ann_list_a are kept as-is. Each annotation in
602
+ ann_list_b is added only if it does not overlap (IoU > 0.5) with
603
+ any already-merged annotation.
604
+
605
+ Args:
606
+ ann_list_a: First list of annotation entries.
607
+ ann_list_b: Second list of annotation entries.
608
+ Returns:
609
+ Merged list with duplicates removed.
610
+ """
611
+
612
+ def iou(a, b):
613
+ ax1, ay1 = a[0]
614
+ ax2, ay2 = a[1]
615
+ bx1, by1 = b[0]
616
+ bx2, by2 = b[1]
617
+ ix1 = max(ax1, bx1)
618
+ iy1 = max(ay1, by1)
619
+ ix2 = min(ax2, bx2)
620
+ iy2 = min(ay2, by2)
621
+ inter = max(0, ix2 - ix1) * max(0, iy2 - iy1)
622
+ if inter == 0:
623
+ return 0.0
624
+ area_a = max(1, (ax2 - ax1) * (ay2 - ay1))
625
+ area_b = max(1, (bx2 - bx1) * (by2 - by1))
626
+ return inter / (area_a + area_b - inter)
627
+
628
+ merged = list(ann_list_a)
629
+ for b in ann_list_b:
630
+ if not any(iou(a, b) > 0.5 for a in merged):
631
+ merged.append(b)
632
+ return merged
633
+
634
+ def draw_annotation_summary(self, image, annotations):
635
+ """Draw a type-count summary overlay on the image.
636
+
637
+ Args:
638
+ image: BGR numpy array.
639
+ annotations: List of annotation entries.
640
+ Returns:
641
+ Annotated BGR numpy array (copy of input).
642
+ """
643
+ out = image.copy()
644
+ counts = self.get_annotation_count(annotations)
645
+ lines = [f"{t}: {c}" for t, c in counts.items()]
646
+ box_h = 20 + len(lines) * 22
647
+ cv2.rectangle(out, (5, 5), (180, box_h), (0, 0, 0), -1)
648
+ for i, line in enumerate(lines):
649
+ cv2.putText(
650
+ out,
651
+ line,
652
+ (10, 22 + i * 22),
653
+ cv2.FONT_HERSHEY_SIMPLEX,
654
+ 0.5,
655
+ (255, 255, 255),
656
+ 1,
657
+ )
658
+ return out
659
+
660
+
661
+ if __name__ == "__main__":
662
+ if len(sys.argv) < 2:
663
+ print("Usage:")
664
+ print(" python form_roi_annotator.py <image_path> [existing_rois.json]")
665
+ print("Example:")
666
+ print(" python form_roi_annotator.py form_sample.jpg")
667
+ print(
668
+ " python form_roi_annotator.py form_sample.jpg annotated_rois_latest.json"
669
+ )
670
+ sys.exit(1)
671
+
672
+ image_path = sys.argv[1]
673
+ load_path = sys.argv[2] if len(sys.argv) > 2 else None
674
+
675
+ try:
676
+ annotator = FormROIAnnotator(image_path, load_path)
677
+ annotator.run()
678
+ except Exception as e:
679
+ print(f"Error: {e}")