fastplateocr-py 0.1.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.
Files changed (34) hide show
  1. fastplateocr/__init__.py +5 -0
  2. fastplateocr/det/__init__.py +1 -0
  3. fastplateocr/det/c3k2/__init__.py +0 -0
  4. fastplateocr/det/c3k2/bottleneck/__init__.py +0 -0
  5. fastplateocr/det/c3k2/bottleneck/rep_mixer.py +81 -0
  6. fastplateocr/license_plate_dict.txt +37 -0
  7. fastplateocr/pipeline.py +185 -0
  8. fastplateocr/rec/losses/__init__.py +71 -0
  9. fastplateocr/rec/losses/ctc_loss.py +33 -0
  10. fastplateocr/rec/metrics/__init__.py +33 -0
  11. fastplateocr/rec/metrics/rec_metric.py +286 -0
  12. fastplateocr/rec/modeling/__init__.py +11 -0
  13. fastplateocr/rec/modeling/base_recognizer.py +58 -0
  14. fastplateocr/rec/modeling/common.py +238 -0
  15. fastplateocr/rec/modeling/decoders/__init__.py +21 -0
  16. fastplateocr/rec/modeling/decoders/efficient_rctc_decoder.py +60 -0
  17. fastplateocr/rec/modeling/encoders/__init__.py +22 -0
  18. fastplateocr/rec/modeling/encoders/svtrv2.py +470 -0
  19. fastplateocr/rec/modeling/encoders/svtrv2_lnconv.py +503 -0
  20. fastplateocr/rec/modeling/encoders/svtrv2_lnconv_two33.py +636 -0
  21. fastplateocr/rec/optimizer/__init__.py +74 -0
  22. fastplateocr/rec/optimizer/lr.py +276 -0
  23. fastplateocr/rec/postprocess/__init__.py +26 -0
  24. fastplateocr/rec/postprocess/ctc_postprocess.py +118 -0
  25. fastplateocr/rec/preprocess/__init__.py +78 -0
  26. fastplateocr/rec/preprocess/auto_augment.py +494 -0
  27. fastplateocr/rec/preprocess/ctc_label_encode.py +124 -0
  28. fastplateocr/rec/preprocess/parseq_aug.py +150 -0
  29. fastplateocr/rec/preprocess/rec_aug.py +13 -0
  30. fastplateocr_py-0.1.0.dist-info/LICENSE +201 -0
  31. fastplateocr_py-0.1.0.dist-info/METADATA +197 -0
  32. fastplateocr_py-0.1.0.dist-info/RECORD +34 -0
  33. fastplateocr_py-0.1.0.dist-info/WHEEL +5 -0
  34. fastplateocr_py-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,5 @@
