From ffce1218c1f1ce065ecc5659fcf42246f82453e7 Mon Sep 17 00:00:00 2001 From: xs1997zju <106210907+xs1997zju@users.noreply.github.com> Date: Wed, 1 Feb 2023 11:43:25 +0800 Subject: [PATCH] Support GradCAM (#7626) * fix slice infer one image save_results (#7654) * Support GradCAM Cascade_rcnn forward bugfix * code style fix * BBoxCAM class name fix * Add gradcam tutorial and demo --------- Co-authored-by: Feng Ni --- docs/images/grad_cam_ppyoloe_demo.jpg | Bin 0 -> 62173 bytes docs/tutorials/GradCAM_cn.md | 37 +++ docs/tutorials/GradCAM_en.md | 37 +++ ppdet/engine/trainer.py | 2 +- ppdet/modeling/architectures/cascade_rcnn.py | 2 +- ppdet/modeling/architectures/faster_rcnn.py | 11 +- ppdet/modeling/architectures/yolo.py | 11 +- ppdet/modeling/heads/ppyoloe_head.py | 4 +- ppdet/modeling/post_process.py | 7 +- ppdet/utils/cam_utils.py | 287 +++++++++++++++++++ tools/cam_ppdet.py | 130 +++++++++ 11 files changed, 515 insertions(+), 13 deletions(-) create mode 100644 docs/images/grad_cam_ppyoloe_demo.jpg create mode 100644 docs/tutorials/GradCAM_cn.md create mode 100644 docs/tutorials/GradCAM_en.md create mode 100644 ppdet/utils/cam_utils.py create mode 100644 tools/cam_ppdet.py diff --git a/docs/images/grad_cam_ppyoloe_demo.jpg b/docs/images/grad_cam_ppyoloe_demo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..83631b0a0590ad1ecb2e67b6932507b1022a29bf GIT binary patch literal 62173 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 j-VH(z?b=(M<*33Bzw^UoZ4UGjmy@u!VY?|$ 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 F{|A(6Hmm>u literal 0 HcmV?d00001 diff --git a/docs/tutorials/GradCAM_cn.md b/docs/tutorials/GradCAM_cn.md new file mode 100644 index 000000000..2dcd06e05 --- /dev/null +++ b/docs/tutorials/GradCAM_cn.md @@ -0,0 +1,37 @@ +# 目标检测热力图 + +## 1.简介 + +基于backbone特征图计算物体预测框的cam(类激活图) + +## 2.使用方法 +* 以PP-YOLOE为例,准备好数据之后,指定网络配置文件、模型权重地址和图片路径以及输出文件夹路径,使用脚本调用tools/cam_ppdet.py计算图片中物体预测框的grad_cam热力图。下面为运行脚本示例。 +```shell +python tools/cam_ppdet.py -c configs/ppyoloe/ppyoloe_crn_l_300e_coco.yml --infer_img demo/000000014439.jpg --cam_out cam_ppyoloe -o weights=https://paddledet.bj.bcebos.com/models/ppyoloe_crn_l_300e_coco.pdparams +``` + +* **参数** + +| FLAG | 用途 | +| :----------------------: |:-----------------------------------------------------------------------------------------------------:| +| -c | 指定配置文件 | +| --infer_img | 用于预测的图片路径 | +| --cam_out | 指定输出路径 | +| -o | 设置或更改配置文件里的参数内容, 如 -o weights=https://paddledet.bj.bcebos.com/models/ppyoloe_crn_l_300e_coco.pdparams | + +* 运行效果 + +
+ +
+
cam_ppyoloe/225.jpg

+ +## 3. 目前支持基于FasterRCNN和YOLOv3系列的网络。 +* FasterRCNN网络热图可视化脚本 +```bash +python tools/cam_ppdet.py -c configs/faster_rcnn/faster_rcnn_r50_vd_fpn_2x_coco.yml --infer_img demo/000000014439.jpg --cam_out cam_faster_rcnn -o weights=https://paddledet.bj.bcebos.com/models/faster_rcnn_r50_vd_fpn_ssld_2x_coco.pdparams +``` +* PPYOLOE网络热图可视化脚本 +```bash +python tools/cam_ppdet.py -c configs/ppyoloe/ppyoloe_crn_l_300e_coco.yml --infer_img demo/000000014439.jpg --cam_out cam_ppyoloe -o weights=https://paddledet.bj.bcebos.com/models/ppyoloe_crn_l_300e_coco.pdparams +``` \ No newline at end of file diff --git a/docs/tutorials/GradCAM_en.md b/docs/tutorials/GradCAM_en.md new file mode 100644 index 000000000..7551c709b --- /dev/null +++ b/docs/tutorials/GradCAM_en.md @@ -0,0 +1,37 @@ +# Object detection grad_cam heatmap + +## 1.Introduction +Calculate the cam (class activation map) of the object predict bbox based on the backbone feature map + +## 2.Usage +* Taking PP-YOLOE as an example, after preparing the data, specify the network configuration file, model weight address, image path and output folder path, and then use the script to call tools/cam_ppdet.py to calculate the grad_cam heat map of the prediction box. Below is an example run script. +```shell +python tools/cam_ppdet.py -c configs/ppyoloe/ppyoloe_crn_l_300e_coco.yml --infer_img demo/000000014439.jpg --cam_out cam_ppyoloe -o weights=https://paddledet.bj.bcebos.com/models/ppyoloe_crn_l_300e_coco.pdparams +``` + +* **Arguments** + +| FLAG | description | +| :----------------------: |:---------------------------------------------------------------------------------------------------------------------------------:| +| -c | Select config file | +| --infer_img | Image path | +| --cam_out | Directory for output | +| -o | Set parameters in configure file, for example: -o weights=https://paddledet.bj.bcebos.com/models/ppyoloe_crn_l_300e_coco.pdparams | + +* result + +
+ +
+
cam_ppyoloe/225.jpg

+ + +## 3.Currently supports networks based on FasterRCNN and YOLOv3 series. +* FasterRCNN bbox heat map visualization script +```bash +python tools/cam_ppdet.py -c configs/faster_rcnn/faster_rcnn_r50_vd_fpn_2x_coco.yml --infer_img demo/000000014439.jpg --cam_out cam_faster_rcnn -o weights=https://paddledet.bj.bcebos.com/models/faster_rcnn_r50_vd_fpn_ssld_2x_coco.pdparams +``` +* PPYOLOE bbox heat map visualization script +```bash +python tools/cam_ppdet.py -c configs/ppyoloe/ppyoloe_crn_l_300e_coco.yml --infer_img demo/000000014439.jpg --cam_out cam_ppyoloe -o weights=https://paddledet.bj.bcebos.com/models/ppyoloe_crn_l_300e_coco.pdparams +``` \ No newline at end of file diff --git a/ppdet/engine/trainer.py b/ppdet/engine/trainer.py index 35aa92d4b..3e83ca6cd 100644 --- a/ppdet/engine/trainer.py +++ b/ppdet/engine/trainer.py @@ -1039,7 +1039,7 @@ class Trainer(object): image.save(save_name, quality=95) start = end - + return results def _get_save_image_name(self, output_dir, image_path): """ Get save image name from source image path. diff --git a/ppdet/modeling/architectures/cascade_rcnn.py b/ppdet/modeling/architectures/cascade_rcnn.py index fc5949af0..e8d330c69 100644 --- a/ppdet/modeling/architectures/cascade_rcnn.py +++ b/ppdet/modeling/architectures/cascade_rcnn.py @@ -108,7 +108,7 @@ class CascadeRCNN(BaseArch): im_shape = self.inputs['im_shape'] scale_factor = self.inputs['scale_factor'] - bbox, bbox_num = self.bbox_post_process( + bbox, bbox_num, before_nms_indexes = self.bbox_post_process( preds, (refined_rois, rois_num), im_shape, scale_factor) # rescale the prediction back to origin image bbox, bbox_pred, bbox_num = self.bbox_post_process.get_pred( diff --git a/ppdet/modeling/architectures/faster_rcnn.py b/ppdet/modeling/architectures/faster_rcnn.py index e5972b2d8..a520d3a16 100644 --- a/ppdet/modeling/architectures/faster_rcnn.py +++ b/ppdet/modeling/architectures/faster_rcnn.py @@ -82,18 +82,21 @@ class FasterRCNN(BaseArch): self.inputs) return rpn_loss, bbox_loss else: + cam_data = {} # record bbox scores and index before nms rois, rois_num, _ = self.rpn_head(body_feats, self.inputs) preds, _ = self.bbox_head(body_feats, rois, rois_num, None) + cam_data['scores'] = preds[1] im_shape = self.inputs['im_shape'] scale_factor = self.inputs['scale_factor'] - bbox, bbox_num = self.bbox_post_process(preds, (rois, rois_num), + bbox, bbox_num, before_nms_indexes = self.bbox_post_process(preds, (rois, rois_num), im_shape, scale_factor) + cam_data['before_nms_indexes'] = before_nms_indexes # , bbox index before nms, for cam # rescale the prediction back to origin image bboxes, bbox_pred, bbox_num = self.bbox_post_process.get_pred( bbox, bbox_num, im_shape, scale_factor) - return bbox_pred, bbox_num + return bbox_pred, bbox_num, cam_data def get_loss(self, ): rpn_loss, bbox_loss = self._forward() @@ -105,8 +108,8 @@ class FasterRCNN(BaseArch): return loss def get_pred(self): - bbox_pred, bbox_num = self._forward() - output = {'bbox': bbox_pred, 'bbox_num': bbox_num} + bbox_pred, bbox_num, cam_data = self._forward() + output = {'bbox': bbox_pred, 'bbox_num': bbox_num, 'cam_data': cam_data} return output def target_bbox_forward(self, data): diff --git a/ppdet/modeling/architectures/yolo.py b/ppdet/modeling/architectures/yolo.py index 78c765491..3dbd5394b 100644 --- a/ppdet/modeling/architectures/yolo.py +++ b/ppdet/modeling/architectures/yolo.py @@ -98,7 +98,9 @@ class YOLOv3(BaseArch): return yolo_losses else: + cam_data = {} # record bbox scores and index before nms yolo_head_outs = self.yolo_head(neck_feats) + cam_data['scores'] = yolo_head_outs[0] if self.for_mot: # the detection part of JDE MOT model @@ -118,14 +120,17 @@ class YOLOv3(BaseArch): yolo_head_outs, self.yolo_head.mask_anchors) elif self.post_process is not None: # anchor based YOLOs: YOLOv3,PP-YOLO,PP-YOLOv2 use mask_anchors - bbox, bbox_num = self.post_process( + bbox, bbox_num, before_nms_indexes = self.post_process( yolo_head_outs, self.yolo_head.mask_anchors, self.inputs['im_shape'], self.inputs['scale_factor']) + cam_data['before_nms_indexes'] = before_nms_indexes else: # anchor free YOLOs: PP-YOLOE, PP-YOLOE+ - bbox, bbox_num = self.yolo_head.post_process( + bbox, bbox_num, before_nms_indexes = self.yolo_head.post_process( yolo_head_outs, self.inputs['scale_factor']) - output = {'bbox': bbox, 'bbox_num': bbox_num} + # data for cam + cam_data['before_nms_indexes'] = before_nms_indexes + output = {'bbox': bbox, 'bbox_num': bbox_num, 'cam_data': cam_data} return output diff --git a/ppdet/modeling/heads/ppyoloe_head.py b/ppdet/modeling/heads/ppyoloe_head.py index d29e9ac73..f1d93bb76 100644 --- a/ppdet/modeling/heads/ppyoloe_head.py +++ b/ppdet/modeling/heads/ppyoloe_head.py @@ -462,8 +462,8 @@ class PPYOLOEHead(nn.Layer): # `exclude_nms=True` just use in benchmark return pred_bboxes, pred_scores else: - bbox_pred, bbox_num, _ = self.nms(pred_bboxes, pred_scores) - return bbox_pred, bbox_num + bbox_pred, bbox_num, before_nms_indexes = self.nms(pred_bboxes, pred_scores) + return bbox_pred, bbox_num, before_nms_indexes def get_activation(name="LeakyReLU"): diff --git a/ppdet/modeling/post_process.py b/ppdet/modeling/post_process.py index 791af6ed2..a0d6432d1 100644 --- a/ppdet/modeling/post_process.py +++ b/ppdet/modeling/post_process.py @@ -67,7 +67,7 @@ class BBoxPostProcess(object): """ if self.nms is not None: bboxes, score = self.decode(head_out, rois, im_shape, scale_factor) - bbox_pred, bbox_num, _ = self.nms(bboxes, score, self.num_classes) + bbox_pred, bbox_num, before_nms_indexes = self.nms(bboxes, score, self.num_classes) else: bbox_pred, bbox_num = self.decode(head_out, rois, im_shape, @@ -82,7 +82,10 @@ class BBoxPostProcess(object): bbox_pred = paddle.concat([bbox_pred, fake_bboxes]) bbox_num = bbox_num + 1 - return bbox_pred, bbox_num + if self.nms is not None: + return bbox_pred, bbox_num, before_nms_indexes + else: + return bbox_pred, bbox_num def get_pred(self, bboxes, bbox_num, im_shape, scale_factor): """ diff --git a/ppdet/utils/cam_utils.py b/ppdet/utils/cam_utils.py new file mode 100644 index 000000000..7aa4b1743 --- /dev/null +++ b/ppdet/utils/cam_utils.py @@ -0,0 +1,287 @@ +import numpy as np +import cv2 +import os +import sys +import glob +from ppdet.utils.logger import setup_logger +logger = setup_logger('ppdet_cam') + +import paddle +from ppdet.engine import Trainer + + +def get_test_images(infer_dir, infer_img): + """ + Get image path list in TEST mode + """ + assert infer_img is not None or infer_dir is not None, \ + "--infer_img or --infer_dir should be set" + assert infer_img is None or os.path.isfile(infer_img), \ + "{} is not a file".format(infer_img) + assert infer_dir is None or os.path.isdir(infer_dir), \ + "{} is not a directory".format(infer_dir) + + # infer_img has a higher priority + if infer_img and os.path.isfile(infer_img): + return [infer_img] + + images = set() + infer_dir = os.path.abspath(infer_dir) + assert os.path.isdir(infer_dir), \ + "infer_dir {} is not a directory".format(infer_dir) + exts = ['jpg', 'jpeg', 'png', 'bmp'] + exts += [ext.upper() for ext in exts] + for ext in exts: + images.update(glob.glob('{}/*.{}'.format(infer_dir, ext))) + images = list(images) + + assert len(images) > 0, "no image found in {}".format(infer_dir) + logger.info("Found {} inference images in total.".format(len(images))) + + return images + + +def compute_ious(boxes1, boxes2): + """[Compute pairwise IOU matrix for given two sets of boxes] + + Args: + boxes1 ([numpy ndarray with shape N,4]): [representing bounding boxes with format (xmin,ymin,xmax,ymax)] + boxes2 ([numpy ndarray with shape M,4]): [representing bounding boxes with format (xmin,ymin,xmax,ymax)] + Returns: + pairwise IOU maxtrix with shape (N,M),where the value at ith row jth column hold the iou between ith + box and jth box from box1 and box2 respectively. + """ + lu = np.maximum( + boxes1[:, None, :2], boxes2[:, :2] + ) # lu with shape N,M,2 ; boxes1[:,None,:2] with shape (N,1,2) boxes2 with shape(M,2) + rd = np.minimum(boxes1[:, None, 2:], boxes2[:, 2:]) # rd same to lu + intersection_wh = np.maximum(0.0, rd - lu) + intersection_area = intersection_wh[:, :, + 0] * intersection_wh[:, :, + 1] # with shape (N,M) + boxes1_wh = np.maximum(0.0, boxes1[:, 2:] - boxes1[:, :2]) + boxes1_area = boxes1_wh[:, 0] * boxes1_wh[:, 1] # with shape (N,) + boxes2_wh = np.maximum(0.0, boxes2[:, 2:] - boxes2[:, :2]) + boxes2_area = boxes2_wh[:, 0] * boxes2_wh[:, 1] # with shape (M,) + union_area = np.maximum( + boxes1_area[:, None] + boxes2_area - intersection_area, + 1e-8) # with shape (N,M) + ious = np.clip(intersection_area / union_area, 0.0, 1.0) + return ious + + +def grad_cam(feat, grad): + """ + + Args: + feat: CxHxW + grad: CxHxW + + Returns: + cam: HxW + """ + exp = (feat * grad.mean((1, 2), keepdims=True)).mean(axis=0) + exp = np.maximum(-exp, 0) + return exp + + +def resize_cam(explanation, resize_shape) -> np.ndarray: + """ + + Args: + explanation: (width, height) + resize_shape: (width, height) + + Returns: + + """ + assert len(explanation.shape) == 2, f"{explanation.shape}. " \ + f"Currently support 2D explanation results for visualization. " \ + "Reduce higher dimensions to 2D for visualization." + + explanation = (explanation - explanation.min()) / ( + explanation.max() - explanation.min()) + + explanation = cv2.resize(explanation, resize_shape) + explanation = np.uint8(255 * explanation) + explanation = cv2.applyColorMap(explanation, cv2.COLORMAP_JET) + explanation = cv2.cvtColor(explanation, cv2.COLOR_BGR2RGB) + + return explanation + + +class BBoxCAM: + def __init__(self, FLAGS, cfg): + self.FLAGS = FLAGS + self.cfg = cfg + # build model + self.trainer = self.build_trainer(cfg) + # num_class + self.num_class = cfg.num_classes + # set hook for extraction of featuremaps and grads + self.set_hook() + + # cam image output_dir + try: + os.makedirs(FLAGS.cam_out) + except: + print('Path already exists.') + pass + + def build_trainer(self, cfg): + # build trainer + trainer = Trainer(cfg, mode='test') + # load weights + trainer.load_weights(cfg.weights) + + # record the bbox index before nms + # Todo: hard code for nms return index + if cfg.architecture == 'FasterRCNN': + trainer.model.bbox_post_process.nms.return_index = True + elif cfg.architecture == 'YOLOv3': + if trainer.model.post_process is not None: + # anchor based YOLOs: YOLOv3,PP-YOLO + trainer.model.post_process.nms.return_index = True + else: + # anchor free YOLOs: PP-YOLOE, PP-YOLOE+ + trainer.model.yolo_head.nms.return_index = True + else: + print( + 'Only supported cam for faster_rcnn based and yolov3 based architecture for now, the others are not supported temporarily!' + ) + sys.exit() + + return trainer + + def set_hook(self): + # set hook for extraction of featuremaps and grads + self.target_feats = {} + self.target_layer_name = 'trainer.model.backbone' + + def hook(layer, input, output): + self.target_feats[layer._layer_name_for_hook] = output + + self.trainer.model.backbone._layer_name_for_hook = self.target_layer_name + self.trainer.model.backbone.register_forward_post_hook(hook) + + def get_bboxes(self): + # get inference images + images = get_test_images(self.FLAGS.infer_dir, self.FLAGS.infer_img) + + # inference + result = self.trainer.predict( + images, + draw_threshold=self.FLAGS.draw_threshold, + output_dir=self.FLAGS.output_dir, + save_results=self.FLAGS.save_results, + visualize=False)[0] + return result + + def get_bboxes_cams(self): + # Get the bboxes prediction(after nms result) of the input + inference_result = self.get_bboxes() + + # read input image + # Todo: Support folder multi-images process + from PIL import Image + img = np.array(Image.open(self.cfg.infer_img)) + + # data for calaulate bbox grad_cam + cam_data = inference_result['cam_data'] + """ + if Faster_RCNN based architecture: + cam_data: {'scores': tensor with shape [num_of_bboxes_before_nms, num_classes], for example: [1000, 80] + 'before_nms_indexes': tensor with shape [num_of_bboxes_after_nms, 1], for example: [300, 1] + } + elif YOLOv3 based architecture: + cam_data: {'scores': tensor with shape [1, num_classes, num_of_yolo_bboxes_before_nms], #for example: [1, 80, 8400] + 'before_nms_indexes': tensor with shape [num_of_yolo_bboxes_after_nms, 1], # for example: [300, 1] + } + """ + + # array index of the predicted bbox before nms + if self.cfg.architecture == 'FasterRCNN': + # the bbox array shape of FasterRCNN before nms is [num_of_bboxes_before_nms, num_classes, 4], + # we need to divide num_classes to get the before_nms_index; + before_nms_indexes = cam_data['before_nms_indexes'].cpu().numpy( + ) // self.num_class # num_class + elif self.cfg.architecture == 'YOLOv3': + before_nms_indexes = cam_data['before_nms_indexes'].cpu().numpy() + else: + print( + 'Only supported cam for faster_rcnn based and yolov3 based architecture for now, the others are not supported temporarily!' + ) + sys.exit() + + # Calculate and visualize the heatmap of per predict bbox + for index, target_bbox in enumerate(inference_result['bbox']): + # target_bbox: [cls, score, x1, y1, x2, y2] + # filter bboxes with low predicted scores + if target_bbox[1] < self.FLAGS.draw_threshold: + continue + + target_bbox_before_nms = int(before_nms_indexes[index]) + + # bbox score vector + if self.cfg.architecture == 'FasterRCNN': + # the shape of faster_rcnn scores tensor is + # [num_of_bboxes_before_nms, num_classes], for example: [1000, 80] + score_out = cam_data['scores'][target_bbox_before_nms] + elif self.cfg.architecture == 'YOLOv3': + # the shape of yolov3 scores tensor is + # [1, num_classes, num_of_yolo_bboxes_before_nms] + score_out = cam_data['scores'][0, :, target_bbox_before_nms] + else: + print( + 'Only supported cam for faster_rcnn based and yolov3 based architecture for now, the others are not supported temporarily!' + ) + sys.exit() + + # construct one_hot label and do backward to get the gradients + predicted_label = paddle.argmax(score_out) + label_onehot = paddle.nn.functional.one_hot( + predicted_label, num_classes=len(score_out)) + label_onehot = label_onehot.squeeze() + target = paddle.sum(score_out * label_onehot) + target.backward(retain_graph=True) + + if isinstance(self.target_feats[self.target_layer_name], list): + # when the backbone output contains features of multiple scales, + # take the featuremap of the last scale + # Todo: fuse the cam result from multisclae featuremaps + backbone_grad = self.target_feats[self.target_layer_name][ + -1].grad.squeeze().cpu().numpy() + backbone_feat = self.target_feats[self.target_layer_name][ + -1].squeeze().cpu().numpy() + else: + backbone_grad = self.target_feats[ + self.target_layer_name].grad.squeeze().cpu().numpy() + backbone_feat = self.target_feats[ + self.target_layer_name].squeeze().cpu().numpy() + + # grad_cam: + exp = grad_cam(backbone_feat, backbone_grad) + + # reshape the cam image to the input image size + resized_exp = resize_cam(exp, (img.shape[1], img.shape[0])) + + # set the area outside the predic bbox to 0 + mask = np.zeros((img.shape[0], img.shape[1], 3)) + mask[int(target_bbox[3]):int(target_bbox[5]), int(target_bbox[2]): + int(target_bbox[4]), :] = 1 + resized_exp = resized_exp * mask + + # add the bbox cam back to the input image + overlay_vis = np.uint8(resized_exp * 0.4 + img * 0.6) + cv2.rectangle( + overlay_vis, (int(target_bbox[2]), int(target_bbox[3])), + (int(target_bbox[4]), int(target_bbox[5])), (0, 0, 255), 2) + + # save visualization result + cam_image = Image.fromarray(overlay_vis) + cam_image.save(self.FLAGS.cam_out + '/' + str(index) + '.jpg') + + # clear gradients after each bbox grad_cam + target.clear_gradient() + for n, v in self.trainer.model.named_sublayers(): + v.clear_gradients() diff --git a/tools/cam_ppdet.py b/tools/cam_ppdet.py new file mode 100644 index 000000000..e1c301995 --- /dev/null +++ b/tools/cam_ppdet.py @@ -0,0 +1,130 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import sys +# add python path of PadleDetection to sys.path +parent_path = os.path.abspath(os.path.join(__file__, *(['..'] * 2))) +sys.path.insert(0, parent_path) + +# ignore warning log +import warnings +warnings.filterwarnings('ignore') +from ppdet.utils.cli import ArgsParser, merge_args +from ppdet.core.workspace import load_config, merge_config +from ppdet.utils.check import check_gpu, check_npu, check_xpu, check_version, check_config +from ppdet.utils.cam_utils import BBoxCAM +import paddle + + + +def parse_args(): + parser = ArgsParser() + parser.add_argument( + "--infer_img", + type=str, + default='demo/000000014439.jpg', # hxw: 404x640 + help="Image path, has higher priority over --infer_dir") + parser.add_argument("--weights", + type=str, + default='output/faster_rcnn_r50_vd_fpn_2x_coco_paddlejob/best_model.pdparams' + ) + parser.add_argument("--cam_out", + type=str, + default='cam_faster_rcnn' + ) + parser.add_argument("--use_gpu", + type=bool, + default=True) + parser.add_argument( + "--infer_dir", + type=str, + default=None, + help="Directory for images to perform inference on.") + parser.add_argument( + "--output_dir", + type=str, + default="output", + help="Directory for storing the output visualization files.") + parser.add_argument( + "--draw_threshold", + type=float, + default=0.8, + help="Threshold to reserve the result for visualization.") + parser.add_argument( + "--save_results", + type=bool, + default=False, + help="Whether to save inference results to output_dir.") + parser.add_argument( + "--slice_infer", + action='store_true', + help="Whether to slice the image and merge the inference results for small object detection." + ) + parser.add_argument( + '--slice_size', + nargs='+', + type=int, + default=[640, 640], + help="Height of the sliced image.") + parser.add_argument( + "--overlap_ratio", + nargs='+', + type=float, + default=[0.25, 0.25], + help="Overlap height ratio of the sliced image.") + parser.add_argument( + "--combine_method", + type=str, + default='nms', + help="Combine method of the sliced images' detection results, choose in ['nms', 'nmm', 'concat']." + ) + args = parser.parse_args() + + return args + +def run(FLAGS, cfg): + assert cfg.architecture in ['FasterRCNN', 'YOLOv3'], 'Only supported cam for faster_rcnn based and yolov3 based architecture for now, the others are not supported temporarily!' + + bbox_cam = BBoxCAM(FLAGS, cfg) + bbox_cam.get_bboxes_cams() + + print('finish') + + + +def main(): + FLAGS = parse_args() + cfg = load_config(FLAGS.config) + merge_args(cfg, FLAGS) + merge_config(FLAGS.opt) + + # disable npu in config by default + if 'use_npu' not in cfg: + cfg.use_npu = False + + # disable xpu in config by default + if 'use_xpu' not in cfg: + cfg.use_xpu = False + + if cfg.use_gpu: + place = paddle.set_device('gpu') + elif cfg.use_npu: + place = paddle.set_device('npu') + elif cfg.use_xpu: + place = paddle.set_device('xpu') + else: + place = paddle.set_device('cpu') + + check_config(cfg) + check_gpu(cfg.use_gpu) + check_npu(cfg.use_npu) + check_xpu(cfg.use_xpu) + check_version() + + run(FLAGS, cfg) + + +if __name__ == '__main__': + main() -- GitLab