wzf19947 commited on
Commit
4d52916
·
1 Parent(s): 45b2cae

上传yolo26示例

Browse files
CPP/ax_yolo26_qrcode_batch ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ef3c3985f481a52e9b4b5ec03b7602e5f11fcd7d383cc93fb054f162df02ac74
3
+ size 6432904
README.md CHANGED
@@ -40,6 +40,7 @@ For those who are interested in model conversion, you can try to export axmodel
40
  |AX650|yolov10n|3.67 ms|
41
  ||yolo11n|3.42 ms|
42
  ||yolo12n|6.87 ms|
 
43
  ||NanodetPlus|2.16 ms|
44
  ||DEIMv2_femto(u16)|3.76 ms|
45
  |||
@@ -49,6 +50,7 @@ For those who are interested in model conversion, you can try to export axmodel
49
  |AX630C|yolov10n|9.71 ms|
50
  ||yolo11n|9.65 ms|
51
  ||yolo12n|20.24 ms|
 
52
  ||NanodetPlus|5.93 ms|
53
  |||
54
  ||yolov5n|2.11 ms|
@@ -57,6 +59,7 @@ For those who are interested in model conversion, you can try to export axmodel
57
  |AX637|yolov10n|4.05 ms|
58
  ||yolo11n|3.84 ms|
59
  ||yolo12n|6.40 ms|
 
60
  ||NanodetPlus|2.38 ms|
61
 
62
  ## How to use
@@ -71,7 +74,8 @@ Download all files from this repository to the device
71
  │   ├── ax_deimv2_qrcode_batch
72
  │   ├── ax_nanodetplus_qrcode_batch
73
  │   ├── ax_yolov5_qrcode_batch
74
- │   ── ax_yolov8_qrcode_batch
 
75
  ├── cpp_result.png
76
  ├── images
77
  │   ├── qrcode_01.jpg
@@ -84,6 +88,7 @@ Download all files from this repository to the device
84
  │   │   ├── nanodet-plus-m_630_npu1.axmodel
85
  │   │   ├── yolo11n_630_npu1.axmodel
86
  │   │   ├── yolo12n_630_npu1.axmodel
 
87
  │   │   ├── yolov10n_630_npu1.axmodel
88
  │   │   ├── yolov5n_630_npu1.axmodel
89
  │   │   ├── yolov8n_630_npu1.axmodel
@@ -92,6 +97,7 @@ Download all files from this repository to the device
92
  │   │   ├── nanodet-plus-m_637_npu1.axmodel
93
  │   │   ├── yolo11n_637_npu1.axmodel
94
  │   │   ├── yolo12n_637_npu1.axmodel
 
95
  │   │   ├── yolov10n_637_npu1.axmodel
96
  │   │   ├── yolov5n_637_npu1.axmodel
97
  │   │   ├── yolov8n_637_npu1.axmodel
@@ -101,6 +107,7 @@ Download all files from this repository to the device
101
  │   ├── nanodet-plus-m_650_npu1.axmodel
102
  │   ├── yolo11n_650_npu1.axmodel
103
  │   ├── yolo12n_650_npu1.axmodel
 
104
  │   ├── yolov10n_650_npu1.axmodel
105
  │   ├── yolov5n_650_npu1.axmodel
106
  │   ├── yolov8n_650_npu1.axmodel
@@ -111,10 +118,12 @@ Download all files from this repository to the device
111
  │   ├── QRCode_axmodel_infer_Nanodet.py
112
  │   ├── QRCode_axmodel_infer_v5.py
113
  │   ├── QRCode_axmodel_infer_v8.py
 
114
  │   ├── QRCode_onnx_infer_DEIMv2.py
115
  │   ├── QRCode_onnx_infer_Nanodet.py
116
  │   ├── QRCode_onnx_infer_v5.py
117
  │   ├── QRCode_onnx_infer_v8.py
 
118
  │   └── requirements.txt
119
  └── README.md
120
 
 
40
  |AX650|yolov10n|3.67 ms|
41
  ||yolo11n|3.42 ms|
42
  ||yolo12n|6.87 ms|
43
+ ||yolo26n|3.24 ms|
44
  ||NanodetPlus|2.16 ms|
45
  ||DEIMv2_femto(u16)|3.76 ms|
46
  |||
 
50
  |AX630C|yolov10n|9.71 ms|
51
  ||yolo11n|9.65 ms|
52
  ||yolo12n|20.24 ms|
53
+ ||yolo26n|10.04 ms|
54
  ||NanodetPlus|5.93 ms|
55
  |||
56
  ||yolov5n|2.11 ms|
 
59
  |AX637|yolov10n|4.05 ms|
60
  ||yolo11n|3.84 ms|
61
  ||yolo12n|6.40 ms|
62
+ ||yolo26n|3.50 ms|
63
  ||NanodetPlus|2.38 ms|
64
 
65
  ## How to use
 
74
  │   ├── ax_deimv2_qrcode_batch
75
  │   ├── ax_nanodetplus_qrcode_batch
76
  │   ├── ax_yolov5_qrcode_batch
77
+ │   ── ax_yolov8_qrcode_batch
78
+ │   └── ax_yolo26_qrcode_batch
79
  ├── cpp_result.png
80
  ├── images
81
  │   ├── qrcode_01.jpg
 
88
  │   │   ├── nanodet-plus-m_630_npu1.axmodel
89
  │   │   ├── yolo11n_630_npu1.axmodel
90
  │   │   ├── yolo12n_630_npu1.axmodel
91
+ │   │   ├── yolo26n_630_npu1.axmodel
92
  │   │   ├── yolov10n_630_npu1.axmodel
93
  │   │   ├── yolov5n_630_npu1.axmodel
94
  │   │   ├── yolov8n_630_npu1.axmodel
 
97
  │   │   ├── nanodet-plus-m_637_npu1.axmodel
98
  │   │   ├── yolo11n_637_npu1.axmodel
99
  │   │   ├── yolo12n_637_npu1.axmodel
100
+ │   │   ├── yolo26n_637_npu1.axmodel
101
  │   │   ├── yolov10n_637_npu1.axmodel
102
  │   │   ├── yolov5n_637_npu1.axmodel
103
  │   │   ├── yolov8n_637_npu1.axmodel
 
107
  │   ├── nanodet-plus-m_650_npu1.axmodel
108
  │   ├── yolo11n_650_npu1.axmodel
109
  │   ├── yolo12n_650_npu1.axmodel
110
+ │   ├── yolo26n_650_npu1.axmodel
111
  │   ├── yolov10n_650_npu1.axmodel
112
  │   ├── yolov5n_650_npu1.axmodel
113
  │   ├── yolov8n_650_npu1.axmodel
 
118
  │   ├── QRCode_axmodel_infer_Nanodet.py
119
  │   ├── QRCode_axmodel_infer_v5.py
120
  │   ├── QRCode_axmodel_infer_v8.py
121
+ │   ├── QRCode_axmodel_infer_26.py
122
  │   ├── QRCode_onnx_infer_DEIMv2.py
123
  │   ├── QRCode_onnx_infer_Nanodet.py
124
  │   ├── QRCode_onnx_infer_v5.py
125
  │   ├── QRCode_onnx_infer_v8.py
126
+ │   ├── QRCode_onnx_infer_26.py
127
  │   └── requirements.txt
128
  └── README.md
129
 
