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,2145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities for organizing datasets.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import concurrent.futures
|
|
8
|
+
import cv2
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import math
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import shutil
|
|
15
|
+
import stat
|
|
16
|
+
import tarfile
|
|
17
|
+
import xml.etree.ElementTree as ET
|
|
18
|
+
import zipfile
|
|
19
|
+
from collections.abc import Iterable
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from tempfile import TemporaryDirectory, mkdtemp
|
|
22
|
+
from time import sleep
|
|
23
|
+
from typing import Protocol, TypeGuard
|
|
24
|
+
from urllib.parse import urlparse
|
|
25
|
+
|
|
26
|
+
import requests
|
|
27
|
+
import numpy as np
|
|
28
|
+
from gdown.download import download
|
|
29
|
+
from gdown.download_folder import download_folder
|
|
30
|
+
from PIL import Image
|
|
31
|
+
from tqdm import tqdm
|
|
32
|
+
|
|
33
|
+
from ...datasets import get_dataset_config
|
|
34
|
+
from .cityscapes import CITYSCAPES_SOURCE_TO_TRAIN_ID
|
|
35
|
+
from .readiness import (
|
|
36
|
+
ADE20K_METADATA_FILES,
|
|
37
|
+
ADE20K_VALIDATION_SAMPLE_COUNT,
|
|
38
|
+
CITYSCAPES_SAMPLE_ID_PATTERN,
|
|
39
|
+
CITYSCAPES_VALIDATION_SAMPLE_COUNT,
|
|
40
|
+
DOTAV1_VALIDATION_SAMPLE_COUNT,
|
|
41
|
+
IMAGE_SUFFIXES,
|
|
42
|
+
NYU_DEPTH_VALIDATION_SAMPLE_COUNT,
|
|
43
|
+
_canonicalize_quadrilateral,
|
|
44
|
+
_path_has_symlink_component,
|
|
45
|
+
_polygon_has_positive_image_overlap,
|
|
46
|
+
dataset_ready,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
DOWNLOAD_CHUNK_SIZE = 1 * 1024 * 1024
|
|
50
|
+
DOWNLOAD_RETRY_LIMIT = 4
|
|
51
|
+
DOWNLOAD_RETRY_BACKOFF_SECONDS = 2.0
|
|
52
|
+
DOWNLOAD_TIMEOUT = (10, 30)
|
|
53
|
+
DOTAV1_DOWNLOAD_CONFIG = get_dataset_config("dotav1")["download"]
|
|
54
|
+
DOTAV1_GOOGLE_DRIVE_ARCHIVES = {
|
|
55
|
+
DOTAV1_DOWNLOAD_CONFIG["images_archive"],
|
|
56
|
+
DOTAV1_DOWNLOAD_CONFIG["labels_archive"],
|
|
57
|
+
}
|
|
58
|
+
DOTAV1_CLASS_TO_IDX = {
|
|
59
|
+
name: int(index) for index, name in get_dataset_config("dotav1")["names"].items()
|
|
60
|
+
}
|
|
61
|
+
COCO_DOWNLOAD_CONFIG = get_dataset_config("coco")["download"]
|
|
62
|
+
ADE20K_DOWNLOAD_CONFIG = get_dataset_config("ade20k")["download"]
|
|
63
|
+
NYU_DEPTH_URL = (
|
|
64
|
+
"https://github.com/ultralytics/assets/releases/download/v0.0.0/nyu-depth.zip"
|
|
65
|
+
)
|
|
66
|
+
ADE20K_URL = ADE20K_DOWNLOAD_CONFIG["url"]
|
|
67
|
+
CITYSCAPES_IMAGE_SUFFIX = "_leftImg8bit.png"
|
|
68
|
+
CITYSCAPES_ANNOTATION_SUFFIX = "_gtFine_labelIds.png"
|
|
69
|
+
IMAGENET_SYNSET_PATTERN = re.compile(r"n\d{8}")
|
|
70
|
+
RETRYABLE_HTTP_STATUS_CODES = frozenset({408, 429})
|
|
71
|
+
CONTENT_RANGE_PATTERN = re.compile(r"^bytes (\d+)-(\d+)/(\d+|\*)$")
|
|
72
|
+
UNSATISFIABLE_CONTENT_RANGE_PATTERN = re.compile(r"^bytes \*/(\d+)$")
|
|
73
|
+
PINNED_ARCHIVE_SHA256 = {
|
|
74
|
+
COCO_DOWNLOAD_CONFIG["images"]: COCO_DOWNLOAD_CONFIG["images_sha256"],
|
|
75
|
+
COCO_DOWNLOAD_CONFIG["annotations"]: COCO_DOWNLOAD_CONFIG["annotations_sha256"],
|
|
76
|
+
ADE20K_DOWNLOAD_CONFIG["url"]: ADE20K_DOWNLOAD_CONFIG["sha256"],
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _resolve_organizer_output_dir(output_dir: str | None, dataset_name: str) -> str:
|
|
81
|
+
"""Return an explicit output directory or the lazily resolved artifact cache."""
|
|
82
|
+
|
|
83
|
+
if output_dir is not None:
|
|
84
|
+
return os.path.expanduser(output_dir)
|
|
85
|
+
from mblt_vision.wrapper import get_mobilint_cache_dir
|
|
86
|
+
|
|
87
|
+
return os.path.join(get_mobilint_cache_dir(), "datasets", dataset_name)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _replace_staged_directories(
|
|
91
|
+
replacements: Iterable[tuple[str, str]],
|
|
92
|
+
output_parent_dir: str,
|
|
93
|
+
backup_prefix: str,
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Atomically install staged directories while preserving failed rollback backups.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
replacements: Pairs of staged and destination directories.
|
|
99
|
+
output_parent_dir: Parent directory where the backup directory is created.
|
|
100
|
+
backup_prefix: Prefix identifying the temporary backup directory.
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
OSError: If installation or rollback fails. A failed rollback leaves its
|
|
104
|
+
backup directory in place and includes its path in the error.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
replacement_list = list(replacements)
|
|
108
|
+
backup_dir = mkdtemp(dir=output_parent_dir, prefix=backup_prefix)
|
|
109
|
+
backups: dict[str, str] = {}
|
|
110
|
+
installed_dirs: list[str] = []
|
|
111
|
+
try:
|
|
112
|
+
for _, destination_dir in replacement_list:
|
|
113
|
+
if os.path.lexists(destination_dir):
|
|
114
|
+
backup_path = os.path.join(
|
|
115
|
+
backup_dir, os.path.basename(destination_dir)
|
|
116
|
+
)
|
|
117
|
+
os.replace(destination_dir, backup_path)
|
|
118
|
+
backups[destination_dir] = backup_path
|
|
119
|
+
for staged_dir, destination_dir in replacement_list:
|
|
120
|
+
os.makedirs(os.path.dirname(destination_dir), exist_ok=True)
|
|
121
|
+
os.replace(staged_dir, destination_dir)
|
|
122
|
+
installed_dirs.append(destination_dir)
|
|
123
|
+
except OSError:
|
|
124
|
+
try:
|
|
125
|
+
for directory in installed_dirs:
|
|
126
|
+
if os.path.isdir(directory) and not os.path.islink(directory):
|
|
127
|
+
shutil.rmtree(directory)
|
|
128
|
+
elif os.path.lexists(directory):
|
|
129
|
+
os.remove(directory)
|
|
130
|
+
for destination_dir, backup_path in backups.items():
|
|
131
|
+
os.makedirs(os.path.dirname(destination_dir), exist_ok=True)
|
|
132
|
+
os.replace(backup_path, destination_dir)
|
|
133
|
+
except OSError as rollback_error:
|
|
134
|
+
raise OSError(
|
|
135
|
+
f"Dataset installation rollback failed; backups are preserved at {backup_dir}."
|
|
136
|
+
) from rollback_error
|
|
137
|
+
shutil.rmtree(backup_dir)
|
|
138
|
+
raise
|
|
139
|
+
shutil.rmtree(backup_dir)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _validate_staged_dataset(
|
|
143
|
+
staged_output_dir: str,
|
|
144
|
+
dataset: str,
|
|
145
|
+
tasks: Iterable[str],
|
|
146
|
+
) -> None:
|
|
147
|
+
"""Validate a complete staged dataset before replacing its managed cache.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
staged_output_dir: Root of the staged organized dataset.
|
|
151
|
+
dataset: Validation dataset taxonomy.
|
|
152
|
+
tasks: Tasks whose required metadata and files must all be ready.
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
ValueError: If the staged dataset is incomplete or has mismatched identity.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
if not all(dataset_ready(staged_output_dir, task, dataset) for task in tasks):
|
|
159
|
+
raise ValueError(
|
|
160
|
+
f"Staged {dataset} validation dataset is incomplete or has mismatched metadata; "
|
|
161
|
+
"the existing dataset cache was not replaced."
|
|
162
|
+
)
|
|
163
|
+
_validate_staged_payloads(Path(staged_output_dir), dataset)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _validate_staged_payloads(staged_root: Path, dataset: str) -> None:
|
|
167
|
+
"""Decode staged data files before a structurally valid cache is replaced."""
|
|
168
|
+
|
|
169
|
+
image_roots = {
|
|
170
|
+
"imagenet": (staged_root,),
|
|
171
|
+
"widerface": (staged_root / "images",),
|
|
172
|
+
"dotav1": (staged_root / "images",),
|
|
173
|
+
"ade20k": (staged_root / "images",),
|
|
174
|
+
"cityscapes": (staged_root / "images",),
|
|
175
|
+
}
|
|
176
|
+
for image_root in image_roots.get(dataset, ()):
|
|
177
|
+
for image_path in image_root.rglob("*"):
|
|
178
|
+
if image_path.is_file() and image_path.suffix.lower() in {
|
|
179
|
+
".bmp",
|
|
180
|
+
".jpeg",
|
|
181
|
+
".jpg",
|
|
182
|
+
".png",
|
|
183
|
+
".tif",
|
|
184
|
+
".tiff",
|
|
185
|
+
".webp",
|
|
186
|
+
}:
|
|
187
|
+
if cv2.imread(str(image_path), cv2.IMREAD_COLOR) is None:
|
|
188
|
+
raise ValueError(
|
|
189
|
+
f"Staged {dataset} image is unreadable: {image_path}."
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if dataset == "coco":
|
|
193
|
+
_validate_staged_coco_image_geometry(staged_root)
|
|
194
|
+
elif dataset in {"ade20k", "cityscapes"}:
|
|
195
|
+
_validate_staged_semantic_masks(staged_root, dataset)
|
|
196
|
+
elif dataset == "dotav1":
|
|
197
|
+
_validate_staged_dotav1_labels(staged_root)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _validate_staged_coco_image_geometry(staged_root: Path) -> None:
|
|
201
|
+
"""Compare every staged COCO image with its JSON-declared geometry."""
|
|
202
|
+
|
|
203
|
+
image_root = staged_root / "val2017"
|
|
204
|
+
decoded_shapes: dict[str, tuple[int, int]] = {}
|
|
205
|
+
for annotation_path in sorted(staged_root.glob("*_val2017.json")):
|
|
206
|
+
try:
|
|
207
|
+
annotation = json.loads(annotation_path.read_text(encoding="utf-8"))
|
|
208
|
+
image_records = annotation["images"]
|
|
209
|
+
except (json.JSONDecodeError, KeyError, OSError, TypeError) as exc:
|
|
210
|
+
raise ValueError(
|
|
211
|
+
f"Staged COCO annotation is unreadable: {annotation_path}."
|
|
212
|
+
) from exc
|
|
213
|
+
if not isinstance(image_records, list):
|
|
214
|
+
raise ValueError(
|
|
215
|
+
f"Staged COCO annotation has an invalid images table: {annotation_path}."
|
|
216
|
+
)
|
|
217
|
+
for record in image_records:
|
|
218
|
+
if not isinstance(record, dict):
|
|
219
|
+
raise ValueError(
|
|
220
|
+
f"Staged COCO annotation has an invalid image record: {annotation_path}."
|
|
221
|
+
)
|
|
222
|
+
file_name, height, width = (
|
|
223
|
+
record.get("file_name"),
|
|
224
|
+
record.get("height"),
|
|
225
|
+
record.get("width"),
|
|
226
|
+
)
|
|
227
|
+
if (
|
|
228
|
+
not isinstance(file_name, str)
|
|
229
|
+
or not file_name
|
|
230
|
+
or Path(file_name).is_absolute()
|
|
231
|
+
or ".." in Path(file_name).parts
|
|
232
|
+
or not isinstance(height, int)
|
|
233
|
+
or isinstance(height, bool)
|
|
234
|
+
or not isinstance(width, int)
|
|
235
|
+
or isinstance(width, bool)
|
|
236
|
+
or height <= 0
|
|
237
|
+
or width <= 0
|
|
238
|
+
):
|
|
239
|
+
raise ValueError(
|
|
240
|
+
f"Staged COCO image metadata is invalid: {annotation_path}."
|
|
241
|
+
)
|
|
242
|
+
image_shape = decoded_shapes.get(file_name)
|
|
243
|
+
if image_shape is None:
|
|
244
|
+
image = cv2.imread(str(image_root / file_name), cv2.IMREAD_COLOR)
|
|
245
|
+
if image is None:
|
|
246
|
+
raise ValueError(
|
|
247
|
+
f"Staged COCO image is unreadable: {image_root / file_name}."
|
|
248
|
+
)
|
|
249
|
+
image_shape = (int(image.shape[0]), int(image.shape[1]))
|
|
250
|
+
decoded_shapes[file_name] = image_shape
|
|
251
|
+
if image_shape != (height, width):
|
|
252
|
+
raise ValueError(
|
|
253
|
+
"Staged COCO image geometry does not match annotation metadata for "
|
|
254
|
+
f"{file_name}: image {image_shape}, annotation {(height, width)}."
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _validate_staged_semantic_masks(staged_root: Path, dataset: str) -> None:
|
|
259
|
+
"""Validate decoded semantic targets against their paired staged images."""
|
|
260
|
+
|
|
261
|
+
image_dir = staged_root / "images"
|
|
262
|
+
annotation_dir = staged_root / "annotations"
|
|
263
|
+
for annotation_path in sorted(annotation_dir.glob("*.png")):
|
|
264
|
+
image_path = next(
|
|
265
|
+
(
|
|
266
|
+
candidate
|
|
267
|
+
for candidate in image_dir.glob(f"{annotation_path.stem}.*")
|
|
268
|
+
if candidate.suffix.lower() in {".jpg", ".jpeg", ".png"}
|
|
269
|
+
),
|
|
270
|
+
None,
|
|
271
|
+
)
|
|
272
|
+
if image_path is None:
|
|
273
|
+
raise ValueError(
|
|
274
|
+
f"Staged {dataset} target has no paired image: {annotation_path}."
|
|
275
|
+
)
|
|
276
|
+
image = cv2.imread(str(image_path), cv2.IMREAD_COLOR)
|
|
277
|
+
try:
|
|
278
|
+
with Image.open(annotation_path) as annotation_image:
|
|
279
|
+
annotation = np.asarray(annotation_image)
|
|
280
|
+
except OSError as exc:
|
|
281
|
+
raise ValueError(
|
|
282
|
+
f"Staged {dataset} annotation is unreadable: {annotation_path}."
|
|
283
|
+
) from exc
|
|
284
|
+
if dataset == "cityscapes" and annotation.ndim == 3:
|
|
285
|
+
if (
|
|
286
|
+
annotation.shape[2] not in {3, 4}
|
|
287
|
+
or not np.array_equal(annotation[..., 0], annotation[..., 1])
|
|
288
|
+
or not np.array_equal(annotation[..., 0], annotation[..., 2])
|
|
289
|
+
):
|
|
290
|
+
raise ValueError(
|
|
291
|
+
f"Staged Cityscapes annotation must be grayscale or RGB-grayscale: {annotation_path}."
|
|
292
|
+
)
|
|
293
|
+
annotation = annotation[..., 0]
|
|
294
|
+
if image is None or annotation.ndim != 2 or annotation.shape != image.shape[:2]:
|
|
295
|
+
raise ValueError(
|
|
296
|
+
f"Staged {dataset} image and annotation geometry is invalid: {annotation_path}."
|
|
297
|
+
)
|
|
298
|
+
if dataset == "ade20k" and (
|
|
299
|
+
annotation.dtype != np.uint8
|
|
300
|
+
or (annotation.size and int(annotation.max()) > 150)
|
|
301
|
+
):
|
|
302
|
+
raise ValueError(
|
|
303
|
+
f"Staged ADE20K annotation must be an 8-bit mask with values in [0, 150]: {annotation_path}."
|
|
304
|
+
)
|
|
305
|
+
if dataset == "cityscapes":
|
|
306
|
+
valid_ids = (annotation <= 33) | (annotation == 255)
|
|
307
|
+
if not np.all(valid_ids):
|
|
308
|
+
invalid_ids = np.unique(annotation[~valid_ids])
|
|
309
|
+
raise ValueError(
|
|
310
|
+
"Staged Cityscapes annotation contains unsupported source IDs "
|
|
311
|
+
f"{invalid_ids.tolist()}; expected IDs in [0, 33] or 255: {annotation_path}."
|
|
312
|
+
)
|
|
313
|
+
has_evaluable_class = bool(
|
|
314
|
+
(
|
|
315
|
+
CITYSCAPES_SOURCE_TO_TRAIN_ID[annotation.astype(np.uint8)] != 255
|
|
316
|
+
).any()
|
|
317
|
+
)
|
|
318
|
+
else:
|
|
319
|
+
has_evaluable_class = bool((annotation > 0).any())
|
|
320
|
+
if not has_evaluable_class:
|
|
321
|
+
raise ValueError(
|
|
322
|
+
f"Staged {dataset} annotation contains no evaluable class IDs: {annotation_path}."
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _validate_staged_dotav1_labels(staged_root: Path) -> None:
|
|
327
|
+
"""Validate both DOTAv1 label representations before cache replacement."""
|
|
328
|
+
|
|
329
|
+
label_dirs = {
|
|
330
|
+
"normalized": staged_root / "labels" / "val",
|
|
331
|
+
"original": staged_root / "labels" / "val_original",
|
|
332
|
+
}
|
|
333
|
+
image_paths = {
|
|
334
|
+
path.stem: path
|
|
335
|
+
for path in (staged_root / "images").iterdir()
|
|
336
|
+
if path.is_file() and path.suffix.lower() in IMAGE_SUFFIXES
|
|
337
|
+
}
|
|
338
|
+
valid_indices = set(DOTAV1_CLASS_TO_IDX.values())
|
|
339
|
+
label_stems = {kind: set() for kind in label_dirs}
|
|
340
|
+
positive_stems = {kind: set() for kind in label_dirs}
|
|
341
|
+
for kind, label_dir in label_dirs.items():
|
|
342
|
+
for label_path in sorted(label_dir.glob("*.txt")):
|
|
343
|
+
label_stems[kind].add(label_path.stem)
|
|
344
|
+
image_path = image_paths.get(label_path.stem)
|
|
345
|
+
if image_path is None:
|
|
346
|
+
raise ValueError(
|
|
347
|
+
f"Staged DOTAv1 label has no matching image: {label_path}."
|
|
348
|
+
)
|
|
349
|
+
image = cv2.imread(str(image_path), cv2.IMREAD_COLOR)
|
|
350
|
+
if image is None:
|
|
351
|
+
raise ValueError(f"Unable to decode staged DOTAv1 image: {image_path}.")
|
|
352
|
+
height, width = image.shape[:2]
|
|
353
|
+
has_positive_target = False
|
|
354
|
+
seen_targets: set[tuple[int | str, tuple[float, ...]]] = set()
|
|
355
|
+
for line_number, line in enumerate(
|
|
356
|
+
label_path.read_text(encoding="utf-8").splitlines(), start=1
|
|
357
|
+
):
|
|
358
|
+
fields = line.split()
|
|
359
|
+
if not fields or (
|
|
360
|
+
kind == "original"
|
|
361
|
+
and (
|
|
362
|
+
fields[0].startswith("imagesource:")
|
|
363
|
+
or fields[0].startswith("gsd:")
|
|
364
|
+
)
|
|
365
|
+
):
|
|
366
|
+
continue
|
|
367
|
+
min_fields = 9 if kind == "normalized" else 10
|
|
368
|
+
if len(fields) < min_fields:
|
|
369
|
+
raise ValueError(
|
|
370
|
+
f"Malformed staged DOTAv1 {kind} annotation at "
|
|
371
|
+
f"{label_path}:{line_number}: expected at least {min_fields} fields."
|
|
372
|
+
)
|
|
373
|
+
try:
|
|
374
|
+
coordinates = [
|
|
375
|
+
float(value)
|
|
376
|
+
for value in (
|
|
377
|
+
fields[1:9] if kind == "normalized" else fields[:8]
|
|
378
|
+
)
|
|
379
|
+
]
|
|
380
|
+
except ValueError as exc:
|
|
381
|
+
raise ValueError(
|
|
382
|
+
f"Malformed staged DOTAv1 coordinates at {label_path}:{line_number}."
|
|
383
|
+
) from exc
|
|
384
|
+
if not all(math.isfinite(value) for value in coordinates):
|
|
385
|
+
raise ValueError(
|
|
386
|
+
f"Staged DOTAv1 coordinates must be finite at {label_path}:{line_number}."
|
|
387
|
+
)
|
|
388
|
+
points = np.asarray(coordinates, dtype=np.float64).reshape(4, 2)
|
|
389
|
+
signed_double_area = np.dot(
|
|
390
|
+
points[:, 0], np.roll(points[:, 1], -1)
|
|
391
|
+
) - np.dot(points[:, 1], np.roll(points[:, 0], -1))
|
|
392
|
+
if abs(signed_double_area) <= 0:
|
|
393
|
+
raise ValueError(
|
|
394
|
+
f"Staged DOTAv1 polygon must have positive area at {label_path}:{line_number}."
|
|
395
|
+
)
|
|
396
|
+
_validate_dotav1_polygon_vertices(coordinates, label_path, line_number)
|
|
397
|
+
image_coordinates = coordinates.copy()
|
|
398
|
+
if kind == "normalized":
|
|
399
|
+
image_coordinates = [
|
|
400
|
+
coordinate * (width if index % 2 == 0 else height)
|
|
401
|
+
for index, coordinate in enumerate(coordinates)
|
|
402
|
+
]
|
|
403
|
+
if not _polygon_has_positive_image_overlap(
|
|
404
|
+
image_coordinates, (height, width)
|
|
405
|
+
):
|
|
406
|
+
raise ValueError(
|
|
407
|
+
"Staged DOTAv1 polygon must overlap its source image at "
|
|
408
|
+
f"{label_path}:{line_number}."
|
|
409
|
+
)
|
|
410
|
+
if kind == "normalized":
|
|
411
|
+
try:
|
|
412
|
+
class_index = int(fields[0])
|
|
413
|
+
except ValueError as exc:
|
|
414
|
+
raise ValueError(
|
|
415
|
+
f"Malformed staged DOTAv1 class index at {label_path}:{line_number}."
|
|
416
|
+
) from exc
|
|
417
|
+
difficulty = fields[9] if len(fields) >= 10 else "0"
|
|
418
|
+
if class_index not in valid_indices:
|
|
419
|
+
raise ValueError(
|
|
420
|
+
f"Unsupported staged DOTAv1 class index at {label_path}:{line_number}."
|
|
421
|
+
)
|
|
422
|
+
target_class: int | str = class_index
|
|
423
|
+
else:
|
|
424
|
+
difficulty = fields[9]
|
|
425
|
+
if fields[8] not in DOTAV1_CLASS_TO_IDX:
|
|
426
|
+
raise ValueError(
|
|
427
|
+
f"Unsupported staged DOTAv1 class at {label_path}:{line_number}."
|
|
428
|
+
)
|
|
429
|
+
target_class = fields[8]
|
|
430
|
+
if difficulty not in {"0", "1", "2"}:
|
|
431
|
+
raise ValueError(
|
|
432
|
+
f"Unsupported staged DOTAv1 difficulty flag at {label_path}:{line_number}."
|
|
433
|
+
)
|
|
434
|
+
target_key = (
|
|
435
|
+
target_class,
|
|
436
|
+
_canonicalize_quadrilateral(image_coordinates),
|
|
437
|
+
)
|
|
438
|
+
if target_key in seen_targets:
|
|
439
|
+
raise ValueError(
|
|
440
|
+
"Duplicate staged DOTAv1 annotation target at "
|
|
441
|
+
f"{label_path}:{line_number}."
|
|
442
|
+
)
|
|
443
|
+
seen_targets.add(target_key)
|
|
444
|
+
has_positive_target |= difficulty == "0"
|
|
445
|
+
if has_positive_target:
|
|
446
|
+
positive_stems[kind].add(label_path.stem)
|
|
447
|
+
authoritative_positive_stems = positive_stems["original"] | (
|
|
448
|
+
positive_stems["normalized"] - label_stems["original"]
|
|
449
|
+
)
|
|
450
|
+
if not authoritative_positive_stems:
|
|
451
|
+
raise ValueError(
|
|
452
|
+
"Staged DOTAv1 dataset must contain at least one non-difficult target."
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _validate_dotav1_polygon_vertices(
|
|
457
|
+
coordinates: list[float], annotation_path: str | Path, line_number: int
|
|
458
|
+
) -> None:
|
|
459
|
+
"""Require four distinct, consistently ordered DOTAv1 quadrilateral vertices."""
|
|
460
|
+
|
|
461
|
+
points = np.asarray(coordinates, dtype=np.float64).reshape(4, 2)
|
|
462
|
+
if len(np.unique(points, axis=0)) != 4:
|
|
463
|
+
raise ValueError(
|
|
464
|
+
"DOTAv1 polygon must contain four distinct vertices in "
|
|
465
|
+
f"{annotation_path} at line {line_number}."
|
|
466
|
+
)
|
|
467
|
+
edges = np.roll(points, -1, axis=0) - points
|
|
468
|
+
next_edges = np.roll(edges, -1, axis=0)
|
|
469
|
+
turns = edges[:, 0] * next_edges[:, 1] - edges[:, 1] * next_edges[:, 0]
|
|
470
|
+
if not (np.all(turns > 0) or np.all(turns < 0)):
|
|
471
|
+
raise ValueError(
|
|
472
|
+
"DOTAv1 polygon vertices must be consistently ordered in "
|
|
473
|
+
f"{annotation_path} at line {line_number}."
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
class _GoogleDriveDownloadEntry(Protocol):
|
|
478
|
+
"""The public attributes needed from a gdown folder-listing entry."""
|
|
479
|
+
|
|
480
|
+
id: str
|
|
481
|
+
path: str
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _is_google_drive_download_entry(
|
|
485
|
+
value: object,
|
|
486
|
+
) -> TypeGuard[_GoogleDriveDownloadEntry]:
|
|
487
|
+
"""Returns whether a folder-listing value has the Google Drive file attributes needed here."""
|
|
488
|
+
|
|
489
|
+
return isinstance(getattr(value, "id", None), str) and isinstance(
|
|
490
|
+
getattr(value, "path", None), str
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _is_url(path_or_url: str) -> bool:
|
|
495
|
+
"""Returns whether the given string looks like an HTTP(S) URL."""
|
|
496
|
+
parsed = urlparse(path_or_url)
|
|
497
|
+
return parsed.scheme in {"http", "https"} and bool(parsed.netloc)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _verify_archive_sha256(
|
|
501
|
+
archive_path: str, expected_sha256: str, source_url: str
|
|
502
|
+
) -> None:
|
|
503
|
+
"""Verify a downloaded archive before it can be extracted."""
|
|
504
|
+
|
|
505
|
+
digest = hashlib.sha256()
|
|
506
|
+
with open(archive_path, "rb") as archive:
|
|
507
|
+
for chunk in iter(lambda: archive.read(DOWNLOAD_CHUNK_SIZE), b""):
|
|
508
|
+
digest.update(chunk)
|
|
509
|
+
actual_sha256 = digest.hexdigest()
|
|
510
|
+
if actual_sha256 != expected_sha256:
|
|
511
|
+
Path(archive_path).unlink(missing_ok=True)
|
|
512
|
+
raise ValueError(
|
|
513
|
+
f"Downloaded archive SHA-256 mismatch for {source_url}: "
|
|
514
|
+
f"expected {expected_sha256}, got {actual_sha256}."
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _has_expected_resume_offset(content_range: str | None, existing_size: int) -> bool:
|
|
519
|
+
"""Return whether a partial response begins exactly after local archive bytes."""
|
|
520
|
+
|
|
521
|
+
if content_range is None:
|
|
522
|
+
return False
|
|
523
|
+
match = CONTENT_RANGE_PATTERN.fullmatch(content_range)
|
|
524
|
+
if match is None:
|
|
525
|
+
return False
|
|
526
|
+
start, end, total = match.groups()
|
|
527
|
+
return (
|
|
528
|
+
int(start) == existing_size
|
|
529
|
+
and int(end) >= int(start)
|
|
530
|
+
and (total == "*" or int(end) < int(total))
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _is_completed_range_response(content_range: str | None, existing_size: int) -> bool:
|
|
535
|
+
"""Return whether a 416 confirms that the local archive is complete."""
|
|
536
|
+
|
|
537
|
+
if content_range is None:
|
|
538
|
+
return False
|
|
539
|
+
match = UNSATISFIABLE_CONTENT_RANGE_PATTERN.fullmatch(content_range)
|
|
540
|
+
return match is not None and int(match.group(1)) == existing_size
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _restart_partial_download(local_path: str, url: str) -> None:
|
|
544
|
+
"""Discard an invalid partial archive before retrying from byte zero."""
|
|
545
|
+
|
|
546
|
+
Path(local_path).unlink(missing_ok=True)
|
|
547
|
+
print(
|
|
548
|
+
f"Server returned an invalid resume response for {os.path.basename(local_path)}; "
|
|
549
|
+
"restarting from byte zero."
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _download_url(url: str, local_path: str, expected_sha256: str | None = None) -> str:
|
|
554
|
+
"""Downloads a URL to a local file with progress and resume support.
|
|
555
|
+
|
|
556
|
+
Args:
|
|
557
|
+
url: HTTP(S) URL to download.
|
|
558
|
+
local_path: Destination file path.
|
|
559
|
+
expected_sha256: Optional pinned SHA-256 digest to verify before return.
|
|
560
|
+
|
|
561
|
+
Returns:
|
|
562
|
+
The local destination path.
|
|
563
|
+
|
|
564
|
+
Raises:
|
|
565
|
+
RuntimeError: If all download attempts fail.
|
|
566
|
+
"""
|
|
567
|
+
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
|
568
|
+
|
|
569
|
+
for attempt in range(1, DOWNLOAD_RETRY_LIMIT + 1):
|
|
570
|
+
existing_size = os.path.getsize(local_path) if os.path.exists(local_path) else 0
|
|
571
|
+
headers: dict[str, str] = {}
|
|
572
|
+
mode = "wb"
|
|
573
|
+
if existing_size > 0:
|
|
574
|
+
headers["Range"] = f"bytes={existing_size}-"
|
|
575
|
+
mode = "ab"
|
|
576
|
+
|
|
577
|
+
try:
|
|
578
|
+
with requests.get(
|
|
579
|
+
url, stream=True, timeout=DOWNLOAD_TIMEOUT, headers=headers
|
|
580
|
+
) as response:
|
|
581
|
+
if response.status_code == 416 and existing_size > 0:
|
|
582
|
+
if _is_completed_range_response(
|
|
583
|
+
response.headers.get("Content-Range"), existing_size
|
|
584
|
+
):
|
|
585
|
+
if expected_sha256 is not None:
|
|
586
|
+
_verify_archive_sha256(local_path, expected_sha256, url)
|
|
587
|
+
return local_path
|
|
588
|
+
_restart_partial_download(local_path, url)
|
|
589
|
+
continue
|
|
590
|
+
response.raise_for_status()
|
|
591
|
+
|
|
592
|
+
if response.status_code == 200 and existing_size > 0:
|
|
593
|
+
existing_size = 0
|
|
594
|
+
mode = "wb"
|
|
595
|
+
elif existing_size > 0 and (
|
|
596
|
+
response.status_code != 206
|
|
597
|
+
or not _has_expected_resume_offset(
|
|
598
|
+
response.headers.get("Content-Range"), existing_size
|
|
599
|
+
)
|
|
600
|
+
):
|
|
601
|
+
_restart_partial_download(local_path, url)
|
|
602
|
+
continue
|
|
603
|
+
|
|
604
|
+
total_size = response.headers.get("Content-Length")
|
|
605
|
+
total_bytes = (
|
|
606
|
+
existing_size + int(total_size) if total_size is not None else None
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
desc = f"Downloading {os.path.basename(local_path)}"
|
|
610
|
+
with tqdm(
|
|
611
|
+
total=total_bytes,
|
|
612
|
+
initial=existing_size,
|
|
613
|
+
unit="B",
|
|
614
|
+
unit_scale=True,
|
|
615
|
+
unit_divisor=1024,
|
|
616
|
+
desc=desc,
|
|
617
|
+
) as pbar:
|
|
618
|
+
with open(local_path, mode) as file_obj:
|
|
619
|
+
for chunk in response.iter_content(
|
|
620
|
+
chunk_size=DOWNLOAD_CHUNK_SIZE
|
|
621
|
+
):
|
|
622
|
+
if not chunk:
|
|
623
|
+
continue
|
|
624
|
+
file_obj.write(chunk)
|
|
625
|
+
pbar.update(len(chunk))
|
|
626
|
+
if expected_sha256 is not None:
|
|
627
|
+
_verify_archive_sha256(local_path, expected_sha256, url)
|
|
628
|
+
return local_path
|
|
629
|
+
except (
|
|
630
|
+
requests.ConnectionError,
|
|
631
|
+
requests.Timeout,
|
|
632
|
+
requests.exceptions.ChunkedEncodingError,
|
|
633
|
+
requests.HTTPError,
|
|
634
|
+
) as exc:
|
|
635
|
+
if isinstance(exc, requests.HTTPError):
|
|
636
|
+
status_code = getattr(exc.response, "status_code", None)
|
|
637
|
+
if not isinstance(status_code, int) or (
|
|
638
|
+
status_code not in RETRYABLE_HTTP_STATUS_CODES
|
|
639
|
+
and not 500 <= status_code < 600
|
|
640
|
+
):
|
|
641
|
+
raise
|
|
642
|
+
if attempt == DOWNLOAD_RETRY_LIMIT:
|
|
643
|
+
raise RuntimeError(
|
|
644
|
+
f"Failed to download {url} after {DOWNLOAD_RETRY_LIMIT} attempts."
|
|
645
|
+
) from exc
|
|
646
|
+
resumed_size = (
|
|
647
|
+
os.path.getsize(local_path) if os.path.exists(local_path) else 0
|
|
648
|
+
)
|
|
649
|
+
print(
|
|
650
|
+
f"Download attempt failed for {os.path.basename(local_path)}; "
|
|
651
|
+
f"retrying from {resumed_size} bytes (attempt {attempt + 1}/{DOWNLOAD_RETRY_LIMIT})..."
|
|
652
|
+
)
|
|
653
|
+
sleep(DOWNLOAD_RETRY_BACKOFF_SECONDS * attempt)
|
|
654
|
+
|
|
655
|
+
raise RuntimeError(
|
|
656
|
+
f"Failed to download {url} after {DOWNLOAD_RETRY_LIMIT} attempts."
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _should_download_serially(path_or_urls: list[str]) -> bool:
|
|
661
|
+
"""Returns whether URL inputs should be downloaded one by one.
|
|
662
|
+
|
|
663
|
+
Dataset hosts such as ImageNet often throttle concurrent archive downloads
|
|
664
|
+
from the same origin. Serializing same-host downloads is slower in the best
|
|
665
|
+
case, but much more stable for the large validation archives used here.
|
|
666
|
+
"""
|
|
667
|
+
|
|
668
|
+
hosts = [
|
|
669
|
+
urlparse(path_or_url).netloc
|
|
670
|
+
for path_or_url in path_or_urls
|
|
671
|
+
if _is_url(path_or_url)
|
|
672
|
+
]
|
|
673
|
+
return len(hosts) > 1 and len(set(hosts)) == 1
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _download_if_url(path_or_url: str, download_dir: str) -> str:
|
|
677
|
+
"""Downloads a remote dataset archive when needed.
|
|
678
|
+
|
|
679
|
+
Args:
|
|
680
|
+
path_or_url: Local path or HTTP(S) URL pointing to a dataset archive.
|
|
681
|
+
download_dir: Directory to store downloaded archives.
|
|
682
|
+
|
|
683
|
+
Returns:
|
|
684
|
+
A local filesystem path to the archive or directory.
|
|
685
|
+
|
|
686
|
+
Raises:
|
|
687
|
+
ValueError: If the URL path does not contain a filename.
|
|
688
|
+
"""
|
|
689
|
+
if not _is_url(path_or_url):
|
|
690
|
+
return path_or_url
|
|
691
|
+
|
|
692
|
+
parsed = urlparse(path_or_url)
|
|
693
|
+
if parsed.scheme != "https":
|
|
694
|
+
raise ValueError(
|
|
695
|
+
"Dataset archive URLs must use HTTPS. Download the archive locally "
|
|
696
|
+
f"and provide its path instead: {path_or_url}"
|
|
697
|
+
)
|
|
698
|
+
filename = os.path.basename(parsed.path)
|
|
699
|
+
if not filename:
|
|
700
|
+
raise ValueError(f"Unable to determine a filename from URL: {path_or_url}")
|
|
701
|
+
|
|
702
|
+
local_path = os.path.join(download_dir, filename)
|
|
703
|
+
print(f"Downloading dataset archive from {path_or_url} to {local_path}...")
|
|
704
|
+
_download_url(
|
|
705
|
+
path_or_url,
|
|
706
|
+
local_path,
|
|
707
|
+
expected_sha256=PINNED_ARCHIVE_SHA256.get(path_or_url),
|
|
708
|
+
)
|
|
709
|
+
print("Download completed")
|
|
710
|
+
return local_path
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def _resolve_source(path_or_url: str, download_dir: str) -> str:
|
|
714
|
+
"""Resolves a local path for a dataset source."""
|
|
715
|
+
|
|
716
|
+
return _download_if_url(path_or_url, download_dir)
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def _resolve_sources(path_or_urls: list[str], download_dir: str) -> list[str]:
|
|
720
|
+
"""Resolves multiple dataset sources, downloading URL inputs in parallel."""
|
|
721
|
+
|
|
722
|
+
if _should_download_serially(path_or_urls):
|
|
723
|
+
return [
|
|
724
|
+
_resolve_source(path_or_url, download_dir) for path_or_url in path_or_urls
|
|
725
|
+
]
|
|
726
|
+
|
|
727
|
+
local_paths: list[str | None] = [None] * len(path_or_urls)
|
|
728
|
+
futures: dict[concurrent.futures.Future[str], int] = {}
|
|
729
|
+
with concurrent.futures.ThreadPoolExecutor(
|
|
730
|
+
max_workers=min(4, len(path_or_urls))
|
|
731
|
+
) as executor:
|
|
732
|
+
for idx, path_or_url in enumerate(path_or_urls):
|
|
733
|
+
if _is_url(path_or_url):
|
|
734
|
+
futures[executor.submit(_resolve_source, path_or_url, download_dir)] = (
|
|
735
|
+
idx
|
|
736
|
+
)
|
|
737
|
+
else:
|
|
738
|
+
local_paths[idx] = path_or_url
|
|
739
|
+
|
|
740
|
+
for future in concurrent.futures.as_completed(futures):
|
|
741
|
+
local_paths[futures[future]] = future.result()
|
|
742
|
+
|
|
743
|
+
return [path for path in local_paths if path is not None]
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _get_object_name(obj: ET.Element, xml_file: str) -> str:
|
|
747
|
+
"""Extracts a non-empty object name from an ImageNet annotation node.
|
|
748
|
+
|
|
749
|
+
Args:
|
|
750
|
+
obj: XML ``object`` element from an annotation file.
|
|
751
|
+
xml_file: Source XML filename used for error context.
|
|
752
|
+
|
|
753
|
+
Returns:
|
|
754
|
+
The validated object name.
|
|
755
|
+
|
|
756
|
+
Raises:
|
|
757
|
+
ValueError: If the object name node is missing or empty.
|
|
758
|
+
"""
|
|
759
|
+
name_element = obj.find("name")
|
|
760
|
+
if name_element is None or name_element.text is None:
|
|
761
|
+
raise ValueError(f"XML file {xml_file} has an object without a valid name")
|
|
762
|
+
|
|
763
|
+
object_name = name_element.text.strip()
|
|
764
|
+
if not object_name:
|
|
765
|
+
raise ValueError(f"XML file {xml_file} has an object with an empty name")
|
|
766
|
+
if IMAGENET_SYNSET_PATTERN.fullmatch(object_name) is None:
|
|
767
|
+
raise ValueError(
|
|
768
|
+
f"XML file {xml_file} has invalid ImageNet synset name {object_name!r}; "
|
|
769
|
+
"expected n########."
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
return object_name
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def _imagenet_class_output_dir(staged_output_dir: str, object_name: str) -> Path:
|
|
776
|
+
"""Return a containment-checked class directory below the staging root."""
|
|
777
|
+
|
|
778
|
+
staged_root = Path(staged_output_dir).resolve()
|
|
779
|
+
class_dir = (staged_root / object_name).resolve()
|
|
780
|
+
if class_dir.parent != staged_root:
|
|
781
|
+
raise ValueError(
|
|
782
|
+
f"ImageNet class directory escapes staging root: {object_name!r}."
|
|
783
|
+
)
|
|
784
|
+
return class_dir
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def construct_imagenet(image_dir: str, xml_dir: str, output_dir: str) -> None:
|
|
788
|
+
"""Constructs the ImageNet dataset by organizing images into category folders.
|
|
789
|
+
|
|
790
|
+
Args:
|
|
791
|
+
image_dir (str): Directory containing the ImageNet validation images.
|
|
792
|
+
xml_dir (str): Directory containing the ImageNet bounding box XML files.
|
|
793
|
+
output_dir (str): Directory where the organized dataset will be stored.
|
|
794
|
+
|
|
795
|
+
Raises:
|
|
796
|
+
ValueError: If an XML file has no objects or contains multiple object names.
|
|
797
|
+
ValueError: If the number of XML files and images do not match.
|
|
798
|
+
"""
|
|
799
|
+
|
|
800
|
+
xml_count = len(os.listdir(xml_dir + "/val"))
|
|
801
|
+
image_count = len(os.listdir(image_dir))
|
|
802
|
+
if xml_count != image_count:
|
|
803
|
+
raise ValueError(
|
|
804
|
+
f"Number of XML and image files do not match: {xml_count} != {image_count}."
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
# validate the XML files
|
|
808
|
+
pbar = tqdm(os.listdir(xml_dir + "/val"), desc="Validating XML files")
|
|
809
|
+
for xml_file in pbar:
|
|
810
|
+
xml_path = os.path.join(xml_dir + "/val", xml_file)
|
|
811
|
+
xml_tree = ET.parse(xml_path)
|
|
812
|
+
root = xml_tree.getroot()
|
|
813
|
+
|
|
814
|
+
if len(root.findall("object")) < 1:
|
|
815
|
+
raise ValueError(
|
|
816
|
+
f"XML file {xml_file} has no object, but expected at least 1"
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
# check whether the object names in the XML files are the same
|
|
820
|
+
object_names = [
|
|
821
|
+
_get_object_name(obj, xml_file) for obj in root.findall("object")
|
|
822
|
+
]
|
|
823
|
+
if len(set(object_names)) != 1:
|
|
824
|
+
raise ValueError(
|
|
825
|
+
f"Object names in XML file {xml_file} are not the same. "
|
|
826
|
+
f"It has {len(set(object_names))} different object names."
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
pbar.close()
|
|
830
|
+
|
|
831
|
+
output_dir = os.path.abspath(output_dir)
|
|
832
|
+
output_parent_dir = os.path.dirname(output_dir)
|
|
833
|
+
os.makedirs(output_parent_dir, exist_ok=True)
|
|
834
|
+
with TemporaryDirectory(
|
|
835
|
+
dir=output_parent_dir, prefix=".imagenet-staging-"
|
|
836
|
+
) as staging_dir:
|
|
837
|
+
staged_output_dir = os.path.join(staging_dir, "imagenet")
|
|
838
|
+
|
|
839
|
+
# construct the ImageNet dataset
|
|
840
|
+
pbar = tqdm(os.listdir(xml_dir + "/val"), desc="Constructing ImageNet dataset")
|
|
841
|
+
for xml_file in pbar:
|
|
842
|
+
xml_path = os.path.join(xml_dir + "/val", xml_file)
|
|
843
|
+
xml_tree = ET.parse(xml_path)
|
|
844
|
+
root = xml_tree.getroot()
|
|
845
|
+
object_name = _get_object_name(root.findall("object")[0], xml_file)
|
|
846
|
+
image_path = os.path.join(image_dir, xml_file.replace(".xml", ".JPEG"))
|
|
847
|
+
if not os.path.isfile(image_path):
|
|
848
|
+
raise FileNotFoundError(f"Image file not found: {image_path}")
|
|
849
|
+
|
|
850
|
+
class_dir = _imagenet_class_output_dir(staged_output_dir, object_name)
|
|
851
|
+
os.makedirs(class_dir, exist_ok=True)
|
|
852
|
+
shutil.copy(
|
|
853
|
+
image_path,
|
|
854
|
+
class_dir / os.path.basename(image_path),
|
|
855
|
+
)
|
|
856
|
+
pbar.close()
|
|
857
|
+
|
|
858
|
+
# validate the staged ImageNet dataset before replacing the managed output root
|
|
859
|
+
pbar = tqdm(os.listdir(staged_output_dir), desc="Validating ImageNet dataset")
|
|
860
|
+
print(f"Number of categories: {len(os.listdir(staged_output_dir))}")
|
|
861
|
+
for object_name in pbar:
|
|
862
|
+
num_images = len(os.listdir(os.path.join(staged_output_dir, object_name)))
|
|
863
|
+
if num_images != 50:
|
|
864
|
+
raise ValueError(
|
|
865
|
+
f"Object {object_name} has {num_images} images, but expected 50"
|
|
866
|
+
)
|
|
867
|
+
pbar.close()
|
|
868
|
+
_validate_staged_dataset(
|
|
869
|
+
staged_output_dir, "imagenet", ("image_classification",)
|
|
870
|
+
)
|
|
871
|
+
_replace_staged_directories(
|
|
872
|
+
((staged_output_dir, output_dir),),
|
|
873
|
+
output_parent_dir,
|
|
874
|
+
".imagenet-backup-",
|
|
875
|
+
)
|
|
876
|
+
print("Each category has 50 images")
|
|
877
|
+
print("ImageNet dataset constructed successfully")
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
def organize_imagenet(
|
|
881
|
+
image_dir: str,
|
|
882
|
+
xml_dir: str,
|
|
883
|
+
output_dir: str | None = None,
|
|
884
|
+
) -> None:
|
|
885
|
+
"""Organizes the ImageNet dataset, unpacking archives if necessary.
|
|
886
|
+
|
|
887
|
+
Args:
|
|
888
|
+
image_dir (str): Path or URL to the image directory or archive (.tar).
|
|
889
|
+
xml_dir (str): Path or URL to the XML directory or archive (.tgz).
|
|
890
|
+
output_dir: Directory to store the organized dataset. Defaults to the
|
|
891
|
+
resolved Mobilint cache directory.
|
|
892
|
+
"""
|
|
893
|
+
output_dir = _resolve_organizer_output_dir(output_dir, "imagenet")
|
|
894
|
+
with TemporaryDirectory() as temp_dir:
|
|
895
|
+
local_image_dir, local_xml_dir = _resolve_sources(
|
|
896
|
+
[image_dir, xml_dir], temp_dir
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
if local_image_dir.endswith(".tar") and local_xml_dir.endswith(".tgz"):
|
|
900
|
+
print("Unpacking image and XML files to temporary directory...")
|
|
901
|
+
_safe_unpack_archive(
|
|
902
|
+
local_image_dir, os.path.join(temp_dir, "ILSVRC2012_img_val")
|
|
903
|
+
)
|
|
904
|
+
_safe_unpack_archive(
|
|
905
|
+
local_xml_dir, os.path.join(temp_dir, "ILSVRC2012_bbox_val_v3")
|
|
906
|
+
)
|
|
907
|
+
print("Unpacking completed")
|
|
908
|
+
construct_imagenet(
|
|
909
|
+
os.path.join(temp_dir, "ILSVRC2012_img_val"),
|
|
910
|
+
os.path.join(temp_dir, "ILSVRC2012_bbox_val_v3"),
|
|
911
|
+
output_dir,
|
|
912
|
+
)
|
|
913
|
+
return
|
|
914
|
+
|
|
915
|
+
construct_imagenet(local_image_dir, local_xml_dir, output_dir)
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def construct_coco(image_dir: str, annotation_dir: str, output_dir: str) -> None:
|
|
919
|
+
"""Constructs the COCO dataset by copying images and annotations to a target directory.
|
|
920
|
+
|
|
921
|
+
Args:
|
|
922
|
+
image_dir (str): Directory containing COCO images.
|
|
923
|
+
annotation_dir (str): Directory containing COCO annotations.
|
|
924
|
+
output_dir (str): Directory where the organized dataset will be stored.
|
|
925
|
+
"""
|
|
926
|
+
print(
|
|
927
|
+
f"Constructing COCO dataset from {image_dir} and {annotation_dir} to {output_dir}"
|
|
928
|
+
)
|
|
929
|
+
output_dir = os.path.abspath(output_dir)
|
|
930
|
+
output_parent_dir = os.path.dirname(output_dir)
|
|
931
|
+
os.makedirs(output_parent_dir, exist_ok=True)
|
|
932
|
+
with TemporaryDirectory(
|
|
933
|
+
dir=output_parent_dir, prefix=".coco-staging-"
|
|
934
|
+
) as staging_dir:
|
|
935
|
+
staged_output_dir = os.path.join(staging_dir, "coco")
|
|
936
|
+
shutil.copytree(image_dir, os.path.join(staged_output_dir, "val2017"))
|
|
937
|
+
for file in os.listdir(os.path.join(annotation_dir, "annotations")):
|
|
938
|
+
if file.endswith("_val2017.json"):
|
|
939
|
+
shutil.copy(
|
|
940
|
+
os.path.join(annotation_dir, "annotations", file),
|
|
941
|
+
os.path.join(staged_output_dir, file),
|
|
942
|
+
)
|
|
943
|
+
_validate_staged_dataset(
|
|
944
|
+
staged_output_dir,
|
|
945
|
+
"coco",
|
|
946
|
+
("object_detection", "instance_segmentation", "pose_estimation"),
|
|
947
|
+
)
|
|
948
|
+
_replace_staged_directories(
|
|
949
|
+
((staged_output_dir, output_dir),),
|
|
950
|
+
output_parent_dir,
|
|
951
|
+
".coco-backup-",
|
|
952
|
+
)
|
|
953
|
+
print("Constructing COCO dataset completed")
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def organize_coco(
|
|
957
|
+
image_dir: str,
|
|
958
|
+
annotation_dir: str,
|
|
959
|
+
output_dir: str | None = None,
|
|
960
|
+
) -> None:
|
|
961
|
+
"""Organizes the COCO dataset, unpacking archives if necessary.
|
|
962
|
+
|
|
963
|
+
Args:
|
|
964
|
+
image_dir (str): Path or URL to the image zip file or directory.
|
|
965
|
+
annotation_dir (str): Path or URL to the annotation zip file or directory.
|
|
966
|
+
output_dir: Directory to store the organized dataset. Defaults to the
|
|
967
|
+
resolved Mobilint cache directory.
|
|
968
|
+
"""
|
|
969
|
+
output_dir = _resolve_organizer_output_dir(output_dir, "coco")
|
|
970
|
+
with TemporaryDirectory() as temp_dir:
|
|
971
|
+
local_image_dir, local_annotation_dir = _resolve_sources(
|
|
972
|
+
[image_dir, annotation_dir], temp_dir
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
if local_image_dir.endswith(".zip") and local_annotation_dir.endswith(".zip"):
|
|
976
|
+
print("Unpacking image and annotation files to temporary directory...")
|
|
977
|
+
_safe_unpack_archive(local_image_dir, temp_dir)
|
|
978
|
+
_safe_unpack_archive(
|
|
979
|
+
local_annotation_dir, os.path.join(temp_dir, "annotations_trainval2017")
|
|
980
|
+
)
|
|
981
|
+
print("Unpacking completed")
|
|
982
|
+
construct_coco(
|
|
983
|
+
os.path.join(temp_dir, "val2017"),
|
|
984
|
+
os.path.join(temp_dir, "annotations_trainval2017"),
|
|
985
|
+
output_dir,
|
|
986
|
+
)
|
|
987
|
+
return
|
|
988
|
+
|
|
989
|
+
construct_coco(local_image_dir, local_annotation_dir, output_dir)
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def construct_widerface(image_dir: str, annotation_dir: str, output_dir: str) -> None:
|
|
993
|
+
"""Constructs the WiderFace dataset by copying images and annotations to a target directory.
|
|
994
|
+
|
|
995
|
+
Args:
|
|
996
|
+
image_dir (str): Directory containing WiderFace images.
|
|
997
|
+
annotation_dir (str): Directory containing WiderFace annotations.
|
|
998
|
+
output_dir (str): Directory where the organized dataset will be stored.
|
|
999
|
+
"""
|
|
1000
|
+
print(
|
|
1001
|
+
f"Constructing WiderFace dataset from {image_dir} and {annotation_dir} to {output_dir}"
|
|
1002
|
+
)
|
|
1003
|
+
output_dir = os.path.abspath(output_dir)
|
|
1004
|
+
output_parent_dir = os.path.dirname(output_dir)
|
|
1005
|
+
os.makedirs(output_parent_dir, exist_ok=True)
|
|
1006
|
+
with TemporaryDirectory(
|
|
1007
|
+
dir=output_parent_dir, prefix=".widerface-staging-"
|
|
1008
|
+
) as staging_dir:
|
|
1009
|
+
staged_output_dir = os.path.join(staging_dir, "widerface")
|
|
1010
|
+
shutil.copytree(
|
|
1011
|
+
os.path.join(image_dir, "images"),
|
|
1012
|
+
os.path.join(staged_output_dir, "images"),
|
|
1013
|
+
)
|
|
1014
|
+
for file in os.listdir(annotation_dir):
|
|
1015
|
+
if "_val" in file:
|
|
1016
|
+
shutil.copy(os.path.join(annotation_dir, file), staged_output_dir)
|
|
1017
|
+
_validate_staged_dataset(staged_output_dir, "widerface", ("face_detection",))
|
|
1018
|
+
_replace_staged_directories(
|
|
1019
|
+
((staged_output_dir, output_dir),),
|
|
1020
|
+
output_parent_dir,
|
|
1021
|
+
".widerface-backup-",
|
|
1022
|
+
)
|
|
1023
|
+
print("Constructing WiderFace dataset completed")
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
def organize_widerface(
|
|
1027
|
+
image_dir: str,
|
|
1028
|
+
annotation_dir: str,
|
|
1029
|
+
output_dir: str | None = None,
|
|
1030
|
+
) -> None:
|
|
1031
|
+
"""Organizes the WiderFace dataset, unpacking archives if necessary.
|
|
1032
|
+
|
|
1033
|
+
Args:
|
|
1034
|
+
image_dir (str): Path or URL to the image zip file or directory.
|
|
1035
|
+
annotation_dir (str): Path or URL to the annotation zip file or directory.
|
|
1036
|
+
output_dir: Directory to store the organized dataset. Defaults to the
|
|
1037
|
+
resolved Mobilint cache directory.
|
|
1038
|
+
"""
|
|
1039
|
+
output_dir = _resolve_organizer_output_dir(output_dir, "widerface")
|
|
1040
|
+
with TemporaryDirectory() as temp_dir:
|
|
1041
|
+
local_image_dir, local_annotation_dir = _resolve_sources(
|
|
1042
|
+
[image_dir, annotation_dir], temp_dir
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
if local_image_dir.endswith(".zip") and local_annotation_dir.endswith(".zip"):
|
|
1046
|
+
print("Unpacking image and annotation files to temporary directory...")
|
|
1047
|
+
_safe_unpack_archive(local_image_dir, temp_dir)
|
|
1048
|
+
_safe_unpack_archive(local_annotation_dir, temp_dir)
|
|
1049
|
+
print("Unpacking completed")
|
|
1050
|
+
construct_widerface(
|
|
1051
|
+
os.path.join(temp_dir, "WIDER_val"),
|
|
1052
|
+
os.path.join(temp_dir, "wider_face_split"),
|
|
1053
|
+
output_dir,
|
|
1054
|
+
)
|
|
1055
|
+
return
|
|
1056
|
+
|
|
1057
|
+
construct_widerface(local_image_dir, local_annotation_dir, output_dir)
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
def _resolve_nyu_depth_validation_dirs(dataset_dir: str) -> tuple[str, str, str]:
|
|
1061
|
+
"""Resolves NYU Depth validation image and depth directories.
|
|
1062
|
+
|
|
1063
|
+
Args:
|
|
1064
|
+
dataset_dir: Directory containing the NYU Depth root or its parent.
|
|
1065
|
+
|
|
1066
|
+
Returns:
|
|
1067
|
+
Paths to the selected dataset root, validation image directory, and
|
|
1068
|
+
validation depth directory.
|
|
1069
|
+
|
|
1070
|
+
Raises:
|
|
1071
|
+
ValueError: If the expected NYU Depth layout is not present.
|
|
1072
|
+
"""
|
|
1073
|
+
|
|
1074
|
+
roots = (os.path.join(dataset_dir, "nyu-depth"), dataset_dir)
|
|
1075
|
+
for root in roots:
|
|
1076
|
+
candidates = (
|
|
1077
|
+
(os.path.join(root, "images", "val"), os.path.join(root, "depth", "val")),
|
|
1078
|
+
(os.path.join(root, "val", "images"), os.path.join(root, "val", "depth")),
|
|
1079
|
+
(os.path.join(root, "images"), os.path.join(root, "depth")),
|
|
1080
|
+
)
|
|
1081
|
+
for image_dir, depth_dir in candidates:
|
|
1082
|
+
if os.path.isdir(image_dir) and os.path.isdir(depth_dir):
|
|
1083
|
+
return root, image_dir, depth_dir
|
|
1084
|
+
raise ValueError(
|
|
1085
|
+
f"NYU Depth dataset must contain matching images/ and depth/ directories: {dataset_dir}"
|
|
1086
|
+
)
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
def _validate_dense_source_file(source_path: str, dataset_root: Path) -> str:
|
|
1090
|
+
"""Resolve a non-symlink regular file contained by a dense dataset root.
|
|
1091
|
+
|
|
1092
|
+
Args:
|
|
1093
|
+
source_path: Candidate data or metadata file.
|
|
1094
|
+
dataset_root: Resolved root of the extracted dataset.
|
|
1095
|
+
|
|
1096
|
+
Returns:
|
|
1097
|
+
Resolved source path safe to copy.
|
|
1098
|
+
|
|
1099
|
+
Raises:
|
|
1100
|
+
ValueError: If the source is a symlink, is not a regular file, cannot be
|
|
1101
|
+
resolved, or escapes the dataset root.
|
|
1102
|
+
"""
|
|
1103
|
+
|
|
1104
|
+
source = Path(source_path)
|
|
1105
|
+
if source.is_symlink():
|
|
1106
|
+
raise ValueError(f"Dense dataset source file must not be a symlink: {source}.")
|
|
1107
|
+
try:
|
|
1108
|
+
resolved_source = source.resolve(strict=True)
|
|
1109
|
+
except OSError as exc:
|
|
1110
|
+
raise ValueError(
|
|
1111
|
+
f"Unable to resolve dense dataset source file {source}: {exc}."
|
|
1112
|
+
) from exc
|
|
1113
|
+
if not resolved_source.is_file():
|
|
1114
|
+
raise ValueError(f"Dense dataset source must be a regular file: {source}.")
|
|
1115
|
+
if not resolved_source.is_relative_to(dataset_root):
|
|
1116
|
+
raise ValueError(
|
|
1117
|
+
f"Dense dataset source must remain within dataset root: {source}."
|
|
1118
|
+
)
|
|
1119
|
+
return str(resolved_source)
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def _collect_unique_dense_sources(
|
|
1123
|
+
source_paths: Iterable[str],
|
|
1124
|
+
dataset_root: Path,
|
|
1125
|
+
source_description: str,
|
|
1126
|
+
) -> dict[str, str]:
|
|
1127
|
+
"""Return validated dense sources keyed by unique filename stem.
|
|
1128
|
+
|
|
1129
|
+
Dense organizers flatten source files into a single output directory. Reject
|
|
1130
|
+
repeated stems up front instead of silently retaining whichever recursive
|
|
1131
|
+
traversal entry happened to be processed last.
|
|
1132
|
+
"""
|
|
1133
|
+
|
|
1134
|
+
sources: dict[str, str] = {}
|
|
1135
|
+
for source_path in sorted(source_paths):
|
|
1136
|
+
sample_id = Path(source_path).stem
|
|
1137
|
+
validated_path = _validate_dense_source_file(source_path, dataset_root)
|
|
1138
|
+
previous_path = sources.get(sample_id)
|
|
1139
|
+
if previous_path is not None:
|
|
1140
|
+
raise ValueError(
|
|
1141
|
+
f"{source_description} contain duplicate filename stem {sample_id!r}: "
|
|
1142
|
+
f"{previous_path} and {validated_path}."
|
|
1143
|
+
)
|
|
1144
|
+
sources[sample_id] = validated_path
|
|
1145
|
+
return sources
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
def _validate_dense_output_root(
|
|
1149
|
+
output_dir: str,
|
|
1150
|
+
dataset_name: str,
|
|
1151
|
+
layout_names: Iterable[str],
|
|
1152
|
+
) -> str:
|
|
1153
|
+
"""Reject symlinks in a dense managed root before organization.
|
|
1154
|
+
|
|
1155
|
+
Args:
|
|
1156
|
+
output_dir: Requested managed dataset root.
|
|
1157
|
+
dataset_name: Human-readable dataset name for error reporting.
|
|
1158
|
+
layout_names: Dataset-specific directories managed below the root.
|
|
1159
|
+
|
|
1160
|
+
Returns:
|
|
1161
|
+
Expanded absolute output path.
|
|
1162
|
+
|
|
1163
|
+
Raises:
|
|
1164
|
+
ValueError: If the managed root, an ancestor, or a managed layout
|
|
1165
|
+
directory is a symlink.
|
|
1166
|
+
"""
|
|
1167
|
+
|
|
1168
|
+
requested_path = Path(output_dir).expanduser()
|
|
1169
|
+
output_path = Path(os.path.abspath(requested_path))
|
|
1170
|
+
if _path_has_symlink_component(requested_path):
|
|
1171
|
+
raise ValueError(
|
|
1172
|
+
f"{dataset_name} output directory and its existing parents must not be symlinks: {output_path}. "
|
|
1173
|
+
"Remove the symlink or choose a path beneath regular directories."
|
|
1174
|
+
)
|
|
1175
|
+
for layout_name in layout_names:
|
|
1176
|
+
layout_path = output_path / layout_name
|
|
1177
|
+
if layout_path.is_symlink():
|
|
1178
|
+
raise ValueError(
|
|
1179
|
+
f"{dataset_name} output layout directories must not be symlinks: {layout_path}. "
|
|
1180
|
+
"Remove the symlink or choose a different output directory."
|
|
1181
|
+
)
|
|
1182
|
+
return str(output_path)
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
def _collect_nyu_depth_validation_files(
|
|
1186
|
+
image_dir: str,
|
|
1187
|
+
depth_dir: str,
|
|
1188
|
+
dataset_root: Path,
|
|
1189
|
+
) -> tuple[dict[str, str], dict[str, str]]:
|
|
1190
|
+
"""Validates and returns matching NYU Depth validation image/depth pairs."""
|
|
1191
|
+
|
|
1192
|
+
images = _collect_unique_dense_sources(
|
|
1193
|
+
_iter_files(image_dir, [".jpg", ".jpeg", ".png"]),
|
|
1194
|
+
dataset_root,
|
|
1195
|
+
"NYU Depth images",
|
|
1196
|
+
)
|
|
1197
|
+
depths = _collect_unique_dense_sources(
|
|
1198
|
+
_iter_files(depth_dir, [".npy"]),
|
|
1199
|
+
dataset_root,
|
|
1200
|
+
"NYU Depth depth maps",
|
|
1201
|
+
)
|
|
1202
|
+
missing_depths = sorted(set(images) - set(depths))
|
|
1203
|
+
missing_images = sorted(set(depths) - set(images))
|
|
1204
|
+
if missing_depths or missing_images:
|
|
1205
|
+
details = []
|
|
1206
|
+
if missing_depths:
|
|
1207
|
+
details.append(
|
|
1208
|
+
f"images without depth maps: {', '.join(missing_depths[:5])}"
|
|
1209
|
+
)
|
|
1210
|
+
if missing_images:
|
|
1211
|
+
details.append(
|
|
1212
|
+
f"depth maps without images: {', '.join(missing_images[:5])}"
|
|
1213
|
+
)
|
|
1214
|
+
raise ValueError(
|
|
1215
|
+
f"NYU Depth validation image/depth mismatch ({'; '.join(details)})."
|
|
1216
|
+
)
|
|
1217
|
+
if len(images) != NYU_DEPTH_VALIDATION_SAMPLE_COUNT:
|
|
1218
|
+
raise ValueError(
|
|
1219
|
+
"NYU Depth validation dataset must contain "
|
|
1220
|
+
f"{NYU_DEPTH_VALIDATION_SAMPLE_COUNT} matching image/depth pairs, found {len(images)}."
|
|
1221
|
+
)
|
|
1222
|
+
return images, depths
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
def construct_nyu_depth(dataset_dir: str, output_dir: str) -> None:
|
|
1226
|
+
"""Constructs the NYU Depth layout from an extracted dataset directory.
|
|
1227
|
+
|
|
1228
|
+
Args:
|
|
1229
|
+
dataset_dir: Directory containing the NYU Depth root or its parent.
|
|
1230
|
+
output_dir: Directory where the organized dataset will be stored.
|
|
1231
|
+
"""
|
|
1232
|
+
|
|
1233
|
+
output_dir = _validate_dense_output_root(
|
|
1234
|
+
output_dir, "NYU Depth", ("images", "depth")
|
|
1235
|
+
)
|
|
1236
|
+
selected_root, image_dir, depth_dir = _resolve_nyu_depth_validation_dirs(
|
|
1237
|
+
dataset_dir
|
|
1238
|
+
)
|
|
1239
|
+
try:
|
|
1240
|
+
dataset_root = Path(selected_root).resolve(strict=True)
|
|
1241
|
+
except OSError as exc:
|
|
1242
|
+
raise ValueError(
|
|
1243
|
+
f"Unable to resolve NYU Depth dataset root {selected_root}: {exc}."
|
|
1244
|
+
) from exc
|
|
1245
|
+
images, depths = _collect_nyu_depth_validation_files(
|
|
1246
|
+
image_dir, depth_dir, dataset_root
|
|
1247
|
+
)
|
|
1248
|
+
print(
|
|
1249
|
+
f"Constructing NYU Depth validation dataset from {dataset_dir} to {output_dir}"
|
|
1250
|
+
)
|
|
1251
|
+
|
|
1252
|
+
output_parent_dir = os.path.dirname(output_dir)
|
|
1253
|
+
os.makedirs(output_parent_dir, exist_ok=True)
|
|
1254
|
+
with TemporaryDirectory(
|
|
1255
|
+
dir=output_parent_dir, prefix=".nyu-depth-staging-"
|
|
1256
|
+
) as staging_dir:
|
|
1257
|
+
staged_image_dir = os.path.join(staging_dir, "images")
|
|
1258
|
+
staged_depth_dir = os.path.join(staging_dir, "depth")
|
|
1259
|
+
os.makedirs(staged_image_dir)
|
|
1260
|
+
os.makedirs(staged_depth_dir)
|
|
1261
|
+
for sample_id in sorted(images):
|
|
1262
|
+
shutil.copy2(
|
|
1263
|
+
images[sample_id],
|
|
1264
|
+
os.path.join(staged_image_dir, os.path.basename(images[sample_id])),
|
|
1265
|
+
)
|
|
1266
|
+
shutil.copy2(
|
|
1267
|
+
depths[sample_id],
|
|
1268
|
+
os.path.join(staged_depth_dir, os.path.basename(depths[sample_id])),
|
|
1269
|
+
)
|
|
1270
|
+
|
|
1271
|
+
_validate_staged_nyu_depth(staging_dir)
|
|
1272
|
+
|
|
1273
|
+
replacements = (
|
|
1274
|
+
(staged_image_dir, os.path.join(output_dir, "images")),
|
|
1275
|
+
(staged_depth_dir, os.path.join(output_dir, "depth")),
|
|
1276
|
+
)
|
|
1277
|
+
_replace_staged_directories(
|
|
1278
|
+
replacements, output_parent_dir, ".nyu-depth-backup-"
|
|
1279
|
+
)
|
|
1280
|
+
print(
|
|
1281
|
+
f"Constructed NYU Depth validation dataset with {len(images)} image/depth pairs"
|
|
1282
|
+
)
|
|
1283
|
+
|
|
1284
|
+
|
|
1285
|
+
def _validate_staged_nyu_depth(staging_dir: str) -> None:
|
|
1286
|
+
"""Decode staged NYU pairs before they can replace an existing cache."""
|
|
1287
|
+
|
|
1288
|
+
image_dir = Path(staging_dir) / "images"
|
|
1289
|
+
depth_dir = Path(staging_dir) / "depth"
|
|
1290
|
+
for image_path in sorted(image_dir.iterdir()):
|
|
1291
|
+
if image_path.suffix.lower() not in {".jpg", ".jpeg", ".png"}:
|
|
1292
|
+
continue
|
|
1293
|
+
depth_path = depth_dir / f"{image_path.stem}.npy"
|
|
1294
|
+
image = cv2.imread(str(image_path))
|
|
1295
|
+
if image is None:
|
|
1296
|
+
raise ValueError(f"Staged NYU Depth image is unreadable: {image_path}.")
|
|
1297
|
+
try:
|
|
1298
|
+
raw_depth = np.load(depth_path, allow_pickle=False)
|
|
1299
|
+
except (OSError, ValueError) as exc:
|
|
1300
|
+
raise ValueError(
|
|
1301
|
+
f"Unable to load staged NYU Depth target {depth_path}: {exc}."
|
|
1302
|
+
) from exc
|
|
1303
|
+
if not np.issubdtype(raw_depth.dtype, np.number) or np.issubdtype(
|
|
1304
|
+
raw_depth.dtype, np.complexfloating
|
|
1305
|
+
):
|
|
1306
|
+
raise ValueError(
|
|
1307
|
+
"Staged NYU Depth target must use a real numeric dtype, "
|
|
1308
|
+
f"got {raw_depth.dtype}: {depth_path}."
|
|
1309
|
+
)
|
|
1310
|
+
depth = np.asarray(raw_depth, dtype=np.float32)
|
|
1311
|
+
if depth.ndim != 2 or depth.shape != image.shape[:2]:
|
|
1312
|
+
raise ValueError(
|
|
1313
|
+
"Staged NYU Depth image and target shapes must match: "
|
|
1314
|
+
f"image {image.shape[:2]}, depth {depth.shape}: {image_path}."
|
|
1315
|
+
)
|
|
1316
|
+
if not bool(np.isfinite(depth).all()):
|
|
1317
|
+
raise ValueError(
|
|
1318
|
+
f"Staged NYU Depth target must contain only finite values: {depth_path}."
|
|
1319
|
+
)
|
|
1320
|
+
if bool((depth < 0).any()):
|
|
1321
|
+
raise ValueError(
|
|
1322
|
+
f"Staged NYU Depth target must not contain negative values: {depth_path}."
|
|
1323
|
+
)
|
|
1324
|
+
if not bool(((depth > 0.001) & (depth < 100.0)).any()):
|
|
1325
|
+
raise ValueError(
|
|
1326
|
+
"Staged NYU Depth target must contain at least one valid metric depth "
|
|
1327
|
+
f"in the (0.001, 100.0) range: {depth_path}."
|
|
1328
|
+
)
|
|
1329
|
+
|
|
1330
|
+
|
|
1331
|
+
def organize_nyu_depth(
|
|
1332
|
+
dataset_path: str = NYU_DEPTH_URL,
|
|
1333
|
+
output_dir: str | None = None,
|
|
1334
|
+
) -> None:
|
|
1335
|
+
"""Organizes NYU Depth, downloading and unpacking an archive when necessary.
|
|
1336
|
+
|
|
1337
|
+
Args:
|
|
1338
|
+
dataset_path: Path or URL to the NYU Depth zip file or extracted dataset directory.
|
|
1339
|
+
output_dir: Directory to store the organized dataset. Defaults to the
|
|
1340
|
+
resolved Mobilint cache directory.
|
|
1341
|
+
"""
|
|
1342
|
+
|
|
1343
|
+
output_dir = _resolve_organizer_output_dir(output_dir, "nyu-depth")
|
|
1344
|
+
output_dir = _validate_dense_output_root(
|
|
1345
|
+
output_dir, "NYU Depth", ("images", "depth")
|
|
1346
|
+
)
|
|
1347
|
+
with TemporaryDirectory() as temp_dir:
|
|
1348
|
+
local_dataset_path = _resolve_source(dataset_path, temp_dir)
|
|
1349
|
+
if local_dataset_path.endswith(".zip"):
|
|
1350
|
+
print("Unpacking NYU Depth files to temporary directory...")
|
|
1351
|
+
_safe_unpack_archive(local_dataset_path, temp_dir)
|
|
1352
|
+
print("Unpacking completed")
|
|
1353
|
+
construct_nyu_depth(temp_dir, output_dir)
|
|
1354
|
+
return
|
|
1355
|
+
|
|
1356
|
+
construct_nyu_depth(local_dataset_path, output_dir)
|
|
1357
|
+
|
|
1358
|
+
|
|
1359
|
+
def _resolve_ade20k_validation_dirs(dataset_dir: str) -> tuple[str, str, str]:
|
|
1360
|
+
"""Resolves the ADE20K root and validation image/mask directories."""
|
|
1361
|
+
|
|
1362
|
+
for root in (os.path.join(dataset_dir, "ADEChallengeData2016"), dataset_dir):
|
|
1363
|
+
for image_dir, annotation_dir in (
|
|
1364
|
+
(
|
|
1365
|
+
os.path.join(root, "images", "validation"),
|
|
1366
|
+
os.path.join(root, "annotations", "validation"),
|
|
1367
|
+
),
|
|
1368
|
+
(os.path.join(root, "images"), os.path.join(root, "annotations")),
|
|
1369
|
+
):
|
|
1370
|
+
if os.path.isdir(image_dir) and os.path.isdir(annotation_dir):
|
|
1371
|
+
return root, image_dir, annotation_dir
|
|
1372
|
+
raise ValueError(
|
|
1373
|
+
f"ADE20K dataset must contain matching images/ and annotations/ directories: {dataset_dir}"
|
|
1374
|
+
)
|
|
1375
|
+
|
|
1376
|
+
|
|
1377
|
+
def construct_ade20k(dataset_dir: str, output_dir: str) -> None:
|
|
1378
|
+
"""Constructs the flat ADE20K validation layout from an extracted dataset.
|
|
1379
|
+
|
|
1380
|
+
Args:
|
|
1381
|
+
dataset_dir: Directory containing the ADE20K root or its parent.
|
|
1382
|
+
output_dir: Directory where the organized validation dataset will be stored.
|
|
1383
|
+
|
|
1384
|
+
Raises:
|
|
1385
|
+
ValueError: If the source does not contain 2,000 matched validation image/mask pairs.
|
|
1386
|
+
"""
|
|
1387
|
+
|
|
1388
|
+
output_dir = _validate_dense_output_root(
|
|
1389
|
+
output_dir, "ADE20K", ("images", "annotations")
|
|
1390
|
+
)
|
|
1391
|
+
dataset_root, image_dir, annotation_dir = _resolve_ade20k_validation_dirs(
|
|
1392
|
+
dataset_dir
|
|
1393
|
+
)
|
|
1394
|
+
try:
|
|
1395
|
+
resolved_dataset_root = Path(dataset_root).resolve(strict=True)
|
|
1396
|
+
except OSError as exc:
|
|
1397
|
+
raise ValueError(
|
|
1398
|
+
f"Unable to resolve ADE20K dataset root {dataset_root}: {exc}."
|
|
1399
|
+
) from exc
|
|
1400
|
+
images = _collect_unique_dense_sources(
|
|
1401
|
+
(
|
|
1402
|
+
os.path.join(image_dir, file_name)
|
|
1403
|
+
for file_name in os.listdir(image_dir)
|
|
1404
|
+
if file_name.startswith("ADE_val_") and file_name.lower().endswith(".jpg")
|
|
1405
|
+
),
|
|
1406
|
+
resolved_dataset_root,
|
|
1407
|
+
"ADE20K images",
|
|
1408
|
+
)
|
|
1409
|
+
annotations = _collect_unique_dense_sources(
|
|
1410
|
+
(
|
|
1411
|
+
os.path.join(annotation_dir, file_name)
|
|
1412
|
+
for file_name in os.listdir(annotation_dir)
|
|
1413
|
+
if file_name.startswith("ADE_val_") and file_name.lower().endswith(".png")
|
|
1414
|
+
),
|
|
1415
|
+
resolved_dataset_root,
|
|
1416
|
+
"ADE20K annotations",
|
|
1417
|
+
)
|
|
1418
|
+
if set(images) != set(annotations):
|
|
1419
|
+
raise ValueError(
|
|
1420
|
+
"ADE20K validation images and annotations must have matching file stems."
|
|
1421
|
+
)
|
|
1422
|
+
if len(images) != ADE20K_VALIDATION_SAMPLE_COUNT:
|
|
1423
|
+
raise ValueError(
|
|
1424
|
+
f"ADE20K validation dataset must contain {ADE20K_VALIDATION_SAMPLE_COUNT} pairs, found {len(images)}."
|
|
1425
|
+
)
|
|
1426
|
+
metadata: dict[str, str] = {}
|
|
1427
|
+
for file_name in ADE20K_METADATA_FILES:
|
|
1428
|
+
metadata_path = os.path.join(dataset_root, file_name)
|
|
1429
|
+
if not os.path.lexists(metadata_path):
|
|
1430
|
+
raise ValueError(
|
|
1431
|
+
f"ADE20K dataset is missing required metadata files: {file_name}."
|
|
1432
|
+
)
|
|
1433
|
+
metadata[file_name] = _validate_dense_source_file(
|
|
1434
|
+
metadata_path, resolved_dataset_root
|
|
1435
|
+
)
|
|
1436
|
+
|
|
1437
|
+
output_parent_dir = os.path.dirname(output_dir)
|
|
1438
|
+
os.makedirs(output_parent_dir, exist_ok=True)
|
|
1439
|
+
with TemporaryDirectory(
|
|
1440
|
+
dir=output_parent_dir, prefix=".ade20k-staging-"
|
|
1441
|
+
) as staging_dir:
|
|
1442
|
+
staged_output_dir = os.path.join(staging_dir, "ade20k")
|
|
1443
|
+
staged_image_dir = os.path.join(staged_output_dir, "images")
|
|
1444
|
+
staged_annotation_dir = os.path.join(staged_output_dir, "annotations")
|
|
1445
|
+
os.makedirs(staged_image_dir)
|
|
1446
|
+
os.makedirs(staged_annotation_dir)
|
|
1447
|
+
for sample_id in sorted(images):
|
|
1448
|
+
shutil.copy2(
|
|
1449
|
+
images[sample_id],
|
|
1450
|
+
os.path.join(staged_image_dir, os.path.basename(images[sample_id])),
|
|
1451
|
+
)
|
|
1452
|
+
shutil.copy2(
|
|
1453
|
+
annotations[sample_id],
|
|
1454
|
+
os.path.join(
|
|
1455
|
+
staged_annotation_dir, os.path.basename(annotations[sample_id])
|
|
1456
|
+
),
|
|
1457
|
+
)
|
|
1458
|
+
for file_name in ADE20K_METADATA_FILES:
|
|
1459
|
+
shutil.copy2(
|
|
1460
|
+
metadata[file_name],
|
|
1461
|
+
os.path.join(staged_output_dir, file_name),
|
|
1462
|
+
)
|
|
1463
|
+
|
|
1464
|
+
_validate_staged_dataset(
|
|
1465
|
+
staged_output_dir, "ade20k", ("semantic_segmentation",)
|
|
1466
|
+
)
|
|
1467
|
+
_replace_staged_directories(
|
|
1468
|
+
((staged_output_dir, output_dir),),
|
|
1469
|
+
output_parent_dir,
|
|
1470
|
+
".ade20k-backup-",
|
|
1471
|
+
)
|
|
1472
|
+
print(f"Constructed ADE20K validation dataset with {len(images)} image/mask pairs")
|
|
1473
|
+
|
|
1474
|
+
|
|
1475
|
+
def organize_ade20k(
|
|
1476
|
+
dataset_path: str = ADE20K_URL,
|
|
1477
|
+
output_dir: str | None = None,
|
|
1478
|
+
) -> None:
|
|
1479
|
+
"""Organizes ADE20K validation data, downloading and unpacking when necessary."""
|
|
1480
|
+
|
|
1481
|
+
output_dir = _resolve_organizer_output_dir(output_dir, "ade20k")
|
|
1482
|
+
output_dir = _validate_dense_output_root(
|
|
1483
|
+
output_dir, "ADE20K", ("images", "annotations")
|
|
1484
|
+
)
|
|
1485
|
+
with TemporaryDirectory() as temp_dir:
|
|
1486
|
+
local_dataset_path = _resolve_source(dataset_path, temp_dir)
|
|
1487
|
+
if local_dataset_path.endswith(".zip"):
|
|
1488
|
+
_safe_unpack_archive(local_dataset_path, temp_dir)
|
|
1489
|
+
construct_ade20k(temp_dir, output_dir)
|
|
1490
|
+
return
|
|
1491
|
+
construct_ade20k(local_dataset_path, output_dir)
|
|
1492
|
+
|
|
1493
|
+
|
|
1494
|
+
def _validate_cityscapes_zip(archive_path: str, source_name: str) -> str:
|
|
1495
|
+
"""Validate one official Cityscapes ZIP source.
|
|
1496
|
+
|
|
1497
|
+
Args:
|
|
1498
|
+
archive_path: Path to the raw Cityscapes archive.
|
|
1499
|
+
source_name: Human-readable source description for errors.
|
|
1500
|
+
|
|
1501
|
+
Returns:
|
|
1502
|
+
Expanded absolute archive path.
|
|
1503
|
+
|
|
1504
|
+
Raises:
|
|
1505
|
+
ValueError: If the path is missing, is not a file, is not a ZIP, or contains duplicate members.
|
|
1506
|
+
"""
|
|
1507
|
+
|
|
1508
|
+
resolved_path = os.path.abspath(os.path.expanduser(archive_path))
|
|
1509
|
+
if not os.path.isfile(resolved_path):
|
|
1510
|
+
raise ValueError(
|
|
1511
|
+
f"Cityscapes {source_name} archive does not exist or is not a file: {resolved_path}."
|
|
1512
|
+
)
|
|
1513
|
+
if not zipfile.is_zipfile(resolved_path):
|
|
1514
|
+
raise ValueError(
|
|
1515
|
+
f"Cityscapes {source_name} source must be a valid ZIP archive: {resolved_path}."
|
|
1516
|
+
)
|
|
1517
|
+
|
|
1518
|
+
with zipfile.ZipFile(resolved_path) as archive:
|
|
1519
|
+
seen_members: set[str] = set()
|
|
1520
|
+
duplicate_members: set[str] = set()
|
|
1521
|
+
for member in archive.infolist():
|
|
1522
|
+
if member.filename in seen_members:
|
|
1523
|
+
duplicate_members.add(member.filename)
|
|
1524
|
+
seen_members.add(member.filename)
|
|
1525
|
+
if duplicate_members:
|
|
1526
|
+
raise ValueError(
|
|
1527
|
+
f"Cityscapes {source_name} archive contains duplicate members: {', '.join(sorted(duplicate_members)[:3])}."
|
|
1528
|
+
)
|
|
1529
|
+
return resolved_path
|
|
1530
|
+
|
|
1531
|
+
|
|
1532
|
+
def _collect_cityscapes_validation_files(
|
|
1533
|
+
split_dir: str,
|
|
1534
|
+
suffix: str,
|
|
1535
|
+
source_name: str,
|
|
1536
|
+
) -> dict[str, str]:
|
|
1537
|
+
"""Collect official Cityscapes validation files keyed by shared sample ID.
|
|
1538
|
+
|
|
1539
|
+
Args:
|
|
1540
|
+
split_dir: Extracted ``leftImg8bit/val`` or ``gtFine/val`` directory.
|
|
1541
|
+
suffix: Required official file suffix.
|
|
1542
|
+
source_name: Human-readable source description for errors.
|
|
1543
|
+
|
|
1544
|
+
Returns:
|
|
1545
|
+
Mapping from ``<city>_<sequence>_<frame>`` to source path.
|
|
1546
|
+
|
|
1547
|
+
Raises:
|
|
1548
|
+
ValueError: If a candidate filename is malformed, misplaced, or duplicates an ID.
|
|
1549
|
+
"""
|
|
1550
|
+
|
|
1551
|
+
files: dict[str, str] = {}
|
|
1552
|
+
if not os.path.isdir(split_dir):
|
|
1553
|
+
return files
|
|
1554
|
+
|
|
1555
|
+
for current_root, _, file_names in os.walk(split_dir):
|
|
1556
|
+
relative_root = os.path.relpath(current_root, split_dir)
|
|
1557
|
+
for file_name in file_names:
|
|
1558
|
+
if not file_name.endswith(suffix):
|
|
1559
|
+
continue
|
|
1560
|
+
if relative_root == "." or os.sep in relative_root:
|
|
1561
|
+
raise ValueError(
|
|
1562
|
+
f"Malformed Cityscapes {source_name} path: "
|
|
1563
|
+
f"{os.path.relpath(os.path.join(current_root, file_name), split_dir)}."
|
|
1564
|
+
)
|
|
1565
|
+
sample_id = file_name.removesuffix(suffix)
|
|
1566
|
+
match = CITYSCAPES_SAMPLE_ID_PATTERN.fullmatch(sample_id)
|
|
1567
|
+
if match is None or match.group("city") != relative_root:
|
|
1568
|
+
raise ValueError(
|
|
1569
|
+
f"Malformed Cityscapes {source_name} filename: {file_name}."
|
|
1570
|
+
)
|
|
1571
|
+
if sample_id in files:
|
|
1572
|
+
raise ValueError(
|
|
1573
|
+
f"Duplicate Cityscapes {source_name} sample ID: {sample_id}."
|
|
1574
|
+
)
|
|
1575
|
+
files[sample_id] = os.path.join(current_root, file_name)
|
|
1576
|
+
return files
|
|
1577
|
+
|
|
1578
|
+
|
|
1579
|
+
def organize_cityscapes(
|
|
1580
|
+
image_dir: str,
|
|
1581
|
+
annotation_dir: str,
|
|
1582
|
+
output_dir: str | None = None,
|
|
1583
|
+
) -> None:
|
|
1584
|
+
"""Install official Cityscapes validation archives as lossless flat PNG pairs.
|
|
1585
|
+
|
|
1586
|
+
Only validation RGB images and ``gtFine_labelIds`` masks are selected.
|
|
1587
|
+
Training, test, and auxiliary annotation files remain excluded.
|
|
1588
|
+
|
|
1589
|
+
Args:
|
|
1590
|
+
image_dir: Path to ``leftImg8bit_trainvaltest.zip``.
|
|
1591
|
+
annotation_dir: Path to ``gtFine_trainvaltest.zip``.
|
|
1592
|
+
output_dir: Directory where the organized validation dataset is stored.
|
|
1593
|
+
Defaults to the resolved Mobilint cache directory.
|
|
1594
|
+
|
|
1595
|
+
Raises:
|
|
1596
|
+
ValueError: If either source is invalid or does not contain exactly 500 matching pairs.
|
|
1597
|
+
OSError: If extraction, copying, or atomic installation fails.
|
|
1598
|
+
"""
|
|
1599
|
+
|
|
1600
|
+
output_dir = _resolve_organizer_output_dir(output_dir, "cityscapes")
|
|
1601
|
+
output_dir = _validate_dense_output_root(
|
|
1602
|
+
output_dir, "Cityscapes", ("images", "annotations")
|
|
1603
|
+
)
|
|
1604
|
+
image_archive = _validate_cityscapes_zip(image_dir, "image")
|
|
1605
|
+
annotation_archive = _validate_cityscapes_zip(annotation_dir, "annotation")
|
|
1606
|
+
output_parent_dir = os.path.dirname(output_dir)
|
|
1607
|
+
os.makedirs(output_parent_dir, exist_ok=True)
|
|
1608
|
+
with TemporaryDirectory(
|
|
1609
|
+
dir=output_parent_dir, prefix=".cityscapes-staging-"
|
|
1610
|
+
) as staging_dir:
|
|
1611
|
+
extracted_image_dir = os.path.join(staging_dir, "raw-images")
|
|
1612
|
+
extracted_annotation_dir = os.path.join(staging_dir, "raw-annotations")
|
|
1613
|
+
_safe_unpack_archive(image_archive, extracted_image_dir)
|
|
1614
|
+
_safe_unpack_archive(annotation_archive, extracted_annotation_dir)
|
|
1615
|
+
images = _collect_cityscapes_validation_files(
|
|
1616
|
+
os.path.join(extracted_image_dir, "leftImg8bit", "val"),
|
|
1617
|
+
CITYSCAPES_IMAGE_SUFFIX,
|
|
1618
|
+
"image",
|
|
1619
|
+
)
|
|
1620
|
+
annotations = _collect_cityscapes_validation_files(
|
|
1621
|
+
os.path.join(extracted_annotation_dir, "gtFine", "val"),
|
|
1622
|
+
CITYSCAPES_ANNOTATION_SUFFIX,
|
|
1623
|
+
"annotation",
|
|
1624
|
+
)
|
|
1625
|
+
missing_annotations = sorted(images.keys() - annotations.keys())
|
|
1626
|
+
missing_images = sorted(annotations.keys() - images.keys())
|
|
1627
|
+
if missing_annotations or missing_images:
|
|
1628
|
+
details = []
|
|
1629
|
+
if missing_annotations:
|
|
1630
|
+
details.append(
|
|
1631
|
+
f"missing annotations for {', '.join(missing_annotations[:3])}"
|
|
1632
|
+
)
|
|
1633
|
+
if missing_images:
|
|
1634
|
+
details.append(f"missing images for {', '.join(missing_images[:3])}")
|
|
1635
|
+
raise ValueError(
|
|
1636
|
+
f"Cityscapes validation image/annotation mismatch ({'; '.join(details)})."
|
|
1637
|
+
)
|
|
1638
|
+
if len(images) != CITYSCAPES_VALIDATION_SAMPLE_COUNT:
|
|
1639
|
+
raise ValueError(
|
|
1640
|
+
"Cityscapes validation archives must contain "
|
|
1641
|
+
f"{CITYSCAPES_VALIDATION_SAMPLE_COUNT} pairs, found {len(images)}."
|
|
1642
|
+
)
|
|
1643
|
+
|
|
1644
|
+
staged_image_dir = os.path.join(staging_dir, "images")
|
|
1645
|
+
staged_annotation_dir = os.path.join(staging_dir, "annotations")
|
|
1646
|
+
os.makedirs(staged_image_dir)
|
|
1647
|
+
os.makedirs(staged_annotation_dir)
|
|
1648
|
+
for sample_id in sorted(images):
|
|
1649
|
+
shutil.copy2(
|
|
1650
|
+
images[sample_id], os.path.join(staged_image_dir, f"{sample_id}.png")
|
|
1651
|
+
)
|
|
1652
|
+
shutil.copy2(
|
|
1653
|
+
annotations[sample_id],
|
|
1654
|
+
os.path.join(staged_annotation_dir, f"{sample_id}.png"),
|
|
1655
|
+
)
|
|
1656
|
+
|
|
1657
|
+
if not dataset_ready(staging_dir, "semantic_segmentation", "cityscapes"):
|
|
1658
|
+
raise ValueError(
|
|
1659
|
+
"Staged Cityscapes validation data failed identity and completeness checks."
|
|
1660
|
+
)
|
|
1661
|
+
_validate_staged_payloads(Path(staging_dir), "cityscapes")
|
|
1662
|
+
|
|
1663
|
+
replacements = (
|
|
1664
|
+
(staged_image_dir, os.path.join(output_dir, "images")),
|
|
1665
|
+
(staged_annotation_dir, os.path.join(output_dir, "annotations")),
|
|
1666
|
+
)
|
|
1667
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
1668
|
+
_replace_staged_directories(
|
|
1669
|
+
replacements, output_parent_dir, ".cityscapes-backup-"
|
|
1670
|
+
)
|
|
1671
|
+
print(
|
|
1672
|
+
f"Constructed Cityscapes validation dataset with {len(images)} image/mask pairs"
|
|
1673
|
+
)
|
|
1674
|
+
|
|
1675
|
+
|
|
1676
|
+
def _resolve_dotav1_root(dataset_dir: str) -> str:
|
|
1677
|
+
"""Resolves a DOTAv1 dataset root from a directory path.
|
|
1678
|
+
|
|
1679
|
+
Args:
|
|
1680
|
+
dataset_dir: Directory containing the DOTAv1 dataset or its parent.
|
|
1681
|
+
|
|
1682
|
+
Returns:
|
|
1683
|
+
Path to the DOTAv1 dataset root.
|
|
1684
|
+
"""
|
|
1685
|
+
dotav1_dir = os.path.join(dataset_dir, "DOTAv1")
|
|
1686
|
+
if os.path.isdir(dotav1_dir):
|
|
1687
|
+
return dotav1_dir
|
|
1688
|
+
return dataset_dir
|
|
1689
|
+
|
|
1690
|
+
|
|
1691
|
+
def _is_google_drive_folder_url(path_or_url: str) -> bool:
|
|
1692
|
+
"""Returns whether a URL points to a Google Drive folder."""
|
|
1693
|
+
|
|
1694
|
+
parsed = urlparse(path_or_url)
|
|
1695
|
+
return parsed.hostname == "drive.google.com" and bool(
|
|
1696
|
+
re.fullmatch(r"/drive(?:/u/[^/]+)?/folders/[^/]+/?", parsed.path)
|
|
1697
|
+
)
|
|
1698
|
+
|
|
1699
|
+
|
|
1700
|
+
def _download_dotav1_google_drive_archives(
|
|
1701
|
+
folder_url: str, download_dir: str
|
|
1702
|
+
) -> tuple[str, str]:
|
|
1703
|
+
"""Downloads the DOTAv1 image and v1.0-label archives from a Google Drive folder.
|
|
1704
|
+
|
|
1705
|
+
Args:
|
|
1706
|
+
folder_url: Public Google Drive folder URL containing the DOTAv1 archives.
|
|
1707
|
+
download_dir: Directory where the selected archives will be stored.
|
|
1708
|
+
|
|
1709
|
+
Returns:
|
|
1710
|
+
Paths to the image archive and original v1.0-label archive.
|
|
1711
|
+
|
|
1712
|
+
Raises:
|
|
1713
|
+
ValueError: If the required archives are absent from the Drive folder.
|
|
1714
|
+
RuntimeError: If gdown fails to download a required archive.
|
|
1715
|
+
"""
|
|
1716
|
+
|
|
1717
|
+
print(f"Retrieving DOTAv1 archive list from {folder_url}...")
|
|
1718
|
+
folder_entries = download_folder(
|
|
1719
|
+
url=folder_url, output=download_dir, quiet=True, skip_download=True
|
|
1720
|
+
)
|
|
1721
|
+
if folder_entries is None:
|
|
1722
|
+
raise RuntimeError(
|
|
1723
|
+
f"Failed to retrieve the DOTAv1 Google Drive folder listing: {folder_url}"
|
|
1724
|
+
)
|
|
1725
|
+
files = [
|
|
1726
|
+
entry for entry in folder_entries if _is_google_drive_download_entry(entry)
|
|
1727
|
+
]
|
|
1728
|
+
archives: dict[str, _GoogleDriveDownloadEntry] = {}
|
|
1729
|
+
for archive_path in DOTAV1_GOOGLE_DRIVE_ARCHIVES:
|
|
1730
|
+
matches = [
|
|
1731
|
+
drive_file
|
|
1732
|
+
for drive_file in files
|
|
1733
|
+
if drive_file.path == archive_path
|
|
1734
|
+
or drive_file.path.endswith(f"/{archive_path}")
|
|
1735
|
+
]
|
|
1736
|
+
if len(matches) == 1:
|
|
1737
|
+
archives[archive_path] = matches[0]
|
|
1738
|
+
continue
|
|
1739
|
+
|
|
1740
|
+
available = ", ".join(sorted(drive_file.path for drive_file in files)) or "none"
|
|
1741
|
+
if not matches:
|
|
1742
|
+
raise ValueError(
|
|
1743
|
+
f"DOTAv1 Drive folder is missing {archive_path}. Available files: {available}."
|
|
1744
|
+
)
|
|
1745
|
+
ambiguous = ", ".join(sorted(drive_file.path for drive_file in matches))
|
|
1746
|
+
raise ValueError(
|
|
1747
|
+
f"DOTAv1 Drive folder has ambiguous matches for {archive_path}: {ambiguous}."
|
|
1748
|
+
)
|
|
1749
|
+
|
|
1750
|
+
local_archives: dict[str, str] = {}
|
|
1751
|
+
for archive_path in sorted(DOTAV1_GOOGLE_DRIVE_ARCHIVES):
|
|
1752
|
+
drive_file = archives[archive_path]
|
|
1753
|
+
local_path = os.path.join(download_dir, os.path.basename(archive_path))
|
|
1754
|
+
print(f"Downloading DOTAv1 {archive_path}...")
|
|
1755
|
+
downloaded_path = download(
|
|
1756
|
+
id=drive_file.id, output=local_path, quiet=False, resume=True
|
|
1757
|
+
)
|
|
1758
|
+
if not isinstance(downloaded_path, str):
|
|
1759
|
+
raise RuntimeError(
|
|
1760
|
+
f"Failed to download DOTAv1 archive {archive_path} from {folder_url}."
|
|
1761
|
+
)
|
|
1762
|
+
local_archives[archive_path] = downloaded_path
|
|
1763
|
+
|
|
1764
|
+
return (
|
|
1765
|
+
local_archives[DOTAV1_DOWNLOAD_CONFIG["images_archive"]],
|
|
1766
|
+
local_archives[DOTAV1_DOWNLOAD_CONFIG["labels_archive"]],
|
|
1767
|
+
)
|
|
1768
|
+
|
|
1769
|
+
|
|
1770
|
+
def _iter_files(root: str, extensions: Iterable[str]) -> Iterable[str]:
|
|
1771
|
+
"""Yields files below a directory with one of the requested suffixes."""
|
|
1772
|
+
|
|
1773
|
+
suffixes = tuple(extension.lower() for extension in extensions)
|
|
1774
|
+
for current_root, _, file_names in os.walk(root):
|
|
1775
|
+
for file_name in file_names:
|
|
1776
|
+
if file_name.lower().endswith(suffixes):
|
|
1777
|
+
yield os.path.join(current_root, file_name)
|
|
1778
|
+
|
|
1779
|
+
|
|
1780
|
+
def _safe_archive_member_path(member_name: str, destination: str) -> str:
|
|
1781
|
+
"""Return an archive member destination after enforcing staging-directory containment.
|
|
1782
|
+
|
|
1783
|
+
Args:
|
|
1784
|
+
member_name: Path stored in an archive member.
|
|
1785
|
+
destination: Archive extraction directory.
|
|
1786
|
+
|
|
1787
|
+
Returns:
|
|
1788
|
+
Absolute destination path for the member.
|
|
1789
|
+
|
|
1790
|
+
Raises:
|
|
1791
|
+
ValueError: If the member path is absolute or escapes the extraction directory.
|
|
1792
|
+
"""
|
|
1793
|
+
|
|
1794
|
+
root = os.path.abspath(destination)
|
|
1795
|
+
target = os.path.abspath(os.path.join(root, member_name))
|
|
1796
|
+
if os.path.commonpath((root, target)) != root:
|
|
1797
|
+
raise ValueError(f"Unsafe archive member path: {member_name!r}.")
|
|
1798
|
+
return target
|
|
1799
|
+
|
|
1800
|
+
|
|
1801
|
+
def _safe_unpack_archive(archive_path: str, destination: str) -> None:
|
|
1802
|
+
"""Extract an archive while rejecting links, special files, and escaping paths.
|
|
1803
|
+
|
|
1804
|
+
Args:
|
|
1805
|
+
archive_path: ZIP or tar-family dataset archive.
|
|
1806
|
+
destination: Empty staging directory where archive members are written.
|
|
1807
|
+
|
|
1808
|
+
Raises:
|
|
1809
|
+
ValueError: If the archive format or any member is unsafe or unsupported.
|
|
1810
|
+
OSError: If a validated archive cannot be read or written.
|
|
1811
|
+
"""
|
|
1812
|
+
|
|
1813
|
+
if zipfile.is_zipfile(archive_path):
|
|
1814
|
+
with zipfile.ZipFile(archive_path) as archive:
|
|
1815
|
+
members = archive.infolist()
|
|
1816
|
+
targets: set[str] = set()
|
|
1817
|
+
for member in members:
|
|
1818
|
+
target = _safe_archive_member_path(member.filename, destination)
|
|
1819
|
+
if target in targets:
|
|
1820
|
+
raise ValueError(
|
|
1821
|
+
f"Duplicate archive member path: {member.filename!r}."
|
|
1822
|
+
)
|
|
1823
|
+
targets.add(target)
|
|
1824
|
+
file_type = stat.S_IFMT(member.external_attr >> 16)
|
|
1825
|
+
if file_type and not (
|
|
1826
|
+
stat.S_ISREG(file_type) or stat.S_ISDIR(file_type)
|
|
1827
|
+
):
|
|
1828
|
+
raise ValueError(
|
|
1829
|
+
f"Unsafe archive member type: {member.filename!r}."
|
|
1830
|
+
)
|
|
1831
|
+
for member in members:
|
|
1832
|
+
target = _safe_archive_member_path(member.filename, destination)
|
|
1833
|
+
if member.is_dir():
|
|
1834
|
+
os.makedirs(target, exist_ok=True)
|
|
1835
|
+
continue
|
|
1836
|
+
os.makedirs(os.path.dirname(target), exist_ok=True)
|
|
1837
|
+
with archive.open(member) as source, open(target, "wb") as output_file:
|
|
1838
|
+
shutil.copyfileobj(source, output_file)
|
|
1839
|
+
return
|
|
1840
|
+
|
|
1841
|
+
if tarfile.is_tarfile(archive_path):
|
|
1842
|
+
with tarfile.open(archive_path) as archive:
|
|
1843
|
+
members = archive.getmembers()
|
|
1844
|
+
targets: set[str] = set()
|
|
1845
|
+
for member in members:
|
|
1846
|
+
target = _safe_archive_member_path(member.name, destination)
|
|
1847
|
+
if target in targets:
|
|
1848
|
+
raise ValueError(f"Duplicate archive member path: {member.name!r}.")
|
|
1849
|
+
targets.add(target)
|
|
1850
|
+
if not (member.isfile() or member.isdir()):
|
|
1851
|
+
raise ValueError(f"Unsafe archive member type: {member.name!r}.")
|
|
1852
|
+
for member in members:
|
|
1853
|
+
target = _safe_archive_member_path(member.name, destination)
|
|
1854
|
+
if member.isdir():
|
|
1855
|
+
os.makedirs(target, exist_ok=True)
|
|
1856
|
+
continue
|
|
1857
|
+
source = archive.extractfile(member)
|
|
1858
|
+
if source is None:
|
|
1859
|
+
raise ValueError(f"Unable to read archive member: {member.name!r}.")
|
|
1860
|
+
os.makedirs(os.path.dirname(target), exist_ok=True)
|
|
1861
|
+
with source, open(target, "wb") as output_file:
|
|
1862
|
+
shutil.copyfileobj(source, output_file)
|
|
1863
|
+
return
|
|
1864
|
+
|
|
1865
|
+
raise ValueError(f"Unsupported archive format: {archive_path}.")
|
|
1866
|
+
|
|
1867
|
+
|
|
1868
|
+
def _write_dotav1_yolo_labels(
|
|
1869
|
+
image_path: str, original_label_path: str, output_path: str
|
|
1870
|
+
) -> None:
|
|
1871
|
+
"""Converts one official DOTAv1 label file into normalized OBB label format."""
|
|
1872
|
+
|
|
1873
|
+
with Image.open(image_path) as image:
|
|
1874
|
+
width, height = image.size
|
|
1875
|
+
converted_lines: list[str] = []
|
|
1876
|
+
seen_targets: set[tuple[str, tuple[float, ...]]] = set()
|
|
1877
|
+
with open(original_label_path, encoding="utf-8") as label_file:
|
|
1878
|
+
for line_number, line in enumerate(label_file, start=1):
|
|
1879
|
+
fields = line.split()
|
|
1880
|
+
if fields and (
|
|
1881
|
+
fields[0].startswith("imagesource:") or fields[0].startswith("gsd:")
|
|
1882
|
+
):
|
|
1883
|
+
continue
|
|
1884
|
+
if len(fields) < 10:
|
|
1885
|
+
raise ValueError(
|
|
1886
|
+
"Malformed DOTAv1 annotation in "
|
|
1887
|
+
f"{original_label_path} at line {line_number}: expected at least "
|
|
1888
|
+
f"10 fields, got {len(fields)}."
|
|
1889
|
+
)
|
|
1890
|
+
class_name = fields[8]
|
|
1891
|
+
if class_name not in DOTAV1_CLASS_TO_IDX:
|
|
1892
|
+
raise ValueError(
|
|
1893
|
+
f"Unsupported DOTAv1 class in {original_label_path}: {class_name}"
|
|
1894
|
+
)
|
|
1895
|
+
coordinates = [float(value) for value in fields[:8]]
|
|
1896
|
+
if not all(math.isfinite(coordinate) for coordinate in coordinates):
|
|
1897
|
+
raise ValueError(
|
|
1898
|
+
f"DOTAv1 coordinates must be finite in {original_label_path} "
|
|
1899
|
+
f"at line {line_number}."
|
|
1900
|
+
)
|
|
1901
|
+
if not _polygon_has_positive_image_overlap(coordinates, (height, width)):
|
|
1902
|
+
raise ValueError(
|
|
1903
|
+
"DOTAv1 polygon must overlap its source image in "
|
|
1904
|
+
f"{original_label_path} at line {line_number}."
|
|
1905
|
+
)
|
|
1906
|
+
_validate_dotav1_polygon_vertices(
|
|
1907
|
+
coordinates, original_label_path, line_number
|
|
1908
|
+
)
|
|
1909
|
+
if fields[9] not in {"0", "1", "2"}:
|
|
1910
|
+
raise ValueError(
|
|
1911
|
+
f"Unsupported DOTAv1 difficulty flag {fields[9]!r} in "
|
|
1912
|
+
f"{original_label_path} at line {line_number}."
|
|
1913
|
+
)
|
|
1914
|
+
target_key = (class_name, _canonicalize_quadrilateral(coordinates))
|
|
1915
|
+
if target_key in seen_targets:
|
|
1916
|
+
raise ValueError(
|
|
1917
|
+
"Duplicate DOTAv1 annotation target in "
|
|
1918
|
+
f"{original_label_path} at line {line_number}."
|
|
1919
|
+
)
|
|
1920
|
+
seen_targets.add(target_key)
|
|
1921
|
+
normalized = [
|
|
1922
|
+
coordinate / (width if index % 2 == 0 else height)
|
|
1923
|
+
for index, coordinate in enumerate(coordinates)
|
|
1924
|
+
]
|
|
1925
|
+
converted_lines.append(
|
|
1926
|
+
f"{DOTAV1_CLASS_TO_IDX[class_name]} "
|
|
1927
|
+
+ " ".join(f"{coordinate:.8g}" for coordinate in normalized)
|
|
1928
|
+
# The trailing flag is normalized-label metadata, not a YOLO OBB
|
|
1929
|
+
# coordinate. It preserves official difficult regions for evaluation.
|
|
1930
|
+
+ f" {int(fields[9] in {'1', '2'})}"
|
|
1931
|
+
)
|
|
1932
|
+
with open(output_path, "w", encoding="utf-8") as output_file:
|
|
1933
|
+
output_file.write("\n".join(converted_lines))
|
|
1934
|
+
if converted_lines:
|
|
1935
|
+
output_file.write("\n")
|
|
1936
|
+
|
|
1937
|
+
|
|
1938
|
+
def construct_dotav1_from_archives(
|
|
1939
|
+
image_archive: str, label_archive: str, output_dir: str
|
|
1940
|
+
) -> None:
|
|
1941
|
+
"""Constructs the DOTAv1 validation layout from the Google Drive archives.
|
|
1942
|
+
|
|
1943
|
+
Args:
|
|
1944
|
+
image_archive: Path to the DOTAv1 validation-image archive.
|
|
1945
|
+
label_archive: Path to the original DOTAv1 v1.0 label archive.
|
|
1946
|
+
output_dir: Directory where the organized validation dataset will be stored.
|
|
1947
|
+
|
|
1948
|
+
Raises:
|
|
1949
|
+
ValueError: If the archives have no validation files or their image and label stems differ.
|
|
1950
|
+
OSError: If staging or replacing the organized dataset files fails.
|
|
1951
|
+
"""
|
|
1952
|
+
|
|
1953
|
+
with TemporaryDirectory() as extract_dir:
|
|
1954
|
+
image_dir = os.path.join(extract_dir, "images")
|
|
1955
|
+
label_dir = os.path.join(extract_dir, "labels")
|
|
1956
|
+
_safe_unpack_archive(image_archive, image_dir)
|
|
1957
|
+
_safe_unpack_archive(label_archive, label_dir)
|
|
1958
|
+
|
|
1959
|
+
label_paths = list(_iter_files(label_dir, [".txt"]))
|
|
1960
|
+
labels = {
|
|
1961
|
+
os.path.splitext(os.path.basename(path))[0]: path for path in label_paths
|
|
1962
|
+
}
|
|
1963
|
+
if len(labels) != len(label_paths):
|
|
1964
|
+
raise ValueError("DOTAv1 archive contains duplicate label stems.")
|
|
1965
|
+
if not labels:
|
|
1966
|
+
raise ValueError(f"No DOTAv1 label files found in {label_archive}.")
|
|
1967
|
+
|
|
1968
|
+
image_paths = list(
|
|
1969
|
+
_iter_files(image_dir, [".bmp", ".jpg", ".jpeg", ".png", ".tif", ".tiff"])
|
|
1970
|
+
)
|
|
1971
|
+
images = {
|
|
1972
|
+
os.path.splitext(os.path.basename(path))[0]: path for path in image_paths
|
|
1973
|
+
}
|
|
1974
|
+
if len(images) != len(image_paths):
|
|
1975
|
+
raise ValueError("DOTAv1 archive contains duplicate image stems.")
|
|
1976
|
+
image_ids = set(images)
|
|
1977
|
+
label_ids = set(labels)
|
|
1978
|
+
missing_labels = sorted(image_ids - label_ids)
|
|
1979
|
+
missing_images = sorted(label_ids - image_ids)
|
|
1980
|
+
if missing_labels or missing_images:
|
|
1981
|
+
details = []
|
|
1982
|
+
if missing_labels:
|
|
1983
|
+
details.append(
|
|
1984
|
+
f"images without labels: {', '.join(missing_labels[:5])}"
|
|
1985
|
+
)
|
|
1986
|
+
if missing_images:
|
|
1987
|
+
details.append(
|
|
1988
|
+
f"labels without images: {', '.join(missing_images[:5])}"
|
|
1989
|
+
)
|
|
1990
|
+
raise ValueError(f"DOTAv1 archive stem mismatch ({'; '.join(details)}).")
|
|
1991
|
+
matching_ids = sorted(image_ids)
|
|
1992
|
+
if len(matching_ids) != DOTAV1_VALIDATION_SAMPLE_COUNT:
|
|
1993
|
+
raise ValueError(
|
|
1994
|
+
"DOTAv1 validation dataset must contain "
|
|
1995
|
+
f"{DOTAV1_VALIDATION_SAMPLE_COUNT} matching image/label pairs, found {len(matching_ids)}."
|
|
1996
|
+
)
|
|
1997
|
+
|
|
1998
|
+
output_dir = os.path.abspath(output_dir)
|
|
1999
|
+
output_parent_dir = os.path.dirname(output_dir)
|
|
2000
|
+
os.makedirs(output_parent_dir, exist_ok=True)
|
|
2001
|
+
with TemporaryDirectory(
|
|
2002
|
+
dir=output_parent_dir, prefix=".dotav1-staging-"
|
|
2003
|
+
) as staging_dir:
|
|
2004
|
+
staged_output_dir = os.path.join(staging_dir, "dotav1")
|
|
2005
|
+
staged_image_dir = os.path.join(staged_output_dir, "images")
|
|
2006
|
+
staged_label_dir = os.path.join(staged_output_dir, "labels", "val")
|
|
2007
|
+
staged_original_label_dir = os.path.join(
|
|
2008
|
+
staged_output_dir, "labels", "val_original"
|
|
2009
|
+
)
|
|
2010
|
+
os.makedirs(staged_image_dir)
|
|
2011
|
+
os.makedirs(staged_label_dir)
|
|
2012
|
+
os.makedirs(staged_original_label_dir)
|
|
2013
|
+
|
|
2014
|
+
for image_id in matching_ids:
|
|
2015
|
+
image_path = images[image_id]
|
|
2016
|
+
shutil.copy2(
|
|
2017
|
+
image_path,
|
|
2018
|
+
os.path.join(staged_image_dir, os.path.basename(image_path)),
|
|
2019
|
+
)
|
|
2020
|
+
|
|
2021
|
+
for image_id in matching_ids:
|
|
2022
|
+
shutil.copy2(
|
|
2023
|
+
labels[image_id],
|
|
2024
|
+
os.path.join(staged_original_label_dir, f"{image_id}.txt"),
|
|
2025
|
+
)
|
|
2026
|
+
_write_dotav1_yolo_labels(
|
|
2027
|
+
images[image_id],
|
|
2028
|
+
labels[image_id],
|
|
2029
|
+
os.path.join(staged_label_dir, f"{image_id}.txt"),
|
|
2030
|
+
)
|
|
2031
|
+
|
|
2032
|
+
_validate_staged_dataset(staged_output_dir, "dotav1", ("obb",))
|
|
2033
|
+
_replace_staged_directories(
|
|
2034
|
+
((staged_output_dir, output_dir),),
|
|
2035
|
+
output_parent_dir,
|
|
2036
|
+
".dotav1-backup-",
|
|
2037
|
+
)
|
|
2038
|
+
|
|
2039
|
+
print(f"Constructed DOTAv1 validation dataset with {len(matching_ids)} images")
|
|
2040
|
+
|
|
2041
|
+
|
|
2042
|
+
def _copy_dotav1_layout_to_staging(dataset_root: str, staged_output_dir: str) -> None:
|
|
2043
|
+
"""Copy a flat or legacy DOTAv1 validation layout into canonical staging."""
|
|
2044
|
+
|
|
2045
|
+
image_root = os.path.join(dataset_root, "images")
|
|
2046
|
+
supported_image_suffixes = (".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff")
|
|
2047
|
+
flat_image_files = (
|
|
2048
|
+
[
|
|
2049
|
+
file_name
|
|
2050
|
+
for file_name in os.listdir(image_root)
|
|
2051
|
+
if os.path.isfile(os.path.join(image_root, file_name))
|
|
2052
|
+
and file_name.lower().endswith(supported_image_suffixes)
|
|
2053
|
+
]
|
|
2054
|
+
if os.path.isdir(image_root)
|
|
2055
|
+
else []
|
|
2056
|
+
)
|
|
2057
|
+
source_image_dir = (
|
|
2058
|
+
image_root if flat_image_files else os.path.join(image_root, "val")
|
|
2059
|
+
)
|
|
2060
|
+
if not os.path.isdir(source_image_dir):
|
|
2061
|
+
raise ValueError(f"No DOTAv1 validation images found in {dataset_root}")
|
|
2062
|
+
|
|
2063
|
+
staged_image_dir = os.path.join(staged_output_dir, "images")
|
|
2064
|
+
os.makedirs(staged_image_dir)
|
|
2065
|
+
for file_name in os.listdir(source_image_dir):
|
|
2066
|
+
source_path = os.path.join(source_image_dir, file_name)
|
|
2067
|
+
if os.path.isfile(source_path) and file_name.lower().endswith(
|
|
2068
|
+
supported_image_suffixes
|
|
2069
|
+
):
|
|
2070
|
+
shutil.copy2(source_path, os.path.join(staged_image_dir, file_name))
|
|
2071
|
+
|
|
2072
|
+
for label_directory in ("val", "val_original"):
|
|
2073
|
+
source_label_dir = os.path.join(dataset_root, "labels", label_directory)
|
|
2074
|
+
if not os.path.isdir(source_label_dir):
|
|
2075
|
+
continue
|
|
2076
|
+
staged_label_dir = os.path.join(staged_output_dir, "labels", label_directory)
|
|
2077
|
+
os.makedirs(staged_label_dir, exist_ok=True)
|
|
2078
|
+
for file_name in os.listdir(source_label_dir):
|
|
2079
|
+
source_path = os.path.join(source_label_dir, file_name)
|
|
2080
|
+
if os.path.isfile(source_path) and file_name.lower().endswith(".txt"):
|
|
2081
|
+
shutil.copy2(source_path, os.path.join(staged_label_dir, file_name))
|
|
2082
|
+
|
|
2083
|
+
|
|
2084
|
+
def construct_dotav1(dataset_dir: str, output_dir: str) -> None:
|
|
2085
|
+
"""Constructs a validation-only DOTAv1 dataset.
|
|
2086
|
+
|
|
2087
|
+
Args:
|
|
2088
|
+
dataset_dir: Directory containing a DOTAv1 dataset or its parent.
|
|
2089
|
+
output_dir: Directory where the organized validation dataset will be stored.
|
|
2090
|
+
|
|
2091
|
+
Raises:
|
|
2092
|
+
ValueError: If the staged validation dataset is incomplete or mismatched.
|
|
2093
|
+
OSError: If staging or replacing the organized dataset files fails.
|
|
2094
|
+
"""
|
|
2095
|
+
dataset_root = _resolve_dotav1_root(dataset_dir)
|
|
2096
|
+
print(f"Constructing DOTAv1 validation dataset from {dataset_root} to {output_dir}")
|
|
2097
|
+
output_dir = os.path.abspath(output_dir)
|
|
2098
|
+
output_parent_dir = os.path.dirname(output_dir)
|
|
2099
|
+
os.makedirs(output_parent_dir, exist_ok=True)
|
|
2100
|
+
with TemporaryDirectory(
|
|
2101
|
+
dir=output_parent_dir, prefix=".dotav1-staging-"
|
|
2102
|
+
) as staging_dir:
|
|
2103
|
+
staged_output_dir = os.path.join(staging_dir, "dotav1")
|
|
2104
|
+
os.makedirs(staged_output_dir)
|
|
2105
|
+
_copy_dotav1_layout_to_staging(dataset_root, staged_output_dir)
|
|
2106
|
+
_validate_staged_dataset(staged_output_dir, "dotav1", ("obb",))
|
|
2107
|
+
_replace_staged_directories(
|
|
2108
|
+
((staged_output_dir, output_dir),),
|
|
2109
|
+
output_parent_dir,
|
|
2110
|
+
".dotav1-backup-",
|
|
2111
|
+
)
|
|
2112
|
+
|
|
2113
|
+
print("Constructing DOTAv1 validation dataset completed")
|
|
2114
|
+
|
|
2115
|
+
|
|
2116
|
+
def organize_dotav1(
|
|
2117
|
+
dataset_path: str,
|
|
2118
|
+
output_dir: str | None = None,
|
|
2119
|
+
) -> None:
|
|
2120
|
+
"""Organizes a validation-only DOTAv1 dataset.
|
|
2121
|
+
|
|
2122
|
+
Args:
|
|
2123
|
+
dataset_path: Path or URL to the DOTAv1 zip file or extracted dataset directory.
|
|
2124
|
+
output_dir: Directory to store the organized dataset. Defaults to the
|
|
2125
|
+
resolved Mobilint cache directory.
|
|
2126
|
+
"""
|
|
2127
|
+
output_dir = _resolve_organizer_output_dir(output_dir, "dotav1")
|
|
2128
|
+
with TemporaryDirectory() as temp_dir:
|
|
2129
|
+
if _is_google_drive_folder_url(dataset_path):
|
|
2130
|
+
image_archive, label_archive = _download_dotav1_google_drive_archives(
|
|
2131
|
+
dataset_path, temp_dir
|
|
2132
|
+
)
|
|
2133
|
+
construct_dotav1_from_archives(image_archive, label_archive, output_dir)
|
|
2134
|
+
return
|
|
2135
|
+
|
|
2136
|
+
local_dataset_path = _resolve_source(dataset_path, temp_dir)
|
|
2137
|
+
|
|
2138
|
+
if local_dataset_path.endswith(".zip"):
|
|
2139
|
+
print("Unpacking DOTAv1 files to temporary directory...")
|
|
2140
|
+
_safe_unpack_archive(local_dataset_path, temp_dir)
|
|
2141
|
+
print("Unpacking completed")
|
|
2142
|
+
construct_dotav1(temp_dir, output_dir)
|
|
2143
|
+
return
|
|
2144
|
+
|
|
2145
|
+
construct_dotav1(local_dataset_path, output_dir)
|