wzf19947 commited on
Commit
8245f38
·
1 Parent(s): dda98d3

first commit

Browse files
.gitattributes CHANGED
@@ -33,3 +33,9 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
37
+ *.axmodel filter=lfs diff=lfs merge=lfs -text
38
+ *.tar.gz filter=lfs diff=lfs merge=lfs -text
39
+ *.json filter=lfs diff=lfs merge=lfs -text
40
+ *.jpg filter=lfs diff=lfs merge=lfs -text
41
+ *.ttc filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # WeDetect demo for AX
2
+
3
+ ## The original repo
4
+
5
+ [WeDetect](https://github.com/WeChatCV/WeDetect/tree/main)
6
+
7
+ ## 背景
8
+ 开放词汇检测旨在利用文本描述来检测任意的物体,Wedetect不利用跨模态交互的方案把识别任务类比成一种检索任务,即在一个统一的特征空间中匹配区域特征和文本特征。本项目用于指导开发者完成以下内容:
9
+
10
+ - 导出 class num = 4 的 WeDetect ONNX 模型;
11
+ - 生成 AXERA NPU 模型转换工具 Pulsar2 编译依赖的 text 量化校准数据集;
12
+ - 完成ONNX模型基于Pulsa2工具链的编译及在AX650N上的部署。
13
+
14
+ ## 模型导出
15
+
16
+ 项目使用模型为wedetect_base,模型可在[huggingface](https://huggingface.co/fushh7/WeDetect/tree/main)下载。
17
+ 生成适合用于 AXera NPU 工具链 Pulsar2 模型转换的 ONNX 模型:
18
+
19
+ - 下载 `wedetect_base.pth`放在checkpoints目录下
20
+ - 使用 export_onnx.py分别导出图像编码模型和文本编码模型:
21
+
22
+ ```
23
+ python export_onnx.py --config config/wedetect_base.py --checkpoint checkpoints/wedetect_base.pth
24
+ ```
25
+
26
+ - 生成 Pulsar2 编译模型时所依赖的量化校准数据 `class_embedding_4cls.tar.gz`、`input_ids.tar.gz`、 `attention_mask.tar.gz`,另准备图片数据如`coco100.tar.gz`作为图像编码器量化对分数据
27
+
28
+ ```
29
+ python generate_class_embedding.py --wedetect_checkpoint checkpoints/wedetect_base.pth --classname_file ./coco_zh_class_texts.json --calib-dir ./quant
30
+ ```
31
+
32
+ ## 模型编译
33
+
34
+ - Pulsar2 安装及使用请参考相关文档
35
+ - [在线文档](https://pulsar2-docs.readthedocs.io/zh-cn/latest/index.html)
36
+
37
+ - 编译命令
38
+ ```
39
+ # image encoder
40
+ pulsar2 build --config quant/image_encoder_4cls.json
41
+
42
+ # text encoder
43
+ pulsar2 build --config quant/text_encoder_4cls.json
44
+ ```
45
+ - 模型性能
46
+
47
+ | Models | Platforms | latency | CMM size(MB) |
48
+ | --------------------------------------- | --------- | ------------- | ------------- |
49
+ | wedetect_image_encoder_npu3_u16.axmodel | AX650 | 100.3ms | 152.2 |
50
+ | wedetect_text_encoder_npu3_u16.axmodel | AX650 | 7.2ms | 457.8 |
51
+
52
+
53
+ ## Python demo
54
+
55
+ ### Onnx demo
56
+ ```
57
+ python3 onnx_infer.py
58
+ ```
59
+
60
+ ### Axmodel demo
61
+
62
+ 需基于[PyAXEngine](https://github.com/AXERA-TECH/pyaxengine)在AX650N上进行部署
63
+ 板端执行命令:
64
+ ```
65
+ root@ax650:~/WeDetect# python3 axmodel_infer.py
66
+ [INFO] Available providers: ['AxEngineExecutionProvider', 'AXCLRTExecutionProvider']
67
+ Classes: ['鞋', '床', '人', '衣架']
68
+ [INFO] Using provider: AxEngineExecutionProvider
69
+ [INFO] Chip type: ChipType.MC50
70
+ [INFO] VNPU type: VNPUType.DISABLED
71
+ [INFO] Engine version: 2.12.0s
72
+ [INFO] Model type: 2 (triple core)
73
+ [INFO] Compiler version: 6.0 6965315a
74
+ [INFO] Using provider: AxEngineExecutionProvider
75
+ [INFO] Model type: 2 (triple core)
76
+ [INFO] Compiler version: 6.0 62ad4ff7
77
+ Detections: 3
78
+ 床 0.863 (336, 427, 837, 679)
79
+ 鞋 0.430 (316, 629, 345, 688)
80
+ 鞋 0.397 (268, 628, 331, 679)
81
+ Saved: axmodel_res.jpg
82
+
83
+ ```
84
+
85
+ ![](axmodel_res.jpg)
assets/demo.jpeg ADDED

Git LFS Details

  • SHA256: 866fcf3d2540bc83154dd45237618a906c170fe296bfb2744a915a6e317c39ac
  • Pointer size: 130 Bytes
  • Size of remote file: 93.9 kB
axmodel/wedetect_image_encoder_npu3_u16.axmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fcbce3f947d827ead3429e7b8f6b52b28ec434e9ca1482328d4e1d605a61b81b
3
+ size 139985400
axmodel/wedetect_text_encoder_npu3_u16.axmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8058303286a3612e6b2e98c602bafca470236a11311904d63fd13e4175198b7b
3
+ size 479847260
axmodel_infer.py ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AXMODEL runtime inference for WeDetect.
3
+
4
+ Usage:
5
+ python axmodel_infer.py --image assets/demo.jpeg --text "鞋,床" --threshold 0.3
6
+
7
+ Dependencies: axengine, numpy, PIL, transformers (tokenizer only)
8
+ """
9
+
10
+ import argparse
11
+ import os
12
+ import sys
13
+
14
+ import numpy as np
15
+ import axengine as axe
16
+ from PIL import Image, ImageDraw, ImageFont
17
+ from transformers import AutoTokenizer
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Image preprocessing (mirrors WeDetectKeepRatioResize + LetterResize)
21
+ # ---------------------------------------------------------------------------
22
+
23
+ def letterbox(image: Image.Image, new_shape=(640, 640), color=(114, 114, 114)):
24
+ """Resize keeping aspect ratio and pad to ``new_shape``."""
25
+ ow, oh = image.size
26
+ r = min(new_shape[0] / ow, new_shape[1] / oh)
27
+ new_w, new_h = int(round(ow * r)), int(round(oh * r))
28
+ image = image.resize((new_w, new_h), Image.BILINEAR)
29
+
30
+ # Paste onto a blank canvas
31
+ canvas = Image.new("RGB", new_shape, color)
32
+ left = (new_shape[0] - new_w) // 2
33
+ top = (new_shape[1] - new_h) // 2
34
+ canvas.paste(image, (left, top))
35
+
36
+ return canvas, r, (left, top)
37
+
38
+
39
+ def preprocess_image(image_path: str, image_size=640):
40
+ """Load, letterbox, and normalise an image.
41
+ """
42
+ img = Image.open(image_path).convert("RGB")
43
+ img, ratio, (pad_left, pad_top) = letterbox(img, (image_size, image_size))
44
+
45
+ # arr = np.array(img, dtype=np.float32) / 255.0 # [0, 1], HWC
46
+ arr = np.array(img, dtype=np.uint8)
47
+ tensor = arr[None] # NHWC
48
+ return tensor, ratio, (pad_left, pad_top)
49
+
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # Tokenization
53
+ # ---------------------------------------------------------------------------
54
+
55
+ def tokenize(texts, tokenizer, max_seq_len=32):
56
+ """Tokenize category names into fixed-length tensors.
57
+
58
+ Returns
59
+ -------
60
+ input_ids : np.ndarray shape (N, max_seq_len) int64
61
+ attention_mask : np.ndarray shape (N, max_seq_len) int64
62
+ """
63
+ if isinstance(texts, str):
64
+ texts = [texts]
65
+ tokens = tokenizer(texts, padding="max_length", max_length=max_seq_len,
66
+ return_tensors="np")
67
+ return tokens["input_ids"], tokens["attention_mask"]
68
+
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # BBox decoding
72
+ # ---------------------------------------------------------------------------
73
+
74
+ def _grid_points(h, w, stride):
75
+ """Generate grid anchor points for one feature-map scale."""
76
+ x = (np.arange(w) + 0.5) * stride
77
+ y = (np.arange(h) + 0.5) * stride
78
+ yy, xx = np.meshgrid(y, x, indexing="ij")
79
+ return np.stack([xx, yy], axis=-1).reshape(-1, 2) # (H*W, 2)
80
+
81
+
82
+ def _decode_bboxes(cls_scores, bbox_preds, h, w, stride, score_thr):
83
+ """Decode one scale: sigmoid → filter → distance2bbox."""
84
+ # cls_scores: (1, C, H, W) bbox_preds: (1, 4, H, W)
85
+ C = cls_scores.shape[1]
86
+ scores = 1.0 / (1.0 + np.exp(-cls_scores[0])) # sigmoid, (C, H, W)
87
+ scores = scores.reshape(C, -1).transpose(1, 0) # (H*W, C)
88
+
89
+ bbox = bbox_preds[0].reshape(4, -1).transpose(1, 0) # (H*W, 4)
90
+ points = _grid_points(h, w, stride) # (H*W, 2)
91
+
92
+ bbox = bbox * stride # scale distances to pixels
93
+ x1 = points[:, 0] - bbox[:, 0]
94
+ y1 = points[:, 1] - bbox[:, 1]
95
+ x2 = points[:, 0] + bbox[:, 2]
96
+ y2 = points[:, 1] + bbox[:, 3]
97
+ boxes = np.stack([x1, y1, x2, y2], axis=-1) # (H*W, 4)
98
+
99
+ # Filter by score
100
+ max_scores = scores.max(axis=1)
101
+ keep = max_scores > score_thr
102
+ return boxes[keep], scores[keep], max_scores[keep]
103
+
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # NMS
107
+ # ---------------------------------------------------------------------------
108
+
109
+ def nms(boxes, scores, iou_threshold=0.7, max_dets=300):
110
+ """Simple numpy NMS."""
111
+ order = scores.argsort()[::-1]
112
+ keep = []
113
+ while order.size > 0 and len(keep) < max_dets:
114
+ i = order[0]
115
+ keep.append(i)
116
+ if order.size == 1:
117
+ break
118
+ ious = _box_iou(boxes[i:i + 1], boxes[order[1:]])[0]
119
+ order = order[1:][ious < iou_threshold]
120
+ return np.array(keep, dtype=np.int64) if keep else np.array([], dtype=np.int64)
121
+
122
+
123
+ def _box_iou(boxes1, boxes2):
124
+ """Pairwise IoU between two sets of xyxy boxes."""
125
+ area1 = (boxes1[:, 2] - boxes1[:, 0]) * (boxes1[:, 3] - boxes1[:, 1])
126
+ area2 = (boxes2[:, 2] - boxes2[:, 0]) * (boxes2[:, 3] - boxes2[:, 1])
127
+
128
+ inter_x1 = np.maximum(boxes1[:, None, 0], boxes2[None, :, 0])
129
+ inter_y1 = np.maximum(boxes1[:, None, 1], boxes2[None, :, 1])
130
+ inter_x2 = np.minimum(boxes1[:, None, 2], boxes2[None, :, 2])
131
+ inter_y2 = np.minimum(boxes1[:, None, 3], boxes2[None, :, 3])
132
+ inter_w = np.maximum(0, inter_x2 - inter_x1)
133
+ inter_h = np.maximum(0, inter_y2 - inter_y1)
134
+ inter = inter_w * inter_h
135
+ return inter / (area1[:, None] + area2[None, :] - inter + 1e-7)
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # Visualisation
139
+ # ---------------------------------------------------------------------------
140
+
141
+ def draw_boxes(image_path, boxes, labels, scores, output_path, font_path=None):
142
+ """Draw bounding boxes with Chinese-capable font and save."""
143
+
144
+ img = Image.open(image_path).convert("RGB")
145
+ draw = ImageDraw.Draw(img)
146
+
147
+ font = ImageFont.truetype(font_path, size=18)
148
+ font_small = ImageFont.truetype(font_path, size=14)
149
+
150
+ colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0),
151
+ (255, 0, 255), (0, 255, 255), (128, 0, 128), (255, 165, 0)]
152
+
153
+ for i, (box, label, score) in enumerate(zip(boxes, labels, scores)):
154
+ x1, y1, x2, y2 = box.astype(int)
155
+ c = colors[i % len(colors)]
156
+ draw.rectangle([x1, y1, x2, y2], outline=c, width=2)
157
+
158
+ # Use textbbox for accurate label background sizing (PIL >= 8.0)
159
+ text = f"{label} {score:.2f}"
160
+ if hasattr(draw, "textbbox"):
161
+ bbox = draw.textbbox((x1 + 2, y1 + 2), text, font=font)
162
+ draw.rectangle([x1, y1, bbox[2] + 4, bbox[3] + 2], fill=c)
163
+ draw.text((x1 + 2, y1 + 2), text, fill="white", font=font)
164
+ else: # older PIL fallback
165
+ draw.rectangle([x1, y1, x1 + len(text) * 10, y1 + 20], fill=c)
166
+ draw.text((x1 + 2, y1 + 2), text, fill="white", font=font)
167
+
168
+ img.save(output_path)
169
+ print(f"Saved: {output_path}")
170
+
171
+
172
+ # ---------------------------------------------------------------------------
173
+ # Main
174
+ # ---------------------------------------------------------------------------
175
+
176
+ def parse_args():
177
+ p = argparse.ArgumentParser(description="WeDetect AXMODEL inference")
178
+ p.add_argument("--image", default='assets/demo.jpeg', help="Input image path")
179
+ p.add_argument("--text", default="鞋,床,人,衣架",
180
+ help="Chinese class names separated by comma, e.g. '鞋,床,人,衣架'")
181
+ p.add_argument("--image_model", default="wedetect_image_encoder_npu3_u16.axmodel")
182
+ p.add_argument("--text_model", default="wedetect_text_encoder_npu3_u16.axmodel")
183
+ p.add_argument("--tokenizer-dir", default="./xlm-roberta-base/")
184
+ p.add_argument("--max-seq-len", type=int, default=32)
185
+ p.add_argument("--threshold", type=float, default=0.3)
186
+ p.add_argument("--nms-threshold", type=float, default=0.7)
187
+ p.add_argument("--output", default="axmodel_res.jpg")
188
+ p.add_argument("--image-size", type=int, default=640)
189
+ p.add_argument("--font", default='wqy-microhei.ttc',
190
+ help="Path to a .ttf/.ttc font file for Chinese label rendering. "
191
+ "Auto-detected if not specified.")
192
+ return p.parse_args()
193
+
194
+
195
+ def main():
196
+ args = parse_args()
197
+
198
+ # ---- Parse texts ----
199
+ texts = [t.strip() for t in args.text.split(",")]
200
+ print(f"Classes: {texts}")
201
+
202
+ # ---- Load AXMODEL sessions ----
203
+ img_sess = axe.InferenceSession(args.image_model,providers=["AxEngineExecutionProvider"])
204
+ txt_sess = axe.InferenceSession(args.text_model,providers=["AxEngineExecutionProvider"])
205
+
206
+ # ---- Tokenize & run text encoder ----
207
+ tokenizer = AutoTokenizer.from_pretrained(args.tokenizer_dir)
208
+ input_ids, attn_mask = tokenize(texts, tokenizer, args.max_seq_len)
209
+
210
+ txt_out = txt_sess.run(["text_features"], {
211
+ "input_ids": input_ids.astype(np.int32),
212
+ "attention_mask": attn_mask.astype(np.int32),
213
+ })
214
+ text_features = txt_out[0] # (1, N, 768) — batch dim already included
215
+
216
+ # ---- Preprocess image & run image encoder ----
217
+ img_tensor, ratio, (pad_left, pad_top) = preprocess_image(args.image,
218
+ args.image_size)
219
+
220
+ img_out = img_sess.run(None, {
221
+ "images": img_tensor,
222
+ "text_features": text_features.astype(np.float32),
223
+ })
224
+
225
+ # img_out = [cls_s8, bbox_s8, cls_s16, bbox_s16, cls_s32, bbox_s32]
226
+ scales = [
227
+ (img_out[0], img_out[1], 80, 80, 8), # stride 8
228
+ (img_out[2], img_out[3], 40, 40, 16), # stride 16
229
+ (img_out[4], img_out[5], 20, 20, 32), # stride 32
230
+ ]
231
+
232
+ # ---- Decode all scales ----
233
+ all_boxes, all_scores, all_labels = [], [], []
234
+ for cls_scores, bbox_preds, h, w, stride in scales:
235
+ boxes, sc, _ = _decode_bboxes(cls_scores, bbox_preds, h, w, stride,
236
+ args.threshold)
237
+ if boxes.size == 0:
238
+ continue
239
+ labels = sc.argmax(axis=1)
240
+ scores = sc.max(axis=1)
241
+ all_boxes.append(boxes)
242
+ all_scores.append(scores)
243
+ all_labels.append(labels)
244
+
245
+ if not all_boxes:
246
+ print("No detections above threshold.")
247
+ return
248
+
249
+ boxes = np.concatenate(all_boxes)
250
+ scores = np.concatenate(all_scores)
251
+ labels = np.concatenate(all_labels)
252
+
253
+ # ---- NMS ----
254
+ keep = nms(boxes, scores, args.nms_threshold)
255
+ boxes, scores, labels = boxes[keep], scores[keep], labels[keep]
256
+
257
+ # ---- Rescale to original image coordinates ----
258
+ boxes[:, [0, 2]] -= pad_left
259
+ boxes[:, [1, 3]] -= pad_top
260
+ boxes /= ratio
261
+
262
+ # Clamp to image bounds
263
+ img = Image.open(args.image)
264
+ boxes[:, 0::2] = np.clip(boxes[:, 0::2], 0, img.size[0])
265
+ boxes[:, 1::2] = np.clip(boxes[:, 1::2], 0, img.size[1])
266
+
267
+ label_names = [texts[l] for l in labels]
268
+ print(f"Detections: {len(boxes)}")
269
+ for name, box, s in zip(label_names, boxes, scores):
270
+ print(f" {name} {s:.3f} ({box[0]:.0f}, {box[1]:.0f}, "
271
+ f"{box[2]:.0f}, {box[3]:.0f})")
272
+
273
+ # ---- Visualise ----
274
+ draw_boxes(args.image, boxes, label_names, scores, args.output,
275
+ font_path=args.font)
276
+
277
+
278
+ if __name__ == "__main__":
279
+ main()
axmodel_res.jpg ADDED

Git LFS Details

  • SHA256: 4441841f8001132bc1e01cf223039c5ad1a6e6cd78bfdb7965b4a5d28413bc2f
  • Pointer size: 131 Bytes
  • Size of remote file: 106 kB
coco_zh_class_texts.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fa535063b870659ec911d1cd93c5f9659465c25e86580d3e2b72d384dffe7b3f
3
+ size 1047
config/wedetect_base.py ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ _base_ = ["default_runtime.py"]
2
+
3
+ # hyper-parameters
4
+ num_classes = 1203
5
+ num_training_classes = 80
6
+ max_epochs = 80 # Maximum training epochs
7
+ close_mosaic_epochs = 2
8
+ save_epoch_intervals = 1
9
+ text_channels = 768
10
+ neck_embed_channels = [128, 256, 512]
11
+ neck_num_heads = [4, 8,16]
12
+ base_lr = 5e-4
13
+ weight_decay = 0.05 / 2
14
+ train_batch_size_per_gpu = 10
15
+
16
+ find_unused_parameters = True
17
+
18
+ model_test_cfg = dict(
19
+ # The config of multi-label for multi-class prediction.
20
+ multi_label=True,
21
+ # The number of boxes before NMS
22
+ nms_pre=30000,
23
+ score_thr=0.001, # Threshold to filter out boxes.
24
+ nms=dict(type='nms', iou_threshold=0.7), # NMS type and threshold
25
+ max_per_img=300) # Max number of detections of each image
26
+
27
+ tal_topk = 10 # Number of bbox selected in each level
28
+ tal_alpha = 0.5 # A Hyper-parameter related to alignment_metrics
29
+ tal_beta = 6.0 # A Hyper-parameter related to alignment_metrics
30
+ # TODO: Automatically scale loss_weight based on number of detection layers
31
+ loss_cls_weight = 0.5
32
+ loss_bbox_weight = 7.5
33
+ # Since the dfloss is implemented differently in the official
34
+ # and mmdet, we're going to divide loss_weight by 4.
35
+ loss_dfl_weight = 1.5 / 4
36
+
37
+ custom_imports = dict(imports=["wedetect"], allow_failed_imports=False)
38
+ # model settings
39
+ model = dict(
40
+ type="YOLOWorldDetector",
41
+ mm_neck=False,
42
+ num_train_classes=num_training_classes,
43
+ num_test_classes=num_classes,
44
+ data_preprocessor=dict(
45
+ type="YOLOWDetDataPreprocessor",
46
+ mean=[0., 0., 0.],
47
+ std=[255., 255., 255.],
48
+ bgr_to_rgb=True),
49
+ backbone=dict(
50
+ type="MultiModalYOLOBackbone",
51
+ image_model=dict(
52
+ type="ConvNextVisionBackbone",
53
+ model_name="base",
54
+ frozen_modules=[],
55
+ ),
56
+ text_model=dict(
57
+ type="XLMRobertaLanguageBackbone",
58
+ model_name="./xlm-roberta-base/",
59
+ model_size="base",
60
+ frozen_modules=[],
61
+ ),
62
+ ),
63
+ neck=dict(
64
+ type="CSPRepBiFPANNeck",
65
+ scale_factor=1.0,
66
+ model_size = 'base',
67
+ ),
68
+ bbox_head=dict(
69
+ type="YOLOWorldHead",
70
+ head_module=dict(
71
+ type="YOLOWorldHeadModule",
72
+ use_bn_head=True,
73
+ embed_dims=text_channels,
74
+ num_classes=num_training_classes,
75
+ model_size = 'base',
76
+ in_channels=[256, 512, 1024],
77
+ ),
78
+ prior_generator=dict(
79
+ type='MlvlPointGenerator', offset=0.5, strides=[8, 16, 32]),
80
+ bbox_coder=dict(type='WeDetectDistancePointBBoxCoder'),
81
+ # scaled based on number of detection layers
82
+ loss_cls=dict(
83
+ type='CrossEntropyLoss',
84
+ use_sigmoid=True,
85
+ reduction='none',
86
+ loss_weight=loss_cls_weight),
87
+ loss_bbox=dict(
88
+ type='mmyoloIoULoss',
89
+ iou_mode='ciou',
90
+ bbox_format='xyxy',
91
+ reduction='sum',
92
+ loss_weight=loss_bbox_weight,
93
+ return_iou=False),
94
+ loss_dfl=dict(
95
+ type='DistributionFocalLoss',
96
+ reduction='mean',
97
+ loss_weight=loss_dfl_weight)),
98
+ train_cfg=dict(
99
+ assigner=dict(
100
+ type='BatchTaskAlignedAssigner',
101
+ num_classes=num_classes,
102
+ use_ciou=True,
103
+ topk=tal_topk,
104
+ alpha=tal_alpha,
105
+ beta=tal_beta,
106
+ eps=1e-9)),
107
+ test_cfg=model_test_cfg)
108
+
109
+ img_scale = (640, 640)
110
+
111
+ test_pipeline = [
112
+ dict(type='LoadImageFromFile', backend_args=None),
113
+ dict(type='WeDetectKeepRatioResize', scale=img_scale),
114
+ dict(
115
+ type='WeDetectLetterResize',
116
+ scale=img_scale,
117
+ allow_scale_up=False,
118
+ pad_val=dict(img=114)),
119
+ dict(type='LoadAnnotations', with_bbox=True, _scope_='mmdet'),
120
+ dict(type="LoadText"),
121
+ dict(
122
+ type="PackDetInputs",
123
+ meta_keys=(
124
+ "img_id",
125
+ "img_path",
126
+ "ori_shape",
127
+ "img_shape",
128
+ "scale_factor",
129
+ "pad_param",
130
+ "texts",
131
+ ),
132
+ ),
133
+ ]
134
+
135
+ dist_cfg = dict(backend="nccl", timeout=10800)
136
+
137
+
138
+ coco_val_dataset = dict(
139
+ type="MultiModalDataset",
140
+ dataset=dict(
141
+ type="WeCocoDataset",
142
+ data_root="data/coco/",
143
+ test_mode=True,
144
+ ann_file="data/coco/annotations/instances_val2017.json",
145
+ data_prefix=dict(img="val2017"),
146
+ batch_shapes_cfg=None,
147
+ ),
148
+ class_text_path="data/texts/coco_zh_class_texts.json",
149
+ pipeline=test_pipeline,
150
+ )
151
+
152
+
153
+ lvis_minival_dataset = dict(
154
+ type="MultiModalDataset",
155
+ dataset=dict(
156
+ type="YOLOv5LVISV1Dataset",
157
+ data_root=f"data/coco/",
158
+ test_mode=True,
159
+ ann_file=f"data/lvis/lvis_v1_minival_inserted_image_name.json",
160
+ data_prefix=dict(img=""),
161
+ batch_shapes_cfg=None,
162
+ ),
163
+ class_text_path=f"data/texts/lvis_v1_zh_class_texts.json",
164
+ pipeline=test_pipeline,
165
+ )
166
+
167
+ lvis_od_val_dataset = dict(
168
+ type="MultiModalDataset",
169
+ dataset=dict(
170
+ type="YOLOv5LVISV1Dataset",
171
+ data_root=f"data/coco/",
172
+ test_mode=True,
173
+ ann_file="data/lvis/lvis_od_val.json",
174
+ data_prefix=dict(img=""),
175
+ batch_shapes_cfg=None,
176
+ ),
177
+ class_text_path=f"data/texts/lvis_v1_zh_class_texts.json",
178
+ pipeline=test_pipeline,
179
+ )
180
+ coco_evaluator = dict(
181
+ type="CocoMetric",
182
+ ann_file=f"data/coco/annotations/instances_val2017.json",
183
+ metric="bbox",
184
+ )
185
+
186
+ lvis_minival_evaluator = dict(
187
+ type="LVISMetric",
188
+ ann_file=f"data/lvis/lvis_v1_minival_inserted_image_name.json",
189
+ metric="bbox",
190
+ )
191
+ lvis_od_val_evaluator = dict(
192
+ type="LVISMetric",
193
+ ann_file="data/lvis/lvis_od_val.json",
194
+ metric="bbox",
195
+ )
196
+
197
+ val_dataloader = dict(
198
+ batch_size=1,
199
+ num_workers=2,
200
+ persistent_workers=True,
201
+ pin_memory=True,
202
+ drop_last=False,
203
+ sampler=dict(type='DefaultSampler', shuffle=False),
204
+ dataset=coco_val_dataset)
205
+
206
+ test_dataloader = val_dataloader
207
+
208
+ # val_evaluator = _base_.lvis_minival_evaluator
209
+ # val_evaluator = _base_.lvis_od_val_evaluator
210
+ val_evaluator = coco_evaluator
211
+ test_evaluator = val_evaluator
212
+
213
+
214
+ val_cfg = dict(type='ValLoop')
215
+ test_cfg = dict(type='TestLoop')
216
+
export_onnx.py ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ONNX export scripts for WeDetect.
3
+
4
+ Two separate exports:
5
+ 1. Image Encoder + Neck + Head → wedetect_image_head.onnx
6
+ 2. Text Encoder → wedetect_text_encoder.onnx
7
+
8
+ Usage:
9
+ python export_onnx.py --config config/wedetect_base.py --checkpoint checkpoints/wedetect_base.pth
10
+
11
+ The text encoder is exported separately so text embeddings can be pre-computed
12
+ offline for any category set. At inference time the image+head ONNX model takes
13
+ the pre-computed text features together with the image and produces detections.
14
+ """
15
+
16
+ import argparse
17
+ import os.path as osp
18
+ import warnings
19
+ from io import BytesIO
20
+
21
+ import onnx
22
+ import torch
23
+ import torch.nn as nn
24
+
25
+ from mmdet.utils import register_all_modules
26
+ register_all_modules()
27
+ from mmengine.config import Config
28
+ from mmdet.apis import init_detector
29
+
30
+ warnings.filterwarnings(action='ignore', category=torch.jit.TracerWarning)
31
+ warnings.filterwarnings(action='ignore', category=torch.jit.ScriptWarning)
32
+ warnings.filterwarnings(action='ignore', category=UserWarning)
33
+ warnings.filterwarnings(action='ignore', category=FutureWarning)
34
+
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Wrapper: Image Encoder + Neck + Head
38
+ # ---------------------------------------------------------------------------
39
+
40
+ class ImageHeadWrapper(nn.Module):
41
+ """Pack ConvNeXt → Neck → Head into a single traceable module.
42
+
43
+ Inputs:
44
+ image [B, 3, 640, 640] float32
45
+ text_features [B, num_classes, 768] float32
46
+
47
+ Outputs (three scales, strides 8 / 16 / 32):
48
+ cls_scores_s8 [B, num_classes, 80, 80]
49
+ cls_scores_s16 [B, num_classes, 40, 40]
50
+ cls_scores_s32 [B, num_classes, 20, 20]
51
+ bbox_preds_s8 [B, 4, 80, 80]
52
+ bbox_preds_s16 [B, 4, 40, 40]
53
+ bbox_preds_s32 [B, 4, 20, 20]
54
+ """
55
+
56
+ def __init__(self, model):
57
+ super().__init__()
58
+ self.backbone = model.backbone
59
+ self.neck = model.neck
60
+ self.head_module = model.bbox_head.head_module
61
+ def forward(self, image, text_features):
62
+ # ConvNeXt (forward_image skips the text encoder)
63
+ img_feats = self.backbone.forward_image(image)
64
+
65
+ # BiFPN neck
66
+ if self.neck is not None:
67
+ img_feats = self.neck(img_feats)
68
+
69
+ # Head: returns ((cls_L0, cls_L1, cls_L2), (bbox_L0, bbox_L1, bbox_L2))
70
+ cls_scores, bbox_preds = self.head_module(img_feats, text_features)
71
+
72
+ # Flatten to a single tuple so ONNX output names align correctly
73
+ return (
74
+ cls_scores[0], bbox_preds[0], # stride 8
75
+ cls_scores[1], bbox_preds[1], # stride 16
76
+ cls_scores[2], bbox_preds[2], # stride 32
77
+ )
78
+
79
+
80
+ # ---------------------------------------------------------------------------
81
+ # Wrapper: Text Encoder (XLM-RoBERTa without tokenizer)
82
+ # ---------------------------------------------------------------------------
83
+
84
+ class TextEncoderWrapper(nn.Module):
85
+ """Wrap the XLM-RoBERTa backbone so it can be exported without a tokenizer.
86
+
87
+ Inputs:
88
+ input_ids [num_texts, max_seq_len] int64 (fixed seq_len)
89
+ attention_mask [num_texts, max_seq_len] int64
90
+
91
+ Output:
92
+ text_features [1, num_texts, 768] float32 (L2-normalised)
93
+
94
+ """
95
+
96
+ def __init__(self, text_model):
97
+ super().__init__()
98
+ self.model = text_model.model # XLMRobertaModel
99
+ self.head = text_model.head # Linear(768 → 768)
100
+
101
+ def forward(self, input_ids, attention_mask):
102
+ # Compute position_ids in float32 to avoid INT32 CumSum/Mul/Add ops
103
+ # that the NPU backend does not support.
104
+ # XLMRoberta uses padding_idx=1, so mask = (input_ids != 1).
105
+ mask = input_ids.ne(1).float() # [N, L] float32
106
+ position_ids = torch.cumsum(mask, dim=1) * mask # cumsum+mul in float32
107
+ position_ids = (position_ids + 1.0).long() # +padding_idx in float, then cast
108
+
109
+ out = self.model(
110
+ input_ids=input_ids,
111
+ attention_mask=attention_mask,
112
+ position_ids=position_ids,
113
+ )
114
+ cls_embed = out["last_hidden_state"][:, 0, :] # [N, 768]
115
+ txt_feats = self.head(cls_embed)
116
+ # L2-normalise + add batch dim → (1, N, 768)
117
+ txt_feats = nn.functional.normalize(txt_feats, dim=-1)
118
+ return txt_feats.unsqueeze(0)
119
+
120
+
121
+ # ---------------------------------------------------------------------------
122
+ # Helpers
123
+ # ---------------------------------------------------------------------------
124
+
125
+ def _onnx_simplify(onnx_model, output_path):
126
+ """Save ONNX, then try to simplify and overwrite.
127
+
128
+ Saving first guarantees a usable file even if onnxsim crashes.
129
+ """
130
+ # Save unsimplified first — always produces a valid file on disk
131
+ onnx.save(onnx_model, output_path)
132
+ print(f"Saved (unsimplified): {output_path}")
133
+
134
+ # Then attempt simplify
135
+ try:
136
+ import onnxsim
137
+ simplified, check = onnxsim.simplify(onnx_model)
138
+ if check:
139
+ onnx.save(simplified, output_path)
140
+ print("ONNX simplify: passed, overwritten")
141
+ else:
142
+ print("ONNX simplify: check failed, kept unsimplified version")
143
+ except Exception as e:
144
+ print(f"ONNX simplify skipped: {e}, kept unsimplified version")
145
+
146
+
147
+ # ---------------------------------------------------------------------------
148
+ # Export entry-points
149
+ # ---------------------------------------------------------------------------
150
+
151
+ def export_image_encoder(model, output_path, num_classes=80, image_size=640):
152
+ """Export Image Encoder + Neck + Head to ONNX."""
153
+ wrapper = ImageHeadWrapper(model)
154
+ wrapper.eval()
155
+
156
+ device = next(model.parameters()).device
157
+
158
+ # Dummy inputs
159
+ image = torch.randn(1, 3, image_size, image_size, device=device)
160
+ text_feats = torch.randn(1, num_classes, 768, device=device)
161
+
162
+ # Dry-run to verify shapes
163
+ with torch.no_grad():
164
+ outputs = wrapper(image, text_feats)
165
+ print("Image + Head output shapes:")
166
+ for i, o in enumerate(outputs):
167
+ print(f" output_{i}: {list(o.shape)}")
168
+
169
+ output_names = [
170
+ "cls_scores_s8", "bbox_preds_s8",
171
+ "cls_scores_s16", "bbox_preds_s16",
172
+ "cls_scores_s32", "bbox_preds_s32",
173
+ ]
174
+
175
+ # Export to buffer → check → simplify → save
176
+ with BytesIO() as f:
177
+ torch.onnx.export(
178
+ wrapper,
179
+ (image, text_feats),
180
+ f,
181
+ input_names=["images", "text_features"],
182
+ output_names=output_names,
183
+ opset_version=14,
184
+ do_constant_folding=True,
185
+ )
186
+ f.seek(0)
187
+ onnx_model = onnx.load(f)
188
+ onnx.checker.check_model(onnx_model)
189
+
190
+ _onnx_simplify(onnx_model, output_path)
191
+ print(f"Exported: {output_path}")
192
+
193
+
194
+ def export_text_encoder(model, output_path, num_texts=4, max_seq_len=32):
195
+ """Export Text Encoder (XLM-RoBERTa + projection) to ONNX.
196
+
197
+ The seq_len dimension is FIXED at export time. At inference, tokenized
198
+ inputs must be padded to exactly ``max_seq_len`` tokens (the tokenizer
199
+ does this automatically when ``padding='max_length'`` is set).
200
+
201
+ Parameters
202
+ ----------
203
+ max_seq_len : int
204
+ Fixed token length. 32 is sufficient for typical Chinese class names.
205
+ """
206
+ text_model = model.backbone.text_model
207
+ wrapper = TextEncoderWrapper(text_model)
208
+ wrapper.eval()
209
+
210
+ device = next(text_model.parameters()).device
211
+
212
+ # Dummy inputs — fixed seq_len, dynamic num_texts
213
+ input_ids = torch.ones(num_texts, max_seq_len, dtype=torch.int64, device=device)
214
+ attention_mask = torch.ones(num_texts, max_seq_len, dtype=torch.int64, device=device)
215
+
216
+ with torch.no_grad():
217
+ out = wrapper(input_ids, attention_mask)
218
+ print(f"Text Encoder output shape: {list(out.shape)}")
219
+
220
+ # Export to buffer → check → simplify → save
221
+ with BytesIO() as f:
222
+ torch.onnx.export(
223
+ wrapper,
224
+ (input_ids, attention_mask),
225
+ f,
226
+ input_names=["input_ids", "attention_mask"],
227
+ output_names=["text_features"],
228
+ opset_version=17,
229
+ do_constant_folding=True,
230
+ )
231
+ f.seek(0)
232
+ onnx_model = onnx.load(f)
233
+ onnx.checker.check_model(onnx_model)
234
+
235
+ _onnx_simplify(onnx_model, output_path)
236
+ print(f"Exported: {output_path}")
237
+
238
+
239
+ # ---------------------------------------------------------------------------
240
+ # CLI
241
+ # ---------------------------------------------------------------------------
242
+
243
+ def parse_args():
244
+ parser = argparse.ArgumentParser(description="Export WeDetect to ONNX")
245
+ parser.add_argument("--config", required=True, help="Config file path")
246
+ parser.add_argument("--checkpoint", required=True, help="Checkpoint file path")
247
+ parser.add_argument("--device", default="cuda:0")
248
+ parser.add_argument("--num-classes", type=int, default=4,
249
+ help="Number of classes for dummy text features (default 4).")
250
+ parser.add_argument("--image-size", type=int, default=640)
251
+ #此处演示导出检测4个类别的模型
252
+ parser.add_argument("--num-texts", type=int, default=4,
253
+ help="Number of dummy texts for text-encoder trace.")
254
+ #可以根据实际检测类别prompt计算选取最大seq_len
255
+ parser.add_argument("--max-seq-len", type=int, default=32,
256
+ help="Fixed token length for text-encoder ONNX. "
257
+ "Inference inputs must be padded to this length.")
258
+ parser.add_argument("--output-dir", default=".",
259
+ help="Directory for the exported .onnx files.")
260
+ return parser.parse_args()
261
+
262
+
263
+ if __name__ == "__main__":
264
+ args = parse_args()
265
+
266
+ cfg = Config.fromfile(args.config)
267
+ cfg.work_dir = osp.join("./work_dirs",
268
+ osp.splitext(osp.basename(args.config))[0])
269
+
270
+ print(f"Loading model from {args.checkpoint} ...")
271
+ model = init_detector(cfg, checkpoint=args.checkpoint, device=args.device, palette=['red'])
272
+ model.eval()
273
+
274
+ # ---- Image + Head ----
275
+ export_image_encoder(
276
+ model,
277
+ osp.join(args.output_dir, "wedetect_image_encoder.onnx"),
278
+ num_classes=args.num_classes,
279
+ image_size=args.image_size,
280
+ )
281
+
282
+ # ---- Text Encoder ----
283
+ export_text_encoder(
284
+ model,
285
+ osp.join(args.output_dir, "wedetect_text_encoder.onnx"),
286
+ num_texts=args.num_texts,
287
+ max_seq_len=args.max_seq_len,
288
+ )
289
+ print("Done.")
generate_class_embedding.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import json
3
+ import os
4
+ import random
5
+ from collections import OrderedDict
6
+ from typing import List, Sequence, Tuple
7
+
8
+ import numpy as np
9
+ import torch
10
+ import torch.nn as nn
11
+ import torch.nn.functional as F
12
+
13
+ try:
14
+ from transformers import AutoTokenizer, AutoConfig, XLMRobertaModel
15
+ except ImportError:
16
+ AutoTokenizer = None
17
+ HFBertModel = None
18
+
19
+
20
+ class XLMRobertaLanguageBackbone(nn.Module):
21
+
22
+ def __init__(
23
+ self,
24
+ ckpt_path,
25
+ frozen_modules: Sequence[str] = (),
26
+ dropout: float = 0.0,
27
+ init_cfg= None,
28
+ ) -> None:
29
+
30
+ super().__init__()
31
+ if 'base' in ckpt_path:
32
+ self.head = nn.Linear(768, 768, bias=True) # XLarge
33
+ model_name = "./xlm-roberta-base/"
34
+ elif 'large' in ckpt_path:
35
+ self.head = nn.Linear(1024, 768, bias=True) # XLarge
36
+ model_name = "./xlm-roberta-large/"
37
+
38
+ self.frozen_modules = frozen_modules
39
+ cfg = AutoConfig.from_pretrained(model_name)
40
+ self.tokenizer = AutoTokenizer.from_pretrained(model_name)
41
+ self.model = XLMRobertaModel(cfg)
42
+ self.language_dim = cfg.hidden_size
43
+
44
+
45
+ # 加载 model 权重
46
+ new_state_dict = OrderedDict()
47
+ state_dict = torch.load(
48
+ ckpt_path,
49
+ map_location="cpu",
50
+ weights_only=False,
51
+ )['state_dict']
52
+ for k, v in state_dict.items():
53
+ if k.startswith('backbone.text_model.'):
54
+ name = k.split("backbone.text_model.")[-1]
55
+ new_state_dict[name] = v
56
+ msg = self.load_state_dict(new_state_dict, strict=True)
57
+ print(msg)
58
+
59
+ print("TEXT-ENCODER xlm-roberta-base LOADING WEIGHTS !!!!")
60
+
61
+
62
+
63
+ def forward(self, text: List[str], max_seq_len: int = 32):
64
+ text = self.tokenizer(text=text, return_tensors="pt",
65
+ padding="max_length", max_length=max_seq_len)
66
+ text = text.to(device=self.model.device)
67
+
68
+ txt_feats = self.model(**text)["last_hidden_state"][:, 0]
69
+ txt_feats = self.head(txt_feats)
70
+
71
+ return txt_feats
72
+
73
+
74
+ if __name__ == '__main__':
75
+
76
+ parser = argparse.ArgumentParser()
77
+ parser.add_argument('--wedetect_checkpoint', type=str, default='checkpoints/wedetect_base.pth')
78
+ parser.add_argument('--classname_file', type=str, default='data/texts/coco_zh_class_texts.json')
79
+ parser.add_argument('--max-seq-len', type=int, default=32,
80
+ help='Fixed token length (must match ONNX export).')
81
+ parser.add_argument('--num-classes-per-group', type=int, default=4,
82
+ help='Number of classes per group npy.')
83
+ parser.add_argument('--num-groups', type=int, default=64,
84
+ help='Number of random groups to generate.')
85
+ parser.add_argument('--calib-dir', type=str, default='calib_data',
86
+ help='Directory for text-encoder quantisation calibration data.')
87
+ args = parser.parse_args()
88
+
89
+ with open(args.classname_file) as f:
90
+ name_chinese = json.load(f)
91
+ name_chinese = [name[0] for name in name_chinese]
92
+
93
+ language_encoder = XLMRobertaLanguageBackbone(args.wedetect_checkpoint).cuda()
94
+
95
+ # Generate random groups: each group picks 4 random classes → shape (1, 4, 768)
96
+ total_classes = len(name_chinese)
97
+ print(f"Total classes: {total_classes} → Generating {args.num_groups} random groups")
98
+ # Generate calibration data for text-encoder quantisation
99
+ # Directories: calib_dir/input_ids/ calib_dir/attention_mask/
100
+ calib_input_ids = os.path.join(args.calib_dir, "input_ids")
101
+ calib_attn_mask = os.path.join(args.calib_dir, "attention_mask")
102
+ for d in (calib_input_ids, calib_attn_mask):
103
+ os.makedirs(d, exist_ok=True)
104
+
105
+ tokenizer = language_encoder.tokenizer
106
+
107
+ for g in range(args.num_groups):
108
+ idx = random.sample(range(total_classes), args.num_classes_per_group)
109
+ group_texts = [name_chinese[i] for i in idx]
110
+ tokens = tokenizer(group_texts, padding="max_length",
111
+ max_length=args.max_seq_len, return_tensors="np")
112
+
113
+ np.save(os.path.join(calib_input_ids, f"{g:03d}.npy"),
114
+ tokens["input_ids"].astype(np.int64))
115
+ np.save(os.path.join(calib_attn_mask, f"{g:03d}.npy"),
116
+ tokens["attention_mask"].astype(np.int64))
117
+ print(f"calib [{g:03d}] input_ids: {tokens['input_ids'].shape} "
118
+ f"classes: {group_texts}")
119
+
120
+ # Compress each subdirectory to .tar.gz
121
+ import tarfile
122
+ for sub_name in ("input_ids", "attention_mask"):
123
+ sub_dir = os.path.join(args.calib_dir, sub_name)
124
+ tar_path = os.path.join(args.calib_dir, f"{sub_name}.tar.gz")
125
+ with tarfile.open(tar_path, "w:gz") as tar:
126
+ for fname in sorted(os.listdir(sub_dir)):
127
+ tar.add(os.path.join(sub_dir, fname), arcname=fname)
128
+ print(f"Compressed: {tar_path}")
129
+
130
+ print(f"Saved calibration data to {args.calib_dir}/")
131
+
132
+ # -------------------------------------------------------------------
133
+ # Generate 64 random 4-class text embeddings → class_embedding_4cls/
134
+ # Each file: (1, 4, 768) float32, L2-normalised (same as the ONNX
135
+ # image encoder expects via text_features input).
136
+ # -------------------------------------------------------------------
137
+ embed_dir = os.path.join(args.calib_dir, "class_embedding_4cls")
138
+ os.makedirs(embed_dir, exist_ok=True)
139
+
140
+ print(f"\nGenerating {args.num_groups} random {args.num_classes_per_group}-class "
141
+ f"text embeddings → {embed_dir}/")
142
+ for g in range(args.num_groups):
143
+ idx = random.sample(range(total_classes), args.num_classes_per_group)
144
+ group_texts = [name_chinese[i] for i in idx]
145
+ with torch.no_grad():
146
+ feats = language_encoder(group_texts, max_seq_len=args.max_seq_len)
147
+ feats = F.normalize(feats, dim=-1).unsqueeze(0) # (1, 4, 768)
148
+ fpath = os.path.join(embed_dir, f"{g:03d}.npy")
149
+ np.save(fpath, feats.cpu().numpy().astype(np.float32))
150
+ if (g + 1) % 16 == 0 or g == args.num_groups - 1:
151
+ print(f" [{g + 1:3d}/{args.num_groups}] shape={feats.shape} "
152
+ f"classes: {group_texts}")
153
+
154
+ # Compress
155
+ tar_path = os.path.join(args.calib_dir, "class_embedding_4cls.tar.gz")
156
+ with tarfile.open(tar_path, "w:gz") as tar:
157
+ for fname in sorted(os.listdir(embed_dir)):
158
+ tar.add(os.path.join(embed_dir, fname), arcname=fname)
159
+ print(f"Compressed: {tar_path}")
160
+
161
+ print(f"Saved calibration data to {args.calib_dir}/")
onnx_infer.py ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ONNX runtime inference for WeDetect.
3
+
4
+ Usage:
5
+ python onnx_infer.py --image assets/demo.jpeg --text "鞋,床" --threshold 0.3
6
+
7
+ Dependencies: onnxruntime, numpy, PIL, transformers (tokenizer only)
8
+ """
9
+
10
+ import argparse
11
+ import os
12
+ import sys
13
+
14
+ import numpy as np
15
+ import onnxruntime as ort
16
+ from PIL import Image, ImageDraw, ImageFont
17
+ from transformers import AutoTokenizer
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Image preprocessing (mirrors WeDetectKeepRatioResize + LetterResize)
21
+ # ---------------------------------------------------------------------------
22
+
23
+ def letterbox(image: Image.Image, new_shape=(640, 640), color=(114, 114, 114)):
24
+ """Resize keeping aspect ratio and pad to ``new_shape``."""
25
+ ow, oh = image.size
26
+ r = min(new_shape[0] / ow, new_shape[1] / oh)
27
+ new_w, new_h = int(round(ow * r)), int(round(oh * r))
28
+ image = image.resize((new_w, new_h), Image.BILINEAR)
29
+
30
+ # Paste onto a blank canvas
31
+ canvas = Image.new("RGB", new_shape, color)
32
+ left = (new_shape[0] - new_w) // 2
33
+ top = (new_shape[1] - new_h) // 2
34
+ canvas.paste(image, (left, top))
35
+
36
+ return canvas, r, (left, top)
37
+
38
+
39
+ def preprocess_image(image_path: str, image_size=640):
40
+ """Load, letterbox, and normalise an image.
41
+ """
42
+ img = Image.open(image_path).convert("RGB")
43
+ img, ratio, (pad_left, pad_top) = letterbox(img, (image_size, image_size))
44
+
45
+ arr = np.array(img, dtype=np.float32) / 255.0 # [0, 1], HWC
46
+ tensor = arr.transpose(2, 0, 1)[None] # NCHW
47
+ return tensor, ratio, (pad_left, pad_top)
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Tokenization
52
+ # ---------------------------------------------------------------------------
53
+
54
+ def tokenize(texts, tokenizer, max_seq_len=32):
55
+ """Tokenize category names into fixed-length tensors.
56
+
57
+ Returns
58
+ -------
59
+ input_ids : np.ndarray shape (N, max_seq_len) int64
60
+ attention_mask : np.ndarray shape (N, max_seq_len) int64
61
+ """
62
+ if isinstance(texts, str):
63
+ texts = [texts]
64
+ tokens = tokenizer(texts, padding="max_length", max_length=max_seq_len,
65
+ return_tensors="np")
66
+ return tokens["input_ids"], tokens["attention_mask"]
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # BBox decoding
71
+ # ---------------------------------------------------------------------------
72
+
73
+ def _grid_points(h, w, stride):
74
+ """Generate grid anchor points for one feature-map scale."""
75
+ x = (np.arange(w) + 0.5) * stride
76
+ y = (np.arange(h) + 0.5) * stride
77
+ yy, xx = np.meshgrid(y, x, indexing="ij")
78
+ return np.stack([xx, yy], axis=-1).reshape(-1, 2) # (H*W, 2)
79
+
80
+
81
+ def _decode_bboxes(cls_scores, bbox_preds, h, w, stride, score_thr):
82
+ """Decode one scale: sigmoid → filter → distance2bbox."""
83
+ # cls_scores: (1, C, H, W) bbox_preds: (1, 4, H, W)
84
+ C = cls_scores.shape[1]
85
+ scores = 1.0 / (1.0 + np.exp(-cls_scores[0])) # sigmoid, (C, H, W)
86
+ scores = scores.reshape(C, -1).transpose(1, 0) # (H*W, C)
87
+
88
+ bbox = bbox_preds[0].reshape(4, -1).transpose(1, 0) # (H*W, 4)
89
+ points = _grid_points(h, w, stride) # (H*W, 2)
90
+
91
+ bbox = bbox * stride # scale distances to pixels
92
+ x1 = points[:, 0] - bbox[:, 0]
93
+ y1 = points[:, 1] - bbox[:, 1]
94
+ x2 = points[:, 0] + bbox[:, 2]
95
+ y2 = points[:, 1] + bbox[:, 3]
96
+ boxes = np.stack([x1, y1, x2, y2], axis=-1) # (H*W, 4)
97
+
98
+ # Filter by score
99
+ max_scores = scores.max(axis=1)
100
+ keep = max_scores > score_thr
101
+ return boxes[keep], scores[keep], max_scores[keep]
102
+
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # NMS
106
+ # ---------------------------------------------------------------------------
107
+
108
+ def nms(boxes, scores, iou_threshold=0.7, max_dets=300):
109
+ """Simple numpy NMS."""
110
+ order = scores.argsort()[::-1]
111
+ keep = []
112
+ while order.size > 0 and len(keep) < max_dets:
113
+ i = order[0]
114
+ keep.append(i)
115
+ if order.size == 1:
116
+ break
117
+ ious = _box_iou(boxes[i:i + 1], boxes[order[1:]])[0]
118
+ order = order[1:][ious < iou_threshold]
119
+ return np.array(keep, dtype=np.int64) if keep else np.array([], dtype=np.int64)
120
+
121
+
122
+ def _box_iou(boxes1, boxes2):
123
+ """Pairwise IoU between two sets of xyxy boxes."""
124
+ area1 = (boxes1[:, 2] - boxes1[:, 0]) * (boxes1[:, 3] - boxes1[:, 1])
125
+ area2 = (boxes2[:, 2] - boxes2[:, 0]) * (boxes2[:, 3] - boxes2[:, 1])
126
+
127
+ inter_x1 = np.maximum(boxes1[:, None, 0], boxes2[None, :, 0])
128
+ inter_y1 = np.maximum(boxes1[:, None, 1], boxes2[None, :, 1])
129
+ inter_x2 = np.minimum(boxes1[:, None, 2], boxes2[None, :, 2])
130
+ inter_y2 = np.minimum(boxes1[:, None, 3], boxes2[None, :, 3])
131
+ inter_w = np.maximum(0, inter_x2 - inter_x1)
132
+ inter_h = np.maximum(0, inter_y2 - inter_y1)
133
+ inter = inter_w * inter_h
134
+ return inter / (area1[:, None] + area2[None, :] - inter + 1e-7)
135
+
136
+ # ---------------------------------------------------------------------------
137
+ # Visualisation
138
+ # ---------------------------------------------------------------------------
139
+
140
+ def draw_boxes(image_path, boxes, labels, scores, output_path, font_path=None):
141
+ """Draw bounding boxes with Chinese-capable font and save."""
142
+
143
+ img = Image.open(image_path).convert("RGB")
144
+ draw = ImageDraw.Draw(img)
145
+
146
+ font = ImageFont.truetype(font_path, size=18)
147
+ font_small = ImageFont.truetype(font_path, size=14)
148
+
149
+ colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0),
150
+ (255, 0, 255), (0, 255, 255), (128, 0, 128), (255, 165, 0)]
151
+
152
+ for i, (box, label, score) in enumerate(zip(boxes, labels, scores)):
153
+ x1, y1, x2, y2 = box.astype(int)
154
+ c = colors[i % len(colors)]
155
+ draw.rectangle([x1, y1, x2, y2], outline=c, width=2)
156
+
157
+ # Use textbbox for accurate label background sizing (PIL >= 8.0)
158
+ text = f"{label} {score:.2f}"
159
+ if hasattr(draw, "textbbox"):
160
+ bbox = draw.textbbox((x1 + 2, y1 + 2), text, font=font)
161
+ draw.rectangle([x1, y1, bbox[2] + 4, bbox[3] + 2], fill=c)
162
+ draw.text((x1 + 2, y1 + 2), text, fill="white", font=font)
163
+ else: # older PIL fallback
164
+ draw.rectangle([x1, y1, x1 + len(text) * 10, y1 + 20], fill=c)
165
+ draw.text((x1 + 2, y1 + 2), text, fill="white", font=font)
166
+
167
+ img.save(output_path)
168
+ print(f"Saved: {output_path}")
169
+
170
+
171
+ # ---------------------------------------------------------------------------
172
+ # Main
173
+ # ---------------------------------------------------------------------------
174
+
175
+ def parse_args():
176
+ p = argparse.ArgumentParser(description="WeDetect ONNX inference")
177
+ p.add_argument("--image", default='assets/demo.jpeg', help="Input image path")
178
+ p.add_argument("--text", default="鞋,床,人,衣架",
179
+ help="Chinese class names separated by comma, e.g. '鞋,床,人,衣架'")
180
+ p.add_argument("--image_model", default="wedetect_image_encoder.onnx")
181
+ p.add_argument("--text_model", default="wedetect_text_encoder.onnx")
182
+ p.add_argument("--tokenizer-dir", default="./xlm-roberta-base/")
183
+ p.add_argument("--max-seq-len", type=int, default=32)
184
+ p.add_argument("--threshold", type=float, default=0.3)
185
+ p.add_argument("--nms-threshold", type=float, default=0.7)
186
+ p.add_argument("--output", default="onnx_res.jpg")
187
+ p.add_argument("--image-size", type=int, default=640)
188
+ p.add_argument("--font", default='wqy-microhei.ttc',
189
+ help="Path to a .ttf/.ttc font file for Chinese label rendering. "
190
+ "Auto-detected if not specified.")
191
+ return p.parse_args()
192
+
193
+
194
+ def main():
195
+ args = parse_args()
196
+
197
+ # ---- Parse texts ----
198
+ texts = [t.strip() for t in args.text.split(",")]
199
+ print(f"Classes: {texts}")
200
+
201
+ # ---- Load ONNX sessions ----
202
+ img_sess = ort.InferenceSession(args.image_model,providers=["CPUExecutionProvider"])
203
+ txt_sess = ort.InferenceSession(args.text_model,providers=["CPUExecutionProvider"])
204
+
205
+ # ---- Tokenize & run text encoder ----
206
+ tokenizer = AutoTokenizer.from_pretrained(args.tokenizer_dir)
207
+ input_ids, attn_mask = tokenize(texts, tokenizer, args.max_seq_len)
208
+
209
+ txt_out = txt_sess.run(["text_features"], {
210
+ "input_ids": input_ids,
211
+ "attention_mask": attn_mask,
212
+ })
213
+ text_features = txt_out[0] # (1, N, 768) — batch dim already included
214
+
215
+ # ---- Preprocess image & run image encoder ----
216
+ img_tensor, ratio, (pad_left, pad_top) = preprocess_image(args.image,
217
+ args.image_size)
218
+
219
+ img_out = img_sess.run(None, {
220
+ "images": img_tensor,
221
+ "text_features": text_features.astype(np.float32),
222
+ })
223
+
224
+ # img_out = [cls_s8, bbox_s8, cls_s16, bbox_s16, cls_s32, bbox_s32]
225
+ scales = [
226
+ (img_out[0], img_out[1], 80, 80, 8), # stride 8
227
+ (img_out[2], img_out[3], 40, 40, 16), # stride 16
228
+ (img_out[4], img_out[5], 20, 20, 32), # stride 32
229
+ ]
230
+
231
+ # ---- Decode all scales ----
232
+ all_boxes, all_scores, all_labels = [], [], []
233
+ for cls_scores, bbox_preds, h, w, stride in scales:
234
+ boxes, sc, _ = _decode_bboxes(cls_scores, bbox_preds, h, w, stride,
235
+ args.threshold)
236
+ if boxes.size == 0:
237
+ continue
238
+ labels = sc.argmax(axis=1)
239
+ scores = sc.max(axis=1)
240
+ all_boxes.append(boxes)
241
+ all_scores.append(scores)
242
+ all_labels.append(labels)
243
+
244
+ if not all_boxes:
245
+ print("No detections above threshold.")
246
+ return
247
+
248
+ boxes = np.concatenate(all_boxes)
249
+ scores = np.concatenate(all_scores)
250
+ labels = np.concatenate(all_labels)
251
+
252
+ # ---- NMS ----
253
+ keep = nms(boxes, scores, args.nms_threshold)
254
+ boxes, scores, labels = boxes[keep], scores[keep], labels[keep]
255
+
256
+ # ---- Rescale to original image coordinates ----
257
+ boxes[:, [0, 2]] -= pad_left
258
+ boxes[:, [1, 3]] -= pad_top
259
+ boxes /= ratio
260
+
261
+ # Clamp to image bounds
262
+ img = Image.open(args.image)
263
+ boxes[:, 0::2] = np.clip(boxes[:, 0::2], 0, img.size[0])
264
+ boxes[:, 1::2] = np.clip(boxes[:, 1::2], 0, img.size[1])
265
+
266
+ label_names = [texts[l] for l in labels]
267
+ print(f"Detections: {len(boxes)}")
268
+ for name, box, s in zip(label_names, boxes, scores):
269
+ print(f" {name} {s:.3f} ({box[0]:.0f}, {box[1]:.0f}, "
270
+ f"{box[2]:.0f}, {box[3]:.0f})")
271
+
272
+ # ---- Visualise ----
273
+ draw_boxes(args.image, boxes, label_names, scores, args.output,
274
+ font_path=args.font)
275
+
276
+
277
+ if __name__ == "__main__":
278
+ main()
quant/attention_mask.tar.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d576d7af2cf122f5600504e6454a54d3d83411bf5f8ed045353b2980f2dfeb80
3
+ size 2092
quant/class_embedding_4cls.tar.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0cb8759b85dd87945b3dd2c072141f5a4958a923257144783c9d3e4f835c8e19
3
+ size 657358
quant/coco100.tar.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:110af521bd5dec72b3626909f3bbb94fc6b91498117298c6f3d25a4f51b7cf43
3
+ size 15039687
quant/image_encoder_4cls.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:632af6200435935d8e82fab173f7a56d5a5048236c582c0a0459a85a6e0c0695
3
+ size 1283
quant/input_ids.tar.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f2135255f2a08c565fa91aa456b794a60909928426cca9f2514ec63c70ed791a
3
+ size 3444
quant/text_encoder_4cls.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bfa19b3f84a5ce68c23b029528978cf9d9d41e626a84e3dc99589d7024f630f6
3
+ size 1182
wqy-microhei.ttc ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2420e8078af796b19a3f6ef13de527a1a91c1e7171eea115926c614ced1009b3
3
+ size 5177387
xlm-roberta-base/config.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:618c004597964d75b68497181ca769a69ea5719524c812edb81cfd74d9014656
3
+ size 662
xlm-roberta-base/sentencepiece.bpe.model ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cfc8146abe2a0488e9e2a0c56de7952f7c11ab059eca145a0a727afce0db2865
3
+ size 5069051
xlm-roberta-base/tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a898ea75433890f6610f4e470b8ebeb0c21dce5c8dd61f892eb09eb5919d2e2c
3
+ size 9096718