matrice-analytics 0.1.2__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.
Potentially problematic release.
This version of matrice-analytics might be problematic. Click here for more details.
- 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 +142 -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 +3188 -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 +681 -0
- matrice_analytics/post_processing/face_reg/face_recognition.py +1870 -0
- matrice_analytics/post_processing/face_reg/face_recognition_client.py +339 -0
- matrice_analytics/post_processing/face_reg/people_activity_logging.py +283 -0
- matrice_analytics/post_processing/ocr/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/easyocr_extractor.py +248 -0
- matrice_analytics/post_processing/ocr/postprocessing.py +271 -0
- matrice_analytics/post_processing/ocr/preprocessing.py +52 -0
- matrice_analytics/post_processing/post_processor.py +1153 -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_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 +1043 -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 +232 -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 +1835 -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 +930 -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 +1112 -0
- matrice_analytics/post_processing/usecases/flare_analysis.py +891 -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 +914 -0
- matrice_analytics/post_processing/usecases/license_plate_monitoring.py +1194 -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 +1728 -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 +950 -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.2.dist-info/METADATA +481 -0
- matrice_analytics-0.1.2.dist-info/RECORD +160 -0
- matrice_analytics-0.1.2.dist-info/WHEEL +5 -0
- matrice_analytics-0.1.2.dist-info/licenses/LICENSE.txt +21 -0
- matrice_analytics-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import easyocr
|
|
2
|
+
import numpy as np
|
|
3
|
+
import torch
|
|
4
|
+
|
|
5
|
+
class EasyOCRExtractor:
|
|
6
|
+
def __init__(self, lang=['en', 'hi', 'ar'], gpu=False, model_storage_directory=None,
|
|
7
|
+
download_enabled=True, detector=True, recognizer=True, verbose=False):
|
|
8
|
+
"""
|
|
9
|
+
Initializes the EasyOCR text extractor with optimized parameters.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
lang (str or list): Language(s) to be used by EasyOCR. Default is ['en', 'hi', 'ar'].
|
|
13
|
+
gpu (bool): Enable GPU acceleration if available. Default is True.
|
|
14
|
+
model_storage_directory (str): Custom path to store models. Default is None.
|
|
15
|
+
download_enabled (bool): Allow downloading models if not found. Default is True.
|
|
16
|
+
detector (bool): Load text detection model. Default is True.
|
|
17
|
+
recognizer (bool): Load text recognition model. Default is True.
|
|
18
|
+
verbose (bool): Enable verbose output (e.g., progress bars). Default is False.
|
|
19
|
+
"""
|
|
20
|
+
self.lang = lang
|
|
21
|
+
self.gpu = gpu
|
|
22
|
+
# Check if GPU is available
|
|
23
|
+
if torch.cuda.is_available():
|
|
24
|
+
self.gpu = True
|
|
25
|
+
else:
|
|
26
|
+
self.gpu = False
|
|
27
|
+
self.model_storage_directory = model_storage_directory
|
|
28
|
+
self.download_enabled = download_enabled
|
|
29
|
+
self.detector = detector
|
|
30
|
+
self.recognizer = recognizer
|
|
31
|
+
self.verbose = verbose
|
|
32
|
+
self.reader = None
|
|
33
|
+
|
|
34
|
+
def setup(self):
|
|
35
|
+
"""
|
|
36
|
+
Initializes the EasyOCR reader if not already initialized.
|
|
37
|
+
"""
|
|
38
|
+
if self.reader is None:
|
|
39
|
+
lang_list = [self.lang] if isinstance(self.lang, str) else self.lang
|
|
40
|
+
self.reader = easyocr.Reader(
|
|
41
|
+
lang_list=lang_list,
|
|
42
|
+
gpu=self.gpu,
|
|
43
|
+
model_storage_directory=self.model_storage_directory,
|
|
44
|
+
download_enabled=self.download_enabled,
|
|
45
|
+
detector=self.detector,
|
|
46
|
+
recognizer=self.recognizer,
|
|
47
|
+
verbose=self.verbose
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def extract(self, image_np, bboxes=None, detail=1, paragraph=False,
|
|
51
|
+
decoder='greedy', beam_width=5, batch_size=1, workers=0,
|
|
52
|
+
allowlist=None, blocklist=None, min_size=10, rotation_info=None,
|
|
53
|
+
contrast_ths=0.1, adjust_contrast=0.5, text_threshold=0.7,
|
|
54
|
+
low_text=0.4, link_threshold=0.4, canvas_size=2560, mag_ratio=1.0,
|
|
55
|
+
slope_ths=0.1, ycenter_ths=0.5, height_ths=0.5, width_ths=0.5,
|
|
56
|
+
add_margin=0.1):
|
|
57
|
+
"""
|
|
58
|
+
Extracts text from the given image or specific regions within the bounding boxes
|
|
59
|
+
with configurable parameters for optimal performance.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
image_np (np.ndarray): Input image as a numpy array.
|
|
63
|
+
bboxes (list): List of bounding boxes. Each box is a list of [xmin, ymin, xmax, ymax].
|
|
64
|
+
If None, OCR is performed on the entire image.
|
|
65
|
+
detail (int): Set to 0 for simple output, 1 for detailed output.
|
|
66
|
+
paragraph (bool): Combine results into paragraphs.
|
|
67
|
+
decoder (str): Decoding method ('greedy', 'beamsearch', 'wordbeamsearch').
|
|
68
|
+
beam_width (int): How many beams to keep when using beam search decoders.
|
|
69
|
+
batch_size (int): Number of images to process in a batch.
|
|
70
|
+
workers (int): Number of worker threads for data loading.
|
|
71
|
+
allowlist (str): Force recognition of only specific characters.
|
|
72
|
+
blocklist (str): Block specific characters from recognition.
|
|
73
|
+
min_size (int): Filter text boxes smaller than this pixel size.
|
|
74
|
+
rotation_info (list): List of rotation angles to try (e.g., [90, 180, 270]).
|
|
75
|
+
contrast_ths (float): Threshold for contrast adjustment.
|
|
76
|
+
adjust_contrast (float): Target contrast level for low-contrast text.
|
|
77
|
+
text_threshold (float): Text confidence threshold.
|
|
78
|
+
low_text (float): Text low-bound score.
|
|
79
|
+
link_threshold (float): Link confidence threshold.
|
|
80
|
+
canvas_size (int): Maximum image size before resizing.
|
|
81
|
+
mag_ratio (float): Image magnification ratio.
|
|
82
|
+
slope_ths (float): Maximum slope for merging boxes.
|
|
83
|
+
ycenter_ths (float): Maximum y-center shift for merging boxes.
|
|
84
|
+
height_ths (float): Maximum height difference for merging boxes.
|
|
85
|
+
width_ths (float): Maximum width for horizontal merging.
|
|
86
|
+
add_margin (float): Margin to add around text boxes.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
list: OCR results containing text, confidence, and bounding boxes.
|
|
90
|
+
"""
|
|
91
|
+
# Make sure the reader is initialized
|
|
92
|
+
self.setup()
|
|
93
|
+
|
|
94
|
+
ocr_results = []
|
|
95
|
+
|
|
96
|
+
# Dictionary of readtext parameters
|
|
97
|
+
readtext_params = {
|
|
98
|
+
'decoder': decoder,
|
|
99
|
+
'beamWidth': beam_width,
|
|
100
|
+
'batch_size': batch_size,
|
|
101
|
+
'workers': workers,
|
|
102
|
+
'allowlist': allowlist,
|
|
103
|
+
'blocklist': blocklist,
|
|
104
|
+
'detail': detail,
|
|
105
|
+
'paragraph': paragraph,
|
|
106
|
+
'min_size': min_size,
|
|
107
|
+
'rotation_info': rotation_info,
|
|
108
|
+
'contrast_ths': contrast_ths,
|
|
109
|
+
'adjust_contrast': adjust_contrast,
|
|
110
|
+
'text_threshold': text_threshold,
|
|
111
|
+
'low_text': low_text,
|
|
112
|
+
'link_threshold': link_threshold,
|
|
113
|
+
'canvas_size': canvas_size,
|
|
114
|
+
'mag_ratio': mag_ratio,
|
|
115
|
+
'slope_ths': slope_ths,
|
|
116
|
+
'ycenter_ths': ycenter_ths,
|
|
117
|
+
'height_ths': height_ths,
|
|
118
|
+
'width_ths': width_ths,
|
|
119
|
+
'add_margin': add_margin
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# If no bounding boxes, perform OCR on the entire image
|
|
123
|
+
if bboxes is None:
|
|
124
|
+
text_data = self.reader.readtext(image_np, **readtext_params)
|
|
125
|
+
if detail == 0:
|
|
126
|
+
return text_data # Simple output format for detail=0
|
|
127
|
+
|
|
128
|
+
for bbox, text, conf in text_data:
|
|
129
|
+
ocr_results.append({
|
|
130
|
+
"text": text,
|
|
131
|
+
"confidence": conf,
|
|
132
|
+
"bounding_box": bbox
|
|
133
|
+
})
|
|
134
|
+
else:
|
|
135
|
+
# Perform OCR on each bounding box
|
|
136
|
+
for box in bboxes:
|
|
137
|
+
xmin, ymin, xmax, ymax = map(int, box)
|
|
138
|
+
cropped_img = image_np[ymin:ymax, xmin:xmax]
|
|
139
|
+
|
|
140
|
+
# Skip empty crops
|
|
141
|
+
if cropped_img.size == 0 or cropped_img.shape[0] == 0 or cropped_img.shape[1] == 0:
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
text_data = self.reader.readtext(cropped_img, **readtext_params)
|
|
145
|
+
|
|
146
|
+
if detail == 0:
|
|
147
|
+
# Adjust coordinates for the cropped region
|
|
148
|
+
adjusted_data = []
|
|
149
|
+
for result in text_data:
|
|
150
|
+
if isinstance(result, tuple) and len(result) >= 1:
|
|
151
|
+
# Adjust coordinates based on crop position
|
|
152
|
+
adjusted_bbox = [[pt[0] + xmin, pt[1] + ymin] for pt in result[0]]
|
|
153
|
+
if len(result) == 3: # (bbox, text, confidence)
|
|
154
|
+
adjusted_data.append((adjusted_bbox, result[1], result[2]))
|
|
155
|
+
elif len(result) == 2: # (bbox, text)
|
|
156
|
+
adjusted_data.append((adjusted_bbox, result[1]))
|
|
157
|
+
ocr_results.extend(adjusted_data)
|
|
158
|
+
else:
|
|
159
|
+
for bbox, text, conf in text_data:
|
|
160
|
+
# Adjust bounding box coordinates relative to the original image
|
|
161
|
+
adjusted_bbox = [
|
|
162
|
+
[pt[0] + xmin, pt[1] + ymin] for pt in bbox
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
ocr_results.append({
|
|
166
|
+
"text": text,
|
|
167
|
+
"confidence": conf,
|
|
168
|
+
"bounding_box": adjusted_bbox
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
return ocr_results
|
|
172
|
+
|
|
173
|
+
def detect_text_regions(self, image_np, min_size=10, text_threshold=0.7,
|
|
174
|
+
low_text=0.4, link_threshold=0.4, canvas_size=2560,
|
|
175
|
+
mag_ratio=1.0, slope_ths=0.1, ycenter_ths=0.5,
|
|
176
|
+
height_ths=0.5, width_ths=0.5, add_margin=0.1,
|
|
177
|
+
optimal_num_chars=None):
|
|
178
|
+
"""
|
|
179
|
+
Detects text regions in the image without performing recognition.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
image_np (np.ndarray): Input image as a numpy array.
|
|
183
|
+
min_size (int): Filter text boxes smaller than this pixel size.
|
|
184
|
+
text_threshold (float): Text confidence threshold.
|
|
185
|
+
low_text (float): Text low-bound score.
|
|
186
|
+
link_threshold (float): Link confidence threshold.
|
|
187
|
+
canvas_size (int): Maximum image size before resizing.
|
|
188
|
+
mag_ratio (float): Image magnification ratio.
|
|
189
|
+
slope_ths (float): Maximum slope for merging boxes.
|
|
190
|
+
ycenter_ths (float): Maximum y-center shift for merging boxes.
|
|
191
|
+
height_ths (float): Maximum height difference for merging boxes.
|
|
192
|
+
width_ths (float): Maximum width for horizontal merging.
|
|
193
|
+
add_margin (float): Margin to add around text boxes.
|
|
194
|
+
optimal_num_chars (int): Prioritize boxes with this estimated character count.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
tuple: (horizontal_list, free_list) containing text regions
|
|
198
|
+
"""
|
|
199
|
+
self.setup()
|
|
200
|
+
return self.reader.detect(
|
|
201
|
+
image_np,
|
|
202
|
+
min_size=min_size,
|
|
203
|
+
text_threshold=text_threshold,
|
|
204
|
+
low_text=low_text,
|
|
205
|
+
link_threshold=link_threshold,
|
|
206
|
+
canvas_size=canvas_size,
|
|
207
|
+
mag_ratio=mag_ratio,
|
|
208
|
+
slope_ths=slope_ths,
|
|
209
|
+
ycenter_ths=ycenter_ths,
|
|
210
|
+
height_ths=height_ths,
|
|
211
|
+
width_ths=width_ths,
|
|
212
|
+
add_margin=add_margin,
|
|
213
|
+
optimal_num_chars=optimal_num_chars
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def recognize_from_regions(self, image_np, horizontal_list=None, free_list=None,
|
|
217
|
+
decoder='greedy', beam_width=5, batch_size=1,
|
|
218
|
+
workers=0, allowlist=None, blocklist=None,
|
|
219
|
+
detail=1, paragraph=False, contrast_ths=0.1,
|
|
220
|
+
adjust_contrast=0.5):
|
|
221
|
+
"""
|
|
222
|
+
Recognizes text from previously detected regions.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
image_np (np.ndarray): Input image as a numpy array.
|
|
226
|
+
horizontal_list (list): List of rectangular regions [x_min, x_max, y_min, y_max].
|
|
227
|
+
free_list (list): List of free-form regions [[x1,y1],[x2,y2],[x3,y3],[x4,y4]].
|
|
228
|
+
Other parameters: Same as extract method.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
list: OCR results for the specified regions
|
|
232
|
+
"""
|
|
233
|
+
self.setup()
|
|
234
|
+
return self.reader.recognize(
|
|
235
|
+
image_np,
|
|
236
|
+
horizontal_list=horizontal_list,
|
|
237
|
+
free_list=free_list,
|
|
238
|
+
decoder=decoder,
|
|
239
|
+
beamWidth=beam_width,
|
|
240
|
+
batch_size=batch_size,
|
|
241
|
+
workers=workers,
|
|
242
|
+
allowlist=allowlist,
|
|
243
|
+
blocklist=blocklist,
|
|
244
|
+
detail=detail,
|
|
245
|
+
paragraph=paragraph,
|
|
246
|
+
contrast_ths=contrast_ths,
|
|
247
|
+
adjust_contrast=adjust_contrast
|
|
248
|
+
)
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
class TextPostprocessor:
|
|
5
|
+
def __init__(self, logging_level=logging.INFO):
|
|
6
|
+
"""
|
|
7
|
+
Initialize the text postprocessor with optional logging configuration.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
logging_level: The level of logging detail. Default is INFO.
|
|
11
|
+
"""
|
|
12
|
+
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging_level)
|
|
13
|
+
self.logger = logging.getLogger('TextPostprocessor')
|
|
14
|
+
|
|
15
|
+
self.task_processors = {
|
|
16
|
+
"license_plate": self._process_license_plate,
|
|
17
|
+
"license_plate_india": self._process_license_plate_india,
|
|
18
|
+
"license_plate_us": self._process_license_plate_us,
|
|
19
|
+
"license_plate_eu": self._process_license_plate_eu,
|
|
20
|
+
"license_plate_qatar": self._process_license_plate_qatar,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
self.char_substitutions = {
|
|
24
|
+
'O': '0',
|
|
25
|
+
'o': '0',
|
|
26
|
+
'I': '1',
|
|
27
|
+
'Z': '2',
|
|
28
|
+
'A': '4',
|
|
29
|
+
'L': '1',
|
|
30
|
+
'AV': 'AV',
|
|
31
|
+
'S': '5',
|
|
32
|
+
'B': '8',
|
|
33
|
+
'D': '0',
|
|
34
|
+
'Q': '0',
|
|
35
|
+
'G': '6',
|
|
36
|
+
'T': '7'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
def postprocess(self, texts, confidences, task=None, confidence_threshold=0.25, cleanup=True, region=None):
|
|
40
|
+
"""
|
|
41
|
+
Postprocesses the extracted text by cleaning and filtering low-confidence results.
|
|
42
|
+
Applies task-specific processing if a task is specified.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
texts (list): List of extracted text strings.
|
|
46
|
+
confidences (list): List of confidence scores corresponding to each text.
|
|
47
|
+
task (str): Specific task for customized postprocessing. Default is None.
|
|
48
|
+
confidence_threshold (float): Minimum confidence required to keep the text. Default is 0.5.
|
|
49
|
+
cleanup (bool): Whether to perform text cleanup.
|
|
50
|
+
region (str): Specific region for license plate processing ('india', 'us', 'eu', 'qatar'). Default is None.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
list: List of processed texts with corresponding confidence scores and validity flags.
|
|
54
|
+
"""
|
|
55
|
+
results = []
|
|
56
|
+
|
|
57
|
+
for text, confidence in zip(texts, confidences):
|
|
58
|
+
if confidence < confidence_threshold:
|
|
59
|
+
self.logger.debug(f"Text '{text}' rejected: confidence {confidence} below threshold {confidence_threshold}")
|
|
60
|
+
results.append((None, confidence, False))
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
if cleanup:
|
|
64
|
+
processed_text = self._clean_text(text)
|
|
65
|
+
else:
|
|
66
|
+
processed_text = text
|
|
67
|
+
|
|
68
|
+
if task and processed_text:
|
|
69
|
+
if task == "license_plate" and region:
|
|
70
|
+
region_task = f"license_plate_{region.lower()}"
|
|
71
|
+
if region_task in self.task_processors:
|
|
72
|
+
processed_text = self.task_processors[region_task](processed_text)
|
|
73
|
+
else:
|
|
74
|
+
processed_text = self.task_processors["license_plate"](processed_text)
|
|
75
|
+
self.logger.warning(f"Region '{region}' not supported, using generic license plate processor")
|
|
76
|
+
elif task in self.task_processors:
|
|
77
|
+
processed_text = self.task_processors[task](processed_text)
|
|
78
|
+
else:
|
|
79
|
+
self.logger.warning(f"Task '{task}' not supported, skipping task-specific processing")
|
|
80
|
+
|
|
81
|
+
if processed_text:
|
|
82
|
+
self.logger.debug(f"Text processed successfully: '{text}' -> '{processed_text}'")
|
|
83
|
+
results.append((processed_text, confidence, True))
|
|
84
|
+
else:
|
|
85
|
+
self.logger.debug(f"Text '{text}' rejected during processing")
|
|
86
|
+
results.append((None, confidence, False))
|
|
87
|
+
|
|
88
|
+
return results
|
|
89
|
+
|
|
90
|
+
def _clean_text(self, text):
|
|
91
|
+
"""
|
|
92
|
+
Basic text cleaning operations.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
text (str): Text to clean.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
str: Cleaned text.
|
|
99
|
+
"""
|
|
100
|
+
clean_text = text.strip()
|
|
101
|
+
clean_text = ''.join(char for char in clean_text if char.isprintable())
|
|
102
|
+
clean_text = ' '.join(clean_text.split())
|
|
103
|
+
|
|
104
|
+
return clean_text
|
|
105
|
+
|
|
106
|
+
def _process_license_plate(self, text):
|
|
107
|
+
"""
|
|
108
|
+
Generic license plate processor that respects the specified region.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
text (str): License plate text to process.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
str: Processed license plate text or None if invalid.
|
|
115
|
+
"""
|
|
116
|
+
plate_text = text.upper()
|
|
117
|
+
plate_text = ''.join(plate_text.split())
|
|
118
|
+
|
|
119
|
+
if self.region and self.region.lower() == 'qatar':
|
|
120
|
+
return self._process_license_plate_qatar(plate_text)
|
|
121
|
+
elif self.region and self.region.lower() == 'india':
|
|
122
|
+
return self._process_license_plate_india(plate_text)
|
|
123
|
+
elif self.region and self.region.lower() == 'us':
|
|
124
|
+
return self._process_license_plate_us(plate_text)
|
|
125
|
+
elif self.region and self.region.lower() == 'eu':
|
|
126
|
+
return self._process_license_plate_eu(plate_text)
|
|
127
|
+
else:
|
|
128
|
+
if re.match(r'^[A-Z]{2}\d{1,2}[A-Z]{1,2}\d{4}$', plate_text):
|
|
129
|
+
return self._process_license_plate_india(plate_text)
|
|
130
|
+
elif re.match(r'^[A-Z0-9]{1,8}$', plate_text) and len(plate_text) <= 8:
|
|
131
|
+
return self._process_license_plate_us(plate_text)
|
|
132
|
+
elif re.match(r'^[A-Z]{1,3}[-\s]?[A-Z0-9]{1,4}[-\s]?[A-Z0-9]{1,3}$', plate_text):
|
|
133
|
+
return self._process_license_plate_eu(plate_text)
|
|
134
|
+
elif re.match(r'^\d{1,6}\s*[A-Z]+?$', plate_text):
|
|
135
|
+
return self._process_license_plate_qatar(plate_text)
|
|
136
|
+
else:
|
|
137
|
+
plate_text = ''.join(char for char in plate_text if char.isalnum())
|
|
138
|
+
if 4 <= len(plate_text) <= 10:
|
|
139
|
+
return plate_text
|
|
140
|
+
|
|
141
|
+
self.logger.warning(f"Could not identify license plate format: '{text}'")
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
def _process_license_plate_india(self, text):
|
|
145
|
+
plate_text = text.upper().replace(" ", "")
|
|
146
|
+
plate_text = ''.join(char for char in plate_text if char.isalnum())
|
|
147
|
+
for old, new in self.char_substitutions.items():
|
|
148
|
+
plate_text = plate_text.replace(old, new)
|
|
149
|
+
|
|
150
|
+
if len(plate_text) >= 7:
|
|
151
|
+
state_code = plate_text[:2]
|
|
152
|
+
rest = plate_text[2:]
|
|
153
|
+
match = re.match(r'^(\d{1,2})[ -]?([A-Z]{1,2})[ -]?(\d{4})$', rest)
|
|
154
|
+
if match and state_code in ['AN', 'AP', 'AR', 'AS', 'BR', 'CH', 'CG', 'DD', 'DL', 'GA', 'GJ', 'HP', 'HR', 'JH', 'JK', 'KA', 'KL', 'LA', 'LD', 'MH', 'ML', 'MN', 'MP', 'MZ', 'NL', 'OD', 'PB', 'PY', 'RJ', 'SK', 'TN', 'TR', 'TG', 'TS', 'UK', 'UP', 'WB']:
|
|
155
|
+
district, series, number = match.groups()
|
|
156
|
+
formatted_plate = f"{state_code}{district}{series}{number}"
|
|
157
|
+
self.logger.info(f"Processed Indian license plate: '{text}' -> '{formatted_plate}'")
|
|
158
|
+
return formatted_plate
|
|
159
|
+
self.logger.warning(f"Invalid Indian license plate format: '{text}'")
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
def _process_license_plate_us(self, text):
|
|
163
|
+
plate_text = text.upper()
|
|
164
|
+
plate_text = ''.join(char for char in plate_text if char.isalnum())
|
|
165
|
+
|
|
166
|
+
for old, new in self.char_substitutions.items():
|
|
167
|
+
plate_text = plate_text.replace(old, new)
|
|
168
|
+
|
|
169
|
+
if re.match(r'^[A-Z]{3}\d{4}$', plate_text) or re.match(r'^\d{3}[A-Z]{4}$', plate_text):
|
|
170
|
+
self.logger.info(f"Processed US license plate (standard format): '{text}' -> '{plate_text}'")
|
|
171
|
+
return plate_text
|
|
172
|
+
if 2 <= len(plate_text) <= 8 and re.match(r'^[A-Z0-9]+$', plate_text):
|
|
173
|
+
self.logger.info(f"Processed US license plate (vanity/other format): '{text}' -> '{plate_text}'")
|
|
174
|
+
return plate_text
|
|
175
|
+
|
|
176
|
+
self.logger.warning(f"Invalid US license plate format: '{text}'")
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
def _process_license_plate_eu(self, text):
|
|
180
|
+
plate_text = text.upper()
|
|
181
|
+
plate_text = ''.join(char for char in plate_text if char.isalnum() or char == '-')
|
|
182
|
+
|
|
183
|
+
if '-' not in plate_text and len(plate_text) > 3:
|
|
184
|
+
for i in range(1, 4):
|
|
185
|
+
if i < len(plate_text) and plate_text[i].isdigit() and plate_text[i-1].isalpha():
|
|
186
|
+
plate_text = plate_text[:i] + '-' + plate_text[i:]
|
|
187
|
+
break
|
|
188
|
+
|
|
189
|
+
for old, new in self.char_substitutions.items():
|
|
190
|
+
plate_text = plate_text.replace(old, new)
|
|
191
|
+
|
|
192
|
+
if re.match(r'^[A-Z]{1,3}-[A-Z]{1,2}\d{1,4}$', plate_text):
|
|
193
|
+
self.logger.info(f"Processed German license plate: '{text}' -> '{plate_text}'")
|
|
194
|
+
return plate_text
|
|
195
|
+
if re.match(r'^[A-Z]{2}\d{2}[A-Z]{3}$', plate_text):
|
|
196
|
+
self.logger.info(f"Processed UK license plate: '{text}' -> '{plate_text}'")
|
|
197
|
+
return plate_text
|
|
198
|
+
if re.match(r'^[A-Z]{2}-\d{3}-[A-Z]{2}$', plate_text) or re.match(r'^\d{4}[A-Z]{3}$', plate_text):
|
|
199
|
+
self.logger.info(f"Processed French license plate: '{text}' -> '{plate_text}'")
|
|
200
|
+
return plate_text
|
|
201
|
+
if re.match(r'^[A-Z]{2}\d{3}[A-Z]{2}$', plate_text):
|
|
202
|
+
self.logger.info(f"Processed Italian license plate: '{text}' -> '{plate_text}'")
|
|
203
|
+
return plate_text
|
|
204
|
+
if re.match(r'^\d{4}[BCDFGHJKLMNPRSTVWXYZ]{3}$', plate_text):
|
|
205
|
+
self.logger.info(f"Processed Spanish license plate: '{text}' -> '{plate_text}'")
|
|
206
|
+
return plate_text
|
|
207
|
+
if re.search(r'[A-Z]', plate_text) and re.search(r'\d', plate_text) and 4 <= len(plate_text) <= 10:
|
|
208
|
+
self.logger.info(f"Processed generic European license plate: '{text}' -> '{plate_text}'")
|
|
209
|
+
return plate_text
|
|
210
|
+
|
|
211
|
+
self.logger.warning(f"Invalid European license plate format: '{text}'")
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
def _process_license_plate_qatar(self, text):
|
|
215
|
+
"""
|
|
216
|
+
Process Qatar license plate text by converting Arabic numerals to Latin and keeping only digits.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
text (str): License plate text to process.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
str: Processed license plate text or None if invalid.
|
|
223
|
+
"""
|
|
224
|
+
# Check for Unicode escape sequences (e.g., \u0664)
|
|
225
|
+
if r'\u' in str(text):
|
|
226
|
+
self.logger.warning(f"Invalid Qatar license plate format: '{text}' contains Unicode escape sequence")
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
# Define Arabic to Latin numeral mapping
|
|
230
|
+
arabic_to_latin = str.maketrans('٠١٢٣٤٥٦٧٨٩', '0123456789')
|
|
231
|
+
|
|
232
|
+
# Convert Arabic numerals to Latin and keep only alphanumeric characters
|
|
233
|
+
plate_text = text.translate(arabic_to_latin)
|
|
234
|
+
plate_text = ''.join(char for char in plate_text if char.isalnum())
|
|
235
|
+
|
|
236
|
+
# Apply character substitutions for common OCR errors
|
|
237
|
+
for old, new in self.char_substitutions.items():
|
|
238
|
+
plate_text = plate_text.replace(old, new)
|
|
239
|
+
|
|
240
|
+
# Keep only digits for Qatar license plates
|
|
241
|
+
plate_text = ''.join(char for char in plate_text if char.isdigit())
|
|
242
|
+
|
|
243
|
+
# Validate: Ensure the text is 1 to 6 digits
|
|
244
|
+
if re.match(r'^\d{1,6}$', plate_text):
|
|
245
|
+
self.logger.info(f"Processed Qatar license plate: '{text}' -> '{plate_text}'")
|
|
246
|
+
return plate_text
|
|
247
|
+
|
|
248
|
+
self.logger.warning(f"Invalid Qatar license plate format: '{text}'")
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
def _string_similarity(self, s1, s2):
|
|
252
|
+
if len(s1) > len(s2):
|
|
253
|
+
s1, s2 = s2, s1
|
|
254
|
+
|
|
255
|
+
distances = range(len(s1) + 1)
|
|
256
|
+
for i2, c2 in enumerate(s2):
|
|
257
|
+
distances_ = [i2+1]
|
|
258
|
+
for i1, c1 in enumerate(s1):
|
|
259
|
+
if c1 == c2:
|
|
260
|
+
distances_.append(distances[i1])
|
|
261
|
+
else:
|
|
262
|
+
distances_.append(1 + min((distances[i1], distances[i1 + 1], distances_[-1])))
|
|
263
|
+
distances = distances_
|
|
264
|
+
|
|
265
|
+
max_len = max(len(s1), len(s2))
|
|
266
|
+
similarity = 1 - (distances[-1] / max_len if max_len > 0 else 0)
|
|
267
|
+
return similarity
|
|
268
|
+
|
|
269
|
+
def add_task_processor(self, task_name, processor_function):
|
|
270
|
+
self.task_processors[task_name] = processor_function
|
|
271
|
+
self.logger.info(f"Added new task processor: {task_name}")
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
class ImagePreprocessor:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
"""Initialize the image preprocessor"""
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
def preprocess(self, image_np, resize_dim=None, grayscale=True):
|
|
10
|
+
"""
|
|
11
|
+
Preprocesses the image with various operations.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
image_np (np.ndarray): Input image as a numpy array.
|
|
15
|
+
resize_dim (tuple): Desired dimensions (width, height). If None, no resizing is done.
|
|
16
|
+
grayscale (bool): Whether to convert the image to grayscale.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
np.ndarray: Preprocessed image.
|
|
20
|
+
"""
|
|
21
|
+
processed_image = image_np.copy()
|
|
22
|
+
|
|
23
|
+
# Convert to grayscale if requested
|
|
24
|
+
if grayscale:
|
|
25
|
+
if len(processed_image.shape) == 3: # Check if image is already grayscale
|
|
26
|
+
processed_image = cv2.cvtColor(processed_image, cv2.COLOR_RGB2GRAY)
|
|
27
|
+
|
|
28
|
+
# Resize image if dimensions are provided
|
|
29
|
+
if resize_dim:
|
|
30
|
+
processed_image = cv2.resize(processed_image, resize_dim, interpolation=cv2.INTER_LINEAR)
|
|
31
|
+
|
|
32
|
+
return processed_image
|
|
33
|
+
|
|
34
|
+
def crop_to_bboxes(self, image_np, bboxes):
|
|
35
|
+
"""
|
|
36
|
+
Crops the image to the specified bounding boxes.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
image_np (np.ndarray): Input image as a numpy array.
|
|
40
|
+
bboxes (list): List of bounding boxes. Each box is a list of [xmin, ymin, xmax, ymax].
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
list: List of cropped images.
|
|
44
|
+
"""
|
|
45
|
+
cropped_images = []
|
|
46
|
+
|
|
47
|
+
for box in bboxes:
|
|
48
|
+
xmin, ymin, xmax, ymax = map(int, box)
|
|
49
|
+
cropped_img = image_np[ymin:ymax, xmin:xmax]
|
|
50
|
+
cropped_images.append(cropped_img)
|
|
51
|
+
|
|
52
|
+
return cropped_images
|