WeDetect-Ref-4B-Base / deploy /eval_onnx.py
gatilin's picture
Upload 171 files
3bcad43 verified
Raw History Blame Contribute Delete
9.55 kB
# Copyright (c) Tencent Inc. All rights reserved.
"""Validate the exported WeDetect ONNX model(s) on COCO ``instances_val2017``.
This script is the ONNX counterpart of ``test_coco_pytorch.py``. It runs the
network through ONNXRuntime instead of PyTorch but reuses *exactly the same*
pre-/post-processing (letterbox + RGB/255, DFL is already baked into the ONNX
graph, then score filtering + class-aware NMS + letterbox rescale) and the same
Chinese COCO vocabulary / id mapping, so the reported mAP is directly
comparable to ``test_coco_pytorch.py``.
Two layouts are supported (``--mode``), matching ``export_onnx.py``:
* ``dual`` : two ONNX files. The language tower runs once (the vocabulary is
fixed) and the vision tower runs per image.
--vision-onnx ... --language-onnx ...
* ``whole`` : a single ONNX file; the text tower is recomputed per image.
--onnx model_whole.onnx
Usage (dual, recommended)
-------------------------
python deploy/eval_onnx.py --mode dual \
--language-model xlm-roberta-base \
--vision-onnx deploy/onnx_models/wedetect_base_vision.onnx \
--language-onnx deploy/onnx_models/wedetect_base_language.onnx \
--coco-ann /path/to/instances_val2017.json \
--coco-img /path/to/val2017
Usage (whole)
-------------
python deploy/eval_onnx.py --mode whole \
--language-model xlm-roberta-base \
--onnx deploy/onnx_models/wedetect_base_whole.onnx \
--coco-ann /path/to/instances_val2017.json \
--coco-img /path/to/val2017
"""
import argparse
import os.path as osp
import sys
import numpy as np
import torch
import torchvision
import tqdm
from PIL import Image
from pycocotools.coco import COCO
from pycocotools.cocoeval import COCOeval
import onnxruntime as ort
from transformers import AutoTokenizer
# Reuse the standalone pre-/post-processing and the COCO vocabulary so the
# ONNX evaluation matches ``test_coco_pytorch.py`` exactly.
sys.path.insert(0, osp.dirname(osp.abspath(__file__)))
from test_coco_pytorch import ( # noqa: E402
COCO_CLASSES_ZH,
COCO_ID_MAP,
filter_scores_and_topk,
letterbox,
)
# --------------------------------------------------------------------------- #
# Pre- / post-processing (mirrors test_coco_pytorch) #
# --------------------------------------------------------------------------- #
def preprocess_image(img_path, img_size):
"""Load an image, letterbox it and return the ONNX input + recovery info.
Returns:
image : float32 [1, 3, H, W], RGB and divided by 255.
ratio : letterbox scale ratio.
offset : (dw/2, dh/2) padding to undo before rescaling.
ori_shape : (height, width) of the original image.
"""
img = Image.open(img_path).convert('RGB')
width, height = img.size
img_lb, ratio, offset = letterbox(img, (img_size, img_size))
arr = np.array(img_lb).astype(np.float32).transpose(2, 0, 1) / 255.0
return arr[None], ratio, offset, (height, width)
def postprocess(bboxes, scores, ratio, offset, ori_shape,
score_thr, nms_iou, pre_nms_topk, post_nms_topk):
"""ONNX outputs (letterbox space) -> boxes/scores/labels (original image).
bboxes : [N, 4] decoded xyxy in the letter-boxed coordinate space.
scores : [N, K] per-class confidence (already sigmoid).
"""
bboxes = torch.from_numpy(bboxes).float()
scores = torch.from_numpy(scores).float()
scores, labels, keep_idxs, _ = filter_scores_and_topk(
scores, score_thr, pre_nms_topk)
bboxes = bboxes[keep_idxs]
keep = torchvision.ops.batched_nms(
bboxes.float(), scores.float(), labels, nms_iou)[:post_nms_topk]
bboxes = bboxes[keep]
scores = scores[keep]
labels = labels[keep]
# undo letterbox padding + resize, then clamp to the original image.
off_x, off_y = offset
bboxes -= bboxes.new_tensor([off_x, off_y, off_x, off_y])
bboxes /= ratio
ori_h, ori_w = ori_shape
bboxes[:, 0::2].clamp_(0, ori_w)
bboxes[:, 1::2].clamp_(0, ori_h)
return bboxes, scores, labels
# --------------------------------------------------------------------------- #
# ONNXRuntime helpers #
# --------------------------------------------------------------------------- #
def make_session(path, device):
providers = ['CUDAExecutionProvider', 'CPUExecutionProvider'] \
if device == 'cuda' else ['CPUExecutionProvider']
return ort.InferenceSession(path, providers=providers)
def tokenise(language_model, class_names):
tokenizer = AutoTokenizer.from_pretrained(language_model)
tokens = tokenizer(text=class_names, return_tensors='np', padding=True)
input_ids = tokens['input_ids'].astype(np.int64)
attention_mask = tokens['attention_mask'].astype(np.int64)
return input_ids, attention_mask
# --------------------------------------------------------------------------- #
# Evaluation #
# --------------------------------------------------------------------------- #
def evaluate(args):
class_names = COCO_CLASSES_ZH
input_ids, attention_mask = tokenise(args.language_model, class_names)
print(f'[text] {len(class_names)} classes tokenised, '
f'seq_len={input_ids.shape[1]}')
# ---- ONNX sessions --------------------------------------------------- #
txt_feats = None
if args.mode == 'whole':
assert args.onnx, '--onnx is required for mode=whole'
whole_sess = make_session(args.onnx, args.device)
else:
assert args.vision_onnx and args.language_onnx, \
'--vision-onnx and --language-onnx are required for mode=dual'
vision_sess = make_session(args.vision_onnx, args.device)
language_sess = make_session(args.language_onnx, args.device)
txt_feats = language_sess.run(
['txt_feats'],
{'input_ids': input_ids, 'attention_mask': attention_mask})[0]
txt_feats = txt_feats.astype(np.float32)
print(f'[text] txt_feats shape = {txt_feats.shape}')
# ---- iterate over COCO val ------------------------------------------- #
coco_gt = COCO(args.coco_ann)
img_ids = coco_gt.getImgIds()
if args.num_images > 0:
img_ids = img_ids[:args.num_images]
results = []
for img_id in tqdm.tqdm(img_ids):
img_info = coco_gt.loadImgs(img_id)[0]
img_path = osp.join(args.coco_img, img_info['file_name'])
image, ratio, offset, ori_shape = preprocess_image(
img_path, args.img_size)
if args.mode == 'whole':
bboxes, scores = whole_sess.run(
['bboxes', 'scores'],
{'image': image,
'input_ids': input_ids,
'attention_mask': attention_mask})
else:
bboxes, scores = vision_sess.run(
['bboxes', 'scores'],
{'image': image, 'txt_feats': txt_feats})
boxes, sc, labels = postprocess(
bboxes[0], scores[0], ratio, offset, ori_shape,
args.score_thr, args.nms_iou, args.pre_nms_topk,
args.post_nms_topk)
for box, score, label in zip(boxes, sc, labels):
xmin, ymin, xmax, ymax = box.cpu().numpy()
results.append({
'image_id': img_id,
'category_id': COCO_ID_MAP[label.item()],
'bbox': [float(xmin), float(ymin),
float(xmax - xmin), float(ymax - ymin)],
'score': float(score.item()),
})
print('\nComputing metrics ...')
coco_dt = coco_gt.loadRes(results)
coco_eval = COCOeval(coco_gt, coco_dt, 'bbox')
if args.num_images > 0:
coco_eval.params.imgIds = img_ids
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
return coco_eval
def parse_args():
parser = argparse.ArgumentParser(description='Evaluate WeDetect ONNX')
parser.add_argument('--mode', choices=['whole', 'dual'], default='dual')
parser.add_argument('--onnx', help='whole-model ONNX (mode=whole)')
parser.add_argument('--vision-onnx', help='vision ONNX (mode=dual)')
parser.add_argument('--language-onnx', help='language ONNX (mode=dual)')
parser.add_argument('--language-model', required=True,
help='path/name of the XLM-RoBERTa model directory '
'(only the tokenizer is used here)')
parser.add_argument('--coco-ann', required=True,
help='path to instances_val2017.json')
parser.add_argument('--coco-img', required=True,
help='path to the val2017 image directory')
parser.add_argument('--img-size', type=int, default=640,
help='must match the size used at export time')
parser.add_argument('--num-images', type=int, default=-1,
help='limit number of evaluated images (-1 = all)')
parser.add_argument('--device', choices=['cpu', 'cuda'], default='cpu')
parser.add_argument('--score-thr', type=float, default=0.001)
parser.add_argument('--nms-iou', type=float, default=0.7)
parser.add_argument('--pre-nms-topk', type=int, default=30000)
parser.add_argument('--post-nms-topk', type=int, default=300)
return parser.parse_args()
def main():
args = parse_args()
evaluate(args)
if __name__ == '__main__':
main()