mblt-vision-python 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mblt_vision/__init__.py +86 -0
- mblt_vision/_api.py +53 -0
- mblt_vision/_compat.py +227 -0
- mblt_vision/_model_paths.py +137 -0
- mblt_vision/_tasks.py +30 -0
- mblt_vision/benchmark/__init__.py +1 -0
- mblt_vision/benchmark/argparse_utils.py +106 -0
- mblt_vision/benchmark/chart_utils.py +146 -0
- mblt_vision/benchmark/io_utils.py +64 -0
- mblt_vision/benchmark/summary_utils.py +770 -0
- mblt_vision/cli/__init__.py +5 -0
- mblt_vision/cli/__main__.py +5 -0
- mblt_vision/cli/_vision.py +337 -0
- mblt_vision/cli/compile.py +117 -0
- mblt_vision/cli/main.py +35 -0
- mblt_vision/cli/predict.py +64 -0
- mblt_vision/cli/val.py +562 -0
- mblt_vision/compile/__init__.py +21 -0
- mblt_vision/compile/vision.py +1120 -0
- mblt_vision/datasets/__init__.py +15 -0
- mblt_vision/datasets/ade20k.yaml +164 -0
- mblt_vision/datasets/cityscapes.yaml +54 -0
- mblt_vision/datasets/coco.yaml +178 -0
- mblt_vision/datasets/dotav1.yaml +28 -0
- mblt_vision/datasets/imagenet.yaml +1019 -0
- mblt_vision/datasets/imagenet_synsets.txt +1000 -0
- mblt_vision/datasets/nyu-depth.yaml +7 -0
- mblt_vision/datasets/registry.py +158 -0
- mblt_vision/datasets/widerface.yaml +9 -0
- mblt_vision/depth_estimation/__init__.py +19 -0
- mblt_vision/face_detection/__init__.py +43 -0
- mblt_vision/image_classification/__init__.py +311 -0
- mblt_vision/instance_segmentation/__init__.py +65 -0
- mblt_vision/models/AlexNet.yaml +23 -0
- mblt_vision/models/CAFormer_B36.yaml +22 -0
- mblt_vision/models/CAFormer_M36.yaml +22 -0
- mblt_vision/models/CAFormer_S18.yaml +22 -0
- mblt_vision/models/CAFormer_S36.yaml +22 -0
- mblt_vision/models/CoAtNet_0_RW_224.yaml +22 -0
- mblt_vision/models/CoAtNet_1_RW_224.yaml +22 -0
- mblt_vision/models/CoAtNet_2_RW_224.yaml +22 -0
- mblt_vision/models/ConvFormer_B36.yaml +22 -0
- mblt_vision/models/ConvFormer_M36.yaml +22 -0
- mblt_vision/models/ConvFormer_S18.yaml +22 -0
- mblt_vision/models/ConvFormer_S36.yaml +22 -0
- mblt_vision/models/ConvNext_Base.yaml +23 -0
- mblt_vision/models/ConvNext_Large.yaml +23 -0
- mblt_vision/models/ConvNext_Small.yaml +23 -0
- mblt_vision/models/ConvNext_Tiny.yaml +23 -0
- mblt_vision/models/DeiT3_Base_Patch16_224.yaml +22 -0
- mblt_vision/models/DeiT3_Base_Patch16_384.yaml +22 -0
- mblt_vision/models/DeiT3_Large_Patch16_224.yaml +22 -0
- mblt_vision/models/DeiT3_Large_Patch16_384.yaml +22 -0
- mblt_vision/models/DeiT3_Medium_Patch16_224.yaml +22 -0
- mblt_vision/models/DeiT3_Small_Patch16_224.yaml +22 -0
- mblt_vision/models/DeiT3_Small_Patch16_384.yaml +22 -0
- mblt_vision/models/DeiT_Base_Patch16_224.yaml +22 -0
- mblt_vision/models/DeiT_Base_Patch16_384.yaml +22 -0
- mblt_vision/models/DeiT_Small_Patch16_224.yaml +22 -0
- mblt_vision/models/DeiT_Tiny_Patch16_224.yaml +22 -0
- mblt_vision/models/DenseNet121.yaml +23 -0
- mblt_vision/models/DenseNet161.yaml +23 -0
- mblt_vision/models/DenseNet169.yaml +23 -0
- mblt_vision/models/DenseNet201.yaml +23 -0
- mblt_vision/models/EfficientFormer_L1.yaml +22 -0
- mblt_vision/models/EfficientFormer_L3.yaml +22 -0
- mblt_vision/models/EfficientFormer_L7.yaml +22 -0
- mblt_vision/models/EfficientNet_B0.yaml +23 -0
- mblt_vision/models/EfficientNet_B1.yaml +32 -0
- mblt_vision/models/EfficientNet_B2.yaml +23 -0
- mblt_vision/models/EfficientNet_B3.yaml +23 -0
- mblt_vision/models/EfficientNet_B4.yaml +23 -0
- mblt_vision/models/EfficientNet_B5.yaml +23 -0
- mblt_vision/models/EfficientNet_B6.yaml +23 -0
- mblt_vision/models/EfficientNet_B7.yaml +23 -0
- mblt_vision/models/EfficientNet_V2_L.yaml +23 -0
- mblt_vision/models/EfficientNet_V2_M.yaml +23 -0
- mblt_vision/models/EfficientNet_V2_S.yaml +23 -0
- mblt_vision/models/FlexiVit_Base.yaml +22 -0
- mblt_vision/models/FlexiVit_Large.yaml +22 -0
- mblt_vision/models/FlexiVit_Small.yaml +22 -0
- mblt_vision/models/GELANc-seg.yaml +29 -0
- mblt_vision/models/GELANc.yaml +28 -0
- mblt_vision/models/GELANe.yaml +28 -0
- mblt_vision/models/GELANm.yaml +28 -0
- mblt_vision/models/GELANs.yaml +28 -0
- mblt_vision/models/GoogLeNet.yaml +23 -0
- mblt_vision/models/Inception_V3.yaml +23 -0
- mblt_vision/models/LeViT_Conv_128.yaml +22 -0
- mblt_vision/models/LeViT_Conv_128S.yaml +22 -0
- mblt_vision/models/LeViT_Conv_192.yaml +22 -0
- mblt_vision/models/LeViT_Conv_256.yaml +22 -0
- mblt_vision/models/LeViT_Conv_384.yaml +22 -0
- mblt_vision/models/MNASNet0_5.yaml +23 -0
- mblt_vision/models/MNASNet0_75.yaml +23 -0
- mblt_vision/models/MNASNet1_0.yaml +23 -0
- mblt_vision/models/MNASNet1_3.yaml +23 -0
- mblt_vision/models/MobileNet_V2.yaml +32 -0
- mblt_vision/models/MobileNet_V3_Large.yaml +32 -0
- mblt_vision/models/MobileNet_V3_Small.yaml +23 -0
- mblt_vision/models/RegNet_X_16GF.yaml +32 -0
- mblt_vision/models/RegNet_X_1_6GF.yaml +32 -0
- mblt_vision/models/RegNet_X_32GF.yaml +32 -0
- mblt_vision/models/RegNet_X_3_2GF.yaml +32 -0
- mblt_vision/models/RegNet_X_400MF.yaml +32 -0
- mblt_vision/models/RegNet_X_800MF.yaml +32 -0
- mblt_vision/models/RegNet_X_8GF.yaml +32 -0
- mblt_vision/models/RegNet_Y_16GF.yaml +76 -0
- mblt_vision/models/RegNet_Y_1_6GF.yaml +32 -0
- mblt_vision/models/RegNet_Y_32GF.yaml +76 -0
- mblt_vision/models/RegNet_Y_3_2GF.yaml +32 -0
- mblt_vision/models/RegNet_Y_400MF.yaml +32 -0
- mblt_vision/models/RegNet_Y_800MF.yaml +32 -0
- mblt_vision/models/RegNet_Y_8GF.yaml +32 -0
- mblt_vision/models/RepViT_M0_9.yaml +22 -0
- mblt_vision/models/RepViT_M1.yaml +22 -0
- mblt_vision/models/RepViT_M1_0.yaml +22 -0
- mblt_vision/models/RepViT_M1_1.yaml +22 -0
- mblt_vision/models/RepViT_M1_5.yaml +22 -0
- mblt_vision/models/RepViT_M2.yaml +22 -0
- mblt_vision/models/RepViT_M2_3.yaml +22 -0
- mblt_vision/models/RepViT_M3.yaml +22 -0
- mblt_vision/models/ResNeXt101_32X8D.yaml +32 -0
- mblt_vision/models/ResNeXt101_64X4D.yaml +23 -0
- mblt_vision/models/ResNeXt50_32X4D.yaml +32 -0
- mblt_vision/models/ResNet101.yaml +32 -0
- mblt_vision/models/ResNet152.yaml +32 -0
- mblt_vision/models/ResNet18.yaml +23 -0
- mblt_vision/models/ResNet34.yaml +23 -0
- mblt_vision/models/ResNet50.yaml +32 -0
- mblt_vision/models/ShuffleNet_V2_X0_5.yaml +23 -0
- mblt_vision/models/ShuffleNet_V2_X1_0.yaml +23 -0
- mblt_vision/models/ShuffleNet_V2_X1_5.yaml +23 -0
- mblt_vision/models/ShuffleNet_V2_X2_0.yaml +23 -0
- mblt_vision/models/SqueezeNet1_0.yaml +23 -0
- mblt_vision/models/SqueezeNet1_1.yaml +23 -0
- mblt_vision/models/Swin_B.yaml +23 -0
- mblt_vision/models/Swin_S.yaml +23 -0
- mblt_vision/models/Swin_T.yaml +23 -0
- mblt_vision/models/VGG11.yaml +23 -0
- mblt_vision/models/VGG11_BN.yaml +23 -0
- mblt_vision/models/VGG13.yaml +23 -0
- mblt_vision/models/VGG13_BN.yaml +23 -0
- mblt_vision/models/VGG16.yaml +23 -0
- mblt_vision/models/VGG16_BN.yaml +23 -0
- mblt_vision/models/VGG19.yaml +23 -0
- mblt_vision/models/VGG19_BN.yaml +23 -0
- mblt_vision/models/ViT_B_16.yaml +67 -0
- mblt_vision/models/ViT_B_32.yaml +23 -0
- mblt_vision/models/ViT_Base_Patch16_224.yaml +22 -0
- mblt_vision/models/ViT_Base_Patch16_384.yaml +22 -0
- mblt_vision/models/ViT_Base_Patch32_224.yaml +22 -0
- mblt_vision/models/ViT_Base_Patch32_384.yaml +22 -0
- mblt_vision/models/ViT_Base_Patch8_224.yaml +22 -0
- mblt_vision/models/ViT_L_16.yaml +67 -0
- mblt_vision/models/ViT_L_32.yaml +23 -0
- mblt_vision/models/ViT_Large_Patch16_224.yaml +22 -0
- mblt_vision/models/ViT_Large_Patch16_384.yaml +22 -0
- mblt_vision/models/ViT_Large_Patch32_384.yaml +22 -0
- mblt_vision/models/ViT_Small_Patch16_224.yaml +22 -0
- mblt_vision/models/ViT_Small_Patch16_384.yaml +22 -0
- mblt_vision/models/ViT_Small_Patch32_224.yaml +22 -0
- mblt_vision/models/ViT_Small_Patch32_384.yaml +22 -0
- mblt_vision/models/ViT_Tiny_Patch16_224.yaml +22 -0
- mblt_vision/models/ViT_Tiny_Patch16_384.yaml +22 -0
- mblt_vision/models/VisFormer_Small.yaml +22 -0
- mblt_vision/models/VisFormer_Tiny.yaml +22 -0
- mblt_vision/models/Wide_ResNet101_2.yaml +32 -0
- mblt_vision/models/Wide_ResNet50_2.yaml +32 -0
- mblt_vision/models/YOLO11l-cls.yaml +23 -0
- mblt_vision/models/YOLO11l-face.yaml +27 -0
- mblt_vision/models/YOLO11l-obb.yaml +28 -0
- mblt_vision/models/YOLO11l-pose.yaml +28 -0
- mblt_vision/models/YOLO11l-seg.yaml +28 -0
- mblt_vision/models/YOLO11l.yaml +27 -0
- mblt_vision/models/YOLO11m-cls.yaml +23 -0
- mblt_vision/models/YOLO11m-face.yaml +27 -0
- mblt_vision/models/YOLO11m-obb.yaml +28 -0
- mblt_vision/models/YOLO11m-pose.yaml +28 -0
- mblt_vision/models/YOLO11m-seg.yaml +28 -0
- mblt_vision/models/YOLO11m.yaml +27 -0
- mblt_vision/models/YOLO11n-cls.yaml +23 -0
- mblt_vision/models/YOLO11n-face.yaml +27 -0
- mblt_vision/models/YOLO11n-obb.yaml +28 -0
- mblt_vision/models/YOLO11n-pose.yaml +28 -0
- mblt_vision/models/YOLO11n-seg.yaml +28 -0
- mblt_vision/models/YOLO11n.yaml +27 -0
- mblt_vision/models/YOLO11s-cls.yaml +23 -0
- mblt_vision/models/YOLO11s-face.yaml +27 -0
- mblt_vision/models/YOLO11s-obb.yaml +28 -0
- mblt_vision/models/YOLO11s-pose.yaml +28 -0
- mblt_vision/models/YOLO11s-seg.yaml +28 -0
- mblt_vision/models/YOLO11s.yaml +27 -0
- mblt_vision/models/YOLO11x-cls.yaml +23 -0
- mblt_vision/models/YOLO11x-obb.yaml +28 -0
- mblt_vision/models/YOLO11x-pose.yaml +28 -0
- mblt_vision/models/YOLO11x-seg.yaml +28 -0
- mblt_vision/models/YOLO11x.yaml +27 -0
- mblt_vision/models/YOLO12l-face.yaml +27 -0
- mblt_vision/models/YOLO12l-seg.yaml +28 -0
- mblt_vision/models/YOLO12l.yaml +27 -0
- mblt_vision/models/YOLO12m-face.yaml +27 -0
- mblt_vision/models/YOLO12m-seg.yaml +28 -0
- mblt_vision/models/YOLO12m.yaml +27 -0
- mblt_vision/models/YOLO12n-face.yaml +27 -0
- mblt_vision/models/YOLO12n-seg.yaml +28 -0
- mblt_vision/models/YOLO12n.yaml +27 -0
- mblt_vision/models/YOLO12s-face.yaml +27 -0
- mblt_vision/models/YOLO12s-seg.yaml +28 -0
- mblt_vision/models/YOLO12s.yaml +27 -0
- mblt_vision/models/YOLO12x-seg.yaml +28 -0
- mblt_vision/models/YOLO12x.yaml +27 -0
- mblt_vision/models/YOLO26l-cls.yaml +23 -0
- mblt_vision/models/YOLO26l-depth.yaml +17 -0
- mblt_vision/models/YOLO26l-distill.yaml +27 -0
- mblt_vision/models/YOLO26l-obb.yaml +28 -0
- mblt_vision/models/YOLO26l-pose.yaml +28 -0
- mblt_vision/models/YOLO26l-seg.yaml +28 -0
- mblt_vision/models/YOLO26l-sem-ade20k.yaml +17 -0
- mblt_vision/models/YOLO26l-sem.yaml +17 -0
- mblt_vision/models/YOLO26l.yaml +27 -0
- mblt_vision/models/YOLO26m-cls.yaml +23 -0
- mblt_vision/models/YOLO26m-depth.yaml +17 -0
- mblt_vision/models/YOLO26m-distill.yaml +27 -0
- mblt_vision/models/YOLO26m-obb.yaml +28 -0
- mblt_vision/models/YOLO26m-pose.yaml +28 -0
- mblt_vision/models/YOLO26m-seg.yaml +28 -0
- mblt_vision/models/YOLO26m-sem-ade20k.yaml +17 -0
- mblt_vision/models/YOLO26m-sem.yaml +17 -0
- mblt_vision/models/YOLO26m.yaml +27 -0
- mblt_vision/models/YOLO26n-cls.yaml +23 -0
- mblt_vision/models/YOLO26n-depth.yaml +17 -0
- mblt_vision/models/YOLO26n-distill.yaml +27 -0
- mblt_vision/models/YOLO26n-obb.yaml +28 -0
- mblt_vision/models/YOLO26n-pose.yaml +28 -0
- mblt_vision/models/YOLO26n-seg.yaml +28 -0
- mblt_vision/models/YOLO26n-sem-ade20k.yaml +17 -0
- mblt_vision/models/YOLO26n-sem.yaml +17 -0
- mblt_vision/models/YOLO26n.yaml +27 -0
- mblt_vision/models/YOLO26s-cls.yaml +23 -0
- mblt_vision/models/YOLO26s-depth.yaml +17 -0
- mblt_vision/models/YOLO26s-distill.yaml +27 -0
- mblt_vision/models/YOLO26s-obb.yaml +28 -0
- mblt_vision/models/YOLO26s-pose.yaml +28 -0
- mblt_vision/models/YOLO26s-seg.yaml +28 -0
- mblt_vision/models/YOLO26s-sem-ade20k.yaml +17 -0
- mblt_vision/models/YOLO26s-sem.yaml +17 -0
- mblt_vision/models/YOLO26s.yaml +27 -0
- mblt_vision/models/YOLO26x-cls.yaml +23 -0
- mblt_vision/models/YOLO26x-depth.yaml +17 -0
- mblt_vision/models/YOLO26x-distill.yaml +27 -0
- mblt_vision/models/YOLO26x-obb.yaml +28 -0
- mblt_vision/models/YOLO26x-pose.yaml +28 -0
- mblt_vision/models/YOLO26x-seg.yaml +28 -0
- mblt_vision/models/YOLO26x-sem-ade20k.yaml +17 -0
- mblt_vision/models/YOLO26x-sem.yaml +17 -0
- mblt_vision/models/YOLO26x.yaml +27 -0
- mblt_vision/models/YOLOv10b.yaml +28 -0
- mblt_vision/models/YOLOv10l-face.yaml +28 -0
- mblt_vision/models/YOLOv10l.yaml +28 -0
- mblt_vision/models/YOLOv10m-face.yaml +28 -0
- mblt_vision/models/YOLOv10m.yaml +28 -0
- mblt_vision/models/YOLOv10n-face.yaml +28 -0
- mblt_vision/models/YOLOv10n.yaml +28 -0
- mblt_vision/models/YOLOv10s-face.yaml +28 -0
- mblt_vision/models/YOLOv10s.yaml +28 -0
- mblt_vision/models/YOLOv10x.yaml +28 -0
- mblt_vision/models/YOLOv3-spp.yaml +44 -0
- mblt_vision/models/YOLOv3-sppu.yaml +27 -0
- mblt_vision/models/YOLOv3-tiny.yaml +38 -0
- mblt_vision/models/YOLOv3-tinyu.yaml +27 -0
- mblt_vision/models/YOLOv3.yaml +44 -0
- mblt_vision/models/YOLOv3u.yaml +27 -0
- mblt_vision/models/YOLOv5l-cls.yaml +22 -0
- mblt_vision/models/YOLOv5l-seg.yaml +45 -0
- mblt_vision/models/YOLOv5l.yaml +44 -0
- mblt_vision/models/YOLOv5l6.yaml +50 -0
- mblt_vision/models/YOLOv5l6u.yaml +27 -0
- mblt_vision/models/YOLOv5lu.yaml +27 -0
- mblt_vision/models/YOLOv5m-cls.yaml +22 -0
- mblt_vision/models/YOLOv5m-seg.yaml +45 -0
- mblt_vision/models/YOLOv5m.yaml +45 -0
- mblt_vision/models/YOLOv5m6.yaml +50 -0
- mblt_vision/models/YOLOv5m6u.yaml +27 -0
- mblt_vision/models/YOLOv5mu.yaml +27 -0
- mblt_vision/models/YOLOv5n-cls.yaml +22 -0
- mblt_vision/models/YOLOv5n-seg.yaml +45 -0
- mblt_vision/models/YOLOv5n.yaml +44 -0
- mblt_vision/models/YOLOv5n6.yaml +50 -0
- mblt_vision/models/YOLOv5n6u.yaml +27 -0
- mblt_vision/models/YOLOv5nu.yaml +27 -0
- mblt_vision/models/YOLOv5s-cls.yaml +22 -0
- mblt_vision/models/YOLOv5s-seg.yaml +45 -0
- mblt_vision/models/YOLOv5s.yaml +44 -0
- mblt_vision/models/YOLOv5s6.yaml +50 -0
- mblt_vision/models/YOLOv5s6u.yaml +27 -0
- mblt_vision/models/YOLOv5su.yaml +27 -0
- mblt_vision/models/YOLOv5x-cls.yaml +22 -0
- mblt_vision/models/YOLOv5x-seg.yaml +45 -0
- mblt_vision/models/YOLOv5x.yaml +44 -0
- mblt_vision/models/YOLOv5x6.yaml +50 -0
- mblt_vision/models/YOLOv5x6u.yaml +27 -0
- mblt_vision/models/YOLOv5xu.yaml +27 -0
- mblt_vision/models/YOLOv6m-face.yaml +27 -0
- mblt_vision/models/YOLOv6n-face.yaml +27 -0
- mblt_vision/models/YOLOv7.yaml +44 -0
- mblt_vision/models/YOLOv7d6.yaml +50 -0
- mblt_vision/models/YOLOv7e6.yaml +50 -0
- mblt_vision/models/YOLOv7e6e.yaml +50 -0
- mblt_vision/models/YOLOv7w6.yaml +50 -0
- mblt_vision/models/YOLOv7x.yaml +44 -0
- mblt_vision/models/YOLOv8l-cls.yaml +23 -0
- mblt_vision/models/YOLOv8l-face.yaml +27 -0
- mblt_vision/models/YOLOv8l-obb.yaml +28 -0
- mblt_vision/models/YOLOv8l-pose.yaml +28 -0
- mblt_vision/models/YOLOv8l-seg.yaml +28 -0
- mblt_vision/models/YOLOv8l.yaml +27 -0
- mblt_vision/models/YOLOv8m-cls.yaml +23 -0
- mblt_vision/models/YOLOv8m-face.yaml +27 -0
- mblt_vision/models/YOLOv8m-obb.yaml +28 -0
- mblt_vision/models/YOLOv8m-pose.yaml +28 -0
- mblt_vision/models/YOLOv8m-seg.yaml +28 -0
- mblt_vision/models/YOLOv8m.yaml +27 -0
- mblt_vision/models/YOLOv8n-cls.yaml +23 -0
- mblt_vision/models/YOLOv8n-face.yaml +27 -0
- mblt_vision/models/YOLOv8n-obb.yaml +28 -0
- mblt_vision/models/YOLOv8n-pose.yaml +28 -0
- mblt_vision/models/YOLOv8n-seg.yaml +28 -0
- mblt_vision/models/YOLOv8n.yaml +27 -0
- mblt_vision/models/YOLOv8s-cls.yaml +23 -0
- mblt_vision/models/YOLOv8s-obb.yaml +28 -0
- mblt_vision/models/YOLOv8s-pose.yaml +28 -0
- mblt_vision/models/YOLOv8s-seg.yaml +28 -0
- mblt_vision/models/YOLOv8s.yaml +27 -0
- mblt_vision/models/YOLOv8x-cls.yaml +23 -0
- mblt_vision/models/YOLOv8x-obb.yaml +28 -0
- mblt_vision/models/YOLOv8x-pose-p6.yaml +28 -0
- mblt_vision/models/YOLOv8x-pose.yaml +28 -0
- mblt_vision/models/YOLOv8x-seg.yaml +28 -0
- mblt_vision/models/YOLOv8x.yaml +27 -0
- mblt_vision/models/YOLOv9c-seg.yaml +28 -0
- mblt_vision/models/YOLOv9c.yaml +27 -0
- mblt_vision/models/YOLOv9e-seg.yaml +28 -0
- mblt_vision/models/YOLOv9e.yaml +27 -0
- mblt_vision/models/YOLOv9m.yaml +27 -0
- mblt_vision/models/YOLOv9s.yaml +27 -0
- mblt_vision/models/YOLOv9t.yaml +27 -0
- mblt_vision/obb/__init__.py +39 -0
- mblt_vision/object_detection/__init__.py +153 -0
- mblt_vision/pose_estimation/__init__.py +41 -0
- mblt_vision/py.typed +1 -0
- mblt_vision/semantic_segmentation/__init__.py +29 -0
- mblt_vision/utils/__init__.py +0 -0
- mblt_vision/utils/datasets/__init__.py +85 -0
- mblt_vision/utils/datasets/ade20k.py +41 -0
- mblt_vision/utils/datasets/cityscapes.py +66 -0
- mblt_vision/utils/datasets/coco.py +243 -0
- mblt_vision/utils/datasets/dataloader.py +1016 -0
- mblt_vision/utils/datasets/dotav1.py +59 -0
- mblt_vision/utils/datasets/imagenet.py +29 -0
- mblt_vision/utils/datasets/organizer.py +2145 -0
- mblt_vision/utils/datasets/readiness.py +1063 -0
- mblt_vision/utils/evaluation/__init__.py +51 -0
- mblt_vision/utils/evaluation/_result.py +22 -0
- mblt_vision/utils/evaluation/eval_ade20k.py +302 -0
- mblt_vision/utils/evaluation/eval_cityscapes.py +32 -0
- mblt_vision/utils/evaluation/eval_coco.py +465 -0
- mblt_vision/utils/evaluation/eval_dota.py +878 -0
- mblt_vision/utils/evaluation/eval_imagenet.py +151 -0
- mblt_vision/utils/evaluation/eval_nyu_depth.py +189 -0
- mblt_vision/utils/evaluation/eval_widerface.py +440 -0
- mblt_vision/utils/letterbox.py +118 -0
- mblt_vision/utils/postprocess/__init__.py +9 -0
- mblt_vision/utils/postprocess/_letterbox.py +102 -0
- mblt_vision/utils/postprocess/base.py +743 -0
- mblt_vision/utils/postprocess/build_post.py +120 -0
- mblt_vision/utils/postprocess/cls_post.py +117 -0
- mblt_vision/utils/postprocess/common.py +1571 -0
- mblt_vision/utils/postprocess/depth_post.py +106 -0
- mblt_vision/utils/postprocess/semantic_seg_post.py +192 -0
- mblt_vision/utils/postprocess/yolo_anchor_post.py +469 -0
- mblt_vision/utils/postprocess/yolo_anchorless_post.py +952 -0
- mblt_vision/utils/postprocess/yolo_dflfree_post.py +983 -0
- mblt_vision/utils/postprocess/yolo_nmsfree_post.py +176 -0
- mblt_vision/utils/preprocess/__init__.py +27 -0
- mblt_vision/utils/preprocess/_validation.py +66 -0
- mblt_vision/utils/preprocess/base.py +157 -0
- mblt_vision/utils/preprocess/build_pre.py +48 -0
- mblt_vision/utils/preprocess/center_crop.py +75 -0
- mblt_vision/utils/preprocess/letterbox.py +123 -0
- mblt_vision/utils/preprocess/normalize.py +94 -0
- mblt_vision/utils/preprocess/order.py +74 -0
- mblt_vision/utils/preprocess/reader.py +94 -0
- mblt_vision/utils/preprocess/resize.py +202 -0
- mblt_vision/utils/preprocess/yolo_pre.py +7 -0
- mblt_vision/utils/results.py +725 -0
- mblt_vision/utils/types.py +15 -0
- mblt_vision/wrapper.py +1121 -0
- mblt_vision_python-0.0.0.dist-info/METADATA +152 -0
- mblt_vision_python-0.0.0.dist-info/RECORD +404 -0
- mblt_vision_python-0.0.0.dist-info/WHEEL +5 -0
- mblt_vision_python-0.0.0.dist-info/entry_points.txt +2 -0
- mblt_vision_python-0.0.0.dist-info/licenses/LICENSE +28 -0
- mblt_vision_python-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1571 @@
|
|
|
1
|
+
"""Common postprocessing utility functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Callable, Sequence
|
|
7
|
+
from typing import Any, TypeGuard, overload
|
|
8
|
+
|
|
9
|
+
import cv2
|
|
10
|
+
import numpy as np
|
|
11
|
+
import torch
|
|
12
|
+
import torch.nn.functional as F
|
|
13
|
+
|
|
14
|
+
from ..datasets import get_coco_inv, get_dotav1_label
|
|
15
|
+
from ..letterbox import RatioPad, resolve_ratio_pad
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _is_ratio_pad(value: object) -> TypeGuard[RatioPad]:
|
|
19
|
+
"""Return whether a value has the nested numeric shape of one RatioPad."""
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
isinstance(value, tuple)
|
|
23
|
+
and len(value) == 2
|
|
24
|
+
and all(
|
|
25
|
+
isinstance(pair, tuple)
|
|
26
|
+
and len(pair) == 2
|
|
27
|
+
and all(isinstance(component, (int, float)) for component in pair)
|
|
28
|
+
for pair in value
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def normalize_image_shapes(
|
|
34
|
+
image_shapes: tuple[int, int] | Sequence[tuple[int, int]],
|
|
35
|
+
batch_size: int | None = None,
|
|
36
|
+
) -> list[tuple[int, int]]:
|
|
37
|
+
"""Normalize one or many image shapes to a list, optionally validating its batch size."""
|
|
38
|
+
|
|
39
|
+
if len(image_shapes) == 2 and isinstance(image_shapes[0], int):
|
|
40
|
+
shapes = [(int(image_shapes[0]), int(image_shapes[1]))] # type: ignore[index]
|
|
41
|
+
if batch_size is not None:
|
|
42
|
+
shapes *= batch_size
|
|
43
|
+
else:
|
|
44
|
+
shapes = [(int(shape[0]), int(shape[1])) for shape in image_shapes] # type: ignore[union-attr]
|
|
45
|
+
if batch_size is not None and len(shapes) != batch_size:
|
|
46
|
+
raise ValueError(f"Expected {batch_size} image shapes, got {len(shapes)}.")
|
|
47
|
+
return shapes
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def normalize_ratio_pads(
|
|
51
|
+
ratio_pads: RatioPad | Sequence[RatioPad | None] | None,
|
|
52
|
+
batch_size: int,
|
|
53
|
+
) -> list[RatioPad | None]:
|
|
54
|
+
"""Normalize optional letterbox metadata to a batch-sized list."""
|
|
55
|
+
|
|
56
|
+
if ratio_pads is None:
|
|
57
|
+
return [None] * batch_size
|
|
58
|
+
if _is_ratio_pad(ratio_pads):
|
|
59
|
+
return [ratio_pads] * batch_size
|
|
60
|
+
pads: list[RatioPad | None] = []
|
|
61
|
+
for ratio_pad in ratio_pads:
|
|
62
|
+
if ratio_pad is None:
|
|
63
|
+
pads.append(None)
|
|
64
|
+
elif _is_ratio_pad(ratio_pad):
|
|
65
|
+
pads.append(ratio_pad)
|
|
66
|
+
else:
|
|
67
|
+
raise TypeError(
|
|
68
|
+
"Each ratio_pad must be a ((ratio_x, ratio_y), (pad_x, pad_y)) tuple or None."
|
|
69
|
+
)
|
|
70
|
+
if len(pads) != batch_size:
|
|
71
|
+
raise ValueError(f"Expected {batch_size} ratio_pad values, got {len(pads)}.")
|
|
72
|
+
return pads
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# --- Box Conversion Utilities ---
|
|
76
|
+
@overload
|
|
77
|
+
def xywh2xyxy(x: np.ndarray) -> np.ndarray:
|
|
78
|
+
"""Converts numpy boxes from ``xywh`` to ``xyxy`` format."""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@overload
|
|
82
|
+
def xywh2xyxy(x: torch.Tensor) -> torch.Tensor:
|
|
83
|
+
"""Converts torch boxes from ``xywh`` to ``xyxy`` format."""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def xywh2xyxy(x: np.ndarray | torch.Tensor) -> np.ndarray | torch.Tensor:
|
|
87
|
+
"""Converts bounding box coordinates from (cx, cy, w, h) to (x1, y1, x2, y2).
|
|
88
|
+
|
|
89
|
+
(x1, y1) is the top-left corner and (x2, y2) is the bottom-right corner.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
x: Input bounding boxes in (cx, cy, w, h) format.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Bounding boxes in (x1, y1, x2, y2) format.
|
|
96
|
+
"""
|
|
97
|
+
if isinstance(x, np.ndarray):
|
|
98
|
+
y = np.copy(x)
|
|
99
|
+
y[..., 0] = x[..., 0] - x[..., 2] / 2
|
|
100
|
+
y[..., 1] = x[..., 1] - x[..., 3] / 2
|
|
101
|
+
y[..., 2] = x[..., 0] + x[..., 2] / 2
|
|
102
|
+
y[..., 3] = x[..., 1] + x[..., 3] / 2
|
|
103
|
+
return y
|
|
104
|
+
|
|
105
|
+
if isinstance(x, torch.Tensor):
|
|
106
|
+
y = torch.clone(x)
|
|
107
|
+
y[..., 0] = x[..., 0] - x[..., 2] / 2
|
|
108
|
+
y[..., 1] = x[..., 1] - x[..., 3] / 2
|
|
109
|
+
y[..., 2] = x[..., 0] + x[..., 2] / 2
|
|
110
|
+
y[..., 3] = x[..., 1] + x[..., 3] / 2
|
|
111
|
+
return y
|
|
112
|
+
|
|
113
|
+
raise ValueError("x should be np.ndarray or torch.Tensor")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@overload
|
|
117
|
+
def xyxy2xywh(x: np.ndarray) -> np.ndarray:
|
|
118
|
+
"""Converts numpy boxes from ``xyxy`` to ``xywh`` format."""
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@overload
|
|
122
|
+
def xyxy2xywh(x: torch.Tensor) -> torch.Tensor:
|
|
123
|
+
"""Converts torch boxes from ``xyxy`` to ``xywh`` format."""
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def xyxy2xywh(x: np.ndarray | torch.Tensor) -> np.ndarray | torch.Tensor:
|
|
127
|
+
"""Converts bounding box coordinates from (x1, y1, x2, y2) to (cx, cy, w, h).
|
|
128
|
+
|
|
129
|
+
(x1, y1) is the top-left corner and (x2, y2) is the bottom-right corner.
|
|
130
|
+
(cx, cy) is the center of the bounding box.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
x: Input bounding boxes in (x1, y1, x2, y2) format.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Bounding boxes in (cx, cy, w, h) format.
|
|
137
|
+
"""
|
|
138
|
+
if isinstance(x, np.ndarray):
|
|
139
|
+
y = np.copy(x)
|
|
140
|
+
y[..., 0] = (x[..., 0] + x[..., 2]) / 2
|
|
141
|
+
y[..., 1] = (x[..., 1] + x[..., 3]) / 2
|
|
142
|
+
y[..., 2] = x[..., 2] - x[..., 0]
|
|
143
|
+
y[..., 3] = x[..., 3] - x[..., 1]
|
|
144
|
+
return y
|
|
145
|
+
|
|
146
|
+
if isinstance(x, torch.Tensor):
|
|
147
|
+
y = torch.clone(x)
|
|
148
|
+
y[..., 0] = (x[..., 0] + x[..., 2]) / 2
|
|
149
|
+
y[..., 1] = (x[..., 1] + x[..., 3]) / 2
|
|
150
|
+
y[..., 2] = x[..., 2] - x[..., 0]
|
|
151
|
+
y[..., 3] = x[..., 3] - x[..., 1]
|
|
152
|
+
return y
|
|
153
|
+
|
|
154
|
+
raise ValueError("x should be np.ndarray or torch.Tensor")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def dist2bbox(
|
|
158
|
+
distance: torch.Tensor,
|
|
159
|
+
anchor_points: torch.Tensor,
|
|
160
|
+
xywh: bool = True,
|
|
161
|
+
dim: int = -1,
|
|
162
|
+
) -> torch.Tensor:
|
|
163
|
+
"""
|
|
164
|
+
Transform distance (ltrb) to bounding box (xywh or xyxy).
|
|
165
|
+
Args:
|
|
166
|
+
distance (torch.Tensor): Distance from anchor points to box boundaries
|
|
167
|
+
(left, top, right, bottom).
|
|
168
|
+
anchor_points (torch.Tensor): Anchor points (center points).
|
|
169
|
+
xywh (bool, optional): If True, return boxes in (cx, cy, w, h) format.
|
|
170
|
+
If False, return in (x1, y1, x2, y2) format. Defaults to True.
|
|
171
|
+
dim (int, optional): Dimension along which to chunk the distance tensor. Defaults to -1.
|
|
172
|
+
Returns:
|
|
173
|
+
torch.Tensor: Transformed bounding boxes.
|
|
174
|
+
"""
|
|
175
|
+
lt, rb = distance.chunk(2, dim)
|
|
176
|
+
x1y1 = anchor_points - lt
|
|
177
|
+
x2y2 = anchor_points + rb
|
|
178
|
+
if xywh:
|
|
179
|
+
return torch.cat(((x1y1 + x2y2) / 2, x2y2 - x1y1), dim) # xywh bbox
|
|
180
|
+
else:
|
|
181
|
+
return torch.cat((x1y1, x2y2), dim) # xyxy bbox
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def dist2rbox(
|
|
185
|
+
distance: torch.Tensor,
|
|
186
|
+
angle: torch.Tensor,
|
|
187
|
+
anchor_points: torch.Tensor,
|
|
188
|
+
dim: int = -1,
|
|
189
|
+
) -> torch.Tensor:
|
|
190
|
+
"""Decode rotated boxes from anchor-relative distances and angles.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
distance: Distance tensor in ``ltrb`` format.
|
|
194
|
+
angle: Rotation angle tensor in radians.
|
|
195
|
+
anchor_points: Anchor center points.
|
|
196
|
+
dim: Dimension along which box channels are split.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Rotated boxes in ``cx, cy, w, h`` format.
|
|
200
|
+
"""
|
|
201
|
+
lt, rb = distance.split(2, dim=dim)
|
|
202
|
+
cos_value = torch.cos(angle)
|
|
203
|
+
sin_value = torch.sin(angle)
|
|
204
|
+
xf, yf = ((rb - lt) / 2).split(1, dim=dim)
|
|
205
|
+
x = xf * cos_value - yf * sin_value
|
|
206
|
+
y = xf * sin_value + yf * cos_value
|
|
207
|
+
xy = torch.cat([x, y], dim=dim) + anchor_points
|
|
208
|
+
return torch.cat([xy, lt + rb], dim=dim)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@overload
|
|
212
|
+
def xywhr2xyxyxyxy(x: np.ndarray) -> np.ndarray:
|
|
213
|
+
"""Converts numpy OBBs from ``xywhr`` to polygon corners."""
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@overload
|
|
217
|
+
def xywhr2xyxyxyxy(x: torch.Tensor) -> torch.Tensor:
|
|
218
|
+
"""Converts torch OBBs from ``xywhr`` to polygon corners."""
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def xywhr2xyxyxyxy(x: np.ndarray | torch.Tensor) -> np.ndarray | torch.Tensor:
|
|
222
|
+
"""Converts oriented boxes from ``cx, cy, w, h, angle`` to four corner points.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
x: Oriented boxes with shape ``(..., 5)`` and angle in radians.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Corner points with shape ``(..., 4, 2)``.
|
|
229
|
+
"""
|
|
230
|
+
if isinstance(x, torch.Tensor):
|
|
231
|
+
ctr = x[..., :2]
|
|
232
|
+
w, h, angle = (x[..., i : i + 1] for i in range(2, 5))
|
|
233
|
+
cos_value = torch.cos(angle)
|
|
234
|
+
sin_value = torch.sin(angle)
|
|
235
|
+
vec1 = torch.cat([w / 2 * cos_value, w / 2 * sin_value], dim=-1)
|
|
236
|
+
vec2 = torch.cat([-h / 2 * sin_value, h / 2 * cos_value], dim=-1)
|
|
237
|
+
return torch.stack(
|
|
238
|
+
[
|
|
239
|
+
ctr + vec1 + vec2,
|
|
240
|
+
ctr + vec1 - vec2,
|
|
241
|
+
ctr - vec1 - vec2,
|
|
242
|
+
ctr - vec1 + vec2,
|
|
243
|
+
],
|
|
244
|
+
dim=-2,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if isinstance(x, np.ndarray):
|
|
248
|
+
ctr = x[..., :2]
|
|
249
|
+
w, h, angle = (x[..., i : i + 1] for i in range(2, 5))
|
|
250
|
+
cos_value = np.cos(angle)
|
|
251
|
+
sin_value = np.sin(angle)
|
|
252
|
+
vec1 = np.concatenate([w / 2 * cos_value, w / 2 * sin_value], axis=-1)
|
|
253
|
+
vec2 = np.concatenate([-h / 2 * sin_value, h / 2 * cos_value], axis=-1)
|
|
254
|
+
return np.stack(
|
|
255
|
+
[
|
|
256
|
+
ctr + vec1 + vec2,
|
|
257
|
+
ctr + vec1 - vec2,
|
|
258
|
+
ctr - vec1 - vec2,
|
|
259
|
+
ctr - vec1 + vec2,
|
|
260
|
+
],
|
|
261
|
+
axis=-2,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
raise ValueError("x should be np.ndarray or torch.Tensor")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def xyxyxyxy2xywhr(points: np.ndarray | torch.Tensor) -> np.ndarray | torch.Tensor:
|
|
268
|
+
"""Converts OBB corner points to regularized ``xywhr`` boxes.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
points: Corner points with shape ``(..., 4, 2)``.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Rotated boxes in ``cx, cy, w, h, angle`` format.
|
|
275
|
+
"""
|
|
276
|
+
is_torch = isinstance(points, torch.Tensor)
|
|
277
|
+
points_np = points.detach().cpu().numpy() if is_torch else np.asarray(points)
|
|
278
|
+
flat_points = points_np.reshape(-1, 4, 2).astype(np.float32)
|
|
279
|
+
rboxes = []
|
|
280
|
+
for pts in flat_points:
|
|
281
|
+
(cx, cy), (w, h), angle = cv2.minAreaRect(pts)
|
|
282
|
+
theta = angle / 180 * np.pi
|
|
283
|
+
if w < h:
|
|
284
|
+
w, h = h, w
|
|
285
|
+
theta += np.pi / 2
|
|
286
|
+
while theta >= 3 * np.pi / 4:
|
|
287
|
+
theta -= np.pi
|
|
288
|
+
while theta < -np.pi / 4:
|
|
289
|
+
theta += np.pi
|
|
290
|
+
rboxes.append([cx, cy, w, h, theta])
|
|
291
|
+
result_np = np.asarray(rboxes, dtype=points_np.dtype).reshape(
|
|
292
|
+
*points_np.shape[:-2], 5
|
|
293
|
+
)
|
|
294
|
+
if is_torch:
|
|
295
|
+
return torch.tensor(result_np, device=points.device, dtype=points.dtype)
|
|
296
|
+
return result_np
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def regularize_rboxes(rboxes: torch.Tensor) -> torch.Tensor:
|
|
300
|
+
"""Regularize rotated boxes to the angle range ``[0, pi / 2)``.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
rboxes: Rotated boxes in ``xywhr`` format.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
Regularized rotated boxes.
|
|
307
|
+
"""
|
|
308
|
+
x, y, w, h, angle = rboxes.unbind(dim=-1)
|
|
309
|
+
swap = angle % math.pi >= math.pi / 2
|
|
310
|
+
regularized_w = torch.where(swap, h, w)
|
|
311
|
+
regularized_h = torch.where(swap, w, h)
|
|
312
|
+
regularized_angle = angle % (math.pi / 2)
|
|
313
|
+
return torch.stack([x, y, regularized_w, regularized_h, regularized_angle], dim=-1)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _get_covariance_matrix(
|
|
317
|
+
boxes: torch.Tensor,
|
|
318
|
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
319
|
+
"""Return Gaussian covariance components for probabilistic OBB IoU."""
|
|
320
|
+
gbbs = torch.cat((boxes[:, 2:4].pow(2) / 12, boxes[:, 4:]), dim=-1)
|
|
321
|
+
a, b, c = gbbs.split(1, dim=-1)
|
|
322
|
+
cos_value = c.cos()
|
|
323
|
+
sin_value = c.sin()
|
|
324
|
+
cos2 = cos_value.pow(2)
|
|
325
|
+
sin2 = sin_value.pow(2)
|
|
326
|
+
return a * cos2 + b * sin2, a * sin2 + b * cos2, (a - b) * cos_value * sin_value
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def batch_probiou(
|
|
330
|
+
obb1: torch.Tensor | np.ndarray, obb2: torch.Tensor | np.ndarray, eps: float = 1e-7
|
|
331
|
+
) -> torch.Tensor:
|
|
332
|
+
"""Calculate pairwise probabilistic IoU for oriented boxes.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
obb1: First set of OBBs in ``xywhr`` format with shape ``(N, 5)``.
|
|
336
|
+
obb2: Second set of OBBs in ``xywhr`` format with shape ``(M, 5)``.
|
|
337
|
+
eps: Small value used for numerical stability.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Pairwise OBB similarities with shape ``(N, M)``.
|
|
341
|
+
"""
|
|
342
|
+
obb1 = torch.from_numpy(obb1) if isinstance(obb1, np.ndarray) else obb1
|
|
343
|
+
obb2 = torch.from_numpy(obb2) if isinstance(obb2, np.ndarray) else obb2
|
|
344
|
+
obb2 = obb2.to(device=obb1.device, dtype=obb1.dtype)
|
|
345
|
+
|
|
346
|
+
x1, y1 = obb1[..., :2].split(1, dim=-1)
|
|
347
|
+
x2, y2 = (x.squeeze(-1)[None] for x in obb2[..., :2].split(1, dim=-1))
|
|
348
|
+
a1, b1, c1 = _get_covariance_matrix(obb1)
|
|
349
|
+
a2, b2, c2 = (x.squeeze(-1)[None] for x in _get_covariance_matrix(obb2))
|
|
350
|
+
|
|
351
|
+
denominator = (a1 + a2) * (b1 + b2) - (c1 + c2).pow(2) + eps
|
|
352
|
+
t1 = (
|
|
353
|
+
((a1 + a2) * (y1 - y2).pow(2) + (b1 + b2) * (x1 - x2).pow(2)) / denominator
|
|
354
|
+
) * 0.25
|
|
355
|
+
t2 = (((c1 + c2) * (x2 - x1) * (y1 - y2)) / denominator) * 0.5
|
|
356
|
+
t3 = (
|
|
357
|
+
((a1 + a2) * (b1 + b2) - (c1 + c2).pow(2))
|
|
358
|
+
/ (
|
|
359
|
+
4
|
|
360
|
+
* ((a1 * b1 - c1.pow(2)).clamp_(0) * (a2 * b2 - c2.pow(2)).clamp_(0)).sqrt()
|
|
361
|
+
+ eps
|
|
362
|
+
)
|
|
363
|
+
+ eps
|
|
364
|
+
).log() * 0.5
|
|
365
|
+
bd = (t1 + t2 + t3).clamp(eps, 100.0)
|
|
366
|
+
hd = (1.0 - (-bd).exp() + eps).sqrt()
|
|
367
|
+
return 1 - hd
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def rotated_nms(
|
|
371
|
+
boxes: torch.Tensor,
|
|
372
|
+
scores: torch.Tensor,
|
|
373
|
+
iou_threshold: float,
|
|
374
|
+
iou_func: Callable[[torch.Tensor, torch.Tensor], torch.Tensor] = batch_probiou,
|
|
375
|
+
) -> torch.Tensor:
|
|
376
|
+
"""Apply fast rotated NMS using an upper-triangular pairwise IoU matrix.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
boxes: OBBs in ``xywhr`` format.
|
|
380
|
+
scores: Confidence scores.
|
|
381
|
+
iou_threshold: IoU threshold for suppression.
|
|
382
|
+
iou_func: Pairwise IoU function.
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
Kept indices into the original inputs.
|
|
386
|
+
"""
|
|
387
|
+
if boxes.numel() == 0:
|
|
388
|
+
return torch.empty((0,), dtype=torch.int64, device=boxes.device)
|
|
389
|
+
sorted_idx = torch.argsort(scores, descending=True)
|
|
390
|
+
sorted_boxes = boxes[sorted_idx]
|
|
391
|
+
ious = iou_func(sorted_boxes, sorted_boxes).triu_(diagonal=1)
|
|
392
|
+
keep = torch.nonzero((ious >= iou_threshold).sum(0) <= 0).squeeze_(-1)
|
|
393
|
+
return sorted_idx[keep]
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# --- Detection Utilities ---
|
|
397
|
+
def non_max_suppression(
|
|
398
|
+
boxes: torch.Tensor, scores: torch.Tensor, iou_threshold: float, max_output: int
|
|
399
|
+
) -> list[int]:
|
|
400
|
+
"""
|
|
401
|
+
Modified non-maximum suppression (NMS) implemented with PyTorch.
|
|
402
|
+
Args:
|
|
403
|
+
boxes (torch.Tensor): Bounding boxes in (x1, y1, x2, y2) format.
|
|
404
|
+
scores (torch.Tensor): Confidence scores for each box (assumed to be sorted in
|
|
405
|
+
descending order).
|
|
406
|
+
iou_threshold (float): IoU threshold for suppression.
|
|
407
|
+
max_output (int): Maximum number of boxes to keep.
|
|
408
|
+
Returns:
|
|
409
|
+
list[int]: Indices of the boxes that have been kept after NMS.
|
|
410
|
+
"""
|
|
411
|
+
if boxes.numel() == 0:
|
|
412
|
+
return []
|
|
413
|
+
# Coordinates of bounding boxes
|
|
414
|
+
start_x = boxes[:, 0]
|
|
415
|
+
start_y = boxes[:, 1]
|
|
416
|
+
end_x = boxes[:, 2]
|
|
417
|
+
end_y = boxes[:, 3]
|
|
418
|
+
picked_indices: list[int] = []
|
|
419
|
+
# Compute areas of bounding boxes
|
|
420
|
+
areas = (end_x - start_x) * (end_y - start_y)
|
|
421
|
+
# Create an index order (assumed scores are already sorted in descending order)
|
|
422
|
+
order = torch.arange(scores.size(0)).to(boxes.device)
|
|
423
|
+
while order.numel() > 0 and len(picked_indices) < max_output:
|
|
424
|
+
# The index with the highest score
|
|
425
|
+
index = int(order[0].item())
|
|
426
|
+
picked_indices.append(index)
|
|
427
|
+
order = order[1:] # Remove the index from the order
|
|
428
|
+
if order.numel() == 0 or len(picked_indices) >= max_output:
|
|
429
|
+
break
|
|
430
|
+
# Compute the coordinates of the intersection boxes
|
|
431
|
+
x1 = torch.maximum(start_x[index], start_x[order])
|
|
432
|
+
y1 = torch.maximum(start_y[index], start_y[order])
|
|
433
|
+
x2 = torch.minimum(end_x[index], end_x[order])
|
|
434
|
+
y2 = torch.minimum(end_y[index], end_y[order])
|
|
435
|
+
# Compute width and height of the intersection boxes
|
|
436
|
+
w = torch.clamp(x2 - x1, min=0.0)
|
|
437
|
+
h = torch.clamp(y2 - y1, min=0.0)
|
|
438
|
+
intersection = w * h
|
|
439
|
+
# Compute the IoU ratio
|
|
440
|
+
union = areas[index] + areas[order] - intersection
|
|
441
|
+
ratio = intersection / union
|
|
442
|
+
# Keep boxes with IoU less than or equal to the threshold
|
|
443
|
+
keep = (ratio <= iou_threshold).to(order.device)
|
|
444
|
+
order = order[keep]
|
|
445
|
+
return picked_indices
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def dual_topk(
|
|
449
|
+
pre_topk: torch.Tensor,
|
|
450
|
+
nc: int,
|
|
451
|
+
n_extra: int,
|
|
452
|
+
max_det: int = 300,
|
|
453
|
+
conf_thres: float = 0.25,
|
|
454
|
+
score_is_logits: bool = False,
|
|
455
|
+
) -> torch.Tensor:
|
|
456
|
+
"""
|
|
457
|
+
Perform dual-stage topk selection for NMS-free models.
|
|
458
|
+
Args:
|
|
459
|
+
pre_topk (torch.Tensor): Input tensor of shape (*, 4 + nc + n_extra).
|
|
460
|
+
nc (int): Number of classes.
|
|
461
|
+
n_extra (int): Number of extra elements (e.g., masks, keypoints).
|
|
462
|
+
max_det (int): Maximum detections to keep. Defaults to 300.
|
|
463
|
+
conf_thres (float): Confidence threshold. Defaults to 0.25.
|
|
464
|
+
score_is_logits (bool): Whether class scores are logits. When true, apply
|
|
465
|
+
the confidence cutoff and both rankings before sigmoid, then convert
|
|
466
|
+
only selected scores to probabilities. Defaults to false.
|
|
467
|
+
Returns:
|
|
468
|
+
torch.Tensor: Filtered detections of shape (*, 6 + n_extra).
|
|
469
|
+
"""
|
|
470
|
+
score_start = 4
|
|
471
|
+
score_end = 4 + nc
|
|
472
|
+
score_view = pre_topk[:, score_start:score_end]
|
|
473
|
+
threshold = (
|
|
474
|
+
math.log(conf_thres / (1.0 - conf_thres)) if score_is_logits else conf_thres
|
|
475
|
+
)
|
|
476
|
+
ic = score_view.amax(dim=-1) > threshold
|
|
477
|
+
pre_topk = pre_topk[ic]
|
|
478
|
+
|
|
479
|
+
if pre_topk.shape[0] == 0:
|
|
480
|
+
return torch.zeros(
|
|
481
|
+
(0, 6 + n_extra), dtype=torch.float32, device=pre_topk.device
|
|
482
|
+
)
|
|
483
|
+
max_det = min(pre_topk.shape[0], max_det)
|
|
484
|
+
|
|
485
|
+
row_index = torch.topk(
|
|
486
|
+
pre_topk[:, score_start:score_end].amax(dim=-1), max_det, dim=0
|
|
487
|
+
).indices
|
|
488
|
+
selected = pre_topk[row_index]
|
|
489
|
+
top_scores, flat_index = torch.topk(
|
|
490
|
+
selected[:, score_start:score_end].reshape(-1), max_det
|
|
491
|
+
)
|
|
492
|
+
keep = top_scores > threshold
|
|
493
|
+
if not torch.any(keep):
|
|
494
|
+
return torch.zeros(
|
|
495
|
+
(0, 6 + n_extra), dtype=torch.float32, device=pre_topk.device
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
top_scores = top_scores[keep]
|
|
499
|
+
flat_index = flat_index[keep]
|
|
500
|
+
box_index = flat_index // nc
|
|
501
|
+
labels = (flat_index % nc).to(selected.dtype).unsqueeze(-1)
|
|
502
|
+
|
|
503
|
+
output = torch.empty(
|
|
504
|
+
(top_scores.shape[0], 6 + n_extra), dtype=selected.dtype, device=selected.device
|
|
505
|
+
)
|
|
506
|
+
output[:, :4] = selected[box_index, :4]
|
|
507
|
+
output[:, 4] = top_scores.sigmoid() if score_is_logits else top_scores
|
|
508
|
+
output[:, 5:6] = labels
|
|
509
|
+
if n_extra > 0:
|
|
510
|
+
output[:, 6:] = selected[box_index, score_end:]
|
|
511
|
+
return output
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def yolo_multilabel_candidates(
|
|
515
|
+
detections: torch.Tensor,
|
|
516
|
+
nc: int,
|
|
517
|
+
n_extra: int,
|
|
518
|
+
conf_thres: float,
|
|
519
|
+
) -> torch.Tensor:
|
|
520
|
+
"""Expand YOLO rows into one detection per class score above threshold.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
detections: Row-major detections with columns ``box, class scores, extra``.
|
|
524
|
+
nc: Number of classes.
|
|
525
|
+
n_extra: Number of extra channels after class scores.
|
|
526
|
+
conf_thres: Confidence threshold.
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
Canonical detection rows with columns ``box, score, class, extra``.
|
|
530
|
+
"""
|
|
531
|
+
if detections.numel() == 0:
|
|
532
|
+
return torch.zeros(
|
|
533
|
+
(0, 6 + n_extra), dtype=torch.float32, device=detections.device
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
boxes = detections[:, :4]
|
|
537
|
+
scores = detections[:, 4 : 4 + nc]
|
|
538
|
+
extra = detections[:, 4 + nc :]
|
|
539
|
+
box_index, class_index = torch.where(scores > conf_thres)
|
|
540
|
+
if box_index.numel() == 0:
|
|
541
|
+
return torch.zeros(
|
|
542
|
+
(0, 6 + n_extra), dtype=torch.float32, device=detections.device
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
output = torch.empty(
|
|
546
|
+
(box_index.numel(), 6 + n_extra),
|
|
547
|
+
dtype=detections.dtype,
|
|
548
|
+
device=detections.device,
|
|
549
|
+
)
|
|
550
|
+
output[:, :4] = boxes[box_index]
|
|
551
|
+
output[:, 4] = scores[box_index, class_index]
|
|
552
|
+
output[:, 5] = class_index.to(detections.dtype)
|
|
553
|
+
if n_extra > 0:
|
|
554
|
+
output[:, 6:] = extra[box_index]
|
|
555
|
+
return output
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def normalize_converted_obb_part(x: torch.Tensor, channel_count: int) -> torch.Tensor:
|
|
559
|
+
"""Normalize a converted OBB output part to ``(batch, anchors, channels)``.
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
x: Converted output part from a model runtime.
|
|
563
|
+
channel_count: Expected feature-channel count for this part.
|
|
564
|
+
|
|
565
|
+
Returns:
|
|
566
|
+
The normalized row-major tensor.
|
|
567
|
+
"""
|
|
568
|
+
while x.ndim > 3:
|
|
569
|
+
singleton_dims = [
|
|
570
|
+
idx for idx, size in enumerate(x.shape) if idx != 0 and size == 1
|
|
571
|
+
]
|
|
572
|
+
if not singleton_dims:
|
|
573
|
+
raise ValueError(
|
|
574
|
+
f"Expected converted OBB part with up to 3 non-batch dimensions, got {tuple(x.shape)}."
|
|
575
|
+
)
|
|
576
|
+
x = x.squeeze(singleton_dims[0])
|
|
577
|
+
if x.ndim == 2:
|
|
578
|
+
x = x.unsqueeze(0)
|
|
579
|
+
if x.ndim != 3:
|
|
580
|
+
raise ValueError(
|
|
581
|
+
f"Expected 2D or 3D converted OBB part, got shape {tuple(x.shape)}."
|
|
582
|
+
)
|
|
583
|
+
if x.shape[-1] == channel_count:
|
|
584
|
+
return x
|
|
585
|
+
if x.shape[1] == channel_count:
|
|
586
|
+
return x.transpose(1, 2)
|
|
587
|
+
raise ValueError(
|
|
588
|
+
f"Could not find channel count {channel_count} in converted OBB part with shape {tuple(x.shape)}."
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def concat_converted_obb_outputs(
|
|
593
|
+
x: list[torch.Tensor], nc: int, n_extra: int
|
|
594
|
+
) -> torch.Tensor:
|
|
595
|
+
"""Concatenate converted OBB box, class, and angle outputs in canonical order.
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
x: Converted OBB runtime outputs.
|
|
599
|
+
nc: Number of OBB classes.
|
|
600
|
+
n_extra: Number of extra OBB channels.
|
|
601
|
+
|
|
602
|
+
Returns:
|
|
603
|
+
Detections in ``cx, cy, w, h, class scores..., angle`` format.
|
|
604
|
+
"""
|
|
605
|
+
if len(x) == 1:
|
|
606
|
+
return x[0]
|
|
607
|
+
if len(x) != 3:
|
|
608
|
+
raise ValueError(f"Expected 1 or 3 converted OBB outputs, got {len(x)}.")
|
|
609
|
+
|
|
610
|
+
expected_parts = {"box": 4, "scores": nc, "angle": n_extra}
|
|
611
|
+
parts: dict[str, torch.Tensor] = {}
|
|
612
|
+
for xi in x:
|
|
613
|
+
matches: list[tuple[str, torch.Tensor]] = []
|
|
614
|
+
for name, channel_count in expected_parts.items():
|
|
615
|
+
try:
|
|
616
|
+
matches.append((name, normalize_converted_obb_part(xi, channel_count)))
|
|
617
|
+
except ValueError:
|
|
618
|
+
continue
|
|
619
|
+
if len(matches) != 1:
|
|
620
|
+
match_names = ", ".join(name for name, _ in matches) or "none"
|
|
621
|
+
raise ValueError(
|
|
622
|
+
f"Could not uniquely classify converted OBB output {tuple(xi.shape)}; matches: {match_names}."
|
|
623
|
+
)
|
|
624
|
+
name, normalized = matches[0]
|
|
625
|
+
if name in parts:
|
|
626
|
+
raise ValueError(f"Duplicate converted OBB {name} output.")
|
|
627
|
+
parts[name] = normalized
|
|
628
|
+
|
|
629
|
+
missing = [name for name in expected_parts if name not in parts]
|
|
630
|
+
if missing:
|
|
631
|
+
raise ValueError(f"Missing converted OBB outputs: {', '.join(missing)}.")
|
|
632
|
+
return torch.cat([parts["box"], parts["scores"], parts["angle"]], dim=-1)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def decode_split_converted_obb_outputs(
|
|
636
|
+
x: list[torch.Tensor],
|
|
637
|
+
nc: int,
|
|
638
|
+
n_extra: int,
|
|
639
|
+
anchors: torch.Tensor,
|
|
640
|
+
stride: torch.Tensor,
|
|
641
|
+
) -> torch.Tensor:
|
|
642
|
+
"""Decode MXQ decode-true OBB outputs split into score, angle, and coordinate tensors.
|
|
643
|
+
|
|
644
|
+
Args:
|
|
645
|
+
x: Five converted runtime outputs: class scores, rotation angle, and
|
|
646
|
+
coordinate tensors containing decoded ``wh`` and pre-rotated center offsets.
|
|
647
|
+
nc: Number of OBB classes.
|
|
648
|
+
n_extra: Number of extra OBB channels.
|
|
649
|
+
anchors: Anchor points in ``(2, anchors)`` format.
|
|
650
|
+
stride: Stride tensor in ``(1, anchors)`` format.
|
|
651
|
+
|
|
652
|
+
Returns:
|
|
653
|
+
Detections in ``cx, cy, w, h, class scores..., angle`` format.
|
|
654
|
+
"""
|
|
655
|
+
if n_extra != 1:
|
|
656
|
+
raise ValueError(f"Expected one OBB angle channel, got n_extra={n_extra}.")
|
|
657
|
+
if len(x) != 5:
|
|
658
|
+
raise ValueError(f"Expected five split converted OBB outputs, got {len(x)}.")
|
|
659
|
+
|
|
660
|
+
try:
|
|
661
|
+
scores = normalize_converted_obb_part(x[0], nc)
|
|
662
|
+
angle = normalize_converted_obb_part(x[1], n_extra)
|
|
663
|
+
except ValueError:
|
|
664
|
+
scores = normalize_converted_obb_part(x[1], nc)
|
|
665
|
+
angle = normalize_converted_obb_part(x[0], n_extra)
|
|
666
|
+
wh = normalize_converted_obb_part(x[2], 2)
|
|
667
|
+
x_offset = normalize_converted_obb_part(x[3], 1)
|
|
668
|
+
y_offset = normalize_converted_obb_part(x[4], 1)
|
|
669
|
+
cos_value = torch.cos(angle)
|
|
670
|
+
sin_value = torch.sin(angle)
|
|
671
|
+
center_offset = torch.cat(
|
|
672
|
+
[
|
|
673
|
+
x_offset * cos_value - y_offset * sin_value,
|
|
674
|
+
x_offset * sin_value + y_offset * cos_value,
|
|
675
|
+
],
|
|
676
|
+
dim=-1,
|
|
677
|
+
)
|
|
678
|
+
anchors_t = (
|
|
679
|
+
anchors.transpose(0, 1).unsqueeze(0).to(device=wh.device, dtype=wh.dtype)
|
|
680
|
+
)
|
|
681
|
+
stride_t = stride.transpose(0, 1).unsqueeze(0).to(device=wh.device, dtype=wh.dtype)
|
|
682
|
+
if anchors_t.shape[1] < wh.shape[1]:
|
|
683
|
+
raise ValueError(
|
|
684
|
+
f"Got {wh.shape[1]} OBB coordinate rows but only {anchors_t.shape[1]} anchors."
|
|
685
|
+
)
|
|
686
|
+
anchors_t = anchors_t[:, : wh.shape[1]]
|
|
687
|
+
stride_t = stride_t[:, : wh.shape[1]]
|
|
688
|
+
box = torch.cat([anchors_t + center_offset, wh], dim=-1) * stride_t
|
|
689
|
+
return torch.cat([box, scores, angle], dim=-1)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
# --- Scaling & Clipping Utilities ---
|
|
693
|
+
@overload
|
|
694
|
+
def scale_boxes(
|
|
695
|
+
img1_shape: tuple[int, int],
|
|
696
|
+
boxes: np.ndarray,
|
|
697
|
+
img0_shape: tuple[int, int],
|
|
698
|
+
ratio_pad: tuple[tuple[float, float], tuple[float, float]] | None = None,
|
|
699
|
+
padding: bool = True,
|
|
700
|
+
) -> np.ndarray: ...
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
@overload
|
|
704
|
+
def scale_boxes(
|
|
705
|
+
img1_shape: tuple[int, int],
|
|
706
|
+
boxes: torch.Tensor,
|
|
707
|
+
img0_shape: tuple[int, int],
|
|
708
|
+
ratio_pad: tuple[tuple[float, float], tuple[float, float]] | None = None,
|
|
709
|
+
padding: bool = True,
|
|
710
|
+
) -> torch.Tensor: ...
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def scale_boxes(
|
|
714
|
+
img1_shape: tuple[int, int],
|
|
715
|
+
boxes: np.ndarray | torch.Tensor,
|
|
716
|
+
img0_shape: tuple[int, int],
|
|
717
|
+
ratio_pad: tuple[tuple[float, float], tuple[float, float]] | None = None,
|
|
718
|
+
padding: bool = True,
|
|
719
|
+
) -> np.ndarray | torch.Tensor:
|
|
720
|
+
"""
|
|
721
|
+
Original Source: https://github.com/ultralytics/ultralytics/blob/main/ultralytics/utils/ops.py#L92
|
|
722
|
+
Rescales bounding boxes (in the format of xyxy) from the shape of the image they
|
|
723
|
+
were originally specified in (img1_shape) to the shape of a different image (img0_shape).
|
|
724
|
+
Args:
|
|
725
|
+
img1_shape (tuple): The shape of the image that the bounding boxes are for,
|
|
726
|
+
in the format of (height, width).
|
|
727
|
+
boxes (np.ndarray | torch.Tensor): the bounding boxes of the objects in the image,
|
|
728
|
+
in the format of (x1, y1, x2, y2)
|
|
729
|
+
img0_shape (tuple): the shape of the target image, in the format of (height, width).
|
|
730
|
+
ratio_pad (tuple): a tuple of (ratio, pad) for scaling the boxes.
|
|
731
|
+
If not provided, the ratio and pad will be calculated based on the size
|
|
732
|
+
difference between the two images.
|
|
733
|
+
padding (bool): If True, assuming the boxes is based on image augmented by
|
|
734
|
+
yolo style. If False then do regular rescaling.
|
|
735
|
+
Returns:
|
|
736
|
+
np.ndarray | torch.Tensor: The scaled bounding boxes, in the format of (x1, y1, x2, y2)
|
|
737
|
+
"""
|
|
738
|
+
ratio, pad = resolve_ratio_pad(img1_shape, img0_shape, ratio_pad)
|
|
739
|
+
gain = ratio[0]
|
|
740
|
+
if isinstance(boxes, np.ndarray):
|
|
741
|
+
if padding:
|
|
742
|
+
boxes[..., [0, 2]] -= pad[0] # x padding
|
|
743
|
+
boxes[..., [1, 3]] -= pad[1] # y padding
|
|
744
|
+
boxes[..., :4] /= gain
|
|
745
|
+
return clip_boxes(boxes, img0_shape)
|
|
746
|
+
if padding:
|
|
747
|
+
boxes[..., [0, 2]] -= pad[0] # x padding
|
|
748
|
+
boxes[..., [1, 3]] -= pad[1] # y padding
|
|
749
|
+
boxes[..., :4] /= gain
|
|
750
|
+
return clip_boxes(boxes, img0_shape)
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
@overload
|
|
754
|
+
def scale_coords(
|
|
755
|
+
img1_shape: tuple[int, int],
|
|
756
|
+
coords: np.ndarray,
|
|
757
|
+
img0_shape: tuple[int, int],
|
|
758
|
+
ratio_pad: tuple[tuple[float, float], tuple[float, float]] | None = None,
|
|
759
|
+
padding: bool = True,
|
|
760
|
+
) -> np.ndarray: ...
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
@overload
|
|
764
|
+
def scale_coords(
|
|
765
|
+
img1_shape: tuple[int, int],
|
|
766
|
+
coords: torch.Tensor,
|
|
767
|
+
img0_shape: tuple[int, int],
|
|
768
|
+
ratio_pad: tuple[tuple[float, float], tuple[float, float]] | None = None,
|
|
769
|
+
padding: bool = True,
|
|
770
|
+
) -> torch.Tensor: ...
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def scale_coords(
|
|
774
|
+
img1_shape: tuple[int, int],
|
|
775
|
+
coords: np.ndarray | torch.Tensor,
|
|
776
|
+
img0_shape: tuple[int, int],
|
|
777
|
+
ratio_pad: tuple[tuple[float, float], tuple[float, float]] | None = None,
|
|
778
|
+
padding: bool = True,
|
|
779
|
+
) -> np.ndarray | torch.Tensor:
|
|
780
|
+
"""
|
|
781
|
+
Original Source:
|
|
782
|
+
https://github.com/ultralytics/ultralytics/blob/main/ultralytics/utils/ops.py#L756
|
|
783
|
+
Args:
|
|
784
|
+
img1_shape (tuple): The shape of the image that the bounding boxes are for, in the format of (height, width).
|
|
785
|
+
coords (np.ndarray | torch.Tensor): The coordinates of the objects in the image, in the format of (x, y).
|
|
786
|
+
img0_shape (tuple): The shape of the target image, in the format of (height, width).
|
|
787
|
+
ratio_pad (tuple): a tuple of (ratio, pad) for scaling the boxes. If not provided, the ratio and pad will be
|
|
788
|
+
calculated based on the size difference between the two images.
|
|
789
|
+
padding (bool): If True, assuming the boxes is based on image augmented by yolo style. If False then do regular
|
|
790
|
+
rescaling.
|
|
791
|
+
Returns:
|
|
792
|
+
np.ndarray | torch.Tensor: The scaled coordinates, in the format of (x, y)
|
|
793
|
+
"""
|
|
794
|
+
ratio, pad = resolve_ratio_pad(img1_shape, img0_shape, ratio_pad)
|
|
795
|
+
gain = ratio[0]
|
|
796
|
+
if isinstance(coords, np.ndarray):
|
|
797
|
+
if padding:
|
|
798
|
+
coords[..., 0] -= pad[0] # x padding
|
|
799
|
+
coords[..., 1] -= pad[1] # y padding
|
|
800
|
+
coords[..., :2] /= gain
|
|
801
|
+
return clip_coords(coords, img0_shape)
|
|
802
|
+
if padding:
|
|
803
|
+
coords[..., 0] -= pad[0] # x padding
|
|
804
|
+
coords[..., 1] -= pad[1] # y padding
|
|
805
|
+
coords[..., :2] /= gain
|
|
806
|
+
return clip_coords(coords, img0_shape)
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def scale_rboxes(
|
|
810
|
+
img1_shape: tuple[int, int],
|
|
811
|
+
rboxes: torch.Tensor,
|
|
812
|
+
img0_shape: tuple[int, int],
|
|
813
|
+
ratio_pad: tuple[tuple[float, float], tuple[float, float]] | None = None,
|
|
814
|
+
padding: bool = True,
|
|
815
|
+
) -> torch.Tensor:
|
|
816
|
+
"""Rescale rotated boxes from model input size to an original image size.
|
|
817
|
+
|
|
818
|
+
Args:
|
|
819
|
+
img1_shape: Processed image shape.
|
|
820
|
+
rboxes: Rotated boxes in ``xywhr`` format.
|
|
821
|
+
img0_shape: Original image shape.
|
|
822
|
+
ratio_pad: Optional precomputed resize ratio and padding.
|
|
823
|
+
padding: Whether YOLO-style letterbox padding was applied.
|
|
824
|
+
|
|
825
|
+
Returns:
|
|
826
|
+
Rescaled rotated boxes in ``xywhr`` format.
|
|
827
|
+
"""
|
|
828
|
+
ratio, pad = resolve_ratio_pad(img1_shape, img0_shape, ratio_pad)
|
|
829
|
+
gain = ratio[0]
|
|
830
|
+
scaled = rboxes.clone()
|
|
831
|
+
if padding:
|
|
832
|
+
scaled[..., 0] -= pad[0]
|
|
833
|
+
scaled[..., 1] -= pad[1]
|
|
834
|
+
scaled[..., :4] /= gain
|
|
835
|
+
return scaled
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def compute_ratio_pad(
|
|
839
|
+
img1_shape: tuple[int, int],
|
|
840
|
+
img0_shape: tuple[int, int],
|
|
841
|
+
ratio_pad: tuple[tuple[float, float], tuple[float, float]] | None = None,
|
|
842
|
+
) -> tuple[float, tuple[float, float]]:
|
|
843
|
+
"""Return letterbox gain and padding for compatibility with existing callers.
|
|
844
|
+
|
|
845
|
+
Args:
|
|
846
|
+
img1_shape (tuple): The target shape (height, width).
|
|
847
|
+
img0_shape (tuple): The original shape (height, width).
|
|
848
|
+
ratio_pad (tuple, optional): Pre-calculated (ratio, pad) tuple.
|
|
849
|
+
If None, it will be calculated from the shapes. Defaults to None.
|
|
850
|
+
|
|
851
|
+
Returns:
|
|
852
|
+
tuple: (gain, pad) where gain is the scaling factor and pad is the (x, y) padding.
|
|
853
|
+
"""
|
|
854
|
+
ratio, pad = resolve_ratio_pad(img1_shape, img0_shape, ratio_pad)
|
|
855
|
+
return ratio[0], pad
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
@overload
|
|
859
|
+
def clip_boxes(boxes: np.ndarray, shape: tuple[int, int]) -> np.ndarray: ...
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
@overload
|
|
863
|
+
def clip_boxes(boxes: torch.Tensor, shape: tuple[int, int]) -> torch.Tensor: ...
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def clip_boxes(
|
|
867
|
+
boxes: np.ndarray | torch.Tensor, shape: tuple[int, int]
|
|
868
|
+
) -> np.ndarray | torch.Tensor:
|
|
869
|
+
"""
|
|
870
|
+
Clip bounding boxes to image shape.
|
|
871
|
+
Args:
|
|
872
|
+
boxes (np.ndarray | torch.Tensor): Bounding boxes.
|
|
873
|
+
shape (tuple): Image shape (height, width).
|
|
874
|
+
Returns:
|
|
875
|
+
np.ndarray | torch.Tensor: Clipped bounding boxes.
|
|
876
|
+
"""
|
|
877
|
+
if isinstance(boxes, torch.Tensor):
|
|
878
|
+
boxes[..., 0] = boxes[..., 0].clamp(0, shape[1])
|
|
879
|
+
boxes[..., 1] = boxes[..., 1].clamp(0, shape[0])
|
|
880
|
+
boxes[..., 2] = boxes[..., 2].clamp(0, shape[1])
|
|
881
|
+
boxes[..., 3] = boxes[..., 3].clamp(0, shape[0])
|
|
882
|
+
else:
|
|
883
|
+
boxes[..., 0] = np.clip(boxes[..., 0], 0, shape[1])
|
|
884
|
+
boxes[..., 1] = np.clip(boxes[..., 1], 0, shape[0])
|
|
885
|
+
boxes[..., 2] = np.clip(boxes[..., 2], 0, shape[1])
|
|
886
|
+
boxes[..., 3] = np.clip(boxes[..., 3], 0, shape[0])
|
|
887
|
+
return boxes
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
@overload
|
|
891
|
+
def clip_coords(coords: np.ndarray, shape: tuple[int, int]) -> np.ndarray: ...
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
@overload
|
|
895
|
+
def clip_coords(coords: torch.Tensor, shape: tuple[int, int]) -> torch.Tensor: ...
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
def clip_coords(
|
|
899
|
+
coords: np.ndarray | torch.Tensor, shape: tuple[int, int]
|
|
900
|
+
) -> np.ndarray | torch.Tensor:
|
|
901
|
+
"""Clips coordinates to the image shape.
|
|
902
|
+
|
|
903
|
+
Args:
|
|
904
|
+
coords (np.ndarray | torch.Tensor): Coordinates to clip.
|
|
905
|
+
shape (tuple): Image shape (height, width).
|
|
906
|
+
|
|
907
|
+
Returns:
|
|
908
|
+
np.ndarray | torch.Tensor: Clipped coordinates.
|
|
909
|
+
"""
|
|
910
|
+
if isinstance(coords, torch.Tensor):
|
|
911
|
+
coords[..., 0] = coords[..., 0].clamp(0, shape[1])
|
|
912
|
+
coords[..., 1] = coords[..., 1].clamp(0, shape[0])
|
|
913
|
+
else:
|
|
914
|
+
coords[..., 0] = np.clip(coords[..., 0], 0, shape[1])
|
|
915
|
+
coords[..., 1] = np.clip(coords[..., 1], 0, shape[0])
|
|
916
|
+
return coords
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
# --- Segmentation Utilities ---
|
|
920
|
+
def process_mask(
|
|
921
|
+
protos: torch.Tensor,
|
|
922
|
+
masks_in: torch.Tensor,
|
|
923
|
+
bboxes: torch.Tensor,
|
|
924
|
+
shape: tuple[int, int],
|
|
925
|
+
upsample: bool = False,
|
|
926
|
+
) -> torch.Tensor:
|
|
927
|
+
"""Processes masks by applying coefficients to prototypes and cropping.
|
|
928
|
+
|
|
929
|
+
Ref: https://github.com/ultralytics/ultralytics/blob/main/ultralytics/utils/ops.py#L680
|
|
930
|
+
|
|
931
|
+
Args:
|
|
932
|
+
protos (torch.Tensor): Prototype masks of shape [mask_dim, mask_h, mask_w].
|
|
933
|
+
masks_in (torch.Tensor): Mask coefficients of shape [n, mask_dim].
|
|
934
|
+
bboxes (torch.Tensor): Bounding boxes of shape [n, 4].
|
|
935
|
+
shape (tuple): Input image size (h, w).
|
|
936
|
+
upsample (bool, optional): Whether to upsample the masks to the original image size.
|
|
937
|
+
Defaults to False.
|
|
938
|
+
|
|
939
|
+
Returns:
|
|
940
|
+
torch.Tensor: Processed binary masks.
|
|
941
|
+
"""
|
|
942
|
+
c, mh, mw = protos.shape # CHW
|
|
943
|
+
ih, iw = shape
|
|
944
|
+
masks = (masks_in @ protos.float().view(c, -1)).view(-1, mh, mw) # n, CHW
|
|
945
|
+
downsampled_bboxes = bboxes.clone()
|
|
946
|
+
downsampled_bboxes[:, 0] *= mw / iw
|
|
947
|
+
downsampled_bboxes[:, 2] *= mw / iw
|
|
948
|
+
downsampled_bboxes[:, 3] *= mh / ih
|
|
949
|
+
downsampled_bboxes[:, 1] *= mh / ih
|
|
950
|
+
masks = crop_mask(masks, downsampled_bboxes) # CHW
|
|
951
|
+
if upsample:
|
|
952
|
+
masks = F.interpolate(masks[None], shape, mode="bilinear", align_corners=False)[
|
|
953
|
+
0
|
|
954
|
+
] # CHW
|
|
955
|
+
return masks.gt_(0.0)
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
def process_mask_upsample(
|
|
959
|
+
protos: torch.Tensor,
|
|
960
|
+
masks_in: torch.Tensor,
|
|
961
|
+
bboxes: torch.Tensor,
|
|
962
|
+
shape: tuple[int, int] | list[int],
|
|
963
|
+
) -> torch.Tensor:
|
|
964
|
+
"""Applies masks to bounding boxes with upsampling for higher quality.
|
|
965
|
+
|
|
966
|
+
Ref: https://github.com/ultralytics/ultralytics/blob/main/ultralytics/utils/ops.py#L713
|
|
967
|
+
This produces higher quality masks than `process_mask` but is slower.
|
|
968
|
+
|
|
969
|
+
Args:
|
|
970
|
+
protos (torch.Tensor): Prototype masks of shape [mask_dim, mask_h, mask_w].
|
|
971
|
+
masks_in (torch.Tensor): Mask coefficients of shape [n, mask_dim].
|
|
972
|
+
bboxes (torch.Tensor): Bounding boxes of shape [n, 4].
|
|
973
|
+
shape (tuple): Target image size (h, w).
|
|
974
|
+
|
|
975
|
+
Returns:
|
|
976
|
+
torch.Tensor: Upsampled and thresholded binary masks.
|
|
977
|
+
"""
|
|
978
|
+
target_shape = (int(shape[0]), int(shape[1]))
|
|
979
|
+
c, mh, mw = protos.shape # CHW
|
|
980
|
+
|
|
981
|
+
# Evaluate only the prototype pixels that contribute to each retained ROI.
|
|
982
|
+
# The ROI interpolation preserves the original global bilinear sampling
|
|
983
|
+
# coordinates, so it produces the same binary mask as the full-mask path.
|
|
984
|
+
if _use_roi_prototype_masks(masks_in, bboxes, c, mh, mw, target_shape):
|
|
985
|
+
return _process_mask_upsample_roi(protos, masks_in, bboxes, target_shape)
|
|
986
|
+
|
|
987
|
+
masks = (masks_in @ protos.float().view(c, -1)).view(-1, mh, mw) # n, CHW
|
|
988
|
+
masks = scale_masks(masks, target_shape) # CHW
|
|
989
|
+
masks = crop_mask(masks, bboxes) # CHW
|
|
990
|
+
return masks.gt_(0.0)
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
def _use_roi_prototype_masks(
|
|
994
|
+
masks_in: torch.Tensor,
|
|
995
|
+
bboxes: torch.Tensor,
|
|
996
|
+
channels: int,
|
|
997
|
+
proto_h: int,
|
|
998
|
+
proto_w: int,
|
|
999
|
+
shape: tuple[int, int],
|
|
1000
|
+
) -> bool:
|
|
1001
|
+
"""Return whether exact low-resolution ROI masking is expected to be cheaper."""
|
|
1002
|
+
count = masks_in.shape[0]
|
|
1003
|
+
if bboxes.numel() == 0:
|
|
1004
|
+
return False
|
|
1005
|
+
height, width = shape
|
|
1006
|
+
clipped = bboxes[:, :4].clone()
|
|
1007
|
+
clipped[:, 0::2].clamp_(0, width)
|
|
1008
|
+
clipped[:, 1::2].clamp_(0, height)
|
|
1009
|
+
roi_pixels = (
|
|
1010
|
+
(clipped[:, 2] - clipped[:, 0]).clamp_min_(0).ceil()
|
|
1011
|
+
* (clipped[:, 3] - clipped[:, 1]).clamp_min_(0).ceil()
|
|
1012
|
+
).sum()
|
|
1013
|
+
# This is a conservative upper-bound for ROI work: the actual dot product
|
|
1014
|
+
# runs at prototype resolution, while interpolation touches only ROI pixels.
|
|
1015
|
+
full_work = count * (channels * proto_h * proto_w + height * width)
|
|
1016
|
+
roi_work = channels * height * width + channels * roi_pixels
|
|
1017
|
+
return bool(roi_work < full_work)
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
def _process_mask_upsample_roi(
|
|
1021
|
+
protos: torch.Tensor,
|
|
1022
|
+
masks_in: torch.Tensor,
|
|
1023
|
+
bboxes: torch.Tensor,
|
|
1024
|
+
shape: tuple[int, int],
|
|
1025
|
+
) -> torch.Tensor:
|
|
1026
|
+
"""Create exact cropped masks from low-resolution prototype ROIs.
|
|
1027
|
+
|
|
1028
|
+
The interpolation grid uses global ``align_corners=False`` coordinates.
|
|
1029
|
+
Therefore every target pixel samples the same prototype neighborhood as
|
|
1030
|
+
``scale_masks(coefficients @ protos)`` without evaluating pixels outside
|
|
1031
|
+
its bounding box.
|
|
1032
|
+
"""
|
|
1033
|
+
protos = protos.float()
|
|
1034
|
+
channels, proto_h, proto_w = protos.shape
|
|
1035
|
+
height, width = shape
|
|
1036
|
+
top, left, bottom, right = _mask_scale_crop_bounds((proto_h, proto_w), shape)
|
|
1037
|
+
crop_h, crop_w = bottom - top, right - left
|
|
1038
|
+
masks = torch.zeros(
|
|
1039
|
+
(masks_in.shape[0], height, width), dtype=torch.float32, device=protos.device
|
|
1040
|
+
)
|
|
1041
|
+
boxes = bboxes.to(protos.device)
|
|
1042
|
+
for index, box in enumerate(boxes):
|
|
1043
|
+
x1 = max(0, min(width, math.ceil(float(box[0]))))
|
|
1044
|
+
y1 = max(0, min(height, math.ceil(float(box[1]))))
|
|
1045
|
+
x2 = max(0, min(width, math.ceil(float(box[2]))))
|
|
1046
|
+
y2 = max(0, min(height, math.ceil(float(box[3]))))
|
|
1047
|
+
if x1 >= x2 or y1 >= y2:
|
|
1048
|
+
continue
|
|
1049
|
+
proto_x1 = max(0, math.floor((x1 + 0.5) * crop_w / width - 0.5))
|
|
1050
|
+
proto_y1 = max(0, math.floor((y1 + 0.5) * crop_h / height - 0.5))
|
|
1051
|
+
proto_x2 = min(crop_w, math.floor((x2 - 0.5) * crop_w / width - 0.5) + 2)
|
|
1052
|
+
proto_y2 = min(crop_h, math.floor((y2 - 0.5) * crop_h / height - 0.5) + 2)
|
|
1053
|
+
proto_x1, proto_x2 = left + proto_x1, left + proto_x2
|
|
1054
|
+
proto_y1, proto_y2 = top + proto_y1, top + proto_y2
|
|
1055
|
+
prototype_roi = protos[:, proto_y1:proto_y2, proto_x1:proto_x2]
|
|
1056
|
+
lowres_mask = (masks_in[index] @ prototype_roi.reshape(channels, -1)).reshape(
|
|
1057
|
+
1, 1, proto_y2 - proto_y1, proto_x2 - proto_x1
|
|
1058
|
+
)
|
|
1059
|
+
ys = torch.arange(y1, y2, device=protos.device, dtype=torch.float32)
|
|
1060
|
+
xs = torch.arange(x1, x2, device=protos.device, dtype=torch.float32)
|
|
1061
|
+
global_y, global_x = torch.meshgrid(ys, xs, indexing="ij")
|
|
1062
|
+
local_y = (global_y + 0.5) * crop_h / height - 0.5 - (proto_y1 - top)
|
|
1063
|
+
local_x = (global_x + 0.5) * crop_w / width - 0.5 - (proto_x1 - left)
|
|
1064
|
+
grid = torch.stack(
|
|
1065
|
+
(
|
|
1066
|
+
(local_x + 0.5) * 2 / (proto_x2 - proto_x1) - 1,
|
|
1067
|
+
(local_y + 0.5) * 2 / (proto_y2 - proto_y1) - 1,
|
|
1068
|
+
),
|
|
1069
|
+
dim=-1,
|
|
1070
|
+
).unsqueeze(0)
|
|
1071
|
+
masks[index, y1:y2, x1:x2] = F.grid_sample(
|
|
1072
|
+
lowres_mask,
|
|
1073
|
+
grid,
|
|
1074
|
+
mode="bilinear",
|
|
1075
|
+
padding_mode="border",
|
|
1076
|
+
align_corners=False,
|
|
1077
|
+
)[0, 0]
|
|
1078
|
+
return masks.gt_(0.0)
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
def _mask_scale_crop_bounds(
|
|
1082
|
+
mask_shape: tuple[int, int], target_shape: tuple[int, int]
|
|
1083
|
+
) -> tuple[int, int, int, int]:
|
|
1084
|
+
"""Return the crop applied by :func:`scale_masks` before interpolation."""
|
|
1085
|
+
mask_h, mask_w = mask_shape
|
|
1086
|
+
target_h, target_w = target_shape
|
|
1087
|
+
gain = min(mask_h / target_h, mask_w / target_w)
|
|
1088
|
+
pad_w = (mask_w - round(target_w * gain)) / 2
|
|
1089
|
+
pad_h = (mask_h - round(target_h * gain)) / 2
|
|
1090
|
+
top, left = round(pad_h - 0.1), round(pad_w - 0.1)
|
|
1091
|
+
bottom, right = mask_h - round(pad_h + 0.1), mask_w - round(pad_w + 0.1)
|
|
1092
|
+
return top, left, bottom, right
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
def crop_mask(masks: torch.Tensor, boxes: torch.Tensor) -> torch.Tensor:
|
|
1096
|
+
"""Crops masks to bounding boxes.
|
|
1097
|
+
|
|
1098
|
+
Args:
|
|
1099
|
+
masks (torch.Tensor): Masks of shape [n, h, w].
|
|
1100
|
+
boxes (torch.Tensor): Bounding boxes of shape [n, 4] in (x1, y1, x2, y2) format.
|
|
1101
|
+
|
|
1102
|
+
Returns:
|
|
1103
|
+
torch.Tensor: Cropped masks.
|
|
1104
|
+
"""
|
|
1105
|
+
if boxes.device != masks.device:
|
|
1106
|
+
boxes = boxes.to(masks.device)
|
|
1107
|
+
_, h, w = masks.shape
|
|
1108
|
+
x1, y1, x2, y2 = torch.chunk(boxes[:, :, None], 4, 1)
|
|
1109
|
+
rows = torch.arange(w, device=masks.device, dtype=x1.dtype)[None, None, :]
|
|
1110
|
+
cols = torch.arange(h, device=masks.device, dtype=x1.dtype)[None, :, None]
|
|
1111
|
+
return masks * ((rows >= x1) * (rows < x2) * (cols >= y1) * (cols < y2))
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
def scale_masks(
|
|
1115
|
+
masks: torch.Tensor,
|
|
1116
|
+
shape: tuple[int, int],
|
|
1117
|
+
ratio_pad: tuple[tuple[float, float], tuple[float, float]] | None = None,
|
|
1118
|
+
padding: bool = True,
|
|
1119
|
+
) -> torch.Tensor:
|
|
1120
|
+
"""Rescales segment masks to the target shape.
|
|
1121
|
+
|
|
1122
|
+
Args:
|
|
1123
|
+
masks (torch.Tensor): Input masks of shape (C, H, W).
|
|
1124
|
+
shape (tuple): Target shape (height, width).
|
|
1125
|
+
ratio_pad (tuple, optional): Pre-calculated (ratio, pad) tuple.
|
|
1126
|
+
If None, it will be calculated from the shapes. Defaults to None.
|
|
1127
|
+
padding (bool, optional): If True, assumes the masks were generated from
|
|
1128
|
+
an image with YOLO-style padding. Defaults to True.
|
|
1129
|
+
|
|
1130
|
+
Returns:
|
|
1131
|
+
torch.Tensor: Rescaled masks of shape (C, target_h, target_w).
|
|
1132
|
+
"""
|
|
1133
|
+
im1_h, im1_w = masks.shape[1:]
|
|
1134
|
+
im0_h, im0_w = shape[:2]
|
|
1135
|
+
if masks.numel() == 0:
|
|
1136
|
+
return torch.zeros((0, im0_h, im0_w), dtype=masks.dtype, device=masks.device)
|
|
1137
|
+
if im1_h == im0_h and im1_w == im0_w:
|
|
1138
|
+
return masks
|
|
1139
|
+
if ratio_pad is None: # calculate from im0_shape
|
|
1140
|
+
gain = min(im1_h / im0_h, im1_w / im0_w) # gain = old / new
|
|
1141
|
+
pad_w, pad_h = (
|
|
1142
|
+
(im1_w - round(im0_w * gain)),
|
|
1143
|
+
(im1_h - round(im0_h * gain)),
|
|
1144
|
+
) # wh padding
|
|
1145
|
+
if padding:
|
|
1146
|
+
pad_w /= 2
|
|
1147
|
+
pad_h /= 2
|
|
1148
|
+
else:
|
|
1149
|
+
pad_w, pad_h = ratio_pad[1]
|
|
1150
|
+
top, left = (round(pad_h - 0.1), round(pad_w - 0.1)) if padding else (0, 0)
|
|
1151
|
+
bottom, right = im1_h - round(pad_h + 0.1), im1_w - round(pad_w + 0.1)
|
|
1152
|
+
masks = masks[..., top:bottom, left:right]
|
|
1153
|
+
if isinstance(masks, np.ndarray):
|
|
1154
|
+
masks = torch.from_numpy(masks)
|
|
1155
|
+
masks = F.interpolate(
|
|
1156
|
+
masks[None], shape, mode="bilinear", align_corners=False
|
|
1157
|
+
) # 1NHW
|
|
1158
|
+
return masks[0]
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
def to_string(counts: list[int]) -> str:
|
|
1162
|
+
"""Converts the RLE object into a compact string representation.
|
|
1163
|
+
|
|
1164
|
+
Each count is delta-encoded and variable-length encoded as a string.
|
|
1165
|
+
|
|
1166
|
+
Args:
|
|
1167
|
+
counts (list[int]): List of RLE counts.
|
|
1168
|
+
|
|
1169
|
+
Returns:
|
|
1170
|
+
str: Compact string representation of the RLE object.
|
|
1171
|
+
"""
|
|
1172
|
+
result = []
|
|
1173
|
+
|
|
1174
|
+
for i, x in enumerate(counts):
|
|
1175
|
+
x = int(x)
|
|
1176
|
+
|
|
1177
|
+
# Apply delta encoding for all counts after the second entry
|
|
1178
|
+
if i > 2:
|
|
1179
|
+
x -= int(counts[i - 2])
|
|
1180
|
+
|
|
1181
|
+
# Variable-length encode the value
|
|
1182
|
+
while True:
|
|
1183
|
+
c = x & 0x1F # Take 5 bits
|
|
1184
|
+
x >>= 5
|
|
1185
|
+
|
|
1186
|
+
# If the sign bit (0x10) is set, continue if x != -1;
|
|
1187
|
+
# otherwise, continue if x != 0
|
|
1188
|
+
more = (x != -1) if (c & 0x10) else (x != 0)
|
|
1189
|
+
if more:
|
|
1190
|
+
c |= 0x20 # Set continuation bit
|
|
1191
|
+
c += 48 # Shift to ASCII
|
|
1192
|
+
result.append(chr(c))
|
|
1193
|
+
if not more:
|
|
1194
|
+
break
|
|
1195
|
+
|
|
1196
|
+
return "".join(result)
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
def multi_encode(pixels: torch.Tensor) -> list[list[int]]:
|
|
1200
|
+
"""Convert multiple binary masks using Run-Length Encoding (RLE).
|
|
1201
|
+
|
|
1202
|
+
Args:
|
|
1203
|
+
pixels (torch.Tensor): A 2D tensor where each row represents a flattened binary mask
|
|
1204
|
+
with shape [N, H*W].
|
|
1205
|
+
|
|
1206
|
+
Returns:
|
|
1207
|
+
list[list[int]]: A list of RLE counts for each mask.
|
|
1208
|
+
"""
|
|
1209
|
+
pixel_rows = pixels.detach().cpu().numpy().astype(np.uint8, copy=False)
|
|
1210
|
+
width = pixel_rows.shape[1]
|
|
1211
|
+
counts = []
|
|
1212
|
+
for i in range(pixel_rows.shape[0]):
|
|
1213
|
+
pixel_row = pixel_rows[i]
|
|
1214
|
+
positions = np.flatnonzero(pixel_row[1:] != pixel_row[:-1]) + 1
|
|
1215
|
+
if positions.size:
|
|
1216
|
+
count = np.diff(positions).tolist()
|
|
1217
|
+
count.insert(0, int(positions[0]))
|
|
1218
|
+
count.append(int(width - positions[-1]))
|
|
1219
|
+
else:
|
|
1220
|
+
count = [width]
|
|
1221
|
+
if pixel_row[0] == 1:
|
|
1222
|
+
count = [0, *count]
|
|
1223
|
+
counts.append(count)
|
|
1224
|
+
|
|
1225
|
+
return counts
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
def _encode_segmentation_masks(seg_result: torch.Tensor) -> list[dict[str, Any]]:
|
|
1229
|
+
"""Threshold resized instance masks and encode them as COCO RLE objects."""
|
|
1230
|
+
|
|
1231
|
+
h, w = seg_result.shape[1:3]
|
|
1232
|
+
binary_masks = seg_result > 0.5
|
|
1233
|
+
encoded_pixels = (
|
|
1234
|
+
binary_masks.permute(0, 2, 1)
|
|
1235
|
+
.contiguous()
|
|
1236
|
+
.view(binary_masks.shape[0], h * w)
|
|
1237
|
+
.to(torch.uint8)
|
|
1238
|
+
)
|
|
1239
|
+
counts = multi_encode(encoded_pixels)
|
|
1240
|
+
if len(counts) != encoded_pixels.shape[0]:
|
|
1241
|
+
raise RuntimeError(
|
|
1242
|
+
f"Encoded {len(counts)} masks for a mask tensor batch of {encoded_pixels.shape[0]}."
|
|
1243
|
+
)
|
|
1244
|
+
return [{"size": [h, w], "counts": to_string(count)} for count in counts]
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
def nmsout2eval(
|
|
1248
|
+
nms_outs: list[torch.Tensor] | torch.Tensor,
|
|
1249
|
+
img1_shape: tuple[int, int],
|
|
1250
|
+
img0_shapes: tuple[int, int] | Sequence[tuple[int, int]],
|
|
1251
|
+
ratio_pads: RatioPad | Sequence[RatioPad | None] | None = None,
|
|
1252
|
+
) -> tuple[list[list[int]], list[list[list[float]]], list[list[float]]]:
|
|
1253
|
+
"""Converts NMS output to COCO evaluation format.
|
|
1254
|
+
|
|
1255
|
+
Args:
|
|
1256
|
+
nms_outs (list[torch.Tensor] | torch.Tensor): The output of the NMS
|
|
1257
|
+
operation of shape (n, 6), where n is the number of objects.
|
|
1258
|
+
img1_shape (tuple): Processed image shape (H, W).
|
|
1259
|
+
img0_shapes (list[tuple]): Original image shapes [(H, W), ...].
|
|
1260
|
+
|
|
1261
|
+
Returns:
|
|
1262
|
+
tuple: A tuple containing:
|
|
1263
|
+
- labels (list[list]): The labels of the objects for each image.
|
|
1264
|
+
- boxes (list[list]): The bounding boxes (xywh) for each image.
|
|
1265
|
+
- scores (list[list]): The confidence scores for each image.
|
|
1266
|
+
"""
|
|
1267
|
+
|
|
1268
|
+
if not isinstance(nms_outs, list):
|
|
1269
|
+
nms_outs = [nms_outs]
|
|
1270
|
+
actual_img0_shapes = normalize_image_shapes(img0_shapes, len(nms_outs))
|
|
1271
|
+
actual_ratio_pads = normalize_ratio_pads(ratio_pads, len(nms_outs))
|
|
1272
|
+
labels_list: list[list[int]] = []
|
|
1273
|
+
boxes_list: list[list[list[float]]] = []
|
|
1274
|
+
scores_list: list[list[float]] = []
|
|
1275
|
+
for nms_out, img0_shape, ratio_pad in zip(
|
|
1276
|
+
nms_outs, actual_img0_shapes, actual_ratio_pads
|
|
1277
|
+
):
|
|
1278
|
+
boxes = nms_out[:, :4].clone()
|
|
1279
|
+
scores = nms_out[:, 4]
|
|
1280
|
+
labels = nms_out[:, 5]
|
|
1281
|
+
valid_labels = (
|
|
1282
|
+
torch.isfinite(labels)
|
|
1283
|
+
& (labels == labels.round())
|
|
1284
|
+
& (labels >= 0)
|
|
1285
|
+
& (labels < 80)
|
|
1286
|
+
)
|
|
1287
|
+
if not bool(valid_labels.all()):
|
|
1288
|
+
invalid_labels = labels[~valid_labels].detach().cpu().tolist()
|
|
1289
|
+
raise ValueError(
|
|
1290
|
+
"COCO class IDs must be finite integral values in [0, 79]; "
|
|
1291
|
+
f"got {invalid_labels}."
|
|
1292
|
+
)
|
|
1293
|
+
boxes = scale_boxes(
|
|
1294
|
+
img1_shape, boxes, img0_shape, ratio_pad=ratio_pad
|
|
1295
|
+
) # scale boxes to original image size
|
|
1296
|
+
boxes[:, 2:] = boxes[:, 2:] - boxes[:, :2] # xyxy to xywh with corner xy
|
|
1297
|
+
|
|
1298
|
+
boxes_tolist = [
|
|
1299
|
+
[round(float(value), 3) for value in box] for box in boxes.tolist()
|
|
1300
|
+
]
|
|
1301
|
+
scores_tolist = [round(float(score), 5) for score in scores.tolist()]
|
|
1302
|
+
labels_tolist = labels.tolist()
|
|
1303
|
+
labels_res = [get_coco_inv(int(label)) for label in labels_tolist]
|
|
1304
|
+
|
|
1305
|
+
labels_list.append(labels_res)
|
|
1306
|
+
boxes_list.append(boxes_tolist)
|
|
1307
|
+
scores_list.append(scores_tolist)
|
|
1308
|
+
|
|
1309
|
+
return labels_list, boxes_list, scores_list
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
def nmsout2eval_seg(
|
|
1313
|
+
nms_outs: Any,
|
|
1314
|
+
img1_shape: tuple[int, int],
|
|
1315
|
+
img0_shapes: tuple[int, int] | list[tuple[int, int]],
|
|
1316
|
+
ratio_pads: RatioPad | list[RatioPad | None] | None = None,
|
|
1317
|
+
) -> tuple[
|
|
1318
|
+
list[list[int]],
|
|
1319
|
+
list[list[list[float]]],
|
|
1320
|
+
list[list[float]],
|
|
1321
|
+
list[list[dict[str, Any]]],
|
|
1322
|
+
]:
|
|
1323
|
+
"""Converts segmentation NMS output to COCO evaluation format.
|
|
1324
|
+
|
|
1325
|
+
Args:
|
|
1326
|
+
nms_outs (Union[list, tuple]): Segmentation postprocess output in one of two forms:
|
|
1327
|
+
`(det_result, seg_result)` for a single image or a list of those pairs for a batch.
|
|
1328
|
+
img1_shape (tuple): Processed image shape (H, W).
|
|
1329
|
+
img0_shapes (tuple | list[tuple]): Original image shape for a single image or
|
|
1330
|
+
a list of original shapes for a batch.
|
|
1331
|
+
|
|
1332
|
+
Returns:
|
|
1333
|
+
tuple: A tuple containing:
|
|
1334
|
+
- labels (list[list]): The labels of the objects for each image.
|
|
1335
|
+
- boxes (list[list]): The bounding boxes (xywh) for each image.
|
|
1336
|
+
- scores (list[list]): The confidence scores for each image.
|
|
1337
|
+
- extra (list[list]): The encoded segmentation masks for each image.
|
|
1338
|
+
"""
|
|
1339
|
+
actual_img0_shapes = normalize_image_shapes(img0_shapes)
|
|
1340
|
+
actual_ratio_pads = normalize_ratio_pads(ratio_pads, len(actual_img0_shapes))
|
|
1341
|
+
|
|
1342
|
+
if not isinstance(nms_outs[0], (list, tuple)):
|
|
1343
|
+
actual_nms_outs = [nms_outs]
|
|
1344
|
+
else:
|
|
1345
|
+
actual_nms_outs = nms_outs
|
|
1346
|
+
|
|
1347
|
+
det_results = []
|
|
1348
|
+
seg_results = []
|
|
1349
|
+
for nms_out in actual_nms_outs:
|
|
1350
|
+
det_results.append(nms_out[0])
|
|
1351
|
+
seg_results.append(nms_out[1])
|
|
1352
|
+
|
|
1353
|
+
labels_list, boxes_list, scores_list = nmsout2eval(
|
|
1354
|
+
det_results,
|
|
1355
|
+
img1_shape,
|
|
1356
|
+
actual_img0_shapes,
|
|
1357
|
+
ratio_pads=actual_ratio_pads,
|
|
1358
|
+
)
|
|
1359
|
+
|
|
1360
|
+
scaled_seg_results = [
|
|
1361
|
+
scale_masks(
|
|
1362
|
+
seg_result.to(torch.float32),
|
|
1363
|
+
(img0_shape[0], img0_shape[1]),
|
|
1364
|
+
ratio_pad=ratio_pad,
|
|
1365
|
+
)
|
|
1366
|
+
for seg_result, img0_shape, ratio_pad in zip(
|
|
1367
|
+
seg_results, actual_img0_shapes, actual_ratio_pads
|
|
1368
|
+
)
|
|
1369
|
+
]
|
|
1370
|
+
|
|
1371
|
+
extra_list = [
|
|
1372
|
+
_encode_segmentation_masks(seg_result) for seg_result in scaled_seg_results
|
|
1373
|
+
]
|
|
1374
|
+
for labels, boxes, scores, extra in zip(
|
|
1375
|
+
labels_list, boxes_list, scores_list, extra_list
|
|
1376
|
+
):
|
|
1377
|
+
if not len(labels) == len(boxes) == len(scores) == len(extra):
|
|
1378
|
+
raise RuntimeError(
|
|
1379
|
+
"Segmentation evaluation produced mismatched label, box, score, and mask counts."
|
|
1380
|
+
)
|
|
1381
|
+
return labels_list, boxes_list, scores_list, extra_list
|
|
1382
|
+
|
|
1383
|
+
|
|
1384
|
+
def nmsout2eval_pose(
|
|
1385
|
+
nms_outs: list[torch.Tensor] | torch.Tensor,
|
|
1386
|
+
img1_shape: tuple[int, int],
|
|
1387
|
+
img0_shapes: tuple[int, int] | list[tuple[int, int]],
|
|
1388
|
+
ratio_pads: RatioPad | list[RatioPad | None] | None = None,
|
|
1389
|
+
) -> tuple[
|
|
1390
|
+
list[list[int]], list[list[list[float]]], list[list[float]], list[list[list[float]]]
|
|
1391
|
+
]:
|
|
1392
|
+
"""Converts pose estimation NMS output to COCO evaluation format.
|
|
1393
|
+
|
|
1394
|
+
Args:
|
|
1395
|
+
nms_outs (list): The output of the NMS operation.
|
|
1396
|
+
img1_shape (tuple): Processed image shape (H, W).
|
|
1397
|
+
img0_shapes (list[tuple]): Original image shapes [(H, W), ...].
|
|
1398
|
+
|
|
1399
|
+
Returns:
|
|
1400
|
+
tuple: A tuple containing:
|
|
1401
|
+
- labels (list[list]): The labels of the objects for each image.
|
|
1402
|
+
- boxes (list[list]): The bounding boxes (xywh) for each image.
|
|
1403
|
+
- scores (list[list]): The confidence scores for each image.
|
|
1404
|
+
- keypoints (list[list]): The scaled keypoints for each image.
|
|
1405
|
+
"""
|
|
1406
|
+
actual_img0_shapes = normalize_image_shapes(img0_shapes)
|
|
1407
|
+
actual_ratio_pads = normalize_ratio_pads(ratio_pads, len(actual_img0_shapes))
|
|
1408
|
+
if not isinstance(nms_outs, list):
|
|
1409
|
+
actual_nms_outs = [nms_outs]
|
|
1410
|
+
else:
|
|
1411
|
+
actual_nms_outs = nms_outs
|
|
1412
|
+
labels_list, boxes_list, scores_list = nmsout2eval(
|
|
1413
|
+
actual_nms_outs,
|
|
1414
|
+
img1_shape,
|
|
1415
|
+
actual_img0_shapes,
|
|
1416
|
+
ratio_pads=actual_ratio_pads,
|
|
1417
|
+
)
|
|
1418
|
+
extra = [
|
|
1419
|
+
scale_coords(
|
|
1420
|
+
img1_shape,
|
|
1421
|
+
nms_out[:, 6:].reshape(-1, 17, 3),
|
|
1422
|
+
img0_shape,
|
|
1423
|
+
ratio_pad=ratio_pad,
|
|
1424
|
+
).reshape(-1, 51)
|
|
1425
|
+
for nms_out, img0_shape, ratio_pad in zip(
|
|
1426
|
+
actual_nms_outs, actual_img0_shapes, actual_ratio_pads
|
|
1427
|
+
)
|
|
1428
|
+
]
|
|
1429
|
+
return labels_list, boxes_list, scores_list, [x.tolist() for x in extra]
|
|
1430
|
+
|
|
1431
|
+
|
|
1432
|
+
def nmsout2eval_obb(
|
|
1433
|
+
nms_outs: list[torch.Tensor] | torch.Tensor,
|
|
1434
|
+
img1_shape: tuple[int, int],
|
|
1435
|
+
img0_shapes: tuple[int, int] | list[tuple[int, int]],
|
|
1436
|
+
ratio_pads: RatioPad | list[RatioPad | None] | None = None,
|
|
1437
|
+
include_xywhr: bool = False,
|
|
1438
|
+
) -> tuple[Any, ...]:
|
|
1439
|
+
"""Converts OBB NMS output to DOTAv1 evaluation format.
|
|
1440
|
+
|
|
1441
|
+
Args:
|
|
1442
|
+
nms_outs: Detections with rows ``cx, cy, w, h, score, cls, angle``.
|
|
1443
|
+
img1_shape: Processed image shape.
|
|
1444
|
+
img0_shapes: Original image shape or shapes.
|
|
1445
|
+
ratio_pads: Optional letterbox metadata.
|
|
1446
|
+
include_xywhr: Whether to include scaled ``xywhr`` boxes in the return value.
|
|
1447
|
+
|
|
1448
|
+
Returns:
|
|
1449
|
+
DOTAv1 labels, polygons, scores, and optionally scaled ``xywhr`` boxes.
|
|
1450
|
+
"""
|
|
1451
|
+
actual_img0_shapes = normalize_image_shapes(img0_shapes)
|
|
1452
|
+
actual_ratio_pads = normalize_ratio_pads(ratio_pads, len(actual_img0_shapes))
|
|
1453
|
+
actual_nms_outs = [nms_outs] if not isinstance(nms_outs, list) else nms_outs
|
|
1454
|
+
|
|
1455
|
+
labels_list: list[list[str]] = []
|
|
1456
|
+
polygons_list: list[list[list[float]]] = []
|
|
1457
|
+
scores_list: list[list[float]] = []
|
|
1458
|
+
xywhr_list: list[list[list[float]]] = []
|
|
1459
|
+
for nms_out, img0_shape, ratio_pad in zip(
|
|
1460
|
+
actual_nms_outs, actual_img0_shapes, actual_ratio_pads
|
|
1461
|
+
):
|
|
1462
|
+
if nms_out.numel() == 0:
|
|
1463
|
+
labels_list.append([])
|
|
1464
|
+
polygons_list.append([])
|
|
1465
|
+
scores_list.append([])
|
|
1466
|
+
xywhr_list.append([])
|
|
1467
|
+
continue
|
|
1468
|
+
|
|
1469
|
+
rboxes = torch.cat([nms_out[:, :4], nms_out[:, 6:7]], dim=-1)
|
|
1470
|
+
rboxes = scale_rboxes(img1_shape, rboxes, img0_shape, ratio_pad=ratio_pad)
|
|
1471
|
+
polygons = xywhr2xyxyxyxy(rboxes).reshape(-1, 8)
|
|
1472
|
+
polygons = scale_coords(
|
|
1473
|
+
img0_shape, polygons.reshape(-1, 4, 2), img0_shape
|
|
1474
|
+
).reshape(-1, 8)
|
|
1475
|
+
|
|
1476
|
+
labels = [get_dotav1_label(int(label)) for label in nms_out[:, 5].tolist()]
|
|
1477
|
+
scores = [round(float(score), 5) for score in nms_out[:, 4].tolist()]
|
|
1478
|
+
polygons_tolist = [
|
|
1479
|
+
[round(float(value), 3) for value in polygon]
|
|
1480
|
+
for polygon in polygons.tolist()
|
|
1481
|
+
]
|
|
1482
|
+
xywhr_tolist = [
|
|
1483
|
+
[round(float(value), 3) for value in rbox] for rbox in rboxes.tolist()
|
|
1484
|
+
]
|
|
1485
|
+
|
|
1486
|
+
labels_list.append(labels)
|
|
1487
|
+
polygons_list.append(polygons_tolist)
|
|
1488
|
+
scores_list.append(scores)
|
|
1489
|
+
xywhr_list.append(xywhr_tolist)
|
|
1490
|
+
|
|
1491
|
+
if include_xywhr:
|
|
1492
|
+
return labels_list, polygons_list, scores_list, xywhr_list
|
|
1493
|
+
return labels_list, polygons_list, scores_list
|
|
1494
|
+
|
|
1495
|
+
|
|
1496
|
+
class YOLOSegPostMixin:
|
|
1497
|
+
"""Mixin class for YOLO segmentation postprocessing."""
|
|
1498
|
+
|
|
1499
|
+
def nmsout2eval(
|
|
1500
|
+
self,
|
|
1501
|
+
nms_out: Any,
|
|
1502
|
+
img1_shape: tuple[int, int],
|
|
1503
|
+
img0_shape: tuple[int, int] | list[tuple[int, int]],
|
|
1504
|
+
ratio_pad: RatioPad | list[RatioPad | None] | None = None,
|
|
1505
|
+
) -> tuple[Any, ...]:
|
|
1506
|
+
"""Converts NMS output to evaluation format for segmentation.
|
|
1507
|
+
|
|
1508
|
+
Args:
|
|
1509
|
+
nms_out: NMS output (detections and prototypes).
|
|
1510
|
+
img1_shape: Resized image shape.
|
|
1511
|
+
img0_shape: List of original image shapes.
|
|
1512
|
+
|
|
1513
|
+
Returns:
|
|
1514
|
+
Tuple: (labels_list, boxes_list, scores_list, extra_list).
|
|
1515
|
+
"""
|
|
1516
|
+
return nmsout2eval_seg(nms_out, img1_shape, img0_shape, ratio_pads=ratio_pad)
|
|
1517
|
+
|
|
1518
|
+
|
|
1519
|
+
class YOLOPosePostMixin:
|
|
1520
|
+
"""Mixin class for YOLO pose estimation postprocessing."""
|
|
1521
|
+
|
|
1522
|
+
def nmsout2eval(
|
|
1523
|
+
self,
|
|
1524
|
+
nms_out: Any,
|
|
1525
|
+
img1_shape: tuple[int, int],
|
|
1526
|
+
img0_shape: tuple[int, int] | list[tuple[int, int]],
|
|
1527
|
+
ratio_pad: RatioPad | list[RatioPad | None] | None = None,
|
|
1528
|
+
) -> tuple[Any, ...]:
|
|
1529
|
+
"""Converts NMS output to evaluation format for pose estimation.
|
|
1530
|
+
|
|
1531
|
+
Args:
|
|
1532
|
+
nms_out: NMS output (detections with keypoints).
|
|
1533
|
+
img1_shape: Resized image shape.
|
|
1534
|
+
img0_shape: List of original image shapes.
|
|
1535
|
+
|
|
1536
|
+
Returns:
|
|
1537
|
+
Tuple: (labels_list, boxes_list, scores_list, extra_list).
|
|
1538
|
+
"""
|
|
1539
|
+
return nmsout2eval_pose(nms_out, img1_shape, img0_shape, ratio_pads=ratio_pad)
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
class YOLOOBBPostMixin:
|
|
1543
|
+
"""Mixin class for YOLO oriented-bounding-box postprocessing."""
|
|
1544
|
+
|
|
1545
|
+
def nmsout2eval(
|
|
1546
|
+
self,
|
|
1547
|
+
nms_out: Any,
|
|
1548
|
+
img1_shape: tuple[int, int],
|
|
1549
|
+
img0_shape: tuple[int, int] | list[tuple[int, int]],
|
|
1550
|
+
ratio_pad: RatioPad | list[RatioPad | None] | None = None,
|
|
1551
|
+
include_xywhr: bool = False,
|
|
1552
|
+
) -> tuple[Any, ...]:
|
|
1553
|
+
"""Converts OBB detections to DOTAv1 labels, polygons, and scores.
|
|
1554
|
+
|
|
1555
|
+
Args:
|
|
1556
|
+
nms_out: NMS output with rows ``cx, cy, w, h, score, cls, angle``.
|
|
1557
|
+
img1_shape: Resized image shape.
|
|
1558
|
+
img0_shape: Original image shape or shapes.
|
|
1559
|
+
ratio_pad: Optional letterbox metadata.
|
|
1560
|
+
include_xywhr: Whether to include scaled rotated boxes.
|
|
1561
|
+
|
|
1562
|
+
Returns:
|
|
1563
|
+
DOTAv1 labels, polygons, scores, and optionally scaled ``xywhr`` boxes.
|
|
1564
|
+
"""
|
|
1565
|
+
return nmsout2eval_obb(
|
|
1566
|
+
nms_out,
|
|
1567
|
+
img1_shape,
|
|
1568
|
+
img0_shape,
|
|
1569
|
+
ratio_pads=ratio_pad,
|
|
1570
|
+
include_xywhr=include_xywhr,
|
|
1571
|
+
)
|