diff --git a/deploy/TensorRT/README.md b/deploy/TensorRT/README.md index 4057acc4..cdd6bf44 100644 --- a/deploy/TensorRT/README.md +++ b/deploy/TensorRT/README.md @@ -79,3 +79,26 @@ python3 deploy/TensorRT/eval_yolo_trt.py -v -m model.trt \ --annotations /workdir/datasets/coco/annotations/instances_val2017.json \ --conf-thres 0.40 --iou-thres 0.45 ``` + + +# YOLOV6 Tensorrt Conversion & Infernce in Python + +for custom model change classe from utils.py + + +Download the onnx weight + +```!wget https://github.com/meituan/YOLOv6/releases/download/0.1.0/yolov6s.onnx``` +### include NMS Plugin +converting .onnx into .trt format ,it has support of --end2end /(using nms) +``` !python export.py -o yolov6s.onnx -e yolov6s.trt --end2end ``` +##### image inference +```!python trt.py -e yolov6s.trt -i src/1.jpg -o yolov6s-1.jpg --end2end``` +##### video inference +```!python trt.py -e yolov6s.trt -v yourvideopath/video.mp4 -o op_video.avi --end2end``` +### exclude NMS Plugin +```!python export.py -o yolov6s.onnx -e yolov6s.trt``` +##### image inference +```!python trt.py -e yolov6s.trt -i data/images/image1.jpg -o yolov6s-1.jpg``` +##### video inference +```!python trt.py -e yolov6s.trt -v yourvideopath/video.mp4 -o op_video.avi ``` diff --git a/deploy/TensorRT/export.py b/deploy/TensorRT/export.py new file mode 100644 index 00000000..6598e0ca --- /dev/null +++ b/deploy/TensorRT/export.py @@ -0,0 +1,290 @@ +import os +import sys +import logging +import argparse + +import numpy as np +import tensorrt as trt +import pycuda.driver as cuda +import pycuda.autoinit + +from image_batch import ImageBatcher + +logging.basicConfig(level=logging.INFO) +logging.getLogger("EngineBuilder").setLevel(logging.INFO) +log = logging.getLogger("EngineBuilder") + +class EngineCalibrator(trt.IInt8EntropyCalibrator2): + """ + Implements the INT8 Entropy Calibrator 2. + """ + + def __init__(self, cache_file): + """ + :param cache_file: The location of the cache file. + """ + super().__init__() + self.cache_file = cache_file + self.image_batcher = None + self.batch_allocation = None + self.batch_generator = None + + def set_image_batcher(self, image_batcher: ImageBatcher): + """ + Define the image batcher to use, if any. If using only the cache file, an image batcher doesn't need + to be defined. + :param image_batcher: The ImageBatcher object + """ + self.image_batcher = image_batcher + size = int(np.dtype(self.image_batcher.dtype).itemsize * np.prod(self.image_batcher.shape)) + self.batch_allocation = cuda.mem_alloc(size) + self.batch_generator = self.image_batcher.get_batch() + + def get_batch_size(self): + """ + Overrides from trt.IInt8EntropyCalibrator2. + Get the batch size to use for calibration. + :return: Batch size. + """ + if self.image_batcher: + return self.image_batcher.batch_size + return 1 + + def get_batch(self, names): + """ + Overrides from trt.IInt8EntropyCalibrator2. + Get the next batch to use for calibration, as a list of device memory pointers. + :param names: The names of the inputs, if useful to define the order of inputs. + :return: A list of int-casted memory pointers. + """ + if not self.image_batcher: + return None + try: + batch, _, _ = next(self.batch_generator) + log.info("Calibrating image {} / {}".format(self.image_batcher.image_index, self.image_batcher.num_images)) + cuda.memcpy_htod(self.batch_allocation, np.ascontiguousarray(batch)) + return [int(self.batch_allocation)] + except StopIteration: + log.info("Finished calibration batches") + return None + + def read_calibration_cache(self): + """ + Overrides from trt.IInt8EntropyCalibrator2. + Read the calibration cache file stored on disk, if it exists. + :return: The contents of the cache file, if any. + """ + if os.path.exists(self.cache_file): + with open(self.cache_file, "rb") as f: + log.info("Using calibration cache file: {}".format(self.cache_file)) + return f.read() + + def write_calibration_cache(self, cache): + """ + Overrides from trt.IInt8EntropyCalibrator2. + Store the calibration cache to a file on disk. + :param cache: The contents of the calibration cache to store. + """ + with open(self.cache_file, "wb") as f: + log.info("Writing calibration cache data to: {}".format(self.cache_file)) + f.write(cache) + +class EngineBuilder: + """ + Parses an ONNX graph and builds a TensorRT engine from it. + """ + def __init__(self, verbose=False, workspace=8): + """ + :param verbose: If enabled, a higher verbosity level will be set on the TensorRT logger. + :param workspace: Max memory workspace to allow, in Gb. + """ + self.trt_logger = trt.Logger(trt.Logger.INFO) + if verbose: + self.trt_logger.min_severity = trt.Logger.Severity.VERBOSE + + trt.init_libnvinfer_plugins(self.trt_logger, namespace="") + + self.builder = trt.Builder(self.trt_logger) + self.config = self.builder.create_builder_config() + self.config.max_workspace_size = workspace * (2 ** 30) + + self.batch_size = None + self.network = None + self.parser = None + + def create_network(self, onnx_path, end2end, conf_thres, iou_thres, max_det): + """ + Parse the ONNX graph and create the corresponding TensorRT network definition. + :param onnx_path: The path to the ONNX graph to load. + """ + network_flags = (1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)) + + self.network = self.builder.create_network(network_flags) + self.parser = trt.OnnxParser(self.network, self.trt_logger) + + onnx_path = os.path.realpath(onnx_path) + with open(onnx_path, "rb") as f: + if not self.parser.parse(f.read()): + print("Failed to load ONNX file: {}".format(onnx_path)) + for error in range(self.parser.num_errors): + print(self.parser.get_error(error)) + sys.exit(1) + + inputs = [self.network.get_input(i) for i in range(self.network.num_inputs)] + outputs = [self.network.get_output(i) for i in range(self.network.num_outputs)] + + print("Network Description") + for input in inputs: + self.batch_size = input.shape[0] + print("Input '{}' with shape {} and dtype {}".format(input.name, input.shape, input.dtype)) + for output in outputs: + print("Output '{}' with shape {} and dtype {}".format(output.name, output.shape, output.dtype)) + assert self.batch_size > 0 + self.builder.max_batch_size = self.batch_size + + if end2end: + previous_output = self.network.get_output(0) + self.network.unmark_output(previous_output) + # output [1, 8400, 85] + # slice boxes, obj_score, class_scores + strides = trt.Dims([1,1,1]) + starts = trt.Dims([0,0,0]) + bs, num_boxes, temp = previous_output.shape + shapes = trt.Dims([bs, num_boxes, 4]) + # [0, 0, 0] [1, 8400, 4] [1, 1, 1] + boxes = self.network.add_slice(previous_output, starts, shapes, strides) + num_classes = temp -5 + starts[2] = 4 + shapes[2] = 1 + # [0, 0, 4] [1, 8400, 1] [1, 1, 1] + obj_score = self.network.add_slice(previous_output, starts, shapes, strides) + starts[2] = 5 + shapes[2] = num_classes + # [0, 0, 5] [1, 8400, 80] [1, 1, 1] + scores = self.network.add_slice(previous_output, starts, shapes, strides) + # scores = obj_score * class_scores => [bs, num_boxes, nc] + updated_scores = self.network.add_elementwise(obj_score.get_output(0), scores.get_output(0), trt.ElementWiseOperation.PROD) + + ''' + "plugin_version": "1", + "background_class": -1, # no background class + "max_output_boxes": detections_per_img, + "score_threshold": score_thresh, + "iou_threshold": nms_thresh, + "score_activation": False, + "box_coding": 1, + ''' + registry = trt.get_plugin_registry() + assert(registry) + creator = registry.get_plugin_creator("EfficientNMS_TRT", "1") + assert(creator) + fc = [] + fc.append(trt.PluginField("background_class", np.array([-1], dtype=np.int32), trt.PluginFieldType.INT32)) + fc.append(trt.PluginField("max_output_boxes", np.array([max_det], dtype=np.int32), trt.PluginFieldType.INT32)) + fc.append(trt.PluginField("score_threshold", np.array([conf_thres], dtype=np.float32), trt.PluginFieldType.FLOAT32)) + fc.append(trt.PluginField("iou_threshold", np.array([iou_thres], dtype=np.float32), trt.PluginFieldType.FLOAT32)) + fc.append(trt.PluginField("box_coding", np.array([1], dtype=np.int32), trt.PluginFieldType.INT32)) + + fc = trt.PluginFieldCollection(fc) + nms_layer = creator.create_plugin("nms_layer", fc) + + layer = self.network.add_plugin_v2([boxes.get_output(0), updated_scores.get_output(0)], nms_layer) + layer.get_output(0).name = "num" + layer.get_output(1).name = "boxes" + layer.get_output(2).name = "scores" + layer.get_output(3).name = "classes" + for i in range(4): + self.network.mark_output(layer.get_output(i)) + + + def create_engine(self, engine_path, precision, calib_input=None, calib_cache=None, calib_num_images=5000, + calib_batch_size=8): + """ + Build the TensorRT engine and serialize it to disk. + :param engine_path: The path where to serialize the engine to. + :param precision: The datatype to use for the engine, either 'fp32', 'fp16' or 'int8'. + :param calib_input: The path to a directory holding the calibration images. + :param calib_cache: The path where to write the calibration cache to, or if it already exists, load it from. + :param calib_num_images: The maximum number of images to use for calibration. + :param calib_batch_size: The batch size to use for the calibration process. + """ + engine_path = os.path.realpath(engine_path) + engine_dir = os.path.dirname(engine_path) + os.makedirs(engine_dir, exist_ok=True) + print("Building {} Engine in {}".format(precision, engine_path)) + inputs = [self.network.get_input(i) for i in range(self.network.num_inputs)] + + # TODO: Strict type is only needed If the per-layer precision overrides are used + # If a better method is found to deal with that issue, this flag can be removed. + self.config.set_flag(trt.BuilderFlag.STRICT_TYPES) + + if precision == "fp16": + if not self.builder.platform_has_fast_fp16: + print("FP16 is not supported natively on this platform/device") + else: + self.config.set_flag(trt.BuilderFlag.FP16) + elif precision == "int8": + if not self.builder.platform_has_fast_int8: + print("INT8 is not supported natively on this platform/device") + else: + if self.builder.platform_has_fast_fp16: + # Also enable fp16, as some layers may be even more efficient in fp16 than int8 + self.config.set_flag(trt.BuilderFlag.FP16) + self.config.set_flag(trt.BuilderFlag.INT8) + self.config.int8_calibrator = EngineCalibrator(calib_cache) + if not os.path.exists(calib_cache): + calib_shape = [calib_batch_size] + list(inputs[0].shape[1:]) + calib_dtype = trt.nptype(inputs[0].dtype) + self.config.int8_calibrator.set_image_batcher( + ImageBatcher(calib_input, calib_shape, calib_dtype, max_num_images=calib_num_images, + exact_batches=True)) + + with self.builder.build_engine(self.network, self.config) as engine, open(engine_path, "wb") as f: + print("Serializing engine to file: {:}".format(engine_path)) + f.write(engine.serialize()) + +def main(args): + builder = EngineBuilder(args.verbose, args.workspace) + builder.create_network(args.onnx, args.end2end, args.conf_thres, args.iou_thres, args.max_det) + builder.create_engine(args.engine, args.precision, args.calib_input, args.calib_cache, args.calib_num_images, + args.calib_batch_size) + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-o", "--onnx", help="The input ONNX model file to load") + parser.add_argument("-e", "--engine", help="The output path for the TRT engine") + parser.add_argument("-p", "--precision", default="fp16", choices=["fp32", "fp16", "int8"], + help="The precision mode to build in, either 'fp32', 'fp16' or 'int8', default: 'fp16'") + parser.add_argument("-v", "--verbose", action="store_true", help="Enable more verbose log output") + parser.add_argument("-w", "--workspace", default=1, type=int, help="The max memory workspace size to allow in Gb, " + "default: 1") + parser.add_argument("--calib_input", help="The directory holding images to use for calibration") + parser.add_argument("--calib_cache", default="./calibration.cache", + help="The file path for INT8 calibration cache to use, default: ./calibration.cache") + parser.add_argument("--calib_num_images", default=5000, type=int, + help="The maximum number of images to use for calibration, default: 5000") + parser.add_argument("--calib_batch_size", default=8, type=int, + help="The batch size for the calibration process, default: 8") + parser.add_argument("--end2end", default=False, action="store_true", + help="export the engine include nms plugin, default: False") + parser.add_argument("--conf_thres", default=0.4, type=float, + help="The conf threshold for the nms, default: 0.4") + parser.add_argument("--iou_thres", default=0.5, type=float, + help="The iou threshold for the nms, default: 0.5") + parser.add_argument("--max_det", default=100, type=int, + help="The total num for results, default: 100") + + args = parser.parse_args() + print(args) + if not all([args.onnx, args.engine]): + parser.print_help() + log.error("These arguments are required: --onnx and --engine") + sys.exit(1) + if args.precision == "int8" and not (args.calib_input or os.path.exists(args.calib_cache)): + parser.print_help() + log.error("When building in int8 precision, --calib_input or an existing --calib_cache file is required") + sys.exit(1) + + main(args) + + diff --git a/deploy/TensorRT/image_batch.py b/deploy/TensorRT/image_batch.py new file mode 100644 index 00000000..2b1fe4aa --- /dev/null +++ b/deploy/TensorRT/image_batch.py @@ -0,0 +1,172 @@ +# +# SPDX-FileCopyrightText: Copyright (c) 1993-2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import os +import sys + +import numpy as np +from PIL import Image + + +class ImageBatcher: + """ + Creates batches of pre-processed images. + """ + + def __init__(self, input, shape, dtype, max_num_images=None, exact_batches=False, preprocessor="fixed_shape_resizer"): + """ + :param input: The input directory to read images from. + :param shape: The tensor shape of the batch to prepare, either in NCHW or NHWC format. + :param dtype: The (numpy) datatype to cast the batched data to. + :param max_num_images: The maximum number of images to read from the directory. + :param exact_batches: This defines how to handle a number of images that is not an exact multiple of the batch + size. If false, it will pad the final batch with zeros to reach the batch size. If true, it will *remove* the + last few images in excess of a batch size multiple, to guarantee batches are exact (useful for calibration). + :param preprocessor: Set the preprocessor to use, depending on which network is being used. + """ + # Find images in the given input path + input = os.path.realpath(input) + self.images = [] + + extensions = [".jpg", ".jpeg", ".png", ".bmp"] + + def is_image(path): + return os.path.isfile(path) and os.path.splitext(path)[1].lower() in extensions + + if os.path.isdir(input): + self.images = [os.path.join(input, f) for f in os.listdir(input) if is_image(os.path.join(input, f))] + self.images.sort() + elif os.path.isfile(input): + if is_image(input): + self.images.append(input) + self.num_images = len(self.images) + if self.num_images < 1: + print("No valid {} images found in {}".format("/".join(extensions), input)) + sys.exit(1) + + # Handle Tensor Shape + self.dtype = dtype + self.shape = shape + assert len(self.shape) == 4 + self.batch_size = shape[0] + assert self.batch_size > 0 + self.format = None + self.width = -1 + self.height = -1 + if self.shape[1] == 3: + self.format = "NCHW" + self.height = self.shape[2] + self.width = self.shape[3] + elif self.shape[3] == 3: + self.format = "NHWC" + self.height = self.shape[1] + self.width = self.shape[2] + assert all([self.format, self.width > 0, self.height > 0]) + + # Adapt the number of images as needed + if max_num_images and 0 < max_num_images < len(self.images): + self.num_images = max_num_images + if exact_batches: + self.num_images = self.batch_size * (self.num_images // self.batch_size) + if self.num_images < 1: + print("Not enough images to create batches") + sys.exit(1) + self.images = self.images[0:self.num_images] + + # Subdivide the list of images into batches + self.num_batches = 1 + int((self.num_images - 1) / self.batch_size) + self.batches = [] + for i in range(self.num_batches): + start = i * self.batch_size + end = min(start + self.batch_size, self.num_images) + self.batches.append(self.images[start:end]) + + # Indices + self.image_index = 0 + self.batch_index = 0 + + self.preprocessor = preprocessor + + def preprocess_image(self, image_path): + """ + The image preprocessor loads an image from disk and prepares it as needed for batching. This includes padding, + resizing, normalization, data type casting, and transposing. + This Image Batcher implements one algorithm for now: + * Resizes and pads the image to fit the input size. + :param image_path: The path to the image on disk to load. + :return: Two values: A numpy array holding the image sample, ready to be contacatenated into the rest of the + batch, and the resize scale used, if any. + """ + + def resize_pad(image, pad_color=(0, 0, 0)): + """ + A subroutine to implement padding and resizing. This will resize the image to fit fully within the input + size, and pads the remaining bottom-right portions with the value provided. + :param image: The PIL image object + :pad_color: The RGB values to use for the padded area. Default: Black/Zeros. + :return: Two values: The PIL image object already padded and cropped, and the resize scale used. + """ + + # Get characteristics. + width, height = image.size + width_scale = width / self.width + height_scale = height / self.height + + # Depending on preprocessor, box scaling will be slightly different. + if self.preprocessor == "fixed_shape_resizer": + scale = [self.width / width, self.height / height] + image = image.resize((self.width, self.height), resample=Image.BILINEAR) + return image, scale + elif self.preprocessor == "keep_aspect_ratio_resizer": + scale = 1.0 / max(width_scale, height_scale) + image = image.resize((round(width * scale), round(height * scale)), resample=Image.BILINEAR) + pad = Image.new("RGB", (self.width, self.height)) + pad.paste(pad_color, [0, 0, self.width, self.height]) + pad.paste(image) + return pad, scale + + scale = None + image = Image.open(image_path) + image = image.convert(mode='RGB') + if self.preprocessor == "fixed_shape_resizer" or self.preprocessor == "keep_aspect_ratio_resizer": + #Resize & Pad with ImageNet mean values and keep as [0,255] Normalization + image, scale = resize_pad(image, (124, 116, 104)) + image = np.asarray(image, dtype=self.dtype) + else: + print("Preprocessing method {} not supported".format(self.preprocessor)) + sys.exit(1) + if self.format == "NCHW": + image = np.transpose(image, (2, 0, 1)) + return image/255., scale + + def get_batch(self): + """ + Retrieve the batches. This is a generator object, so you can use it within a loop as: + for batch, images in batcher.get_batch(): + ... + Or outside of a batch with the next() function. + :return: A generator yielding three items per iteration: a numpy array holding a batch of images, the list of + paths to the images loaded within this batch, and the list of resize scales for each image in the batch. + """ + for i, batch_images in enumerate(self.batches): + batch_data = np.zeros(self.shape, dtype=self.dtype) + batch_scales = [None] * len(batch_images) + for i, image in enumerate(batch_images): + self.image_index += 1 + batch_data[i], batch_scales[i] = self.preprocess_image(image) + self.batch_index += 1 + yield batch_data, batch_images, batch_scales diff --git a/deploy/TensorRT/trt.py b/deploy/TensorRT/trt.py new file mode 100644 index 00000000..007c26e6 --- /dev/null +++ b/deploy/TensorRT/trt.py @@ -0,0 +1,36 @@ +from utils.utils import preproc, vis +from utils.utils import BaseEngine +import numpy as np +import cv2 +import time +import os +import argparse + +class Predictor(BaseEngine): + def __init__(self, engine_path , imgsz=(640,640)): + super(Predictor, self).__init__(engine_path) + self.imgsz = imgsz # your model infer image size + self.n_classes = 80 # your model classes + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("-e", "--engine", help="TRT engine Path") + parser.add_argument("-i", "--image", help="image path") + parser.add_argument("-o", "--output", help="image output path") + parser.add_argument("-v", "--video", help="video path or camera index ") + parser.add_argument("--end2end", default=False, action="store_true", + help="use end2end engine") + + args = parser.parse_args() + print(args) + + pred = Predictor(engine_path=args.engine) + pred.get_fps() + img_path = args.image + video = args.video + if img_path: + origin_img = pred.inference(img_path, conf=0.1, end2end=args.end2end) + + cv2.imwrite("%s" %args.output , origin_img) + if video: + pred.detect_video(video, conf=0.1, end2end=args.end2end) # set 0 use a webcam diff --git a/deploy/TensorRT/utils/__init__.py b/deploy/TensorRT/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/deploy/TensorRT/utils/utils.py b/deploy/TensorRT/utils/utils.py new file mode 100644 index 00000000..4407dec9 --- /dev/null +++ b/deploy/TensorRT/utils/utils.py @@ -0,0 +1,344 @@ +import tensorrt as trt +import pycuda.autoinit +import pycuda.driver as cuda +import numpy as np +import cv2 + + +class BaseEngine(object): + def __init__(self, engine_path, imgsz=(640,640)): + self.imgsz = imgsz + self.mean = None + self.std = None + self.n_classes = 80 + self.class_names = [ 'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light', + 'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow', + 'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee', + 'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard', + 'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple', + 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', + 'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', + 'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear', + 'hair drier', 'toothbrush' ] + + logger = trt.Logger(trt.Logger.WARNING) + runtime = trt.Runtime(logger) + trt.init_libnvinfer_plugins(logger,'') # initialize TensorRT plugins + with open(engine_path, "rb") as f: + serialized_engine = f.read() + engine = runtime.deserialize_cuda_engine(serialized_engine) + self.context = engine.create_execution_context() + self.inputs, self.outputs, self.bindings = [], [], [] + self.stream = cuda.Stream() + for binding in engine: + size = trt.volume(engine.get_binding_shape(binding)) + dtype = trt.nptype(engine.get_binding_dtype(binding)) + host_mem = cuda.pagelocked_empty(size, dtype) + device_mem = cuda.mem_alloc(host_mem.nbytes) + self.bindings.append(int(device_mem)) + if engine.binding_is_input(binding): + self.inputs.append({'host': host_mem, 'device': device_mem}) + else: + self.outputs.append({'host': host_mem, 'device': device_mem}) + + + def infer(self, img): + self.inputs[0]['host'] = np.ravel(img) + # transfer data to the gpu + for inp in self.inputs: + cuda.memcpy_htod_async(inp['device'], inp['host'], self.stream) + # run inference + self.context.execute_async_v2( + bindings=self.bindings, + stream_handle=self.stream.handle) + # fetch outputs from gpu + for out in self.outputs: + cuda.memcpy_dtoh_async(out['host'], out['device'], self.stream) + # synchronize stream + self.stream.synchronize() + + data = [out['host'] for out in self.outputs] + return data + + def detect_video(self, video_path, conf=0.5, end2end=False): + cap = cv2.VideoCapture(video_path) + fourcc = cv2.VideoWriter_fourcc(*'XVID') + fps = int(round(cap.get(cv2.CAP_PROP_FPS))) + width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + out = cv2.VideoWriter('results.avi',fourcc,fps,(width,height)) + fps = 0 + import time + while True: + ret, frame = cap.read() + if not ret: + break + blob, ratio = preproc(frame, self.imgsz, self.mean, self.std) + t1 = time.time() + data = self.infer(blob) + fps = (fps + (1. / (time.time() - t1))) / 2 + frame = cv2.putText(frame, "FPS:%d " %fps, (0, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, + (0, 0, 255), 2) + if end2end: + num, final_boxes, final_scores, final_cls_inds = data + final_boxes = np.reshape(final_boxes/ratio, (-1, 4)) + dets = np.concatenate([final_boxes[:num[0]], np.array(final_scores)[:num[0]].reshape(-1, 1), np.array(final_cls_inds)[:num[0]].reshape(-1, 1)], axis=-1) + else: + predictions = np.reshape(data, (1, -1, int(5+self.n_classes)))[0] + dets = self.postprocess(predictions,ratio) + + if dets is not None: + final_boxes, final_scores, final_cls_inds = dets[:, + :4], dets[:, 4], dets[:, 5] + frame = vis(frame, final_boxes, final_scores, final_cls_inds, + conf=conf, class_names=self.class_names) + cv2.imshow('frame', frame) + out.write(frame) + if cv2.waitKey(25) & 0xFF == ord('q'): + break + out.release() + cap.release() + cv2.destroyAllWindows() + + def inference(self, img_path, conf=0.5, end2end=False): + origin_img = cv2.imread(img_path) + img, ratio = preproc(origin_img, self.imgsz, self.mean, self.std) + data = self.infer(img) + if end2end: + num, final_boxes, final_scores, final_cls_inds = data + final_boxes = np.reshape(final_boxes/ratio, (-1, 4)) + dets = np.concatenate([final_boxes[:num[0]], np.array(final_scores)[:num[0]].reshape(-1, 1), np.array(final_cls_inds)[:num[0]].reshape(-1, 1)], axis=-1) + else: + predictions = np.reshape(data, (1, -1, int(5+self.n_classes)))[0] + dets = self.postprocess(predictions,ratio) + + if dets is not None: + final_boxes, final_scores, final_cls_inds = dets[:, + :4], dets[:, 4], dets[:, 5] + origin_img = vis(origin_img, final_boxes, final_scores, final_cls_inds, + conf=conf, class_names=self.class_names) + return origin_img + + @staticmethod + def postprocess(predictions, ratio): + boxes = predictions[:, :4] + scores = predictions[:, 4:5] * predictions[:, 5:] + boxes_xyxy = np.ones_like(boxes) + boxes_xyxy[:, 0] = boxes[:, 0] - boxes[:, 2] / 2. + boxes_xyxy[:, 1] = boxes[:, 1] - boxes[:, 3] / 2. + boxes_xyxy[:, 2] = boxes[:, 0] + boxes[:, 2] / 2. + boxes_xyxy[:, 3] = boxes[:, 1] + boxes[:, 3] / 2. + boxes_xyxy /= ratio + dets = multiclass_nms(boxes_xyxy, scores, nms_thr=0.45, score_thr=0.1) + return dets + + def get_fps(self): + # warmup + import time + img = np.ones((1,3,self.imgsz[0], self.imgsz[1])) + img = np.ascontiguousarray(img, dtype=np.float32) + for _ in range(20): + _ = self.infer(img) + t1 = time.perf_counter() + _ = self.infer(img) + print(1/(time.perf_counter() - t1), 'FPS') + + +def nms(boxes, scores, nms_thr): + """Single class NMS implemented in Numpy.""" + x1 = boxes[:, 0] + y1 = boxes[:, 1] + x2 = boxes[:, 2] + y2 = boxes[:, 3] + + areas = (x2 - x1 + 1) * (y2 - y1 + 1) + order = scores.argsort()[::-1] + + keep = [] + while order.size > 0: + i = order[0] + keep.append(i) + xx1 = np.maximum(x1[i], x1[order[1:]]) + yy1 = np.maximum(y1[i], y1[order[1:]]) + xx2 = np.minimum(x2[i], x2[order[1:]]) + yy2 = np.minimum(y2[i], y2[order[1:]]) + + w = np.maximum(0.0, xx2 - xx1 + 1) + h = np.maximum(0.0, yy2 - yy1 + 1) + inter = w * h + ovr = inter / (areas[i] + areas[order[1:]] - inter) + + inds = np.where(ovr <= nms_thr)[0] + order = order[inds + 1] + + return keep + + +def multiclass_nms(boxes, scores, nms_thr, score_thr): + """Multiclass NMS implemented in Numpy""" + final_dets = [] + num_classes = scores.shape[1] + for cls_ind in range(num_classes): + cls_scores = scores[:, cls_ind] + valid_score_mask = cls_scores > score_thr + if valid_score_mask.sum() == 0: + continue + else: + valid_scores = cls_scores[valid_score_mask] + valid_boxes = boxes[valid_score_mask] + keep = nms(valid_boxes, valid_scores, nms_thr) + if len(keep) > 0: + cls_inds = np.ones((len(keep), 1)) * cls_ind + dets = np.concatenate( + [valid_boxes[keep], valid_scores[keep, None], cls_inds], 1 + ) + final_dets.append(dets) + if len(final_dets) == 0: + return None + return np.concatenate(final_dets, 0) + + +def preproc(image, input_size, mean, std, swap=(2, 0, 1)): + if len(image.shape) == 3: + padded_img = np.ones((input_size[0], input_size[1], 3)) * 114.0 + else: + padded_img = np.ones(input_size) * 114.0 + img = np.array(image) + r = min(input_size[0] / img.shape[0], input_size[1] / img.shape[1]) + resized_img = cv2.resize( + img, + (int(img.shape[1] * r), int(img.shape[0] * r)), + interpolation=cv2.INTER_LINEAR, + ).astype(np.float32) + padded_img[: int(img.shape[0] * r), : int(img.shape[1] * r)] = resized_img + # if use yolox set + # padded_img = padded_img[:, :, ::-1] + # padded_img /= 255.0 + padded_img = padded_img[:, :, ::-1] + padded_img /= 255.0 + if mean is not None: + padded_img -= mean + if std is not None: + padded_img /= std + padded_img = padded_img.transpose(swap) + padded_img = np.ascontiguousarray(padded_img, dtype=np.float32) + return padded_img, r + + +_COLORS = np.array( + [ + 0.000, 0.447, 0.741, + 0.850, 0.325, 0.098, + 0.929, 0.694, 0.125, + 0.494, 0.184, 0.556, + 0.466, 0.674, 0.188, + 0.301, 0.745, 0.933, + 0.635, 0.078, 0.184, + 0.300, 0.300, 0.300, + 0.600, 0.600, 0.600, + 1.000, 0.000, 0.000, + 1.000, 0.500, 0.000, + 0.749, 0.749, 0.000, + 0.000, 1.000, 0.000, + 0.000, 0.000, 1.000, + 0.667, 0.000, 1.000, + 0.333, 0.333, 0.000, + 0.333, 0.667, 0.000, + 0.333, 1.000, 0.000, + 0.667, 0.333, 0.000, + 0.667, 0.667, 0.000, + 0.667, 1.000, 0.000, + 1.000, 0.333, 0.000, + 1.000, 0.667, 0.000, + 1.000, 1.000, 0.000, + 0.000, 0.333, 0.500, + 0.000, 0.667, 0.500, + 0.000, 1.000, 0.500, + 0.333, 0.000, 0.500, + 0.333, 0.333, 0.500, + 0.333, 0.667, 0.500, + 0.333, 1.000, 0.500, + 0.667, 0.000, 0.500, + 0.667, 0.333, 0.500, + 0.667, 0.667, 0.500, + 0.667, 1.000, 0.500, + 1.000, 0.000, 0.500, + 1.000, 0.333, 0.500, + 1.000, 0.667, 0.500, + 1.000, 1.000, 0.500, + 0.000, 0.333, 1.000, + 0.000, 0.667, 1.000, + 0.000, 1.000, 1.000, + 0.333, 0.000, 1.000, + 0.333, 0.333, 1.000, + 0.333, 0.667, 1.000, + 0.333, 1.000, 1.000, + 0.667, 0.000, 1.000, + 0.667, 0.333, 1.000, + 0.667, 0.667, 1.000, + 0.667, 1.000, 1.000, + 1.000, 0.000, 1.000, + 1.000, 0.333, 1.000, + 1.000, 0.667, 1.000, + 0.333, 0.000, 0.000, + 0.500, 0.000, 0.000, + 0.667, 0.000, 0.000, + 0.833, 0.000, 0.000, + 1.000, 0.000, 0.000, + 0.000, 0.167, 0.000, + 0.000, 0.333, 0.000, + 0.000, 0.500, 0.000, + 0.000, 0.667, 0.000, + 0.000, 0.833, 0.000, + 0.000, 1.000, 0.000, + 0.000, 0.000, 0.167, + 0.000, 0.000, 0.333, + 0.000, 0.000, 0.500, + 0.000, 0.000, 0.667, + 0.000, 0.000, 0.833, + 0.000, 0.000, 1.000, + 0.000, 0.000, 0.000, + 0.143, 0.143, 0.143, + 0.286, 0.286, 0.286, + 0.429, 0.429, 0.429, + 0.571, 0.571, 0.571, + 0.714, 0.714, 0.714, + 0.857, 0.857, 0.857, + 0.000, 0.447, 0.741, + 0.314, 0.717, 0.741, + 0.50, 0.5, 0 + ] +).astype(np.float32).reshape(-1, 3) + + +def vis(img, boxes, scores, cls_ids, conf=0.5, class_names=None): + for i in range(len(boxes)): + box = boxes[i] + cls_id = int(cls_ids[i]) + score = scores[i] + if score < conf: + continue + x0 = int(box[0]) + y0 = int(box[1]) + x1 = int(box[2]) + y1 = int(box[3]) + + color = (_COLORS[cls_id] * 255).astype(np.uint8).tolist() + text = '{}:{:.1f}%'.format(class_names[cls_id], score * 100) + txt_color = (0, 0, 0) if np.mean(_COLORS[cls_id]) > 0.5 else (255, 255, 255) + font = cv2.FONT_HERSHEY_SIMPLEX + + txt_size = cv2.getTextSize(text, font, 0.4, 1)[0] + cv2.rectangle(img, (x0, y0), (x1, y1), color, 2) + + txt_bk_color = (_COLORS[cls_id] * 255 * 0.7).astype(np.uint8).tolist() + cv2.rectangle( + img, + (x0, y0 + 1), + (x0 + txt_size[0] + 1, y0 + int(1.5 * txt_size[1])), + txt_bk_color, + -1 + ) + cv2.putText(img, text, (x0, y0 + txt_size[1]), font, 0.4, txt_color, thickness=1) + + return img \ No newline at end of file