# Copyright (c) Qualcomm Innovation Center, Inc. # All rights reserved # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. import json import os import sys from multiprocessing.connection import Client import numpy as np import torch from executorch.backends.qualcomm.quantizer.quantizer import QuantDtype from executorch.examples.qualcomm.utils import ( build_executorch_binary, make_output_dir, parse_skip_delegation_node, setup_common_args_and_variables, SimpleADB, ) def get_instance(): import torchvision from torchvision.models.detection import RetinaNet_ResNet50_FPN_V2_Weights model = torchvision.models.detection.retinanet_resnet50_fpn_v2( weights=RetinaNet_ResNet50_FPN_V2_Weights.DEFAULT ) # the post-process part in vanilla forward method failed to be exported # here we only gather the network structure for torch.export.export to work def forward_without_metrics(self, image): features = self.backbone(image) return self.head(list(features.values())) model.forward = lambda img: forward_without_metrics(model, img) return model.eval() def get_dataset(data_size, dataset_dir): from torchvision import datasets, transforms class COCODataset(datasets.CocoDetection): def __init__(self, dataset_root): self.images_path = os.path.join(dataset_root, "val2017") self.annots_path = os.path.join( dataset_root, "annotations/instances_val2017.json" ) self.img_shape = (640, 640) self.preprocess = transforms.Compose( [ transforms.PILToTensor(), transforms.ConvertImageDtype(torch.float), transforms.Resize(self.img_shape), transforms.Normalize( mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ), ] ) with open(self.annots_path, "r") as f: data = json.load(f) categories = data["categories"] self.label_names = { category["id"]: category["name"] for category in categories } super().__init__(root=self.images_path, annFile=self.annots_path) def __getitem__(self, index): img, target = super().__getitem__(index) bboxes, labels = [], [] for obj in target: bboxes.append(self.resize_bbox(obj["bbox"], img.size)) labels.append(obj["category_id"]) # return empty list if no label exists return ( self.preprocess(img), torch.stack(bboxes) if len(bboxes) > 0 else [], torch.Tensor(labels).to(torch.int) if len(labels) > 0 else [], ) def resize_bbox(self, bbox, orig_shape): # bypass if no label exists if len(bbox) == 0: return y_scale = float(self.img_shape[0]) / orig_shape[0] x_scale = float(self.img_shape[1]) / orig_shape[1] # bbox: [(upper-left) x, y, w, h] bbox[2] += bbox[0] bbox[3] += bbox[1] # rescale bbox according to image shape bbox[0] = y_scale * bbox[0] bbox[2] = y_scale * bbox[2] bbox[1] = x_scale * bbox[1] bbox[3] = x_scale * bbox[3] return torch.Tensor(bbox) dataset = COCODataset(dataset_root=dataset_dir) test_loader = torch.utils.data.DataLoader(dataset=dataset, shuffle=True) inputs, input_list = [], "" bboxes, targets = [], [] for index, (img, boxes, labels) in enumerate(test_loader): if index >= data_size: break inputs.append((img,)) input_list += f"input_{index}_0.raw\n" bboxes.append(boxes) targets.append(labels) return inputs, input_list, bboxes, targets, dataset.label_names def calculate_precision( true_boxes, true_labels, det_boxes, det_labels, tp, fp, top_k, iou_thres ): import torchvision def collect_data(boxes, labels, top_k=-1): # extract data up to top_k length top_k = labels.size(0) if top_k == -1 else top_k len_labels = min(labels.size(0), top_k) boxes, labels = boxes[:len_labels, :], labels[:len_labels] # how many labels do we have in current data cls = set(labels[:len_labels].tolist()) map = {index: [] for index in cls} # stack data in same class for j in range(len_labels): index = labels[j].item() if index in cls: map[index].append(boxes[j, :]) return {k: torch.stack(v) for k, v in map.items()} preds = collect_data(det_boxes, det_labels, top_k=top_k) targets = collect_data(true_boxes.squeeze(0), true_labels.squeeze(0)) # evaluate data with labels presenting in ground truth data for index in targets.keys(): # there is no precision gain for predictions not present in ground truth data if index in preds: # targets shape: (M, 4), preds shape: (N, 4) # shape after box_iou: (M, N), iou shape: (M) # true-positive: how many predictions meet the iou threshold. i.e. k of M # false-positive: M - true-positive = M - k iou, _ = torchvision.ops.box_iou(targets[index], preds[index]).max(0) tps = torch.where(iou >= iou_thres, 1, 0).sum().item() tp[index - 1] += tps fp[index - 1] += iou.nelement() - tps def eval_metric(instance, heads, images, bboxes, targets, classes): tp, fp = classes * [0], classes * [0] head_label = ["cls_logits", "bbox_regression"] # feature size should be changed if input size got altered feature_size = [80, 40, 20, 10, 5] feature_maps = [torch.zeros(1, 256, h, h) for h in feature_size] for head, image, true_boxes, true_labels in zip(heads, images, bboxes, targets): anchors = instance.anchor_generator( image_list=image, feature_maps=feature_maps, ) num_anchors_per_level = [hw**2 * 9 for hw in feature_size] # split outputs per level split_head_outputs = { head_label[i]: list(h.split(num_anchors_per_level, dim=1)) for i, h in enumerate(head) } split_anchors = [list(a.split(num_anchors_per_level)) for a in anchors] # compute the detections (based on official post-process method) detection = instance.postprocess_detections( head_outputs=split_head_outputs, anchors=split_anchors, image_shapes=[image.image_sizes], ) # no contribution to precision if len(true_labels) == 0: continue # here we select top 10 confidence and iou >= 0.5 as the criteria calculate_precision( true_boxes=true_boxes, true_labels=true_labels, det_boxes=detection[0]["boxes"], det_labels=detection[0]["labels"], tp=tp, fp=fp, top_k=10, iou_thres=0.5, ) # remove labels which does not appear in current dataset AP = torch.Tensor( [ tp[i] * 1.0 / (tp[i] + fp[i]) if tp[i] + fp[i] > 0 else -1 for i in range(len(tp)) ] ) missed_labels = torch.where(AP == -1, 1, 0).sum() mAP = AP.where(AP != -1, 0).sum() / (AP.nelement() - missed_labels) return AP, mAP.item() def main(args): from pprint import PrettyPrinter from torchvision.models.detection.image_list import ImageList skip_node_id_set, skip_node_op_set = parse_skip_delegation_node(args) # ensure the working directory exist os.makedirs(args.artifact, exist_ok=True) if not args.compile_only and args.device is None: raise RuntimeError( "device serial is required if not compile only. " "Please specify a device serial by -s/--device argument." ) model = get_instance() # retrieve dataset data_num = 100 # 91 classes appear in COCO dataset n_classes, n_coord_of_bbox = 91, 4 inputs, input_list, bboxes, targets, label_names = get_dataset( data_size=data_num, dataset_dir=args.dataset ) pte_filename = "retinanet_qnn" build_executorch_binary( model, inputs[0], args.model, f"{args.artifact}/{pte_filename}", inputs, skip_node_id_set=skip_node_id_set, skip_node_op_set=skip_node_op_set, quant_dtype=QuantDtype.use_8a8w, shared_buffer=args.shared_buffer, ) if args.compile_only: sys.exit(0) adb = SimpleADB( qnn_sdk=os.getenv("QNN_SDK_ROOT"), build_path=f"{args.build_folder}", pte_path=f"{args.artifact}/{pte_filename}.pte", workspace=f"/data/local/tmp/executorch/{pte_filename}", device_id=args.device, host_id=args.host, soc_model=args.model, shared_buffer=args.shared_buffer, ) adb.push(inputs=inputs, input_list=input_list) adb.execute() # collect output data output_data_folder = f"{args.artifact}/outputs" make_output_dir(output_data_folder) adb.pull(output_path=args.artifact) predictions, classes = [], [n_classes, n_coord_of_bbox] for i in range(data_num): result = [] for j, dim in enumerate(classes): data_np = np.fromfile( os.path.join(output_data_folder, f"output_{i}_{j}.raw"), dtype=np.float32, ) result.append(torch.from_numpy(data_np).reshape(1, -1, dim)) predictions.append(result) # evaluate metrics AP, mAP = eval_metric( instance=model, heads=predictions, images=[ImageList(img[0], tuple(img[0].shape[-2:])) for img in inputs], bboxes=bboxes, targets=targets, classes=n_classes, ) if args.ip and args.port != -1: with Client((args.ip, args.port)) as conn: conn.send(json.dumps({"mAP": mAP})) else: print("\nMean Average Precision (mAP): %.3f" % mAP) print("\nAverage Precision of Classes (AP):") PrettyPrinter().pprint( {label_names[i + 1]: AP[i].item() for i in range(n_classes) if AP[i] != -1} ) if __name__ == "__main__": parser = setup_common_args_and_variables() parser.add_argument( "-a", "--artifact", help="path for storing generated artifacts by this example. " "Default ./retinanet", default="./retinanet", type=str, ) parser.add_argument( "-d", "--dataset", help=( "path to the validation folder of COCO2017 dataset. " "e.g. --dataset PATH/TO/COCO (which contains 'val_2017' & 'annotations'), " "dataset could be downloaded via http://images.cocodataset.org/zips/val2017.zip & " "http://images.cocodataset.org/annotations/annotations_trainval2017.zip" ), type=str, required=True, ) args = parser.parse_args() try: main(args) except Exception as e: if args.ip and args.port != -1: with Client((args.ip, args.port)) as conn: conn.send(json.dumps({"Error": str(e)})) else: raise Exception(e)