model/AX620E/yolo26n_630_npu1.axmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2538321a9e3121d621be6a98d182440d35184efe284bc8b114ba80f59b30299a
3
+ size 3126146
model/AX637/yolo26n_637_npu1.axmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:668b3b259c1b12c020e1d29ef43e2684e701f768a949fcd3e19ee225fcef5084
3
+ size 2752068
model/AX650/yolo26n_650_npu1.axmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0407af623596db0ed16e66223fd2cb1107e4cb2f6008884f4521e9f99bda38c7
3
+ size 2847884
python/QRCode_axmodel_infer_26.py ADDED
@@ -0,0 +1,597 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axengine as axe
2
+ import cv2
3
+ import numpy as np
4
+ import time
5
+ import yaml
6
+ import glob
7
+ import os
8
+ import torch
9
+ from pyzbar import pyzbar
10
+ names = [
11
+ "QRCode"
12
+ ]
13
+
14
+ def non_max_suppression(
15
+ prediction,
16
+ conf_thres: float = 0.25,
17
+ iou_thres: float = 0.45,
18
+ classes=None,
19
+ agnostic: bool = False,
20
+ multi_label: bool = False,
21
+ labels=(),
22
+ max_det: int = 300,
23
+ nc: int = 0, # number of classes (optional)
24
+ max_time_img: float = 0.05,
25
+ max_nms: int = 30000,
26
+ max_wh: int = 7680,
27
+ rotated: bool = False,
28
+ end2end: bool = False,
29
+ return_idxs: bool = False,
30
+ ):
31
+ """Perform non-maximum suppression (NMS) on prediction results.
32
+
33
+ Applies NMS to filter overlapping bounding boxes based on confidence and IoU thresholds. Supports multiple detection
34
+ formats including standard boxes, rotated boxes, and masks.
35
+
36
+ Args:
37
+ prediction (torch.Tensor): Predictions with shape (batch_size, num_classes + 4 + num_masks, num_boxes)
38
+ containing boxes, classes, and optional masks.
39
+ conf_thres (float): Confidence threshold for filtering detections. Valid values are between 0.0 and 1.0.
40
+ iou_thres (float): IoU threshold for NMS filtering. Valid values are between 0.0 and 1.0.
41
+ classes (list[int], optional): List of class indices to consider. If None, all classes are considered.
42
+ agnostic (bool): Whether to perform class-agnostic NMS.
43
+ multi_label (bool): Whether each box can have multiple labels.
44
+ labels (list[list[Union[int, float, torch.Tensor]]]): A priori labels for each image.
45
+ max_det (int): Maximum number of detections to keep per image.
46
+ nc (int): Number of classes. Indices after this are considered masks.
47
+ max_time_img (float): Maximum time in seconds for processing one image.
48
+ max_nms (int): Maximum number of boxes for NMS.
49
+ max_wh (int): Maximum box width and height in pixels.
50
+ rotated (bool): Whether to handle Oriented Bounding Boxes (OBB).
51
+ end2end (bool): Whether the model is end-to-end and doesn't require NMS.
52
+ return_idxs (bool): Whether to return the indices of kept detections.
53
+
54
+ Returns:
55
+ output (list[torch.Tensor]): List of detections per image with shape (num_boxes, 6 + num_masks) containing (x1,
56
+ y1, x2, y2, confidence, class, mask1, mask2, ...).
57
+ keepi (list[torch.Tensor]): Indices of kept detections if return_idxs=True.
58
+ """
59
+ # Checks
60
+ assert 0 <= conf_thres <= 1, f"Invalid Confidence threshold {conf_thres}, valid values are between 0.0 and 1.0"
61
+ assert 0 <= iou_thres <= 1, f"Invalid IoU {iou_thres}, valid values are between 0.0 and 1.0"
62
+ if isinstance(prediction, (list, tuple)): # YOLOv8 model in validation model, output = (inference_out, loss_out)
63
+ prediction = prediction[0] # select only inference output
64
+ if classes is not None:
65
+ classes = torch.tensor(classes, device=prediction.device)
66
+
67
+ if prediction.shape[-1] == 6 or end2end: # end-to-end model (BNC, i.e. 1,300,6)
68
+ output = [pred[pred[:, 4] > conf_thres][:max_det] for pred in prediction]
69
+ if classes is not None:
70
+ output = [pred[(pred[:, 5:6] == classes).any(1)] for pred in output]
71
+ return output
72
+
73
+ bs = prediction.shape[0] # batch size (BCN, i.e. 1,84,6300)
74
+ nc = nc or (prediction.shape[1] - 4) # number of classes
75
+ extra = prediction.shape[1] - nc - 4 # number of extra info
76
+ mi = 4 + nc # mask start index
77
+ xc = prediction[:, 4:mi].amax(1) > conf_thres # candidates
78
+ xinds = torch.arange(prediction.shape[-1], device=prediction.device).expand(bs, -1)[..., None] # to track idxs
79
+
80
+ # Settings
81
+ # min_wh = 2 # (pixels) minimum box width and height
82
+ time_limit = 2.0 + max_time_img * bs # seconds to quit after
83
+ multi_label &= nc > 1 # multiple labels per box (adds 0.5ms/img)
84
+
85
+ prediction = prediction.transpose(-1, -2) # shape(1,84,6300) to shape(1,6300,84)
86
+ if not rotated:
87
+ prediction[..., :4] = xywh2xyxy(prediction[..., :4]) # xywh to xyxy
88
+
89
+ t = time.time()
90
+ output = [torch.zeros((0, 6 + extra), device=prediction.device)] * bs
91
+ keepi = [torch.zeros((0, 1), device=prediction.device)] * bs # to store the kept idxs
92
+ for xi, (x, xk) in enumerate(zip(prediction, xinds)): # image index, (preds, preds indices)
93
+ # Apply constraints
94
+ # x[((x[:, 2:4] < min_wh) | (x[:, 2:4] > max_wh)).any(1), 4] = 0 # width-height
95
+ filt = xc[xi] # confidence
96
+ x = x[filt]
97
+ if return_idxs:
98
+ xk = xk[filt]
99
+
100
+ # Cat apriori labels if autolabelling
101
+ if labels and len(labels[xi]) and not rotated:
102
+ lb = labels[xi]
103
+ v = torch.zeros((len(lb), nc + extra + 4), device=x.device)
104
+ v[:, :4] = xywh2xyxy(lb[:, 1:5]) # box
105
+ v[range(len(lb)), lb[:, 0].long() + 4] = 1.0 # cls
106
+ x = torch.cat((x, v), 0)
107
+
108
+ # If none remain process next image
109
+ if not x.shape[0]:
110
+ continue
111
+
112
+ # Detections matrix nx6 (xyxy, conf, cls)
113
+ box, cls, mask = x.split((4, nc, extra), 1)
114
+
115
+ if multi_label:
116
+ i, j = torch.where(cls > conf_thres)
117
+ x = torch.cat((box[i], x[i, 4 + j, None], j[:, None].float(), mask[i]), 1)
118
+ if return_idxs:
119
+ xk = xk[i]
120
+ else: # best class only
121
+ conf, j = cls.max(1, keepdim=True)
122
+ filt = conf.view(-1) > conf_thres
123
+ x = torch.cat((box, conf, j.float(), mask), 1)[filt]
124
+ if return_idxs:
125
+ xk = xk[filt]
126
+
127
+ # Filter by class
128
+ if classes is not None:
129
+ filt = (x[:, 5:6] == classes).any(1)
130
+ x = x[filt]
131
+ if return_idxs:
132
+ xk = xk[filt]
133
+
134
+ # Check shape
135
+ n = x.shape[0] # number of boxes
136
+ if not n: # no boxes
137
+ continue
138
+ if n > max_nms: # excess boxes
139
+ filt = x[:, 4].argsort(descending=True)[:max_nms] # sort by confidence and remove excess boxes
140
+ x = x[filt]
141
+ if return_idxs:
142
+ xk = xk[filt]
143
+
144
+ c = x[:, 5:6] * (0 if agnostic else max_wh) # classes
145
+ scores = x[:, 4] # scores
146
+ if rotated:
147
+ boxes = torch.cat((x[:, :2] + c, x[:, 2:4], x[:, -1:]), dim=-1) # xywhr
148
+ i = TorchNMS.fast_nms(boxes, scores, iou_thres, iou_func=batch_probiou)
149
+ else:
150
+ boxes = x[:, :4] + c # boxes (offset by class)
151
+ # Speed strategy: torchvision for val or already loaded (faster), TorchNMS for predict (lower latency)
152
+ if "torchvision" in sys.modules:
153
+ import torchvision # scope as slow import
154
+
155
+ i = torchvision.ops.nms(boxes, scores, iou_thres)
156
+ else:
157
+ i = TorchNMS.nms(boxes, scores, iou_thres)
158
+ i = i[:max_det] # limit detections
159
+
160
+ output[xi] = x[i]
161
+ if return_idxs:
162
+ keepi[xi] = xk[i].view(-1)
163
+ if (time.time() - t) > time_limit:
164
+ LOGGER.warning(f"NMS time limit {time_limit:.3f}s exceeded")
165
+ break # time limit exceeded
166
+
167
+ return (output, keepi) if return_idxs else output
168
+
169
+ def letterbox(im, new_shape=(640, 640), color=(114, 114, 114), auto=True, scaleFill=False, scaleup=True, stride=32):
170
+
171
+ shape = im.shape[:2]
172
+ if isinstance(new_shape, int):
173
+ new_shape = (new_shape, new_shape)
174
+
175
+ r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
176
+ if not scaleup:
177
+ r = min(r, 1.0)
178
+
179
+ ratio = r, r
180
+ new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
181
+ dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]
182
+ if auto:
183
+ dw, dh = np.mod(dw, stride), np.mod(dh, stride)
184
+ elif scaleFill:
185
+ dw, dh = 0.0, 0.0
186
+ new_unpad = (new_shape[1], new_shape[0])
187
+ ratio = new_shape[1] / shape[1], new_shape[0] / shape[0]
188
+
189
+ dw /= 2
190
+ dh /= 2
191
+
192
+ if shape[::-1] != new_unpad:
193
+ im = cv2.resize(im, new_unpad, interpolation=cv2.INTER_LINEAR)
194
+ top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
195
+ left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
196
+ im = cv2.copyMakeBorder(im, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)
197
+ return im, ratio, (dw, dh)
198
+
199
+ def data_process_cv2(frame, input_shape):
200
+ '''
201
+ 对输入的图像进行预处理
202
+ :param frame:
203
+ :param input_shape:
204
+ :return:
205
+ '''
206
+ im0 = cv2.imread(frame)
207
+ img = letterbox(im0, input_shape, auto=False, stride=32)[0]
208
+ org_data = img.copy()
209
+ img = np.ascontiguousarray(img[:, :, ::-1])
210
+ img = np.asarray(img, dtype=np.uint8)
211
+ img = np.expand_dims(img, 0)
212
+ return img, im0, org_data
213
+
214
+ # Define xywh2xyxy function for converting bounding box format
215
+ def xywh2xyxy(x):
216
+ """
217
+ Convert bounding boxes from (center_x, center_y, width, height) to (x1, y1, x2, y2) format.
218
+
219
+ Parameters:
220
+ x (ndarray): Bounding boxes in (center_x, center_y, width, height) format, shaped (N, 4).
221
+
222
+ Returns:
223
+ ndarray: Bounding boxes in (x1, y1, x2, y2) format, shaped (N, 4).
224
+ """
225
+ y = x.copy()
226
+ y[:, 0] = x[:, 0] - x[:, 2] / 2
227
+ y[:, 1] = x[:, 1] - x[:, 3] / 2
228
+ y[:, 2] = x[:, 0] + x[:, 2] / 2
229
+ y[:, 3] = x[:, 1] + x[:, 3] / 2
230
+ return y
231
+
232
+ def xyxy2xywh(x):
233
+ # Convert nx4 boxes from [x1, y1, x2, y2] to [x, y, w, h] where xy1=top-left, xy2=bottom-right
234
+ y = np.copy(x)
235
+ y[:, 0] = (x[:, 0] + x[:, 2]) / 2 # x center
236
+ y[:, 1] = (x[:, 1] + x[:, 3]) / 2 # y center
237
+ y[:, 2] = x[:, 2] - x[:, 0] # width
238
+ y[:, 3] = x[:, 3] - x[:, 1] # height
239
+ return y
240
+
241
+ def post_process_yolo(det, im, im0, gn, save_path, img_name):
242
+ detections = []
243
+ if len(det):
244
+ det[:, :4] = scale_boxes(im.shape[:2], det[:, :4], im0.shape).round()
245
+ colors = Colors()
246
+ for *xyxy, conf, cls in reversed(det):
247
+ print("class:",int(cls), "left:%.0f" % xyxy[0],"top:%.0f" % xyxy[1],"right:%.0f" % xyxy[2],"bottom:%.0f" % xyxy[3], "conf:",'{:.0f}%'.format(float(conf)*100))
248
+ int_coords = [int(tensor.item()) for tensor in xyxy]
249
+ detections.append(int_coords)
250
+ # c = int(cls)
251
+ # label = names[c]
252
+ # res_img = plot_one_box(xyxy, im0, label=f'{label}:{conf:.2f}', color=colors(c, True), line_thickness=4)
253
+ # cv2.imwrite(f'{save_path}/{img_name}.jpg',res_img)
254
+ # xywh = (xyxy2xywh(np.array(xyxy,dtype=np.float32).reshape(1, 4)) / gn).reshape(-1).tolist() # normalized xywh
255
+ # line = (cls, *xywh) # label format
256
+ # with open(f'{save_path}/{img_name}.txt', 'a') as f:
257
+ # f.write(('%g ' * len(line)).rstrip() % line + '\n')
258
+ return detections
259
+
260
+ def scale_boxes(img1_shape, boxes, img0_shape, ratio_pad=None):
261
+ if ratio_pad is None:
262
+ gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1])
263
+ pad = (img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2
264
+ else:
265
+ gain = ratio_pad[0][0]
266
+ pad = ratio_pad[1]
267
+
268
+ boxes[..., [0, 2]] -= pad[0]
269
+ boxes[..., [1, 3]] -= pad[1]
270
+ boxes[..., :4] /= gain
271
+ clip_boxes(boxes, img0_shape)
272
+ return boxes
273
+
274
+ def clip_boxes(boxes, shape):
275
+ boxes[..., [0, 2]] = boxes[..., [0, 2]].clip(0, shape[1])
276
+ boxes[..., [1, 3]] = boxes[..., [1, 3]].clip(0, shape[0])
277
+
278
+
279
+ def yaml_load(file='coco128.yaml'):
280
+ with open(file, errors='ignore') as f:
281
+ return yaml.safe_load(f)
282
+
283
+
284
+ class Colors:
285
+ # Ultralytics color palette https://ultralytics.com/
286
+ def __init__(self):
287
+ """
288
+ Initializes the Colors class with a palette derived from Ultralytics color scheme, converting hex codes to RGB.
289
+ Colors derived from `hex = matplotlib.colors.TABLEAU_COLORS.values()`.
290
+ """
291
+ hexs = (
292
+ "FF3838",
293
+ "FF9D97",
294
+ "FF701F",
295
+ "FFB21D",
296
+ "CFD231",
297
+ "48F90A",
298
+ "92CC17",
299
+ "3DDB86",
300
+ "1A9334",
301
+ "00D4BB",
302
+ "2C99A8",
303
+ "00C2FF",
304
+ "344593",
305
+ "6473FF",
306
+ "0018EC",
307
+ "8438FF",
308
+ "520085",
309
+ "CB38FF",
310
+ "FF95C8",
311
+ "FF37C7",
312
+ )
313
+ self.palette = [self.hex2rgb(f"#{c}") for c in hexs]
314
+ self.n = len(self.palette)
315
+
316
+ def __call__(self, i, bgr=False):
317
+ """Returns color from palette by index `i`, in BGR format if `bgr=True`, else RGB; `i` is an integer index."""
318
+ c = self.palette[int(i) % self.n]
319
+ return (c[2], c[1], c[0]) if bgr else c
320
+
321
+ @staticmethod
322
+ def hex2rgb(h):
323
+ """Converts hex color codes to RGB values (i.e. default PIL order)."""
324
+ return tuple(int(h[1 + i: 1 + i + 2], 16) for i in (0, 2, 4))
325
+
326
+ def plot_one_box(x, im, color=None, label=None, line_thickness=3, steps=2, orig_shape=None):
327
+ assert im.data.contiguous, 'Image not contiguous. Apply np.ascontiguousarray(im) to plot_on_box() input image.'
328
+ tl = line_thickness or round(0.002 * (im.shape[0] + im.shape[1]) / 2) + 1
329
+ c1, c2 = (int(x[0]), int(x[1])), (int(x[2]), int(x[3]))
330
+ cv2.rectangle(im, c1, c2, color, thickness=tl*1//3, lineType=cv2.LINE_AA)
331
+ if label:
332
+ if len(label.split(':')) > 1:
333
+ tf = max(tl - 1, 1)
334
+ t_size = cv2.getTextSize(label, 0, fontScale=tl / 6, thickness=tf)[0]
335
+ c2 = c1[0] + t_size[0], c1[1] - t_size[1] - 3
336
+ cv2.rectangle(im, c1, c2, color, -1, cv2.LINE_AA)
337
+ cv2.putText(im, label, (c1[0], c1[1] - 2), 0, tl / 6, [225, 255, 255], thickness=tf//2, lineType=cv2.LINE_AA)
338
+ return im
339
+
340
+ def model_load(model):
341
+ session = axe.InferenceSession(model)
342
+ input_name = session.get_inputs()[0].name
343
+ output_names = [ x.name for x in session.get_outputs()]
344
+ return session, output_names
345
+
346
+ def make_anchors(feats, strides, grid_cell_offset=0.5):
347
+ """Generate anchors from features."""
348
+ anchor_points, stride_tensor = [], []
349
+ assert feats is not None
350
+ dtype = feats[0].dtype
351
+ for i, stride in enumerate(strides):
352
+ # _, _, h, w = feats[i].shape
353
+ h, w = feats[i].shape[2:] if isinstance(feats, list) else (int(feats[i][0]), int(feats[i][1]))
354
+ sx = np.arange(w, dtype=dtype) + grid_cell_offset # shift x
355
+ sy = np.arange(h, dtype=dtype) + grid_cell_offset # shift y
356
+ sy, sx = np.meshgrid(sy, sx, indexing='ij')
357
+ anchor_points.append(np.stack((sx, sy), axis=-1).reshape(-1, 2))
358
+ stride_tensor.append(np.full((h * w, 1), stride, dtype=dtype))
359
+ return np.concatenate(anchor_points), np.concatenate(stride_tensor)
360
+
361
+ def dist2bbox(distance, anchor_points, xywh=True, dim=-1):
362
+ """Transform distance(ltrb) to box(xywh or xyxy)."""
363
+ lt, rb = np.split(distance, 2, axis=dim)
364
+ x1y1 = anchor_points - lt
365
+ x2y2 = anchor_points + rb
366
+ if xywh:
367
+ c_xy = (x1y1 + x2y2) / 2
368
+ wh = x2y2 - x1y1
369
+ return np.concatenate((c_xy, wh), axis=dim) # xywh bbox
370
+ return np.concatenate((x1y1, x2y2), axis=dim) # xyxy bbox
371
+
372
+
373
+ class DFL:
374
+ """
375
+ NumPy implementation of Distribution Focal Loss (DFL) integral module.
376
+ Original paper: Generalized Focal Loss (IEEE TPAMI 2023)
377
+ """
378
+
379
+ def __init__(self, c1=16):
380
+ """Initialize with given number of distribution channels"""
381
+ self.c1 = c1
382
+ # 初始化权重矩阵(等效于原conv层的固定权重)
383
+ self.weights = np.arange(c1, dtype=np.float32).reshape(1, c1, 1, 1)
384
+
385
+
386
+ def __call__(self, x):
387
+ """
388
+ 前向传播逻辑
389
+ 参数:
390
+ x: 输入张量,形状为(batch, channels, anchors)
391
+ 返回:
392
+ 处理后的张量,形状为(batch, 4, anchors)
393
+ """
394
+ b, c, a = x.shape
395
+
396
+ # 等效于原view->transpose->softmax操作
397
+ x_reshaped = x.reshape(b, 4, self.c1, a)
398
+ x_transposed = np.transpose(x_reshaped, (0, 2, 1, 3))
399
+ x_softmax = np.exp(x_transposed) / np.sum(np.exp(x_transposed), axis=1, keepdims=True)
400
+
401
+ # 等效卷积操作(通过张量乘积实现)
402
+ conv_result = np.sum(self.weights * x_softmax, axis=1)
403
+
404
+ return conv_result.reshape(b, 4, a)
405
+
406
+ class YOLOV8Detector:
407
+ def __init__(self, model_path, imgsz=[640,640]):
408
+ self.model_path = model_path
409
+ self.session, self.output_names = model_load(self.model_path)
410
+ self.imgsz = imgsz
411
+ self.stride = [8.,16.,32.]
412
+ self.reg_max = 1
413
+ self.nc = len(names)
414
+ self.nl = len(self.stride)
415
+ self.dfl = DFL(self.reg_max)
416
+ self.max_det = 300
417
+
418
+ def postprocess(self, preds: torch.Tensor) -> torch.Tensor:
419
+ """Post-processes YOLO model predictions.
420
+
421
+ Args:
422
+ preds (torch.Tensor): Raw predictions with shape (batch_size, num_anchors, 4 + nc) with last dimension
423
+ format [x, y, w, h, class_probs].
424
+
425
+ Returns:
426
+ (torch.Tensor): Processed predictions with shape (batch_size, min(max_det, num_anchors), 6) and last
427
+ dimension format [x, y, w, h, max_class_prob, class_index].
428
+ """
429
+ boxes, scores = preds.split([4, self.nc], dim=-1)
430
+ scores, conf, idx = self.get_topk_index(scores, self.max_det)
431
+ boxes = boxes.gather(dim=1, index=idx.repeat(1, 1, 4))
432
+ return torch.cat([boxes, scores, conf], dim=-1)
433
+
434
+ def get_topk_index(self, scores: torch.Tensor, max_det: int) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
435
+ """Get top-k indices from scores.
436
+
437
+ Args:
438
+ scores (torch.Tensor): Scores tensor with shape (batch_size, num_anchors, num_classes).
439
+ max_det (int): Maximum detections per image.
440
+
441
+ Returns:
442
+ (torch.Tensor, torch.Tensor, torch.Tensor): Top scores, class indices, and filtered indices.
443
+ """
444
+ batch_size, anchors, nc = scores.shape # i.e. shape(1,8400,84)
445
+ # Use max_det directly during export for TensorRT compatibility (requires k to be constant),
446
+ # otherwise use min(max_det, anchors) for safety with small inputs during Python inference
447
+ k = max_det
448
+ #对8400个anchor取其80类中的最大类概率,shape[1,8400]--再取topk,shape[1,k]--unsqueeze,shape[1,k,1]
449
+ ori_index = scores.max(dim=-1)[0].topk(k)[1].unsqueeze(-1)
450
+ #[1,k,1]repeat变为[1,k,80],从[1,8400,80]中取topk个完整logit
451
+ scores = scores.gather(dim=1, index=ori_index.repeat(1, 1, nc))
452
+ #展平从k*80个分数中取topk。总体就是先删选topk个最可能anchor,再从该anchor中取topk个最可能class
453
+ scores, index = scores.flatten(1).topk(k)
454
+ #映射回原位置
455
+ idx = ori_index[torch.arange(batch_size)[..., None], index // nc] # original index
456
+ return scores[..., None], (index % nc)[..., None].float(), idx
457
+
458
+ def detect_objects(self, image, save_path):
459
+ im, im0, org_data = data_process_cv2(image, self.imgsz)
460
+ img_name = os.path.basename(image).split('.')[0]
461
+ infer_start_time = time.time()
462
+ x = self.session.run(self.output_names, {self.session.get_inputs()[0].name: im})
463
+ infer_end_time = time.time()
464
+ print(f"infer time: {infer_end_time - infer_start_time:.4f}s")
465
+ x = [np.transpose(x[i],(0,3,1,2)) for i in range(self.nl)] #to nchw
466
+ anchors,strides = (np.transpose(x,(1, 0)) for x in make_anchors(x, self.stride, 0.5))
467
+ box = [x[i][:, :self.reg_max * 4,:] for i in range(self.nl)]
468
+ cls = [x[i][:, self.reg_max * 4:,:] for i in range(self.nl)]
469
+ boxes = np.concatenate([box[i].reshape(1, 4 * self.reg_max, -1) for i in range(self.nl)], axis=-1)
470
+ scores = np.concatenate([cls[i].reshape(1, self.nc, -1) for i in range(self.nl)], axis=-1)
471
+ if self.reg_max > 1:
472
+ dbox = dist2bbox(self.dfl(boxes), np.expand_dims(anchors, axis=0), xywh=False, dim=1) * strides
473
+ else: #弃用DFL
474
+ dbox = dist2bbox(boxes, np.expand_dims(anchors, axis=0), xywh=False, dim=1) * strides
475
+ y = np.concatenate((dbox, 1/(1 + np.exp(-scores))), axis=1)
476
+ y = y.transpose([0, 2, 1])
477
+ pred = self.postprocess(torch.from_numpy(y))
478
+ pred = non_max_suppression(
479
+ pred.cpu().numpy(),
480
+ 0.25,
481
+ 0.7,
482
+ None,
483
+ False,
484
+ max_det=300,
485
+ nc=0,
486
+ end2end=True,
487
+ rotated=False,
488
+ return_idxs=None,
489
+ )
490
+ gn = np.array(org_data.shape)[[1, 0, 1, 0]].astype(np.float32)
491
+ res = post_process_yolo(pred[0], org_data, im0, gn, save_path, img_name)
492
+ return res, im0
493
+
494
+ class QRCodeDecoder:
495
+ def crop_qr_regions(self, image, regions):
496
+ """
497
+ 根据检测到的边界框裁剪二维码区域
498
+ """
499
+ cropped_images = []
500
+ for idx, region in enumerate(regions):
501
+ x1, y1, x2, y2 = region
502
+ # 外扩15个像素缓解因检测截断造成无法识别的情况,视检测情况而定
503
+ # x1-=15
504
+ # y1-=15
505
+ # x2+=15
506
+ # y2+=15
507
+ # 裁剪图像
508
+ cropped = image[y1:y2, x1:x2]
509
+ if cropped.size > 0:
510
+ cropped_images.append({
511
+ 'image': cropped,
512
+ 'bbox': region,
513
+ })
514
+ # cv2.imwrite(f'cropped_qr_{idx}.jpg', cropped)
515
+ return cropped_images
516
+
517
+ def decode_qrcode_pyzbar(self, cropped_image):
518
+ """
519
+ 使用pyzbar解码二维码
520
+ """
521
+ try:
522
+ # 转换为灰度图像
523
+ if len(cropped_image.shape) == 3:
524
+ gray = cv2.cvtColor(cropped_image, cv2.COLOR_BGR2GRAY)
525
+ else:
526
+ gray = cropped_image
527
+ # cv2.imwrite('cropped_gray.jpg',gray)
528
+ # 使用pyzbar解码
529
+ decoded_objects = pyzbar.decode(gray)
530
+ results = []
531
+ for obj in decoded_objects:
532
+ try:
533
+ data = obj.data.decode('utf-8')
534
+ results.append({
535
+ 'data': data,
536
+ 'type': obj.type,
537
+ 'points': obj.polygon
538
+ })
539
+ except:
540
+ continue
541
+
542
+ return results
543
+ except Exception as e:
544
+ print(f"decode error: {e}")
545
+ return []
546
+
547
+ if __name__ == '__main__':
548
+ import time
549
+
550
+ detector = YOLOV8Detector(model_path='./yolo26n.axmodel',imgsz=[640,640])
551
+ decoder = QRCodeDecoder()
552
+ img_path = './qrcode_test'
553
+ det_path='./det_res'
554
+ crop_path='./crop_res'
555
+ os.makedirs(det_path, exist_ok=True)
556
+ os.makedirs(crop_path, exist_ok=True)
557
+ imgs = glob.glob(f"{img_path}/*.jpg")
558
+ totoal = len(imgs)
559
+ success = 0
560
+ fail = 0
561
+ start_time = time.time()
562
+ for idx,img in enumerate(imgs):
563
+ pic_name=os.path.basename(img).split('.')[0]
564
+ loop_start_time = time.time()
565
+ det_result, res_img = detector.detect_objects(img,det_path)
566
+ # cv2.imwrite(os.path.join(det_path, pic_name+'.jpg'), res_img)
567
+
568
+ # Crop deteted QRCode & decode QRCode by pyzbar
569
+ cropped_images = decoder.crop_qr_regions(res_img, det_result)
570
+ # for i,cropped in enumerate(cropped_images):
571
+ # cv2.imwrite(os.path.join(crop_path, f'{pic_name}_crop_{i}.jpg'), cropped['image'])
572
+
573
+ all_decoded_results = []
574
+ for i, cropped_data in enumerate(cropped_images):
575
+ decoded_results = decoder.decode_qrcode_pyzbar(cropped_data['image'])
576
+ all_decoded_results.extend(decoded_results)
577
+
578
+ # for result in decoded_results:
579
+ # print(f"decode result: {result['data']} (type: {result['type']})")
580
+ if all_decoded_results:
581
+ success += 1
582
+ print(f"{pic_name} 识别成功!")
583
+ else:
584
+ fail += 1
585
+ print(f"{pic_name} 识别失败!")
586
+ loop_end_time = time.time()
587
+ print(f"图片 {img} 处理耗时: {loop_end_time - loop_start_time:.4f} 秒")
588
+
589
+ end_time = time.time() # 记录总结束时间
590
+ total_time = end_time - start_time # 记录总耗时
591
+
592
+ print(f"总共测试图片数量: {totoal}")
593
+ print(f"识别成功数量: {success}")
594
+ print(f"识别失败数量: {fail}")
595
+ print(f"识别成功率: {success/totoal*100:.2f}%")
596
+ print(f"整体处理耗时: {total_time:.4f} 秒")
597
+ print(f"平均每张图片处理耗时: {total_time/totoal:.4f} 秒")
python/QRCode_onnx_infer_26.py ADDED
@@ -0,0 +1,599 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import onnxruntime as ort
2
+ import cv2
3
+ import numpy as np
4
+ import time
5
+ import yaml
6
+ import glob
7
+ import os
8
+ import torch
9
+ from pyzbar import pyzbar
10
+ names = [
11
+ "QRCode"
12
+ ]
13
+
14
+ def non_max_suppression(
15
+ prediction,
16
+ conf_thres: float = 0.25,
17
+ iou_thres: float = 0.45,
18
+ classes=None,
19
+ agnostic: bool = False,
20
+ multi_label: bool = False,
21
+ labels=(),
22
+ max_det: int = 300,
23
+ nc: int = 0, # number of classes (optional)
24
+ max_time_img: float = 0.05,
25
+ max_nms: int = 30000,
26
+ max_wh: int = 7680,
27
+ rotated: bool = False,
28
+ end2end: bool = False,
29
+ return_idxs: bool = False,
30
+ ):
31
+ """Perform non-maximum suppression (NMS) on prediction results.
32
+
33
+ Applies NMS to filter overlapping bounding boxes based on confidence and IoU thresholds. Supports multiple detection
34
+ formats including standard boxes, rotated boxes, and masks.
35
+
36
+ Args:
37
+ prediction (torch.Tensor): Predictions with shape (batch_size, num_classes + 4 + num_masks, num_boxes)
38
+ containing boxes, classes, and optional masks.
39
+ conf_thres (float): Confidence threshold for filtering detections. Valid values are between 0.0 and 1.0.
40
+ iou_thres (float): IoU threshold for NMS filtering. Valid values are between 0.0 and 1.0.
41
+ classes (list[int], optional): List of class indices to consider. If None, all classes are considered.
42
+ agnostic (bool): Whether to perform class-agnostic NMS.
43
+ multi_label (bool): Whether each box can have multiple labels.
44
+ labels (list[list[Union[int, float, torch.Tensor]]]): A priori labels for each image.
45
+ max_det (int): Maximum number of detections to keep per image.
46
+ nc (int): Number of classes. Indices after this are considered masks.
47
+ max_time_img (float): Maximum time in seconds for processing one image.
48
+ max_nms (int): Maximum number of boxes for NMS.
49
+ max_wh (int): Maximum box width and height in pixels.
50
+ rotated (bool): Whether to handle Oriented Bounding Boxes (OBB).
51
+ end2end (bool): Whether the model is end-to-end and doesn't require NMS.
52
+ return_idxs (bool): Whether to return the indices of kept detections.
53
+
54
+ Returns:
55
+ output (list[torch.Tensor]): List of detections per image with shape (num_boxes, 6 + num_masks) containing (x1,
56
+ y1, x2, y2, confidence, class, mask1, mask2, ...).
57
+ keepi (list[torch.Tensor]): Indices of kept detections if return_idxs=True.
58
+ """
59
+ # Checks
60
+ assert 0 <= conf_thres <= 1, f"Invalid Confidence threshold {conf_thres}, valid values are between 0.0 and 1.0"
61
+ assert 0 <= iou_thres <= 1, f"Invalid IoU {iou_thres}, valid values are between 0.0 and 1.0"
62
+ if isinstance(prediction, (list, tuple)): # YOLOv8 model in validation model, output = (inference_out, loss_out)
63
+ prediction = prediction[0] # select only inference output
64
+ if classes is not None:
65
+ classes = torch.tensor(classes, device=prediction.device)
66
+
67
+ if prediction.shape[-1] == 6 or end2end: # end-to-end model (BNC, i.e. 1,300,6)
68
+ output = [pred[pred[:, 4] > conf_thres][:max_det] for pred in prediction]
69
+ if classes is not None:
70
+ output = [pred[(pred[:, 5:6] == classes).any(1)] for pred in output]
71
+ return output
72
+
73
+ bs = prediction.shape[0] # batch size (BCN, i.e. 1,84,6300)
74
+ nc = nc or (prediction.shape[1] - 4) # number of classes
75
+ extra = prediction.shape[1] - nc - 4 # number of extra info
76
+ mi = 4 + nc # mask start index
77
+ xc = prediction[:, 4:mi].amax(1) > conf_thres # candidates
78
+ xinds = torch.arange(prediction.shape[-1], device=prediction.device).expand(bs, -1)[..., None] # to track idxs
79
+
80
+ # Settings
81
+ # min_wh = 2 # (pixels) minimum box width and height
82
+ time_limit = 2.0 + max_time_img * bs # seconds to quit after
83
+ multi_label &= nc > 1 # multiple labels per box (adds 0.5ms/img)
84
+
85
+ prediction = prediction.transpose(-1, -2) # shape(1,84,6300) to shape(1,6300,84)
86
+ if not rotated:
87
+ prediction[..., :4] = xywh2xyxy(prediction[..., :4]) # xywh to xyxy
88
+
89
+ t = time.time()
90
+ output = [torch.zeros((0, 6 + extra), device=prediction.device)] * bs
91
+ keepi = [torch.zeros((0, 1), device=prediction.device)] * bs # to store the kept idxs
92
+ for xi, (x, xk) in enumerate(zip(prediction, xinds)): # image index, (preds, preds indices)
93
+ # Apply constraints
94
+ # x[((x[:, 2:4] < min_wh) | (x[:, 2:4] > max_wh)).any(1), 4] = 0 # width-height
95
+ filt = xc[xi] # confidence
96
+ x = x[filt]
97
+ if return_idxs:
98
+ xk = xk[filt]
99
+
100
+ # Cat apriori labels if autolabelling
101
+ if labels and len(labels[xi]) and not rotated:
102
+ lb = labels[xi]
103
+ v = torch.zeros((len(lb), nc + extra + 4), device=x.device)
104
+ v[:, :4] = xywh2xyxy(lb[:, 1:5]) # box
105
+ v[range(len(lb)), lb[:, 0].long() + 4] = 1.0 # cls
106
+ x = torch.cat((x, v), 0)
107
+
108
+ # If none remain process next image
109
+ if not x.shape[0]:
110
+ continue
111
+
112
+ # Detections matrix nx6 (xyxy, conf, cls)
113
+ box, cls, mask = x.split((4, nc, extra), 1)
114
+
115
+ if multi_label:
116
+ i, j = torch.where(cls > conf_thres)
117
+ x = torch.cat((box[i], x[i, 4 + j, None], j[:, None].float(), mask[i]), 1)
118
+ if return_idxs:
119
+ xk = xk[i]
120
+ else: # best class only
121
+ conf, j = cls.max(1, keepdim=True)
122
+ filt = conf.view(-1) > conf_thres
123
+ x = torch.cat((box, conf, j.float(), mask), 1)[filt]
124
+ if return_idxs:
125
+ xk = xk[filt]
126
+
127
+ # Filter by class
128
+ if classes is not None:
129
+ filt = (x[:, 5:6] == classes).any(1)
130
+ x = x[filt]
131
+ if return_idxs:
132
+ xk = xk[filt]
133
+
134
+ # Check shape
135
+ n = x.shape[0] # number of boxes
136
+ if not n: # no boxes
137
+ continue
138
+ if n > max_nms: # excess boxes
139
+ filt = x[:, 4].argsort(descending=True)[:max_nms] # sort by confidence and remove excess boxes
140
+ x = x[filt]
141
+ if return_idxs:
142
+ xk = xk[filt]
143
+
144
+ c = x[:, 5:6] * (0 if agnostic else max_wh) # classes
145
+ scores = x[:, 4] # scores
146
+ if rotated:
147
+ boxes = torch.cat((x[:, :2] + c, x[:, 2:4], x[:, -1:]), dim=-1) # xywhr
148
+ i = TorchNMS.fast_nms(boxes, scores, iou_thres, iou_func=batch_probiou)
149
+ else:
150
+ boxes = x[:, :4] + c # boxes (offset by class)
151
+ # Speed strategy: torchvision for val or already loaded (faster), TorchNMS for predict (lower latency)
152
+ if "torchvision" in sys.modules:
153
+ import torchvision # scope as slow import
154
+
155
+ i = torchvision.ops.nms(boxes, scores, iou_thres)
156
+ else:
157
+ i = TorchNMS.nms(boxes, scores, iou_thres)
158
+ i = i[:max_det] # limit detections
159
+
160
+ output[xi] = x[i]
161
+ if return_idxs:
162
+ keepi[xi] = xk[i].view(-1)
163
+ if (time.time() - t) > time_limit:
164
+ LOGGER.warning(f"NMS time limit {time_limit:.3f}s exceeded")
165
+ break # time limit exceeded
166
+
167
+ return (output, keepi) if return_idxs else output
168
+
169
+ def letterbox(im, new_shape=(640, 640), color=(114, 114, 114), auto=True, scaleFill=False, scaleup=True, stride=32):
170
+
171
+ shape = im.shape[:2]
172
+ if isinstance(new_shape, int):
173
+ new_shape = (new_shape, new_shape)
174
+
175
+ r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
176
+ if not scaleup:
177
+ r = min(r, 1.0)
178
+
179
+ ratio = r, r
180
+ new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
181
+ dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]
182
+ if auto:
183
+ dw, dh = np.mod(dw, stride), np.mod(dh, stride)
184
+ elif scaleFill:
185
+ dw, dh = 0.0, 0.0
186
+ new_unpad = (new_shape[1], new_shape[0])
187
+ ratio = new_shape[1] / shape[1], new_shape[0] / shape[0]
188
+
189
+ dw /= 2
190
+ dh /= 2
191
+
192
+ if shape[::-1] != new_unpad:
193
+ im = cv2.resize(im, new_unpad, interpolation=cv2.INTER_LINEAR)
194
+ top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
195
+ left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
196
+ im = cv2.copyMakeBorder(im, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)
197
+ return im, ratio, (dw, dh)
198
+
199
+ def data_process_cv2(frame, input_shape):
200
+ '''
201
+ 对输入的图像进行预处理
202
+ :param frame:
203
+ :param input_shape:
204
+ :return:
205
+ '''
206
+ im0 = cv2.imread(frame)
207
+ img = letterbox(im0, input_shape, auto=False, stride=32)[0]
208
+ org_data = img.copy()
209
+ img = np.ascontiguousarray(img[:, :, ::-1].transpose(2, 0, 1))
210
+ img = np.asarray(img, dtype=np.float32)
211
+ img = np.expand_dims(img, 0)
212
+ img /= 255.0
213
+ return img, im0, org_data
214
+
215
+ # Define xywh2xyxy function for converting bounding box format
216
+ def xywh2xyxy(x):
217
+ """
218
+ Convert bounding boxes from (center_x, center_y, width, height) to (x1, y1, x2, y2) format.
219
+
220
+ Parameters:
221
+ x (ndarray): Bounding boxes in (center_x, center_y, width, height) format, shaped (N, 4).
222
+
223
+ Returns:
224
+ ndarray: Bounding boxes in (x1, y1, x2, y2) format, shaped (N, 4).
225
+ """
226
+ y = x.copy()
227
+ y[:, 0] = x[:, 0] - x[:, 2] / 2
228
+ y[:, 1] = x[:, 1] - x[:, 3] / 2
229
+ y[:, 2] = x[:, 0] + x[:, 2] / 2
230
+ y[:, 3] = x[:, 1] + x[:, 3] / 2
231
+ return y
232
+
233
+ def xyxy2xywh(x):
234
+ # Convert nx4 boxes from [x1, y1, x2, y2] to [x, y, w, h] where xy1=top-left, xy2=bottom-right
235
+ y = np.copy(x)
236
+ y[:, 0] = (x[:, 0] + x[:, 2]) / 2 # x center
237
+ y[:, 1] = (x[:, 1] + x[:, 3]) / 2 # y center
238
+ y[:, 2] = x[:, 2] - x[:, 0] # width
239
+ y[:, 3] = x[:, 3] - x[:, 1] # height
240
+ return y
241
+
242
+ def post_process_yolo(det, im, im0, gn, save_path, img_name):
243
+ detections = []
244
+ if len(det):
245
+ det[:, :4] = scale_boxes(im.shape[:2], det[:, :4], im0.shape).round()
246
+ colors = Colors()
247
+ for *xyxy, conf, cls in reversed(det):
248
+ print("class:",int(cls), "left:%.0f" % xyxy[0],"top:%.0f" % xyxy[1],"right:%.0f" % xyxy[2],"bottom:%.0f" % xyxy[3], "conf:",'{:.0f}%'.format(float(conf)*100))
249
+ int_coords = [int(tensor.item()) for tensor in xyxy]
250
+ detections.append(int_coords)
251
+ # c = int(cls)
252
+ # label = names[c]
253
+ # res_img = plot_one_box(xyxy, im0, label=f'{label}:{conf:.2f}', color=colors(c, True), line_thickness=4)
254
+ # cv2.imwrite(f'{save_path}/{img_name}.jpg',res_img)
255
+ # xywh = (xyxy2xywh(np.array(xyxy,dtype=np.float32).reshape(1, 4)) / gn).reshape(-1).tolist() # normalized xywh
256
+ # line = (cls, *xywh) # label format
257
+ # with open(f'{save_path}/{img_name}.txt', 'a') as f:
258
+ # f.write(('%g ' * len(line)).rstrip() % line + '\n')
259
+ return detections
260
+
261
+ def scale_boxes(img1_shape, boxes, img0_shape, ratio_pad=None):
262
+ if ratio_pad is None:
263
+ gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1])
264
+ pad = (img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2
265
+ else:
266
+ gain = ratio_pad[0][0]
267
+ pad = ratio_pad[1]
268
+
269
+ boxes[..., [0, 2]] -= pad[0]
270
+ boxes[..., [1, 3]] -= pad[1]
271
+ boxes[..., :4] /= gain
272
+ clip_boxes(boxes, img0_shape)
273
+ return boxes
274
+
275
+ def clip_boxes(boxes, shape):
276
+ boxes[..., [0, 2]] = boxes[..., [0, 2]].clip(0, shape[1])
277
+ boxes[..., [1, 3]] = boxes[..., [1, 3]].clip(0, shape[0])
278
+
279
+
280
+ def yaml_load(file='coco128.yaml'):
281
+ with open(file, errors='ignore') as f:
282
+ return yaml.safe_load(f)
283
+
284
+
285
+ class Colors:
286
+ # Ultralytics color palette https://ultralytics.com/
287
+ def __init__(self):
288
+ """
289
+ Initializes the Colors class with a palette derived from Ultralytics color scheme, converting hex codes to RGB.
290
+ Colors derived from `hex = matplotlib.colors.TABLEAU_COLORS.values()`.
291
+ """
292
+ hexs = (
293
+ "FF3838",
294
+ "FF9D97",
295
+ "FF701F",
296
+ "FFB21D",
297
+ "CFD231",
298
+ "48F90A",
299
+ "92CC17",
300
+ "3DDB86",
301
+ "1A9334",
302
+ "00D4BB",
303
+ "2C99A8",
304
+ "00C2FF",
305
+ "344593",
306
+ "6473FF",
307
+ "0018EC",
308
+ "8438FF",
309
+ "520085",
310
+ "CB38FF",
311
+ "FF95C8",
312
+ "FF37C7",
313
+ )
314
+ self.palette = [self.hex2rgb(f"#{c}") for c in hexs]
315
+ self.n = len(self.palette)
316
+
317
+ def __call__(self, i, bgr=False):
318
+ """Returns color from palette by index `i`, in BGR format if `bgr=True`, else RGB; `i` is an integer index."""
319
+ c = self.palette[int(i) % self.n]
320
+ return (c[2], c[1], c[0]) if bgr else c
321
+
322
+ @staticmethod
323
+ def hex2rgb(h):
324
+ """Converts hex color codes to RGB values (i.e. default PIL order)."""
325
+ return tuple(int(h[1 + i: 1 + i + 2], 16) for i in (0, 2, 4))
326
+
327
+ def plot_one_box(x, im, color=None, label=None, line_thickness=3, steps=2, orig_shape=None):
328
+ assert im.data.contiguous, 'Image not contiguous. Apply np.ascontiguousarray(im) to plot_on_box() input image.'
329
+ tl = line_thickness or round(0.002 * (im.shape[0] + im.shape[1]) / 2) + 1
330
+ c1, c2 = (int(x[0]), int(x[1])), (int(x[2]), int(x[3]))
331
+ cv2.rectangle(im, c1, c2, color, thickness=tl*1//3, lineType=cv2.LINE_AA)
332
+ if label:
333
+ if len(label.split(':')) > 1:
334
+ tf = max(tl - 1, 1)
335
+ t_size = cv2.getTextSize(label, 0, fontScale=tl / 6, thickness=tf)[0]
336
+ c2 = c1[0] + t_size[0], c1[1] - t_size[1] - 3
337
+ cv2.rectangle(im, c1, c2, color, -1, cv2.LINE_AA)
338
+ cv2.putText(im, label, (c1[0], c1[1] - 2), 0, tl / 6, [225, 255, 255], thickness=tf//2, lineType=cv2.LINE_AA)
339
+ return im
340
+
341
+ def model_load(model):
342
+ providers = ['CPUExecutionProvider']
343
+ session = ort.InferenceSession(model, providers=providers)
344
+ input_name = session.get_inputs()[0].name
345
+ output_names = [ x.name for x in session.get_outputs()]
346
+ return session, output_names
347
+
348
+ def make_anchors(feats, strides, grid_cell_offset=0.5):
349
+ """Generate anchors from features."""
350
+ anchor_points, stride_tensor = [], []
351
+ assert feats is not None
352
+ dtype = feats[0].dtype
353
+ for i, stride in enumerate(strides):
354
+ # _, _, h, w = feats[i].shape
355
+ h, w = feats[i].shape[2:] if isinstance(feats, list) else (int(feats[i][0]), int(feats[i][1]))
356
+ sx = np.arange(w, dtype=dtype) + grid_cell_offset # shift x
357
+ sy = np.arange(h, dtype=dtype) + grid_cell_offset # shift y
358
+ sy, sx = np.meshgrid(sy, sx, indexing='ij')
359
+ anchor_points.append(np.stack((sx, sy), axis=-1).reshape(-1, 2))
360
+ stride_tensor.append(np.full((h * w, 1), stride, dtype=dtype))
361
+ return np.concatenate(anchor_points), np.concatenate(stride_tensor)
362
+
363
+ def dist2bbox(distance, anchor_points, xywh=True, dim=-1):
364
+ """Transform distance(ltrb) to box(xywh or xyxy)."""
365
+ lt, rb = np.split(distance, 2, axis=dim)
366
+ x1y1 = anchor_points - lt
367
+ x2y2 = anchor_points + rb
368
+ if xywh:
369
+ c_xy = (x1y1 + x2y2) / 2
370
+ wh = x2y2 - x1y1
371
+ return np.concatenate((c_xy, wh), axis=dim) # xywh bbox
372
+ return np.concatenate((x1y1, x2y2), axis=dim) # xyxy bbox
373
+
374
+
375
+ class DFL:
376
+ """
377
+ NumPy implementation of Distribution Focal Loss (DFL) integral module.
378
+ Original paper: Generalized Focal Loss (IEEE TPAMI 2023)
379
+ """
380
+
381
+ def __init__(self, c1=16):
382
+ """Initialize with given number of distribution channels"""
383
+ self.c1 = c1
384
+ # 初始化权重矩阵(等效于原conv层的固定权重)
385
+ self.weights = np.arange(c1, dtype=np.float32).reshape(1, c1, 1, 1)
386
+
387
+
388
+ def __call__(self, x):
389
+ """
390
+ 前向传播逻辑
391
+ 参数:
392
+ x: 输入张量,形状为(batch, channels, anchors)
393
+ 返回:
394
+ 处理后的张量,形状为(batch, 4, anchors)
395
+ """
396
+ b, c, a = x.shape
397
+
398
+ # 等效于原view->transpose->softmax操作
399
+ x_reshaped = x.reshape(b, 4, self.c1, a)
400
+ x_transposed = np.transpose(x_reshaped, (0, 2, 1, 3))
401
+ x_softmax = np.exp(x_transposed) / np.sum(np.exp(x_transposed), axis=1, keepdims=True)
402
+
403
+ # 等效卷积操作(通过张量乘积实现)
404
+ conv_result = np.sum(self.weights * x_softmax, axis=1)
405
+
406
+ return conv_result.reshape(b, 4, a)
407
+
408
+ class YOLOV8Detector:
409
+ def __init__(self, model_path, imgsz=[640,640]):
410
+ self.model_path = model_path
411
+ self.session, self.output_names = model_load(self.model_path)
412
+ self.imgsz = imgsz
413
+ self.stride = [8.,16.,32.]
414
+ self.reg_max = 1
415
+ self.nc = len(names)
416
+ self.nl = len(self.stride)
417
+ self.dfl = DFL(self.reg_max)
418
+ self.max_det = 300
419
+
420
+ def postprocess(self, preds: torch.Tensor) -> torch.Tensor:
421
+ """Post-processes YOLO model predictions.
422
+
423
+ Args:
424
+ preds (torch.Tensor): Raw predictions with shape (batch_size, num_anchors, 4 + nc) with last dimension
425
+ format [x, y, w, h, class_probs].
426
+
427
+ Returns:
428
+ (torch.Tensor): Processed predictions with shape (batch_size, min(max_det, num_anchors), 6) and last
429
+ dimension format [x, y, w, h, max_class_prob, class_index].
430
+ """
431
+ boxes, scores = preds.split([4, self.nc], dim=-1)
432
+ scores, conf, idx = self.get_topk_index(scores, self.max_det)
433
+ boxes = boxes.gather(dim=1, index=idx.repeat(1, 1, 4))
434
+ return torch.cat([boxes, scores, conf], dim=-1)
435
+
436
+ def get_topk_index(self, scores: torch.Tensor, max_det: int) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
437
+ """Get top-k indices from scores.
438
+
439
+ Args:
440
+ scores (torch.Tensor): Scores tensor with shape (batch_size, num_anchors, num_classes).
441
+ max_det (int): Maximum detections per image.
442
+
443
+ Returns:
444
+ (torch.Tensor, torch.Tensor, torch.Tensor): Top scores, class indices, and filtered indices.
445
+ """
446
+ batch_size, anchors, nc = scores.shape # i.e. shape(1,8400,84)
447
+ # Use max_det directly during export for TensorRT compatibility (requires k to be constant),
448
+ # otherwise use min(max_det, anchors) for safety with small inputs during Python inference
449
+ k = max_det
450
+ #对8400个anchor取其80类中的最大类概率,shape[1,8400]--再取topk,shape[1,k]--unsqueeze,shape[1,k,1]
451
+ ori_index = scores.max(dim=-1)[0].topk(k)[1].unsqueeze(-1)
452
+ #[1,k,1]repeat变为[1,k,80],从[1,8400,80]中取topk个完整logit
453
+ scores = scores.gather(dim=1, index=ori_index.repeat(1, 1, nc))
454
+ #展平从k*80个分数中取topk。总体就是先删选topk个最可能anchor,再从该anchor中取topk个最可能class
455
+ scores, index = scores.flatten(1).topk(k)
456
+ #映射回原位置
457
+ idx = ori_index[torch.arange(batch_size)[..., None], index // nc] # original index
458
+ return scores[..., None], (index % nc)[..., None].float(), idx
459
+
460
+ def detect_objects(self, image, save_path):
461
+ im, im0, org_data = data_process_cv2(image, self.imgsz)
462
+ img_name = os.path.basename(image).split('.')[0]
463
+ infer_start_time = time.time()
464
+ x = self.session.run(self.output_names, {self.session.get_inputs()[0].name: im})
465
+ infer_end_time = time.time()
466
+ print(f"infer time: {infer_end_time - infer_start_time:.4f}s")
467
+ x = [np.transpose(x[i],(0,3,1,2)) for i in range(self.nl)] #to nchw
468
+ anchors,strides = (np.transpose(x,(1, 0)) for x in make_anchors(x, self.stride, 0.5))
469
+ box = [x[i][:, :self.reg_max * 4,:] for i in range(self.nl)]
470
+ cls = [x[i][:, self.reg_max * 4:,:] for i in range(self.nl)]
471
+ boxes = np.concatenate([box[i].reshape(1, 4 * self.reg_max, -1) for i in range(self.nl)], axis=-1)
472
+ scores = np.concatenate([cls[i].reshape(1, self.nc, -1) for i in range(self.nl)], axis=-1)
473
+ if self.reg_max > 1:
474
+ dbox = dist2bbox(self.dfl(boxes), np.expand_dims(anchors, axis=0), xywh=False, dim=1) * strides
475
+ else: #弃用DFL
476
+ dbox = dist2bbox(boxes, np.expand_dims(anchors, axis=0), xywh=False, dim=1) * strides
477
+ y = np.concatenate((dbox, 1/(1 + np.exp(-scores))), axis=1)
478
+ y = y.transpose([0, 2, 1])
479
+ pred = self.postprocess(torch.from_numpy(y))
480
+ pred = non_max_suppression(
481
+ pred.cpu().numpy(),
482
+ 0.25,
483
+ 0.7,
484
+ None,
485
+ False,
486
+ max_det=300,
487
+ nc=0,
488
+ end2end=True,
489
+ rotated=False,
490
+ return_idxs=None,
491
+ )
492
+ gn = np.array(org_data.shape)[[1, 0, 1, 0]].astype(np.float32)
493
+ res = post_process_yolo(pred[0], org_data, im0, gn, save_path, img_name)
494
+ return res, im0
495
+
496
+ class QRCodeDecoder:
497
+ def crop_qr_regions(self, image, regions):
498
+ """
499
+ 根据检测到的边界框裁剪二维码区域
500
+ """
501
+ cropped_images = []
502
+ for idx, region in enumerate(regions):
503
+ x1, y1, x2, y2 = region
504
+ # 外扩15个像素缓解因检测截断造成无法识别的情况,视检测情况而定
505
+ # x1-=15
506
+ # y1-=15
507
+ # x2+=15
508
+ # y2+=15
509
+ # 裁剪图像
510
+ cropped = image[y1:y2, x1:x2]
511
+ if cropped.size > 0:
512
+ cropped_images.append({
513
+ 'image': cropped,
514
+ 'bbox': region,
515
+ })
516
+ # cv2.imwrite(f'cropped_qr_{idx}.jpg', cropped)
517
+ return cropped_images
518
+
519
+ def decode_qrcode_pyzbar(self, cropped_image):
520
+ """
521
+ 使用pyzbar解码二维码
522
+ """
523
+ try:
524
+ # 转换为灰度图像
525
+ if len(cropped_image.shape) == 3:
526
+ gray = cv2.cvtColor(cropped_image, cv2.COLOR_BGR2GRAY)
527
+ else:
528
+ gray = cropped_image
529
+ # cv2.imwrite('cropped_gray.jpg',gray)
530
+ # 使用pyzbar解码
531
+ decoded_objects = pyzbar.decode(gray)
532
+ results = []
533
+ for obj in decoded_objects:
534
+ try:
535
+ data = obj.data.decode('utf-8')
536
+ results.append({
537
+ 'data': data,
538
+ 'type': obj.type,
539
+ 'points': obj.polygon
540
+ })
541
+ except:
542
+ continue
543
+
544
+ return results
545
+ except Exception as e:
546
+ print(f"decode error: {e}")
547
+ return []
548
+
549
+ if __name__ == '__main__':
550
+ import time
551
+
552
+ detector = YOLOV8Detector(model_path='./yolo26n.onnx',imgsz=[640,640])
553
+ decoder = QRCodeDecoder()
554
+ img_path = './qrcode_test'
555
+ det_path='./det_res'
556
+ crop_path='./crop_res'
557
+ os.makedirs(det_path, exist_ok=True)
558
+ os.makedirs(crop_path, exist_ok=True)
559
+ imgs = glob.glob(f"{img_path}/*.jpg")
560
+ totoal = len(imgs)
561
+ success = 0
562
+ fail = 0
563
+ start_time = time.time()
564
+ for idx,img in enumerate(imgs):
565
+ pic_name=os.path.basename(img).split('.')[0]
566
+ loop_start_time = time.time()
567
+ det_result, res_img = detector.detect_objects(img,det_path)
568
+ # cv2.imwrite(os.path.join(det_path, pic_name+'.jpg'), res_img)
569
+
570
+ # Crop deteted QRCode & decode QRCode by pyzbar
571
+ cropped_images = decoder.crop_qr_regions(res_img, det_result)
572
+ for i,cropped in enumerate(cropped_images):
573
+ cv2.imwrite(os.path.join(crop_path, f'{pic_name}_crop_{i}.jpg'), cropped['image'])
574
+
575
+ all_decoded_results = []
576
+ for i, cropped_data in enumerate(cropped_images):
577
+ decoded_results = decoder.decode_qrcode_pyzbar(cropped_data['image'])
578
+ all_decoded_results.extend(decoded_results)
579
+
580
+ # for result in decoded_results:
581
+ # print(f"decode result: {result['data']} (type: {result['type']})")
582
+ if all_decoded_results:
583
+ success += 1
584
+ print(f"{pic_name} 识别成功!")
585
+ else:
586
+ fail += 1
587
+ print(f"{pic_name} 识别失败!")
588
+ loop_end_time = time.time()
589
+ print(f"图片 {img} 处理耗时: {loop_end_time - loop_start_time:.4f} 秒")
590
+
591
+ end_time = time.time() # 记录总结束时间
592
+ total_time = end_time - start_time # 记录总耗时
593
+
594
+ print(f"总共测试图片数量: {totoal}")
595
+ print(f"识别成功数量: {success}")
596
+ print(f"识别失败数量: {fail}")
597
+ print(f"识别成功率: {success/totoal*100:.2f}%")
598
+ print(f"整体处理耗时: {total_time:.4f} 秒")
599
+ print(f"平均每张图片处理耗时: {total_time/totoal:.4f} 秒")