first commit
Browse files- .gitattributes +6 -0
- README.md +85 -0
- assets/demo.jpeg +3 -0
- axmodel/wedetect_image_encoder_npu3_u16.axmodel +3 -0
- axmodel/wedetect_text_encoder_npu3_u16.axmodel +3 -0
- axmodel_infer.py +279 -0
- axmodel_res.jpg +3 -0
- coco_zh_class_texts.json +3 -0
- config/wedetect_base.py +216 -0
- export_onnx.py +289 -0
- generate_class_embedding.py +161 -0
- onnx_infer.py +278 -0
- quant/attention_mask.tar.gz +3 -0
- quant/class_embedding_4cls.tar.gz +3 -0
- quant/coco100.tar.gz +3 -0
- quant/image_encoder_4cls.json +3 -0
- quant/input_ids.tar.gz +3 -0
- quant/text_encoder_4cls.json +3 -0
- wqy-microhei.ttc +3 -0
- xlm-roberta-base/config.json +3 -0
- xlm-roberta-base/sentencepiece.bpe.model +3 -0
- xlm-roberta-base/tokenizer.json +3 -0
.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 |
+

|
assets/demo.jpeg
ADDED
|
Git LFS Details
|
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
|
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
|