新增video-object-detection 算法
Link: https://code.alibaba-inc.com/Ali-MaaS/MaaS-lib/codereview/10247489
master
| @@ -0,0 +1,3 @@ | |||
| version https://git-lfs.github.com/spec/v1 | |||
| oid sha256:f58df1d25590c158ae0a04b3999bd44b610cdaddb17d78afd84c34b3f00d4e87 | |||
| size 4068783 | |||
| @@ -14,6 +14,7 @@ class Models(object): | |||
| # vision models | |||
| detection = 'detection' | |||
| realtime_object_detection = 'realtime-object-detection' | |||
| realtime_video_object_detection = 'realtime-video-object-detection' | |||
| scrfd = 'scrfd' | |||
| classification_model = 'ClassificationModel' | |||
| nafnet = 'nafnet' | |||
| @@ -170,6 +171,7 @@ class Pipelines(object): | |||
| face_image_generation = 'gan-face-image-generation' | |||
| product_retrieval_embedding = 'resnet50-product-retrieval-embedding' | |||
| realtime_object_detection = 'cspnet_realtime-object-detection_yolox' | |||
| realtime_video_object_detection = 'cspnet_realtime-video-object-detection_streamyolo' | |||
| face_recognition = 'ir101-face-recognition-cfglint' | |||
| image_instance_segmentation = 'cascade-mask-rcnn-swin-image-instance-segmentation' | |||
| image2image_translation = 'image-to-image-translation' | |||
| @@ -5,9 +5,11 @@ from modelscope.utils.import_utils import LazyImportModule | |||
| if TYPE_CHECKING: | |||
| from .realtime_detector import RealtimeDetector | |||
| from .realtime_video_detector import RealtimeVideoDetector | |||
| else: | |||
| _import_structure = { | |||
| 'realtime_detector': ['RealtimeDetector'], | |||
| 'realtime_video_detector': ['RealtimeVideoDetector'], | |||
| } | |||
| import sys | |||
| @@ -0,0 +1,117 @@ | |||
| # Copyright (c) Alibaba, Inc. and its affiliates. | |||
| import argparse | |||
| import logging as logger | |||
| import os | |||
| import os.path as osp | |||
| import time | |||
| import cv2 | |||
| import json | |||
| import torch | |||
| from tqdm import tqdm | |||
| from modelscope.metainfo import Models | |||
| from modelscope.models.base.base_torch_model import TorchModel | |||
| from modelscope.models.builder import MODELS | |||
| from modelscope.preprocessors import LoadImage | |||
| from modelscope.utils.config import Config | |||
| from modelscope.utils.constant import ModelFile, Tasks | |||
| from .yolox.data.data_augment import ValTransform | |||
| from .yolox.exp import get_exp_by_name | |||
| from .yolox.utils import postprocess | |||
| @MODELS.register_module( | |||
| group_key=Tasks.video_object_detection, | |||
| module_name=Models.realtime_video_object_detection) | |||
| class RealtimeVideoDetector(TorchModel): | |||
| def __init__(self, model_dir: str, *args, **kwargs): | |||
| super().__init__(model_dir, *args, **kwargs) | |||
| self.config = Config.from_file( | |||
| os.path.join(self.model_dir, ModelFile.CONFIGURATION)) | |||
| # model type | |||
| self.exp = get_exp_by_name(self.config.model_type) | |||
| # build model | |||
| self.model = self.exp.get_model() | |||
| model_path = osp.join(model_dir, ModelFile.TORCH_MODEL_BIN_FILE) | |||
| ckpt = torch.load(model_path, map_location='cpu') | |||
| # load the model state dict | |||
| self.model.load_state_dict(ckpt['model']) | |||
| self.model.eval() | |||
| # params setting | |||
| self.exp.num_classes = self.config.num_classes | |||
| self.confthre = self.config.conf_thr | |||
| self.num_classes = self.exp.num_classes | |||
| self.nmsthre = self.exp.nmsthre | |||
| self.test_size = self.exp.test_size | |||
| self.preproc = ValTransform(legacy=False) | |||
| self.current_buffer = None | |||
| self.label_mapping = self.config['labels'] | |||
| def inference(self, img): | |||
| with torch.no_grad(): | |||
| outputs, self.current_buffer = self.model( | |||
| img, buffer=self.current_buffer, mode='on_pipe') | |||
| return outputs | |||
| def forward(self, inputs): | |||
| return self.inference_video(inputs) | |||
| def preprocess(self, img): | |||
| img = LoadImage.convert_to_ndarray(img) | |||
| height, width = img.shape[:2] | |||
| self.ratio = min(self.test_size[0] / img.shape[0], | |||
| self.test_size[1] / img.shape[1]) | |||
| img, _ = self.preproc(img, None, self.test_size) | |||
| img = torch.from_numpy(img).unsqueeze(0) | |||
| img = img.float() | |||
| # Video decoding and preprocessing automatically are not supported by Pipeline/Model | |||
| # Sending preprocessed video frame tensor to GPU buffer self-adaptively | |||
| if next(self.model.parameters()).is_cuda: | |||
| img = img.to(next(self.model.parameters()).device) | |||
| return img | |||
| def postprocess(self, input): | |||
| outputs = postprocess( | |||
| input, | |||
| self.num_classes, | |||
| self.confthre, | |||
| self.nmsthre, | |||
| class_agnostic=True) | |||
| if len(outputs) == 1: | |||
| bboxes = outputs[0][:, 0:4].cpu().numpy() / self.ratio | |||
| scores = outputs[0][:, 5].cpu().numpy() | |||
| labels = outputs[0][:, 6].cpu().int().numpy() | |||
| pred_label_names = [] | |||
| for lab in labels: | |||
| pred_label_names.append(self.label_mapping[lab]) | |||
| return bboxes, scores, pred_label_names | |||
| def inference_video(self, v_path): | |||
| outputs = [] | |||
| desc = 'Detecting video: {}'.format(v_path) | |||
| for frame, result in tqdm( | |||
| self.inference_video_iter(v_path), desc=desc): | |||
| outputs.append(result) | |||
| return outputs | |||
| def inference_video_iter(self, v_path): | |||
| capture = cv2.VideoCapture(v_path) | |||
| while capture.isOpened(): | |||
| ret, frame = capture.read() | |||
| if not ret: | |||
| break | |||
| output = self.preprocess(frame) | |||
| output = self.inference(output) | |||
| output = self.postprocess(output) | |||
| yield frame, output | |||
| @@ -13,6 +13,8 @@ def get_exp_by_name(exp_name): | |||
| from .default import YoloXNanoExp as YoloXExp | |||
| elif exp == 'yolox_tiny': | |||
| from .default import YoloXTinyExp as YoloXExp | |||
| elif exp == 'streamyolo': | |||
| from .default import StreamYoloExp as YoloXExp | |||
| else: | |||
| pass | |||
| return YoloXExp() | |||
| @@ -1,5 +1,5 @@ | |||
| # The implementation is based on YOLOX, available at https://github.com/Megvii-BaseDetection/YOLOX | |||
| from .streamyolo import StreamYoloExp | |||
| from .yolox_nano import YoloXNanoExp | |||
| from .yolox_s import YoloXSExp | |||
| from .yolox_tiny import YoloXTinyExp | |||
| @@ -0,0 +1,43 @@ | |||
| # The implementation is based on StreamYOLO, available at https://github.com/yancie-yjr/StreamYOLO | |||
| import os | |||
| import sys | |||
| import torch | |||
| from ..yolox_base import Exp as YoloXExp | |||
| class StreamYoloExp(YoloXExp): | |||
| def __init__(self): | |||
| super(YoloXExp, self).__init__() | |||
| self.depth = 1.0 | |||
| self.width = 1.0 | |||
| self.num_classes = 8 | |||
| self.test_size = (600, 960) | |||
| self.test_conf = 0.3 | |||
| self.nmsthre = 0.65 | |||
| def get_model(self): | |||
| from ...models import StreamYOLO, DFPPAFPN, TALHead | |||
| def init_yolo(M): | |||
| for m in M.modules(): | |||
| if isinstance(m, nn.BatchNorm2d): | |||
| m.eps = 1e-3 | |||
| m.momentum = 0.03 | |||
| if getattr(self, 'model', None) is None: | |||
| in_channels = [256, 512, 1024] | |||
| backbone = DFPPAFPN( | |||
| self.depth, self.width, in_channels=in_channels) | |||
| head = TALHead( | |||
| self.num_classes, | |||
| self.width, | |||
| in_channels=in_channels, | |||
| gamma=1.0, | |||
| ignore_thr=0.5, | |||
| ignore_value=1.6) | |||
| self.model = StreamYOLO(backbone, head) | |||
| return self.model | |||
| @@ -1,5 +1,4 @@ | |||
| # The implementation is based on YOLOX, available at https://github.com/Megvii-BaseDetection/YOLOX | |||
| import os | |||
| import random | |||
| @@ -1,6 +1,9 @@ | |||
| # The implementation is based on YOLOX, available at https://github.com/Megvii-BaseDetection/YOLOX | |||
| from .darknet import CSPDarknet, Darknet | |||
| from .dfp_pafpn import DFPPAFPN | |||
| from .streamyolo import StreamYOLO | |||
| from .tal_head import TALHead | |||
| from .yolo_fpn import YOLOFPN | |||
| from .yolo_head import YOLOXHead | |||
| from .yolo_pafpn import YOLOPAFPN | |||
| @@ -0,0 +1,307 @@ | |||
| # The implementation is based on StreamYOLO, available at https://github.com/yancie-yjr/StreamYOLO | |||
| import torch | |||
| import torch.nn as nn | |||
| import torch.nn.functional as F | |||
| from .darknet import CSPDarknet | |||
| from .network_blocks import BaseConv, CSPLayer, DWConv | |||
| class DFPPAFPN(nn.Module): | |||
| """ | |||
| YOLOv3 model. Darknet 53 is the default backbone of this model. | |||
| """ | |||
| def __init__( | |||
| self, | |||
| depth=1.0, | |||
| width=1.0, | |||
| in_features=('dark3', 'dark4', 'dark5'), | |||
| in_channels=[256, 512, 1024], | |||
| depthwise=False, | |||
| act='silu', | |||
| ): | |||
| super().__init__() | |||
| self.backbone = CSPDarknet(depth, width, depthwise=depthwise, act=act) | |||
| self.in_features = in_features | |||
| self.in_channels = in_channels | |||
| Conv = DWConv if depthwise else BaseConv | |||
| self.lateral_conv0 = BaseConv( | |||
| int(in_channels[2] * width), | |||
| int(in_channels[1] * width), | |||
| 1, | |||
| 1, | |||
| act=act) | |||
| self.C3_p4 = CSPLayer( | |||
| int(2 * in_channels[1] * width), | |||
| int(in_channels[1] * width), | |||
| round(3 * depth), | |||
| False, | |||
| depthwise=depthwise, | |||
| act=act, | |||
| ) # cat | |||
| self.reduce_conv1 = BaseConv( | |||
| int(in_channels[1] * width), | |||
| int(in_channels[0] * width), | |||
| 1, | |||
| 1, | |||
| act=act) | |||
| self.C3_p3 = CSPLayer( | |||
| int(2 * in_channels[0] * width), | |||
| int(in_channels[0] * width), | |||
| round(3 * depth), | |||
| False, | |||
| depthwise=depthwise, | |||
| act=act, | |||
| ) | |||
| # bottom-up conv | |||
| self.bu_conv2 = Conv( | |||
| int(in_channels[0] * width), | |||
| int(in_channels[0] * width), | |||
| 3, | |||
| 2, | |||
| act=act) | |||
| self.C3_n3 = CSPLayer( | |||
| int(2 * in_channels[0] * width), | |||
| int(in_channels[1] * width), | |||
| round(3 * depth), | |||
| False, | |||
| depthwise=depthwise, | |||
| act=act, | |||
| ) | |||
| # bottom-up conv | |||
| self.bu_conv1 = Conv( | |||
| int(in_channels[1] * width), | |||
| int(in_channels[1] * width), | |||
| 3, | |||
| 2, | |||
| act=act) | |||
| self.C3_n4 = CSPLayer( | |||
| int(2 * in_channels[1] * width), | |||
| int(in_channels[2] * width), | |||
| round(3 * depth), | |||
| False, | |||
| depthwise=depthwise, | |||
| act=act, | |||
| ) | |||
| self.jian2 = Conv( | |||
| in_channels=int(in_channels[0] * width), | |||
| out_channels=int(in_channels[0] * width) // 2, | |||
| ksize=1, | |||
| stride=1, | |||
| act=act, | |||
| ) | |||
| self.jian1 = Conv( | |||
| in_channels=int(in_channels[1] * width), | |||
| out_channels=int(in_channels[1] * width) // 2, | |||
| ksize=1, | |||
| stride=1, | |||
| act=act, | |||
| ) | |||
| self.jian0 = Conv( | |||
| in_channels=int(in_channels[2] * width), | |||
| out_channels=int(in_channels[2] * width) // 2, | |||
| ksize=1, | |||
| stride=1, | |||
| act=act, | |||
| ) | |||
| def off_forward(self, input): | |||
| """ | |||
| Args: | |||
| inputs: input images. | |||
| Returns: | |||
| Tuple[Tensor]: FPN feature. | |||
| """ | |||
| # backbone | |||
| rurrent_out_features = self.backbone(torch.split(input, 3, dim=1)[0]) | |||
| rurrent_features = [rurrent_out_features[f] for f in self.in_features] | |||
| [rurrent_x2, rurrent_x1, rurrent_x0] = rurrent_features | |||
| rurrent_fpn_out0 = self.lateral_conv0(rurrent_x0) # 1024->512/32 | |||
| rurrent_f_out0 = F.interpolate( | |||
| rurrent_fpn_out0, size=rurrent_x1.shape[2:4], | |||
| mode='nearest') # 512/16 | |||
| rurrent_f_out0 = torch.cat([rurrent_f_out0, rurrent_x1], | |||
| 1) # 512->1024/16 | |||
| rurrent_f_out0 = self.C3_p4(rurrent_f_out0) # 1024->512/16 | |||
| rurrent_fpn_out1 = self.reduce_conv1(rurrent_f_out0) # 512->256/16 | |||
| rurrent_f_out1 = F.interpolate( | |||
| rurrent_fpn_out1, size=rurrent_x2.shape[2:4], | |||
| mode='nearest') # 256/8 | |||
| rurrent_f_out1 = torch.cat([rurrent_f_out1, rurrent_x2], | |||
| 1) # 256->512/8 | |||
| rurrent_pan_out2 = self.C3_p3(rurrent_f_out1) # 512->256/8 | |||
| rurrent_p_out1 = self.bu_conv2(rurrent_pan_out2) # 256->256/16 | |||
| rurrent_p_out1 = torch.cat([rurrent_p_out1, rurrent_fpn_out1], | |||
| 1) # 256->512/16 | |||
| rurrent_pan_out1 = self.C3_n3(rurrent_p_out1) # 512->512/16 | |||
| rurrent_p_out0 = self.bu_conv1(rurrent_pan_out1) # 512->512/32 | |||
| rurrent_p_out0 = torch.cat([rurrent_p_out0, rurrent_fpn_out0], | |||
| 1) # 512->1024/32 | |||
| rurrent_pan_out0 = self.C3_n4(rurrent_p_out0) # 1024->1024/32 | |||
| ##### | |||
| support_out_features = self.backbone(torch.split(input, 3, dim=1)[1]) | |||
| support_features = [support_out_features[f] for f in self.in_features] | |||
| [support_x2, support_x1, support_x0] = support_features | |||
| support_fpn_out0 = self.lateral_conv0(support_x0) # 1024->512/32 | |||
| support_f_out0 = F.interpolate( | |||
| support_fpn_out0, size=support_x1.shape[2:4], | |||
| mode='nearest') # 512/16 | |||
| support_f_out0 = torch.cat([support_f_out0, support_x1], | |||
| 1) # 512->1024/16 | |||
| support_f_out0 = self.C3_p4(support_f_out0) # 1024->512/16 | |||
| support_fpn_out1 = self.reduce_conv1(support_f_out0) # 512->256/16 | |||
| support_f_out1 = F.interpolate( | |||
| support_fpn_out1, size=support_x2.shape[2:4], | |||
| mode='nearest') # 256/8 | |||
| support_f_out1 = torch.cat([support_f_out1, support_x2], | |||
| 1) # 256->512/8 | |||
| support_pan_out2 = self.C3_p3(support_f_out1) # 512->256/8 | |||
| support_p_out1 = self.bu_conv2(support_pan_out2) # 256->256/16 | |||
| support_p_out1 = torch.cat([support_p_out1, support_fpn_out1], | |||
| 1) # 256->512/16 | |||
| support_pan_out1 = self.C3_n3(support_p_out1) # 512->512/16 | |||
| support_p_out0 = self.bu_conv1(support_pan_out1) # 512->512/32 | |||
| support_p_out0 = torch.cat([support_p_out0, support_fpn_out0], | |||
| 1) # 512->1024/32 | |||
| support_pan_out0 = self.C3_n4(support_p_out0) # 1024->1024/32 | |||
| # 0.5 channel | |||
| pan_out2 = torch.cat( | |||
| [self.jian2(rurrent_pan_out2), | |||
| self.jian2(support_pan_out2)], | |||
| dim=1) + rurrent_pan_out2 | |||
| pan_out1 = torch.cat( | |||
| [self.jian1(rurrent_pan_out1), | |||
| self.jian1(support_pan_out1)], | |||
| dim=1) + rurrent_pan_out1 | |||
| pan_out0 = torch.cat( | |||
| [self.jian0(rurrent_pan_out0), | |||
| self.jian0(support_pan_out0)], | |||
| dim=1) + rurrent_pan_out0 | |||
| outputs = (pan_out2, pan_out1, pan_out0) | |||
| return outputs | |||
| def online_forward(self, input, buffer=None, node='star'): | |||
| """ | |||
| Args: | |||
| inputs: input images. | |||
| Returns: | |||
| Tuple[Tensor]: FPN feature. | |||
| """ | |||
| # backbone | |||
| rurrent_out_features = self.backbone(input) | |||
| rurrent_features = [rurrent_out_features[f] for f in self.in_features] | |||
| [rurrent_x2, rurrent_x1, rurrent_x0] = rurrent_features | |||
| rurrent_fpn_out0 = self.lateral_conv0(rurrent_x0) # 1024->512/32 | |||
| rurrent_f_out0 = F.interpolate( | |||
| rurrent_fpn_out0, size=rurrent_x1.shape[2:4], | |||
| mode='nearest') # 512/16 | |||
| rurrent_f_out0 = torch.cat([rurrent_f_out0, rurrent_x1], | |||
| 1) # 512->1024/16 | |||
| rurrent_f_out0 = self.C3_p4(rurrent_f_out0) # 1024->512/16 | |||
| rurrent_fpn_out1 = self.reduce_conv1(rurrent_f_out0) # 512->256/16 | |||
| rurrent_f_out1 = F.interpolate( | |||
| rurrent_fpn_out1, size=rurrent_x2.shape[2:4], | |||
| mode='nearest') # 256/8 | |||
| rurrent_f_out1 = torch.cat([rurrent_f_out1, rurrent_x2], | |||
| 1) # 256->512/8 | |||
| rurrent_pan_out2 = self.C3_p3(rurrent_f_out1) # 512->256/8 | |||
| rurrent_p_out1 = self.bu_conv2(rurrent_pan_out2) # 256->256/16 | |||
| rurrent_p_out1 = torch.cat([rurrent_p_out1, rurrent_fpn_out1], | |||
| 1) # 256->512/16 | |||
| rurrent_pan_out1 = self.C3_n3(rurrent_p_out1) # 512->512/16 | |||
| rurrent_p_out0 = self.bu_conv1(rurrent_pan_out1) # 512->512/32 | |||
| rurrent_p_out0 = torch.cat([rurrent_p_out0, rurrent_fpn_out0], | |||
| 1) # 512->1024/32 | |||
| rurrent_pan_out0 = self.C3_n4(rurrent_p_out0) # 1024->1024/32 | |||
| ##### | |||
| if node == 'star': | |||
| pan_out2 = torch.cat( | |||
| [self.jian2(rurrent_pan_out2), | |||
| self.jian2(rurrent_pan_out2)], | |||
| dim=1) + rurrent_pan_out2 | |||
| pan_out1 = torch.cat( | |||
| [self.jian1(rurrent_pan_out1), | |||
| self.jian1(rurrent_pan_out1)], | |||
| dim=1) + rurrent_pan_out1 | |||
| pan_out0 = torch.cat( | |||
| [self.jian0(rurrent_pan_out0), | |||
| self.jian0(rurrent_pan_out0)], | |||
| dim=1) + rurrent_pan_out0 | |||
| elif node == 'buffer': | |||
| [support_pan_out2, support_pan_out1, support_pan_out0] = buffer | |||
| pan_out2 = torch.cat( | |||
| [self.jian2(rurrent_pan_out2), | |||
| self.jian2(support_pan_out2)], | |||
| dim=1) + rurrent_pan_out2 | |||
| pan_out1 = torch.cat( | |||
| [self.jian1(rurrent_pan_out1), | |||
| self.jian1(support_pan_out1)], | |||
| dim=1) + rurrent_pan_out1 | |||
| pan_out0 = torch.cat( | |||
| [self.jian0(rurrent_pan_out0), | |||
| self.jian0(support_pan_out0)], | |||
| dim=1) + rurrent_pan_out0 | |||
| outputs = (pan_out2, pan_out1, pan_out0) | |||
| buffer_ = (rurrent_pan_out2, rurrent_pan_out1, rurrent_pan_out0) | |||
| return outputs, buffer_ | |||
| def forward(self, input, buffer=None, mode='off_pipe'): | |||
| if mode == 'off_pipe': | |||
| # Glops caculate mode | |||
| if input.size()[1] == 3: | |||
| input = torch.cat([input, input], dim=1) | |||
| output = self.off_forward(input) | |||
| # offline train mode | |||
| elif input.size()[1] == 6: | |||
| output = self.off_forward(input) | |||
| return output | |||
| elif mode == 'on_pipe': | |||
| # online star state | |||
| if buffer is None: | |||
| output, buffer_ = self.online_forward(input, node='star') | |||
| # online inference | |||
| else: | |||
| assert len(buffer) == 3 | |||
| assert input.size()[1] == 3 | |||
| output, buffer_ = self.online_forward( | |||
| input, buffer=buffer, node='buffer') | |||
| return output, buffer_ | |||
| @@ -1,5 +1,4 @@ | |||
| # The implementation is based on YOLOX, available at https://github.com/Megvii-BaseDetection/YOLOX | |||
| import torch | |||
| import torch.nn as nn | |||
| @@ -0,0 +1,41 @@ | |||
| # The implementation is based on StreamYOLO, available at https://github.com/yancie-yjr/StreamYOLO | |||
| import torch.nn as nn | |||
| from .dfp_pafpn import DFPPAFPN | |||
| from .tal_head import TALHead | |||
| class StreamYOLO(nn.Module): | |||
| """ | |||
| YOLOX model module. The module list is defined by create_yolov3_modules function. | |||
| The network returns loss values from three YOLO layers during training | |||
| and detection results during test. | |||
| """ | |||
| def __init__(self, backbone=None, head=None): | |||
| super().__init__() | |||
| if backbone is None: | |||
| backbone = DFPPAFPN() | |||
| if head is None: | |||
| head = TALHead(20) | |||
| self.backbone = backbone | |||
| self.head = head | |||
| def forward(self, x, targets=None, buffer=None, mode='off_pipe'): | |||
| # fpn output content features of [dark3, dark4, dark5] | |||
| assert mode in ['off_pipe', 'on_pipe'] | |||
| if mode == 'off_pipe': | |||
| fpn_outs = self.backbone(x, buffer=buffer, mode='off_pipe') | |||
| if self.training: | |||
| pass | |||
| else: | |||
| outputs = self.head(fpn_outs, imgs=x) | |||
| return outputs | |||
| elif mode == 'on_pipe': | |||
| fpn_outs, buffer_ = self.backbone(x, buffer=buffer, mode='on_pipe') | |||
| outputs = self.head(fpn_outs) | |||
| return outputs, buffer_ | |||
| @@ -0,0 +1,170 @@ | |||
| # The implementation is based on StreamYOLO, available at https://github.com/yancie-yjr/StreamYOLO | |||
| import torch | |||
| import torch.nn as nn | |||
| import torch.nn.functional as F | |||
| from .network_blocks import BaseConv, DWConv | |||
| class TALHead(nn.Module): | |||
| def __init__( | |||
| self, | |||
| num_classes, | |||
| width=1.0, | |||
| strides=[8, 16, 32], | |||
| in_channels=[256, 512, 1024], | |||
| act='silu', | |||
| depthwise=False, | |||
| gamma=1.5, | |||
| ignore_thr=0.2, | |||
| ignore_value=0.2, | |||
| ): | |||
| """ | |||
| Args: | |||
| act (str): activation type of conv. Defalut value: "silu". | |||
| depthwise (bool): wheather apply depthwise conv in conv branch. Defalut value: False. | |||
| """ | |||
| super().__init__() | |||
| self.gamma = gamma | |||
| self.ignore_thr = ignore_thr | |||
| self.ignore_value = ignore_value | |||
| self.n_anchors = 1 | |||
| self.num_classes = num_classes | |||
| self.decode_in_inference = True # for deploy, set to False | |||
| self.cls_convs = nn.ModuleList() | |||
| self.reg_convs = nn.ModuleList() | |||
| self.cls_preds = nn.ModuleList() | |||
| self.reg_preds = nn.ModuleList() | |||
| self.obj_preds = nn.ModuleList() | |||
| self.stems = nn.ModuleList() | |||
| Conv = DWConv if depthwise else BaseConv | |||
| for i in range(len(in_channels)): | |||
| self.stems.append( | |||
| BaseConv( | |||
| in_channels=int(in_channels[i] * width), | |||
| out_channels=int(256 * width), | |||
| ksize=1, | |||
| stride=1, | |||
| act=act, | |||
| )) | |||
| self.cls_convs.append( | |||
| nn.Sequential(*[ | |||
| Conv( | |||
| in_channels=int(256 * width), | |||
| out_channels=int(256 * width), | |||
| ksize=3, | |||
| stride=1, | |||
| act=act, | |||
| ), | |||
| Conv( | |||
| in_channels=int(256 * width), | |||
| out_channels=int(256 * width), | |||
| ksize=3, | |||
| stride=1, | |||
| act=act, | |||
| ), | |||
| ])) | |||
| self.reg_convs.append( | |||
| nn.Sequential(*[ | |||
| Conv( | |||
| in_channels=int(256 * width), | |||
| out_channels=int(256 * width), | |||
| ksize=3, | |||
| stride=1, | |||
| act=act, | |||
| ), | |||
| Conv( | |||
| in_channels=int(256 * width), | |||
| out_channels=int(256 * width), | |||
| ksize=3, | |||
| stride=1, | |||
| act=act, | |||
| ), | |||
| ])) | |||
| self.cls_preds.append( | |||
| nn.Conv2d( | |||
| in_channels=int(256 * width), | |||
| out_channels=self.n_anchors * self.num_classes, | |||
| kernel_size=1, | |||
| stride=1, | |||
| padding=0, | |||
| )) | |||
| self.reg_preds.append( | |||
| nn.Conv2d( | |||
| in_channels=int(256 * width), | |||
| out_channels=4, | |||
| kernel_size=1, | |||
| stride=1, | |||
| padding=0, | |||
| )) | |||
| self.obj_preds.append( | |||
| nn.Conv2d( | |||
| in_channels=int(256 * width), | |||
| out_channels=self.n_anchors * 1, | |||
| kernel_size=1, | |||
| stride=1, | |||
| padding=0, | |||
| )) | |||
| self.strides = strides | |||
| self.grids = [torch.zeros(1)] * len(in_channels) | |||
| self.expanded_strides = [None] * len(in_channels) | |||
| def forward(self, xin, labels=None, imgs=None): | |||
| outputs = [] | |||
| for k, (cls_conv, reg_conv, stride_this_level, x) in enumerate( | |||
| zip(self.cls_convs, self.reg_convs, self.strides, xin)): | |||
| x = self.stems[k](x) | |||
| cls_x = x | |||
| reg_x = x | |||
| cls_feat = cls_conv(cls_x) | |||
| cls_output = self.cls_preds[k](cls_feat) | |||
| reg_feat = reg_conv(reg_x) | |||
| reg_output = self.reg_preds[k](reg_feat) | |||
| obj_output = self.obj_preds[k](reg_feat) | |||
| if self.training: | |||
| pass | |||
| else: | |||
| output = torch.cat( | |||
| [reg_output, | |||
| obj_output.sigmoid(), | |||
| cls_output.sigmoid()], 1) | |||
| outputs.append(output) | |||
| if self.training: | |||
| pass | |||
| else: | |||
| self.hw = [x.shape[-2:] for x in outputs] | |||
| outputs = torch.cat([x.flatten(start_dim=2) for x in outputs], | |||
| dim=2).permute(0, 2, 1) | |||
| if self.decode_in_inference: | |||
| return self.decode_outputs(outputs, dtype=xin[0].type()) | |||
| else: | |||
| return outputs | |||
| def decode_outputs(self, outputs, dtype): | |||
| grids = [] | |||
| strides = [] | |||
| for (hsize, wsize), stride in zip(self.hw, self.strides): | |||
| yv, xv = torch.meshgrid([torch.arange(hsize), torch.arange(wsize)]) | |||
| grid = torch.stack((xv, yv), 2).view(1, -1, 2) | |||
| grids.append(grid) | |||
| shape = grid.shape[:2] | |||
| strides.append(torch.full((*shape, 1), stride)) | |||
| grids = torch.cat(grids, dim=1).type(dtype) | |||
| strides = torch.cat(strides, dim=1).type(dtype) | |||
| outputs[..., :2] = (outputs[..., :2] + grids) * strides | |||
| outputs[..., 2:4] = torch.exp(outputs[..., 2:4]) * strides | |||
| return outputs | |||
| @@ -165,6 +165,32 @@ TASK_OUTPUTS = { | |||
| Tasks.image_object_detection: | |||
| [OutputKeys.SCORES, OutputKeys.LABELS, OutputKeys.BOXES], | |||
| # video object detection result for single sample | |||
| # { | |||
| # "scores": [[0.8, 0.25, 0.05, 0.05], [0.9, 0.1, 0.05, 0.05]] | |||
| # "labels": [["person", "traffic light", "car", "bus"], | |||
| # ["person", "traffic light", "car", "bus"]] | |||
| # "boxes": | |||
| # [ | |||
| # [ | |||
| # [x1, y1, x2, y2], | |||
| # [x1, y1, x2, y2], | |||
| # [x1, y1, x2, y2], | |||
| # [x1, y1, x2, y2], | |||
| # ], | |||
| # [ | |||
| # [x1, y1, x2, y2], | |||
| # [x1, y1, x2, y2], | |||
| # [x1, y1, x2, y2], | |||
| # [x1, y1, x2, y2], | |||
| # ] | |||
| # ], | |||
| # } | |||
| Tasks.video_object_detection: | |||
| [OutputKeys.SCORES, OutputKeys.LABELS, OutputKeys.BOXES], | |||
| # instance segmentation result for single sample | |||
| # { | |||
| # "scores": [0.9, 0.1, 0.05, 0.05], | |||
| @@ -676,8 +702,9 @@ TASK_OUTPUTS = { | |||
| # "text_embedding": np.array with shape [1, D], | |||
| # "similarity": float | |||
| # } | |||
| Tasks.multi_modal_similarity: | |||
| [OutputKeys.IMG_EMBEDDING, OutputKeys.TEXT_EMBEDDING, OutputKeys.SCORES], | |||
| Tasks.multi_modal_similarity: [ | |||
| OutputKeys.IMG_EMBEDDING, OutputKeys.TEXT_EMBEDDING, OutputKeys.SCORES | |||
| ], | |||
| # VQA result for a sample | |||
| # {"text": "this is a text answser. "} | |||
| @@ -0,0 +1,59 @@ | |||
| # Copyright (c) Alibaba, Inc. and its affiliates. | |||
| import os.path as osp | |||
| from typing import Any, Dict, List, Union | |||
| import cv2 | |||
| import json | |||
| import numpy as np | |||
| import torch | |||
| from PIL import Image | |||
| from torchvision import transforms | |||
| from modelscope.metainfo import Pipelines | |||
| from modelscope.models.cv.realtime_object_detection import \ | |||
| RealtimeVideoDetector | |||
| from modelscope.outputs import OutputKeys | |||
| from modelscope.pipelines import pipeline | |||
| from modelscope.pipelines.base import Input, Model, Pipeline, Tensor | |||
| from modelscope.pipelines.builder import PIPELINES | |||
| from modelscope.preprocessors import load_image | |||
| from modelscope.utils.constant import ModelFile, Tasks | |||
| from modelscope.utils.logger import get_logger | |||
| logger = get_logger() | |||
| @PIPELINES.register_module( | |||
| Tasks.video_object_detection, | |||
| module_name=Pipelines.realtime_video_object_detection) | |||
| class RealtimeVideoObjectDetectionPipeline(Pipeline): | |||
| def __init__(self, model: str, **kwargs): | |||
| super().__init__(model=model, **kwargs) | |||
| self.model = RealtimeVideoDetector(model) | |||
| def preprocess(self, input: Input) -> Dict[Tensor, Union[str, np.ndarray]]: | |||
| return input | |||
| def forward(self, input: Input) -> Dict[Tensor, Dict[str, np.ndarray]]: | |||
| self.video_path = input | |||
| # Processing the whole video and return results for each frame | |||
| forward_output = self.model.inference_video(self.video_path) | |||
| return {'forward_output': forward_output} | |||
| def postprocess(self, input: Dict[Tensor, Dict[str, np.ndarray]], | |||
| **kwargs) -> str: | |||
| forward_output = input['forward_output'] | |||
| scores, boxes, labels = [], [], [] | |||
| for result in forward_output: | |||
| box, score, label = result | |||
| scores.append(score) | |||
| boxes.append(box) | |||
| labels.append(label) | |||
| return { | |||
| OutputKeys.BOXES: boxes, | |||
| OutputKeys.SCORES: scores, | |||
| OutputKeys.LABELS: labels, | |||
| } | |||
| @@ -38,6 +38,7 @@ class CVTasks(object): | |||
| image_classification_dailylife = 'image-classification-dailylife' | |||
| image_object_detection = 'image-object-detection' | |||
| video_object_detection = 'video-object-detection' | |||
| image_segmentation = 'image-segmentation' | |||
| semantic_segmentation = 'semantic-segmentation' | |||
| @@ -231,6 +231,66 @@ def show_video_tracking_result(video_in_path, bboxes, video_save_path): | |||
| cap.release() | |||
| def show_video_object_detection_result(video_in_path, bboxes_list, labels_list, | |||
| video_save_path): | |||
| PALETTE = { | |||
| 'person': [128, 0, 0], | |||
| 'bicycle': [128, 128, 0], | |||
| 'car': [64, 0, 0], | |||
| 'motorcycle': [0, 128, 128], | |||
| 'bus': [64, 128, 0], | |||
| 'truck': [192, 128, 0], | |||
| 'traffic light': [64, 0, 128], | |||
| 'stop sign': [192, 0, 128], | |||
| } | |||
| from tqdm import tqdm | |||
| import math | |||
| cap = cv2.VideoCapture(video_in_path) | |||
| with tqdm(total=len(bboxes_list)) as pbar: | |||
| pbar.set_description( | |||
| 'Writing results to video: {}'.format(video_save_path)) | |||
| for i in range(len(bboxes_list)): | |||
| bboxes = bboxes_list[i].astype(int) | |||
| labels = labels_list[i] | |||
| success, frame = cap.read() | |||
| if success is False: | |||
| raise Exception(video_in_path, | |||
| ' can not be correctly decoded by OpenCV.') | |||
| if i == 0: | |||
| size = (frame.shape[1], frame.shape[0]) | |||
| fourcc = cv2.VideoWriter_fourcc('M', 'J', 'P', 'G') | |||
| video_writer = cv2.VideoWriter(video_save_path, fourcc, | |||
| cap.get(cv2.CAP_PROP_FPS), size, | |||
| True) | |||
| FONT_SCALE = 1e-3 # Adjust for larger font size in all images | |||
| THICKNESS_SCALE = 1e-3 # Adjust for larger thickness in all images | |||
| TEXT_Y_OFFSET_SCALE = 1e-2 # Adjust for larger Y-offset of text and bounding box | |||
| H, W, _ = frame.shape | |||
| zeros_mask = np.zeros((frame.shape)).astype(np.uint8) | |||
| for bbox, l in zip(bboxes, labels): | |||
| cv2.rectangle(frame, (bbox[0], bbox[1]), (bbox[2], bbox[3]), | |||
| PALETTE[l], 1) | |||
| cv2.putText( | |||
| frame, | |||
| l, (bbox[0], bbox[1] - int(TEXT_Y_OFFSET_SCALE * H)), | |||
| fontFace=cv2.FONT_HERSHEY_TRIPLEX, | |||
| fontScale=min(H, W) * FONT_SCALE, | |||
| thickness=math.ceil(min(H, W) * THICKNESS_SCALE), | |||
| color=PALETTE[l]) | |||
| zeros_mask = cv2.rectangle( | |||
| zeros_mask, (bbox[0], bbox[1]), (bbox[2], bbox[3]), | |||
| color=PALETTE[l], | |||
| thickness=-1) | |||
| frame = cv2.addWeighted(frame, 1., zeros_mask, .65, 0) | |||
| video_writer.write(frame) | |||
| pbar.update(1) | |||
| video_writer.release | |||
| cap.release() | |||
| def panoptic_seg_masks_to_image(masks): | |||
| draw_img = np.zeros([masks[0].shape[0], masks[0].shape[1], 3]) | |||
| from mmdet.core.visualization.palette import get_palette | |||
| @@ -0,0 +1,46 @@ | |||
| # Copyright (c) Alibaba, Inc. and its affiliates. | |||
| import unittest | |||
| import cv2 | |||
| import numpy as np | |||
| from modelscope.outputs import OutputKeys | |||
| from modelscope.pipelines import pipeline | |||
| from modelscope.pipelines.base import Pipeline | |||
| from modelscope.utils.constant import Tasks | |||
| from modelscope.utils.cv.image_utils import show_video_object_detection_result | |||
| from modelscope.utils.demo_utils import DemoCompatibilityCheck | |||
| from modelscope.utils.logger import get_logger | |||
| from modelscope.utils.test_utils import test_level | |||
| logger = get_logger() | |||
| class RealtimeVideoObjectDetectionTest(unittest.TestCase, | |||
| DemoCompatibilityCheck): | |||
| def setUp(self) -> None: | |||
| self.model_id = 'damo/cv_cspnet_video-object-detection_streamyolo' | |||
| self.test_video = 'data/test/videos/test_realtime_vod.mp4' | |||
| @unittest.skipUnless(test_level() >= 0, 'skip test in current test level') | |||
| def test_run_modelhub(self): | |||
| realtime_video_object_detection = pipeline( | |||
| Tasks.video_object_detection, model=self.model_id) | |||
| result = realtime_video_object_detection(self.test_video) | |||
| if result: | |||
| logger.info('Video output to test_vod_results.avi') | |||
| show_video_object_detection_result(self.test_video, | |||
| result[OutputKeys.BOXES], | |||
| result[OutputKeys.LABELS], | |||
| 'test_vod_results.avi') | |||
| else: | |||
| raise ValueError('process error') | |||
| @unittest.skip('demo compatibility test is only enabled on a needed-basis') | |||
| def test_demo_compatibility(self): | |||
| self.compatibility_check() | |||
| if __name__ == '__main__': | |||
| unittest.main() | |||