matrice-analytics 0.1.60__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matrice_analytics/__init__.py +28 -0
- matrice_analytics/boundary_drawing_internal/README.md +305 -0
- matrice_analytics/boundary_drawing_internal/__init__.py +45 -0
- matrice_analytics/boundary_drawing_internal/boundary_drawing_internal.py +1207 -0
- matrice_analytics/boundary_drawing_internal/boundary_drawing_tool.py +429 -0
- matrice_analytics/boundary_drawing_internal/boundary_tool_template.html +1036 -0
- matrice_analytics/boundary_drawing_internal/data/.gitignore +12 -0
- matrice_analytics/boundary_drawing_internal/example_usage.py +206 -0
- matrice_analytics/boundary_drawing_internal/usage/README.md +110 -0
- matrice_analytics/boundary_drawing_internal/usage/boundary_drawer_launcher.py +102 -0
- matrice_analytics/boundary_drawing_internal/usage/simple_boundary_launcher.py +107 -0
- matrice_analytics/post_processing/README.md +455 -0
- matrice_analytics/post_processing/__init__.py +732 -0
- matrice_analytics/post_processing/advanced_tracker/README.md +650 -0
- matrice_analytics/post_processing/advanced_tracker/__init__.py +17 -0
- matrice_analytics/post_processing/advanced_tracker/base.py +99 -0
- matrice_analytics/post_processing/advanced_tracker/config.py +77 -0
- matrice_analytics/post_processing/advanced_tracker/kalman_filter.py +370 -0
- matrice_analytics/post_processing/advanced_tracker/matching.py +195 -0
- matrice_analytics/post_processing/advanced_tracker/strack.py +230 -0
- matrice_analytics/post_processing/advanced_tracker/tracker.py +367 -0
- matrice_analytics/post_processing/config.py +146 -0
- matrice_analytics/post_processing/core/__init__.py +63 -0
- matrice_analytics/post_processing/core/base.py +704 -0
- matrice_analytics/post_processing/core/config.py +3291 -0
- matrice_analytics/post_processing/core/config_utils.py +925 -0
- matrice_analytics/post_processing/face_reg/__init__.py +43 -0
- matrice_analytics/post_processing/face_reg/compare_similarity.py +556 -0
- matrice_analytics/post_processing/face_reg/embedding_manager.py +950 -0
- matrice_analytics/post_processing/face_reg/face_recognition.py +2234 -0
- matrice_analytics/post_processing/face_reg/face_recognition_client.py +606 -0
- matrice_analytics/post_processing/face_reg/people_activity_logging.py +321 -0
- matrice_analytics/post_processing/ocr/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/easyocr_extractor.py +250 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/__init__.py +9 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/__init__.py +4 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/cli.py +33 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/dataset_stats.py +139 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/export.py +398 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/train.py +447 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/utils.py +129 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/valid.py +93 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/validate_dataset.py +240 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_augmentation.py +176 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/cli/visualize_predictions.py +96 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/__init__.py +3 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/process.py +246 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/types.py +60 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/core/utils.py +87 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/__init__.py +3 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/config.py +82 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/hub.py +141 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/inference/plate_recognizer.py +323 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/py.typed +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/augmentation.py +101 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/data/dataset.py +97 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/config.py +114 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/layers.py +553 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/loss.py +55 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/metric.py +86 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_builders.py +95 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/model/model_schema.py +395 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/__init__.py +0 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/backend_utils.py +38 -0
- matrice_analytics/post_processing/ocr/fast_plate_ocr_py38/train/utilities/utils.py +214 -0
- matrice_analytics/post_processing/ocr/postprocessing.py +270 -0
- matrice_analytics/post_processing/ocr/preprocessing.py +52 -0
- matrice_analytics/post_processing/post_processor.py +1175 -0
- matrice_analytics/post_processing/test_cases/__init__.py +1 -0
- matrice_analytics/post_processing/test_cases/run_tests.py +143 -0
- matrice_analytics/post_processing/test_cases/test_advanced_customer_service.py +841 -0
- matrice_analytics/post_processing/test_cases/test_basic_counting_tracking.py +523 -0
- matrice_analytics/post_processing/test_cases/test_comprehensive.py +531 -0
- matrice_analytics/post_processing/test_cases/test_config.py +852 -0
- matrice_analytics/post_processing/test_cases/test_customer_service.py +585 -0
- matrice_analytics/post_processing/test_cases/test_data_generators.py +583 -0
- matrice_analytics/post_processing/test_cases/test_people_counting.py +510 -0
- matrice_analytics/post_processing/test_cases/test_processor.py +524 -0
- matrice_analytics/post_processing/test_cases/test_usecases.py +165 -0
- matrice_analytics/post_processing/test_cases/test_utilities.py +356 -0
- matrice_analytics/post_processing/test_cases/test_utils.py +743 -0
- matrice_analytics/post_processing/usecases/Histopathological_Cancer_Detection_img.py +604 -0
- matrice_analytics/post_processing/usecases/__init__.py +267 -0
- matrice_analytics/post_processing/usecases/abandoned_object_detection.py +797 -0
- matrice_analytics/post_processing/usecases/advanced_customer_service.py +1601 -0
- matrice_analytics/post_processing/usecases/age_detection.py +842 -0
- matrice_analytics/post_processing/usecases/age_gender_detection.py +1085 -0
- matrice_analytics/post_processing/usecases/anti_spoofing_detection.py +656 -0
- matrice_analytics/post_processing/usecases/assembly_line_detection.py +841 -0
- matrice_analytics/post_processing/usecases/banana_defect_detection.py +624 -0
- matrice_analytics/post_processing/usecases/basic_counting_tracking.py +667 -0
- matrice_analytics/post_processing/usecases/blood_cancer_detection_img.py +881 -0
- matrice_analytics/post_processing/usecases/car_damage_detection.py +834 -0
- matrice_analytics/post_processing/usecases/car_part_segmentation.py +946 -0
- matrice_analytics/post_processing/usecases/car_service.py +1601 -0
- matrice_analytics/post_processing/usecases/cardiomegaly_classification.py +864 -0
- matrice_analytics/post_processing/usecases/cell_microscopy_segmentation.py +897 -0
- matrice_analytics/post_processing/usecases/chicken_pose_detection.py +648 -0
- matrice_analytics/post_processing/usecases/child_monitoring.py +814 -0
- matrice_analytics/post_processing/usecases/color/clip.py +660 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/merges.txt +48895 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/preprocessor_config.json +28 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/special_tokens_map.json +30 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer.json +245079 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/tokenizer_config.json +32 -0
- matrice_analytics/post_processing/usecases/color/clip_processor/vocab.json +1 -0
- matrice_analytics/post_processing/usecases/color/color_map_utils.py +70 -0
- matrice_analytics/post_processing/usecases/color/color_mapper.py +468 -0
- matrice_analytics/post_processing/usecases/color_detection.py +1936 -0
- matrice_analytics/post_processing/usecases/color_map_utils.py +70 -0
- matrice_analytics/post_processing/usecases/concrete_crack_detection.py +827 -0
- matrice_analytics/post_processing/usecases/crop_weed_detection.py +781 -0
- matrice_analytics/post_processing/usecases/customer_service.py +1008 -0
- matrice_analytics/post_processing/usecases/defect_detection_products.py +936 -0
- matrice_analytics/post_processing/usecases/distracted_driver_detection.py +822 -0
- matrice_analytics/post_processing/usecases/drone_traffic_monitoring.py +585 -0
- matrice_analytics/post_processing/usecases/drowsy_driver_detection.py +829 -0
- matrice_analytics/post_processing/usecases/dwell_detection.py +829 -0
- matrice_analytics/post_processing/usecases/emergency_vehicle_detection.py +827 -0
- matrice_analytics/post_processing/usecases/face_emotion.py +813 -0
- matrice_analytics/post_processing/usecases/face_recognition.py +827 -0
- matrice_analytics/post_processing/usecases/fashion_detection.py +835 -0
- matrice_analytics/post_processing/usecases/field_mapping.py +902 -0
- matrice_analytics/post_processing/usecases/fire_detection.py +1146 -0
- matrice_analytics/post_processing/usecases/flare_analysis.py +836 -0
- matrice_analytics/post_processing/usecases/flower_segmentation.py +1006 -0
- matrice_analytics/post_processing/usecases/gas_leak_detection.py +837 -0
- matrice_analytics/post_processing/usecases/gender_detection.py +832 -0
- matrice_analytics/post_processing/usecases/human_activity_recognition.py +871 -0
- matrice_analytics/post_processing/usecases/intrusion_detection.py +1672 -0
- matrice_analytics/post_processing/usecases/leaf.py +821 -0
- matrice_analytics/post_processing/usecases/leaf_disease.py +840 -0
- matrice_analytics/post_processing/usecases/leak_detection.py +837 -0
- matrice_analytics/post_processing/usecases/license_plate_detection.py +1188 -0
- matrice_analytics/post_processing/usecases/license_plate_monitoring.py +1781 -0
- matrice_analytics/post_processing/usecases/litter_monitoring.py +717 -0
- matrice_analytics/post_processing/usecases/mask_detection.py +869 -0
- matrice_analytics/post_processing/usecases/natural_disaster.py +907 -0
- matrice_analytics/post_processing/usecases/parking.py +787 -0
- matrice_analytics/post_processing/usecases/parking_space_detection.py +822 -0
- matrice_analytics/post_processing/usecases/pcb_defect_detection.py +888 -0
- matrice_analytics/post_processing/usecases/pedestrian_detection.py +808 -0
- matrice_analytics/post_processing/usecases/people_counting.py +706 -0
- matrice_analytics/post_processing/usecases/people_counting_bckp.py +1683 -0
- matrice_analytics/post_processing/usecases/people_tracking.py +1842 -0
- matrice_analytics/post_processing/usecases/pipeline_detection.py +605 -0
- matrice_analytics/post_processing/usecases/plaque_segmentation_img.py +874 -0
- matrice_analytics/post_processing/usecases/pothole_segmentation.py +915 -0
- matrice_analytics/post_processing/usecases/ppe_compliance.py +645 -0
- matrice_analytics/post_processing/usecases/price_tag_detection.py +822 -0
- matrice_analytics/post_processing/usecases/proximity_detection.py +1901 -0
- matrice_analytics/post_processing/usecases/road_lane_detection.py +623 -0
- matrice_analytics/post_processing/usecases/road_traffic_density.py +832 -0
- matrice_analytics/post_processing/usecases/road_view_segmentation.py +915 -0
- matrice_analytics/post_processing/usecases/shelf_inventory_detection.py +583 -0
- matrice_analytics/post_processing/usecases/shoplifting_detection.py +822 -0
- matrice_analytics/post_processing/usecases/shopping_cart_analysis.py +899 -0
- matrice_analytics/post_processing/usecases/skin_cancer_classification_img.py +864 -0
- matrice_analytics/post_processing/usecases/smoker_detection.py +833 -0
- matrice_analytics/post_processing/usecases/solar_panel.py +810 -0
- matrice_analytics/post_processing/usecases/suspicious_activity_detection.py +1030 -0
- matrice_analytics/post_processing/usecases/template_usecase.py +380 -0
- matrice_analytics/post_processing/usecases/theft_detection.py +648 -0
- matrice_analytics/post_processing/usecases/traffic_sign_monitoring.py +724 -0
- matrice_analytics/post_processing/usecases/underground_pipeline_defect_detection.py +775 -0
- matrice_analytics/post_processing/usecases/underwater_pollution_detection.py +842 -0
- matrice_analytics/post_processing/usecases/vehicle_monitoring.py +1029 -0
- matrice_analytics/post_processing/usecases/warehouse_object_segmentation.py +899 -0
- matrice_analytics/post_processing/usecases/waterbody_segmentation.py +923 -0
- matrice_analytics/post_processing/usecases/weapon_detection.py +771 -0
- matrice_analytics/post_processing/usecases/weld_defect_detection.py +615 -0
- matrice_analytics/post_processing/usecases/wildlife_monitoring.py +898 -0
- matrice_analytics/post_processing/usecases/windmill_maintenance.py +834 -0
- matrice_analytics/post_processing/usecases/wound_segmentation.py +856 -0
- matrice_analytics/post_processing/utils/__init__.py +150 -0
- matrice_analytics/post_processing/utils/advanced_counting_utils.py +400 -0
- matrice_analytics/post_processing/utils/advanced_helper_utils.py +317 -0
- matrice_analytics/post_processing/utils/advanced_tracking_utils.py +461 -0
- matrice_analytics/post_processing/utils/alerting_utils.py +213 -0
- matrice_analytics/post_processing/utils/category_mapping_utils.py +94 -0
- matrice_analytics/post_processing/utils/color_utils.py +592 -0
- matrice_analytics/post_processing/utils/counting_utils.py +182 -0
- matrice_analytics/post_processing/utils/filter_utils.py +261 -0
- matrice_analytics/post_processing/utils/format_utils.py +293 -0
- matrice_analytics/post_processing/utils/geometry_utils.py +300 -0
- matrice_analytics/post_processing/utils/smoothing_utils.py +358 -0
- matrice_analytics/post_processing/utils/tracking_utils.py +234 -0
- matrice_analytics/py.typed +0 -0
- matrice_analytics-0.1.60.dist-info/METADATA +481 -0
- matrice_analytics-0.1.60.dist-info/RECORD +196 -0
- matrice_analytics-0.1.60.dist-info/WHEEL +5 -0
- matrice_analytics-0.1.60.dist-info/licenses/LICENSE.txt +21 -0
- matrice_analytics-0.1.60.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions module
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import pathlib
|
|
7
|
+
import pkgutil
|
|
8
|
+
import random
|
|
9
|
+
from collections.abc import Iterator
|
|
10
|
+
from importlib import import_module
|
|
11
|
+
from typing import Optional, Union
|
|
12
|
+
|
|
13
|
+
import cv2
|
|
14
|
+
import keras
|
|
15
|
+
import numpy as np
|
|
16
|
+
import numpy.typing as npt
|
|
17
|
+
|
|
18
|
+
from fast_plate_ocr.core.process import read_and_resize_plate_image
|
|
19
|
+
from fast_plate_ocr.core.types import ImageColorMode, ImageInterpolation, PaddingColor
|
|
20
|
+
from fast_plate_ocr.train.model.config import PlateOCRConfig
|
|
21
|
+
from fast_plate_ocr.train.model.loss import cce_loss, focal_cce_loss
|
|
22
|
+
from fast_plate_ocr.train.model.metric import (
|
|
23
|
+
cat_acc_metric,
|
|
24
|
+
plate_acc_metric,
|
|
25
|
+
plate_len_acc_metric,
|
|
26
|
+
top_3_k_metric,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def one_hot_plate(plate: str, alphabet: str) -> list[list[int]]:
|
|
31
|
+
return [[0 if char != letter else 1 for char in alphabet] for letter in plate]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def target_transform(
|
|
35
|
+
plate_text: str,
|
|
36
|
+
max_plate_slots: int,
|
|
37
|
+
alphabet: str,
|
|
38
|
+
pad_char: str,
|
|
39
|
+
) -> npt.NDArray[np.uint8]:
|
|
40
|
+
# Pad the plates which length is smaller than 'max_plate_slots'
|
|
41
|
+
plate_text = plate_text.ljust(max_plate_slots, pad_char)
|
|
42
|
+
# Generate numpy arrays with one-hot encoding of plates
|
|
43
|
+
encoded_plate = np.array(one_hot_plate(plate_text, alphabet=alphabet), dtype=np.uint8)
|
|
44
|
+
return encoded_plate
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _register_custom_keras():
|
|
48
|
+
base_pkg = "fast_plate_ocr.train.model"
|
|
49
|
+
for _, name, _ in pkgutil.walk_packages(
|
|
50
|
+
import_module(base_pkg).__path__, prefix=f"{base_pkg}."
|
|
51
|
+
):
|
|
52
|
+
if any(m in name for m in ("layers",)):
|
|
53
|
+
import_module(name)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def load_keras_model(
|
|
57
|
+
model_path: Union[str, pathlib.Path],
|
|
58
|
+
plate_config: PlateOCRConfig,
|
|
59
|
+
) -> keras.Model:
|
|
60
|
+
"""
|
|
61
|
+
Utility helper function to load the keras OCR model.
|
|
62
|
+
"""
|
|
63
|
+
_register_custom_keras()
|
|
64
|
+
custom_objects = {
|
|
65
|
+
"cce": cce_loss(
|
|
66
|
+
vocabulary_size=plate_config.vocabulary_size,
|
|
67
|
+
),
|
|
68
|
+
"focal_cce": focal_cce_loss(
|
|
69
|
+
vocabulary_size=plate_config.vocabulary_size,
|
|
70
|
+
),
|
|
71
|
+
"cat_acc": cat_acc_metric(
|
|
72
|
+
max_plate_slots=plate_config.max_plate_slots,
|
|
73
|
+
vocabulary_size=plate_config.vocabulary_size,
|
|
74
|
+
),
|
|
75
|
+
"plate_acc": plate_acc_metric(
|
|
76
|
+
max_plate_slots=plate_config.max_plate_slots,
|
|
77
|
+
vocabulary_size=plate_config.vocabulary_size,
|
|
78
|
+
),
|
|
79
|
+
"top_3_k": top_3_k_metric(
|
|
80
|
+
vocabulary_size=plate_config.vocabulary_size,
|
|
81
|
+
),
|
|
82
|
+
"plate_len_acc": plate_len_acc_metric(
|
|
83
|
+
max_plate_slots=plate_config.max_plate_slots,
|
|
84
|
+
vocabulary_size=plate_config.vocabulary_size,
|
|
85
|
+
pad_token_index=plate_config.pad_idx,
|
|
86
|
+
),
|
|
87
|
+
}
|
|
88
|
+
model = keras.models.load_model(model_path, custom_objects=custom_objects)
|
|
89
|
+
return model
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
IMG_EXTENSIONS: set[str] = {".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp"}
|
|
93
|
+
"""Valid image extensions for the scope of this script."""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def load_images_from_folder( # noqa: PLR0913
|
|
97
|
+
img_dir: pathlib.Path,
|
|
98
|
+
width: int,
|
|
99
|
+
height: int,
|
|
100
|
+
image_color_mode: ImageColorMode = "grayscale",
|
|
101
|
+
keep_aspect_ratio: bool = False,
|
|
102
|
+
interpolation_method: ImageInterpolation = "linear",
|
|
103
|
+
padding_color: PaddingColor = (114, 114, 114),
|
|
104
|
+
shuffle: bool = False,
|
|
105
|
+
limit: Optional[int] = None,
|
|
106
|
+
) -> Iterator[npt.NDArray]:
|
|
107
|
+
"""
|
|
108
|
+
Return all images read from a directory. This uses the same read function used during training.
|
|
109
|
+
"""
|
|
110
|
+
# pylint: disable=too-many-arguments
|
|
111
|
+
image_paths = sorted(
|
|
112
|
+
str(f.resolve()) for f in img_dir.iterdir() if f.is_file() and f.suffix in IMG_EXTENSIONS
|
|
113
|
+
)
|
|
114
|
+
if limit:
|
|
115
|
+
image_paths = image_paths[:limit]
|
|
116
|
+
if shuffle:
|
|
117
|
+
random.shuffle(image_paths)
|
|
118
|
+
yield from (
|
|
119
|
+
read_and_resize_plate_image(
|
|
120
|
+
i,
|
|
121
|
+
img_height=height,
|
|
122
|
+
img_width=width,
|
|
123
|
+
image_color_mode=image_color_mode,
|
|
124
|
+
keep_aspect_ratio=keep_aspect_ratio,
|
|
125
|
+
interpolation_method=interpolation_method,
|
|
126
|
+
padding_color=padding_color,
|
|
127
|
+
)
|
|
128
|
+
for i in image_paths
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def postprocess_model_output(
|
|
133
|
+
prediction: npt.NDArray,
|
|
134
|
+
alphabet: str,
|
|
135
|
+
max_plate_slots: int,
|
|
136
|
+
vocab_size: int,
|
|
137
|
+
) -> tuple[str, npt.NDArray]:
|
|
138
|
+
"""
|
|
139
|
+
Return plate text and confidence scores from raw model output.
|
|
140
|
+
"""
|
|
141
|
+
prediction = prediction.reshape((max_plate_slots, vocab_size))
|
|
142
|
+
probs = np.max(prediction, axis=-1)
|
|
143
|
+
prediction = np.argmax(prediction, axis=-1)
|
|
144
|
+
plate = "".join([alphabet[x] for x in prediction])
|
|
145
|
+
return plate, probs
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def low_confidence_positions(probs, thresh=0.3) -> npt.NDArray:
|
|
149
|
+
"""Returns indices of elements in `probs` less than `thresh`, indicating low confidence."""
|
|
150
|
+
return np.where(np.array(probs) < thresh)[0]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def display_predictions(
|
|
154
|
+
image: npt.NDArray,
|
|
155
|
+
plate: str,
|
|
156
|
+
probs: npt.NDArray,
|
|
157
|
+
low_conf_thresh: float,
|
|
158
|
+
) -> None:
|
|
159
|
+
"""
|
|
160
|
+
Display plate and corresponding prediction.
|
|
161
|
+
"""
|
|
162
|
+
plate_str = "".join(plate)
|
|
163
|
+
logging.info("Plate: %s", plate_str)
|
|
164
|
+
logging.info("Confidence: %s", probs)
|
|
165
|
+
image_to_show = cv2.resize(image, None, fx=3, fy=3, interpolation=cv2.INTER_LINEAR)
|
|
166
|
+
if len(image_to_show.shape) == 2:
|
|
167
|
+
image_to_show = cv2.cvtColor(image_to_show, cv2.COLOR_GRAY2RGB)
|
|
168
|
+
elif image_to_show.shape[2] == 3:
|
|
169
|
+
image_to_show = cv2.cvtColor(image_to_show, cv2.COLOR_BGR2RGB)
|
|
170
|
+
# Average probabilities
|
|
171
|
+
avg_prob = np.mean(probs) * 100
|
|
172
|
+
cv2.putText(
|
|
173
|
+
image_to_show,
|
|
174
|
+
f"{plate_str} {avg_prob:.{2}f}%",
|
|
175
|
+
org=(5, 30),
|
|
176
|
+
fontFace=cv2.FONT_HERSHEY_SIMPLEX,
|
|
177
|
+
fontScale=1,
|
|
178
|
+
color=(0, 0, 0),
|
|
179
|
+
lineType=1,
|
|
180
|
+
thickness=6,
|
|
181
|
+
)
|
|
182
|
+
cv2.putText(
|
|
183
|
+
image_to_show,
|
|
184
|
+
f"{plate_str} {avg_prob:.{2}f}%",
|
|
185
|
+
org=(5, 30),
|
|
186
|
+
fontFace=cv2.FONT_HERSHEY_SIMPLEX,
|
|
187
|
+
fontScale=1,
|
|
188
|
+
color=(255, 255, 255),
|
|
189
|
+
lineType=1,
|
|
190
|
+
thickness=2,
|
|
191
|
+
)
|
|
192
|
+
# Display character with low confidence
|
|
193
|
+
low_conf_chars = "Low conf. on: " + " ".join(
|
|
194
|
+
[plate[i] for i in low_confidence_positions(probs, thresh=low_conf_thresh)]
|
|
195
|
+
)
|
|
196
|
+
cv2.putText(
|
|
197
|
+
image_to_show,
|
|
198
|
+
low_conf_chars,
|
|
199
|
+
org=(5, 200),
|
|
200
|
+
fontFace=cv2.FONT_HERSHEY_SIMPLEX,
|
|
201
|
+
fontScale=0.7,
|
|
202
|
+
color=(0, 0, 220),
|
|
203
|
+
lineType=1,
|
|
204
|
+
thickness=2,
|
|
205
|
+
)
|
|
206
|
+
try:
|
|
207
|
+
cv2.imshow("plates", image_to_show)
|
|
208
|
+
if cv2.waitKey(0) & 0xFF == ord("q"):
|
|
209
|
+
return
|
|
210
|
+
except cv2.error as e: # pylint: disable=catching-non-exception
|
|
211
|
+
raise RuntimeError( # pylint: disable=bad-exception-cause
|
|
212
|
+
"This visualization requires full OpenCV with GUI support. "
|
|
213
|
+
"Install with `pip install opencv-python` instead of headless."
|
|
214
|
+
) from e
|
|
@@ -0,0 +1,270 @@
|
|
|
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
|
+
self.logger = logging.getLogger('TextPostprocessor')
|
|
13
|
+
|
|
14
|
+
self.task_processors = {
|
|
15
|
+
"license_plate": self._process_license_plate,
|
|
16
|
+
"license_plate_india": self._process_license_plate_india,
|
|
17
|
+
"license_plate_us": self._process_license_plate_us,
|
|
18
|
+
"license_plate_eu": self._process_license_plate_eu,
|
|
19
|
+
"license_plate_qatar": self._process_license_plate_qatar,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
self.char_substitutions = {
|
|
23
|
+
'O': '0',
|
|
24
|
+
'o': '0',
|
|
25
|
+
'I': '1',
|
|
26
|
+
'Z': '2',
|
|
27
|
+
'A': '4',
|
|
28
|
+
'L': '1',
|
|
29
|
+
'AV': 'AV',
|
|
30
|
+
'S': '5',
|
|
31
|
+
'B': '8',
|
|
32
|
+
'D': '0',
|
|
33
|
+
'Q': '0',
|
|
34
|
+
'G': '6',
|
|
35
|
+
'T': '7'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def postprocess(self, texts, confidences, task=None, confidence_threshold=0.25, cleanup=True, region=None):
|
|
39
|
+
"""
|
|
40
|
+
Postprocesses the extracted text by cleaning and filtering low-confidence results.
|
|
41
|
+
Applies task-specific processing if a task is specified.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
texts (list): List of extracted text strings.
|
|
45
|
+
confidences (list): List of confidence scores corresponding to each text.
|
|
46
|
+
task (str): Specific task for customized postprocessing. Default is None.
|
|
47
|
+
confidence_threshold (float): Minimum confidence required to keep the text. Default is 0.5.
|
|
48
|
+
cleanup (bool): Whether to perform text cleanup.
|
|
49
|
+
region (str): Specific region for license plate processing ('india', 'us', 'eu', 'qatar'). Default is None.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
list: List of processed texts with corresponding confidence scores and validity flags.
|
|
53
|
+
"""
|
|
54
|
+
results = []
|
|
55
|
+
|
|
56
|
+
for text, confidence in zip(texts, confidences):
|
|
57
|
+
if confidence < confidence_threshold:
|
|
58
|
+
self.logger.debug(f"Text '{text}' rejected: confidence {confidence} below threshold {confidence_threshold}")
|
|
59
|
+
results.append((None, confidence, False))
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
if cleanup:
|
|
63
|
+
processed_text = self._clean_text(text)
|
|
64
|
+
else:
|
|
65
|
+
processed_text = text
|
|
66
|
+
|
|
67
|
+
if task and processed_text:
|
|
68
|
+
if task == "license_plate" and region:
|
|
69
|
+
region_task = f"license_plate_{region.lower()}"
|
|
70
|
+
if region_task in self.task_processors:
|
|
71
|
+
processed_text = self.task_processors[region_task](processed_text)
|
|
72
|
+
else:
|
|
73
|
+
processed_text = self.task_processors["license_plate"](processed_text)
|
|
74
|
+
self.logger.warning(f"Region '{region}' not supported, using generic license plate processor")
|
|
75
|
+
elif task in self.task_processors:
|
|
76
|
+
processed_text = self.task_processors[task](processed_text)
|
|
77
|
+
else:
|
|
78
|
+
self.logger.warning(f"Task '{task}' not supported, skipping task-specific processing")
|
|
79
|
+
|
|
80
|
+
if processed_text:
|
|
81
|
+
self.logger.debug(f"Text processed successfully: '{text}' -> '{processed_text}'")
|
|
82
|
+
results.append((processed_text, confidence, True))
|
|
83
|
+
else:
|
|
84
|
+
self.logger.debug(f"Text '{text}' rejected during processing")
|
|
85
|
+
results.append((None, confidence, False))
|
|
86
|
+
|
|
87
|
+
return results
|
|
88
|
+
|
|
89
|
+
def _clean_text(self, text):
|
|
90
|
+
"""
|
|
91
|
+
Basic text cleaning operations.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
text (str): Text to clean.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
str: Cleaned text.
|
|
98
|
+
"""
|
|
99
|
+
clean_text = text.strip()
|
|
100
|
+
clean_text = ''.join(char for char in clean_text if char.isprintable())
|
|
101
|
+
clean_text = ' '.join(clean_text.split())
|
|
102
|
+
|
|
103
|
+
return clean_text
|
|
104
|
+
|
|
105
|
+
def _process_license_plate(self, text):
|
|
106
|
+
"""
|
|
107
|
+
Generic license plate processor that respects the specified region.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
text (str): License plate text to process.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
str: Processed license plate text or None if invalid.
|
|
114
|
+
"""
|
|
115
|
+
plate_text = text.upper()
|
|
116
|
+
plate_text = ''.join(plate_text.split())
|
|
117
|
+
|
|
118
|
+
if self.region and self.region.lower() == 'qatar':
|
|
119
|
+
return self._process_license_plate_qatar(plate_text)
|
|
120
|
+
elif self.region and self.region.lower() == 'india':
|
|
121
|
+
return self._process_license_plate_india(plate_text)
|
|
122
|
+
elif self.region and self.region.lower() == 'us':
|
|
123
|
+
return self._process_license_plate_us(plate_text)
|
|
124
|
+
elif self.region and self.region.lower() == 'eu':
|
|
125
|
+
return self._process_license_plate_eu(plate_text)
|
|
126
|
+
else:
|
|
127
|
+
if re.match(r'^[A-Z]{2}\d{1,2}[A-Z]{1,2}\d{4}$', plate_text):
|
|
128
|
+
return self._process_license_plate_india(plate_text)
|
|
129
|
+
elif re.match(r'^[A-Z0-9]{1,8}$', plate_text) and len(plate_text) <= 8:
|
|
130
|
+
return self._process_license_plate_us(plate_text)
|
|
131
|
+
elif re.match(r'^[A-Z]{1,3}[-\s]?[A-Z0-9]{1,4}[-\s]?[A-Z0-9]{1,3}$', plate_text):
|
|
132
|
+
return self._process_license_plate_eu(plate_text)
|
|
133
|
+
elif re.match(r'^\d{1,6}\s*[A-Z]+?$', plate_text):
|
|
134
|
+
return self._process_license_plate_qatar(plate_text)
|
|
135
|
+
else:
|
|
136
|
+
plate_text = ''.join(char for char in plate_text if char.isalnum())
|
|
137
|
+
if 4 <= len(plate_text) <= 10:
|
|
138
|
+
return plate_text
|
|
139
|
+
|
|
140
|
+
self.logger.warning(f"Could not identify license plate format: '{text}'")
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
def _process_license_plate_india(self, text):
|
|
144
|
+
plate_text = text.upper().replace(" ", "")
|
|
145
|
+
plate_text = ''.join(char for char in plate_text if char.isalnum())
|
|
146
|
+
for old, new in self.char_substitutions.items():
|
|
147
|
+
plate_text = plate_text.replace(old, new)
|
|
148
|
+
|
|
149
|
+
if len(plate_text) >= 7:
|
|
150
|
+
state_code = plate_text[:2]
|
|
151
|
+
rest = plate_text[2:]
|
|
152
|
+
match = re.match(r'^(\d{1,2})[ -]?([A-Z]{1,2})[ -]?(\d{4})$', rest)
|
|
153
|
+
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']:
|
|
154
|
+
district, series, number = match.groups()
|
|
155
|
+
formatted_plate = f"{state_code}{district}{series}{number}"
|
|
156
|
+
self.logger.info(f"Processed Indian license plate: '{text}' -> '{formatted_plate}'")
|
|
157
|
+
return formatted_plate
|
|
158
|
+
self.logger.warning(f"Invalid Indian license plate format: '{text}'")
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
def _process_license_plate_us(self, text):
|
|
162
|
+
plate_text = text.upper()
|
|
163
|
+
plate_text = ''.join(char for char in plate_text if char.isalnum())
|
|
164
|
+
|
|
165
|
+
for old, new in self.char_substitutions.items():
|
|
166
|
+
plate_text = plate_text.replace(old, new)
|
|
167
|
+
|
|
168
|
+
if re.match(r'^[A-Z]{3}\d{4}$', plate_text) or re.match(r'^\d{3}[A-Z]{4}$', plate_text):
|
|
169
|
+
self.logger.info(f"Processed US license plate (standard format): '{text}' -> '{plate_text}'")
|
|
170
|
+
return plate_text
|
|
171
|
+
if 2 <= len(plate_text) <= 8 and re.match(r'^[A-Z0-9]+$', plate_text):
|
|
172
|
+
self.logger.info(f"Processed US license plate (vanity/other format): '{text}' -> '{plate_text}'")
|
|
173
|
+
return plate_text
|
|
174
|
+
|
|
175
|
+
self.logger.warning(f"Invalid US license plate format: '{text}'")
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
def _process_license_plate_eu(self, text):
|
|
179
|
+
plate_text = text.upper()
|
|
180
|
+
plate_text = ''.join(char for char in plate_text if char.isalnum() or char == '-')
|
|
181
|
+
|
|
182
|
+
if '-' not in plate_text and len(plate_text) > 3:
|
|
183
|
+
for i in range(1, 4):
|
|
184
|
+
if i < len(plate_text) and plate_text[i].isdigit() and plate_text[i-1].isalpha():
|
|
185
|
+
plate_text = plate_text[:i] + '-' + plate_text[i:]
|
|
186
|
+
break
|
|
187
|
+
|
|
188
|
+
for old, new in self.char_substitutions.items():
|
|
189
|
+
plate_text = plate_text.replace(old, new)
|
|
190
|
+
|
|
191
|
+
if re.match(r'^[A-Z]{1,3}-[A-Z]{1,2}\d{1,4}$', plate_text):
|
|
192
|
+
self.logger.info(f"Processed German license plate: '{text}' -> '{plate_text}'")
|
|
193
|
+
return plate_text
|
|
194
|
+
if re.match(r'^[A-Z]{2}\d{2}[A-Z]{3}$', plate_text):
|
|
195
|
+
self.logger.info(f"Processed UK license plate: '{text}' -> '{plate_text}'")
|
|
196
|
+
return plate_text
|
|
197
|
+
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):
|
|
198
|
+
self.logger.info(f"Processed French license plate: '{text}' -> '{plate_text}'")
|
|
199
|
+
return plate_text
|
|
200
|
+
if re.match(r'^[A-Z]{2}\d{3}[A-Z]{2}$', plate_text):
|
|
201
|
+
self.logger.info(f"Processed Italian license plate: '{text}' -> '{plate_text}'")
|
|
202
|
+
return plate_text
|
|
203
|
+
if re.match(r'^\d{4}[BCDFGHJKLMNPRSTVWXYZ]{3}$', plate_text):
|
|
204
|
+
self.logger.info(f"Processed Spanish license plate: '{text}' -> '{plate_text}'")
|
|
205
|
+
return plate_text
|
|
206
|
+
if re.search(r'[A-Z]', plate_text) and re.search(r'\d', plate_text) and 4 <= len(plate_text) <= 10:
|
|
207
|
+
self.logger.info(f"Processed generic European license plate: '{text}' -> '{plate_text}'")
|
|
208
|
+
return plate_text
|
|
209
|
+
|
|
210
|
+
self.logger.warning(f"Invalid European license plate format: '{text}'")
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
def _process_license_plate_qatar(self, text):
|
|
214
|
+
"""
|
|
215
|
+
Process Qatar license plate text by converting Arabic numerals to Latin and keeping only digits.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
text (str): License plate text to process.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
str: Processed license plate text or None if invalid.
|
|
222
|
+
"""
|
|
223
|
+
# Check for Unicode escape sequences (e.g., \u0664)
|
|
224
|
+
if r'\u' in str(text):
|
|
225
|
+
self.logger.warning(f"Invalid Qatar license plate format: '{text}' contains Unicode escape sequence")
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
# Define Arabic to Latin numeral mapping
|
|
229
|
+
arabic_to_latin = str.maketrans('٠١٢٣٤٥٦٧٨٩', '0123456789')
|
|
230
|
+
|
|
231
|
+
# Convert Arabic numerals to Latin and keep only alphanumeric characters
|
|
232
|
+
plate_text = text.translate(arabic_to_latin)
|
|
233
|
+
plate_text = ''.join(char for char in plate_text if char.isalnum())
|
|
234
|
+
|
|
235
|
+
# Apply character substitutions for common OCR errors
|
|
236
|
+
for old, new in self.char_substitutions.items():
|
|
237
|
+
plate_text = plate_text.replace(old, new)
|
|
238
|
+
|
|
239
|
+
# Keep only digits for Qatar license plates
|
|
240
|
+
plate_text = ''.join(char for char in plate_text if char.isdigit())
|
|
241
|
+
|
|
242
|
+
# Validate: Ensure the text is 1 to 6 digits
|
|
243
|
+
if re.match(r'^\d{1,6}$', plate_text):
|
|
244
|
+
self.logger.info(f"Processed Qatar license plate: '{text}' -> '{plate_text}'")
|
|
245
|
+
return plate_text
|
|
246
|
+
|
|
247
|
+
self.logger.warning(f"Invalid Qatar license plate format: '{text}'")
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
def _string_similarity(self, s1, s2):
|
|
251
|
+
if len(s1) > len(s2):
|
|
252
|
+
s1, s2 = s2, s1
|
|
253
|
+
|
|
254
|
+
distances = range(len(s1) + 1)
|
|
255
|
+
for i2, c2 in enumerate(s2):
|
|
256
|
+
distances_ = [i2+1]
|
|
257
|
+
for i1, c1 in enumerate(s1):
|
|
258
|
+
if c1 == c2:
|
|
259
|
+
distances_.append(distances[i1])
|
|
260
|
+
else:
|
|
261
|
+
distances_.append(1 + min((distances[i1], distances[i1 + 1], distances_[-1])))
|
|
262
|
+
distances = distances_
|
|
263
|
+
|
|
264
|
+
max_len = max(len(s1), len(s2))
|
|
265
|
+
similarity = 1 - (distances[-1] / max_len if max_len > 0 else 0)
|
|
266
|
+
return similarity
|
|
267
|
+
|
|
268
|
+
def add_task_processor(self, task_name, processor_function):
|
|
269
|
+
self.task_processors[task_name] = processor_function
|
|
270
|
+
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
|