1
+ __version__ = '0.1.0'
2
+
3
+ from .pipeline import FastPlateOCR
4
+
5
+ __all__ = ['FastPlateOCR']
@@ -0,0 +1 @@
1
+ # det package
File without changes
File without changes
@@ -0,0 +1,81 @@
1
+ import torch
2
+ import torch.nn as nn
3
+
4
+
5
+ class RepMixerBlock(nn.Module):
6
+ """
7
+ Macro-architectural Reparameterization (Large Kernel 5x5).
8
+ Replace two consecutive 3x3 layers with a single 5x5 layer.
9
+ During training, use 4 branches (5x5, 3x3, 1x1, Identity)
10
+ to maximize the network's feature extraction capability.
11
+ """
12
+ def __init__(self, in_channels, out_channels, deploy=False):
13
+ super().__init__()
14
+ self.deploy = deploy
15
+ self.in_channels = in_channels
16
+ self.out_channels = out_channels
17
+ self.act = nn.SiLU(inplace=True)
18
+
19
+ if deploy:
20
+ # At deploy time: Fuse into a single 5x5 matrix
21
+ self.rbr_reparam = nn.Conv2d(in_channels, out_channels, kernel_size=5, padding=2, bias=True)
22
+ else:
23
+ # Branch 1: 5x5 (Ultra-wide receptive field)
24
+ self.rbr_5x5 = nn.Sequential(
25
+ nn.Conv2d(in_channels, out_channels, kernel_size=5, padding=2, bias=False),
26
+ nn.BatchNorm2d(out_channels)
27
+ )
28
+ # Branch 2: 3x3 (Learn local details)
29
+ self.rbr_3x3 = nn.Sequential(
30
+ nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1, bias=False),
31
+ nn.BatchNorm2d(out_channels)
32
+ )
33
+ # Branch 3: 1x1 (Learn channel cross-talk)
34
+ self.rbr_1x1 = nn.Sequential(
35
+ nn.Conv2d(in_channels, out_channels, kernel_size=1, padding=0, bias=False),
36
+ nn.BatchNorm2d(out_channels)
37
+ )
38
+ # Branch 4: Identity (Skip computation)
39
+ if in_channels == out_channels:
40
+ self.rbr_identity = nn.BatchNorm2d(in_channels)
41
+ else:
42
+ self.rbr_identity = None
43
+
44
+ def forward(self, x):
45
+ if self.deploy:
46
+ return self.act(self.rbr_reparam(x))
47
+
48
+ # Thanks to standard padding (2, 1, 0), all 3 conv branches output Tensors with the same H, W.
49
+ out = self.rbr_5x5(x) + self.rbr_3x3(x) + self.rbr_1x1(x)
50
+ if self.rbr_identity is not None:
51
+ out += self.rbr_identity(x)
52
+ return self.act(out)
53
+
54
+
55
+ def setup_monkey_patch_injection():
56
+ """
57
+ Remove the entire old Bottleneck structure (including cv1 3x3 and cv2 3x3).
58
+ Replace it with a 5x5 RepMixerBlock going straight from input channel (c1) to output channel (c2).
59
+ """
60
+ import ultralytics.nn.modules.block as block
61
+
62
+ def new_init(self, c1, c2, shortcut=True, g=1, k=(3, 3), e=0.5):
63
+ # Initialize the base nn.Module class (Discard all old Bottleneck code)
64
+ nn.Module.__init__(self)
65
+ self.c1 = c1
66
+ self.c2 = c2
67
+ # Shortcut only activated if c1 == c2 (keeping YOLO's rule)
68
+ self.add = shortcut and c1 == c2
69
+
70
+ # ARCHITECTURAL BREAKTHROUGH: Remove the channel compression layer (c_)
71
+ # Use exactly 1 multi-branch Large Kernel block.
72
+ self.rep_block = RepMixerBlock(c1, c2, deploy=False)
73
+
74
+ def new_forward(self, x):
75
+ # Straight signal transmission through RepMixerBlock
76
+ return x + self.rep_block(x) if self.add else self.rep_block(x)
77
+
78
+ # Overwrite YOLO architecture
79
+ block.Bottleneck.__init__ = new_init
80
+ block.Bottleneck.forward = new_forward
81
+ print("[INFO] Monkey-Patch successful: Overhauled Bottleneck architecture with multi-branch 5x5 Large Kernel!")
@@ -0,0 +1,37 @@
1
+ 0
2
+ 1
3
+ 2
4
+ 3
5
+ 4
6
+ 5
7
+ 6
8
+ 7
9
+ 8
10
+ 9
11
+ A
12
+ B
13
+ C
14
+ D
15
+ Đ
16
+ E
17
+ F
18
+ G
19
+ H
20
+ I
21
+ J
22
+ K
23
+ L
24
+ M
25
+ N
26
+ O
27
+ P
28
+ Q
29
+ R
30
+ S
31
+ T
32
+ U
33
+ V
34
+ W
35
+ X
36
+ Y
37
+ Z
@@ -0,0 +1,185 @@
1
+ import os
2
+ import sys
3
+ import torch
4
+ import cv2
5
+ import numpy as np
6
+ import warnings
7
+ warnings.filterwarnings("ignore")
8
+
9
+ # This ensures that we can import det and rec if running from this file
10
+ __dir__ = os.path.dirname(os.path.abspath(__file__))
11
+ root_dir = os.path.abspath(os.path.join(__dir__, '..'))
12
+ if root_dir not in sys.path:
13
+ sys.path.insert(0, root_dir)
14
+
15
+ from ultralytics import YOLO
16
+ from fastplateocr.det.c3k2.bottleneck.rep_mixer import setup_monkey_patch_injection, RepMixerBlock
17
+ from fastplateocr.rec.modeling import build_model
18
+ from fastplateocr.rec.postprocess import build_post_process
19
+
20
+ # Trick for PyTorch Unpickler: Inject RepMixerBlock into __main__
21
+ import __main__
22
+ if not hasattr(__main__, 'RepMixerBlock'):
23
+ setattr(__main__, 'RepMixerBlock', RepMixerBlock)
24
+
25
+
26
+ def switch_deploy_flag(model):
27
+ """Recursively convert RepMixer layers in SVTR to deploy mode."""
28
+ for m in model.modules():
29
+ if hasattr(m, 'switch_to_deploy'):
30
+ m.switch_to_deploy()
31
+ return model
32
+
33
+
34
+ def download_from_hf(filename):
35
+ try:
36
+ from huggingface_hub import hf_hub_download
37
+ print(f"[FastPlateOCR] Downloading/Verifying {filename} from HuggingFace...")
38
+ return hf_hub_download(repo_id="anhone3/FastPlateOCR", filename=filename)
39
+ except ImportError:
40
+ raise ImportError("Please install huggingface_hub to auto-download models: pip install huggingface_hub")
41
+
42
+
43
+ class FastPlateOCR:
44
+ def __init__(self,
45
+ use_det=True,
46
+ use_rec=True,
47
+ det_model_path=None,
48
+ rec_model_path=None,
49
+ device=None):
50
+
51
+ self.device = torch.device(device if device else ('cuda:0' if torch.cuda.is_available() else 'cpu'))
52
+ print(f"[FastPlateOCR] Initializing on {self.device}...")
53
+
54
+ self.det_model = None
55
+ self.rec_model = None
56
+ self.post_process_class = None
57
+
58
+ # 1. Initialize DET (YOLO)
59
+ if use_det:
60
+ if det_model_path is None:
61
+ det_model_path = download_from_hf("yolo26n_rep_mixer/best.pt")
62
+
63
+ print(f"[FastPlateOCR] Loading Detection Model: {det_model_path}")
64
+ setup_monkey_patch_injection()
65
+ self.det_model = YOLO(det_model_path)
66
+ self.det_model.to(self.device)
67
+ self.det_model.fuse()
68
+
69
+ # Fuse RepMixerBlock inside YOLO if they exist
70
+ if hasattr(self.det_model, 'model'):
71
+ for m in self.det_model.model.modules():
72
+ if hasattr(m, 'switch_to_deploy'):
73
+ m.switch_to_deploy()
74
+
75
+ # 2. Initialize REC (SVTRv2)
76
+ if use_rec:
77
+ if rec_model_path is None:
78
+ rec_model_path = download_from_hf("svtrv2_tiny_efficient_rctc/best.pth")
79
+
80
+ print(f"[FastPlateOCR] Loading Recognition Model: {rec_model_path}")
81
+ checkpoint = torch.load(rec_model_path, map_location='cpu')
82
+ cfg = checkpoint['config']
83
+
84
+ # Monkey patch the dictionary path
85
+ dict_path = os.path.join(os.path.dirname(__file__), 'license_plate_dict.txt')
86
+ cfg['Global']['character_dict_path'] = dict_path
87
+
88
+ self.post_process_class = build_post_process(cfg['PostProcess'], cfg['Global'])
89
+ cfg['Architecture']['Decoder']['out_channels'] = self.post_process_class.get_character_num()
90
+
91
+ self.rec_model = build_model(cfg['Architecture'])
92
+ self.rec_model.load_state_dict(checkpoint['state_dict'], strict=True)
93
+ self.rec_model.to(self.device)
94
+ self.rec_model.eval()
95
+ self.rec_model = switch_deploy_flag(self.rec_model)
96
+
97
+ if self.det_model or self.rec_model:
98
+ print("[FastPlateOCR] Models loaded successfully!")
99
+ else:
100
+ print("[FastPlateOCR] WARNING: No models loaded. Please provide det_model_path or rec_model_path.")
101
+
102
+ def _preprocess_crop(self, img_crop, target_h=32, target_w=128):
103
+ """Preprocesses cropped image for SVTR."""
104
+ img = cv2.resize(img_crop, (target_w, target_h))
105
+ img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
106
+ img = img.astype(np.float32) / 255.0
107
+ img -= 0.5
108
+ img /= 0.5
109
+ img = img.transpose((2, 0, 1))
110
+ img = np.expand_dims(img, axis=0)
111
+ return torch.from_numpy(img).to(self.device)
112
+
113
+ def detect(self, img, conf_thresh=0.25):
114
+ """
115
+ Detect license plates in an image.
116
+ Returns: list of bounding boxes [[x1, y1, x2, y2], ...]
117
+ """
118
+ if self.det_model is None:
119
+ raise ValueError("Detection model not loaded! Initialize with det_model_path.")
120
+
121
+ if isinstance(img, str):
122
+ img = cv2.imread(img)
123
+
124
+ det_results = self.det_model(img, verbose=False, conf=conf_thresh)[0]
125
+ boxes = det_results.boxes.data.cpu().numpy() # [x1, y1, x2, y2, conf, cls]
126
+
127
+ results = []
128
+ for box in boxes:
129
+ x1, y1, x2, y2, conf, cls = box
130
+ results.append([int(x1), int(y1), int(x2), int(y2)])
131
+ return results
132
+
133
+ def recognize(self, crop_img):
134
+ """
135
+ Recognize text in a cropped license plate image.
136
+ Returns: tuple (text, confidence_score)
137
+ """
138
+ if self.rec_model is None:
139
+ raise ValueError("Recognition model not loaded! Initialize with rec_model_path.")
140
+
141
+ if crop_img.size == 0:
142
+ return "", 0.0
143
+
144
+ tensor = self._preprocess_crop(crop_img)
145
+ with torch.no_grad():
146
+ preds = self.rec_model(tensor)
147
+
148
+ post_result = self.post_process_class(preds)
149
+ text, score = post_result[0]
150
+ return text, float(score)
151
+
152
+ def read(self, image_path, conf_thresh=0.25):
153
+ """
154
+ End-to-End inference.
155
+ Returns: list of dicts [{'box': [x1,y1,x2,y2], 'text': '51F1234', 'score': 0.99}]
156
+ """
157
+ if self.det_model is None or self.rec_model is None:
158
+ raise ValueError("End-to-End read() requires BOTH det_model_path and rec_model_path to be loaded.")
159
+
160
+ img = cv2.imread(image_path)
161
+ if img is None:
162
+ raise ValueError(f"Could not read image: {image_path}")
163
+
164
+ boxes = self.detect(img, conf_thresh)
165
+ final_results = []
166
+
167
+ h, w, _ = img.shape
168
+ for box in boxes:
169
+ x1, y1, x2, y2 = box
170
+
171
+ # Ensure bounds
172
+ x1_c, y1_c = max(0, x1), max(0, y1)
173
+ x2_c, y2_c = min(w, x2), min(h, y2)
174
+
175
+ crop_img = img[y1_c:y2_c, x1_c:x2_c]
176
+
177
+ text, score = self.recognize(crop_img)
178
+
179
+ final_results.append({
180
+ 'box': box,
181
+ 'text': text,
182
+ 'score': score
183
+ })
184
+
185
+ return final_results
@@ -0,0 +1,71 @@
1
+ import copy
2
+ from importlib import import_module
3
+ from torch import nn
4
+
5
+ name_to_module = {
6
+ 'ABINetLoss': '.abinet_loss',
7
+ 'ARLoss': '.ar_loss',
8
+ 'CDistNetLoss': '.cdistnet_loss',
9
+ 'CELoss': '.ce_loss',
10
+ 'CPPDLoss': '.cppd_loss',
11
+ 'CTCLoss': '.ctc_loss',
12
+ 'IGTRLoss': '.igtr_loss',
13
+ 'LISTERLoss': '.lister_loss',
14
+ 'LPVLoss': '.lpv_loss',
15
+ 'MGPLoss': '.mgp_loss',
16
+ 'PARSeqLoss': '.parseq_loss',
17
+ 'RobustScannerLoss': '.robustscanner_loss',
18
+ 'SEEDLoss': '.seed_loss',
19
+ 'SMTRLoss': '.smtr_loss',
20
+ 'SRNLoss': '.srn_loss',
21
+ 'VisionLANLoss': '.visionlan_loss',
22
+ 'CAMLoss': '.cam_loss',
23
+ 'MDiffLoss': '.mdiff_loss',
24
+ 'UniRecLoss': '.unirec_loss',
25
+ 'CMERLoss': '.cmer_loss',
26
+ }
27
+
28
+
29
+ def build_loss(config):
30
+ config = copy.deepcopy(config)
31
+ module_name = config.pop('name')
32
+
33
+ if module_name in globals():
34
+ module_class = globals()[module_name]
35
+ else:
36
+ assert module_name in name_to_module, Exception(
37
+ '{} is not supported. The losses in {} are supportes'.format(
38
+ module_name, list(name_to_module.keys())))
39
+ module_path = name_to_module[module_name]
40
+ module = import_module(module_path, package=__package__)
41
+ module_class = getattr(module, module_name)
42
+
43
+ return module_class(**config)
44
+
45
+
46
+ class GTCLoss(nn.Module):
47
+
48
+ def __init__(self,
49
+ gtc_loss,
50
+ gtc_weight=1.0,
51
+ ctc_weight=1.0,
52
+ zero_infinity=True,
53
+ **kwargs):
54
+ super(GTCLoss, self).__init__()
55
+ # Dynamically build CTCLoss
56
+ ctc_config = {'name': 'CTCLoss', 'zero_infinity': zero_infinity}
57
+ self.ctc_loss = build_loss(ctc_config)
58
+ # Build GTC loss
59
+ self.gtc_loss = build_loss(gtc_loss)
60
+ self.gtc_weight = gtc_weight
61
+ self.ctc_weight = ctc_weight
62
+
63
+ def forward(self, predicts, batch):
64
+ ctc_loss = self.ctc_loss(predicts['ctc_pred'],
65
+ [None] + batch[-2:])['loss']
66
+ gtc_loss = self.gtc_loss(predicts['gtc_pred'], batch[:-2])['loss']
67
+ return {
68
+ 'loss': self.ctc_weight * ctc_loss + self.gtc_weight * gtc_loss,
69
+ 'ctc_loss': ctc_loss,
70
+ 'gtc_loss': gtc_loss
71
+ }
@@ -0,0 +1,33 @@
1
+ import torch
2
+ from torch import nn
3
+
4
+
5
+ class CTCLoss(nn.Module):
6
+
7
+ def __init__(self, use_focal_loss=False, zero_infinity=False, **kwargs):
8
+ super(CTCLoss, self).__init__()
9
+ self.loss_func = nn.CTCLoss(blank=0,
10
+ reduction='none',
11
+ zero_infinity=zero_infinity)
12
+ self.use_focal_loss = use_focal_loss
13
+
14
+ def forward(self, predicts, batch):
15
+ # predicts = predicts['res']
16
+
17
+ batch_size = predicts.size(0)
18
+ label, label_length = batch[1], batch[2]
19
+ predicts = predicts.log_softmax(2)
20
+ predicts = predicts.permute(1, 0, 2)
21
+ preds_lengths = torch.tensor([predicts.size(0)] * batch_size,
22
+ dtype=torch.long)
23
+ loss = self.loss_func(predicts, label, preds_lengths, label_length)
24
+
25
+ if self.use_focal_loss:
26
+ # Use torch.clamp to limit the range of loss, avoiding overflow in exponential calculation
27
+ clamped_loss = torch.clamp(loss, min=-20, max=20)
28
+ weight = 1 - torch.exp(-clamped_loss)
29
+ weight = torch.square(weight)
30
+ # Use torch.where to avoid multiplying by zero weight
31
+ loss = torch.where(weight > 0, loss * weight, loss)
32
+ loss = loss.mean()
33
+ return {'loss': loss}
@@ -0,0 +1,33 @@
1
+ import copy
2
+
3
+ __all__ = ['build_metric']
4
+
5
+ support_dict = [
6
+ 'RecMetric', 'RecMetricLong', 'RecGTCMetric', 'RecMPGMetric', 'CMERMetric'
7
+ ]
8
+
9
+
10
+ def build_metric(config):
11
+ config = copy.deepcopy(config)
12
+ module_name = config.pop('name')
13
+ assert module_name in support_dict, Exception(
14
+ 'metric only support {}'.format(support_dict))
15
+
16
+ # Lazy import
17
+ if module_name == 'RecMetric':
18
+ from .rec_metric import RecMetric
19
+ module_class = RecMetric(**config)
20
+ elif module_name == 'RecGTCMetric':
21
+ from .rec_metric_gtc import RecGTCMetric
22
+ module_class = RecGTCMetric(**config)
23
+ elif module_name == 'RecMetricLong':
24
+ from .rec_metric_long import RecMetricLong
25
+ module_class = RecMetricLong(**config)
26
+ elif module_name == 'RecMPGMetric':
27
+ from .rec_metric_mgp import RecMPGMetric
28
+ module_class = RecMPGMetric(**config)
29
+ elif module_name == 'CMERMetric':
30
+ from .rec_metric_cmer import CMERMetric
31
+ module_class = CMERMetric(**config)
32
+
33
+ return module_class