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.
Files changed (404) hide show
  1. mblt_vision/__init__.py +86 -0
  2. mblt_vision/_api.py +53 -0
  3. mblt_vision/_compat.py +227 -0
  4. mblt_vision/_model_paths.py +137 -0
  5. mblt_vision/_tasks.py +30 -0
  6. mblt_vision/benchmark/__init__.py +1 -0
  7. mblt_vision/benchmark/argparse_utils.py +106 -0
  8. mblt_vision/benchmark/chart_utils.py +146 -0
  9. mblt_vision/benchmark/io_utils.py +64 -0
  10. mblt_vision/benchmark/summary_utils.py +770 -0
  11. mblt_vision/cli/__init__.py +5 -0
  12. mblt_vision/cli/__main__.py +5 -0
  13. mblt_vision/cli/_vision.py +337 -0
  14. mblt_vision/cli/compile.py +117 -0
  15. mblt_vision/cli/main.py +35 -0
  16. mblt_vision/cli/predict.py +64 -0
  17. mblt_vision/cli/val.py +562 -0
  18. mblt_vision/compile/__init__.py +21 -0
  19. mblt_vision/compile/vision.py +1120 -0
  20. mblt_vision/datasets/__init__.py +15 -0
  21. mblt_vision/datasets/ade20k.yaml +164 -0
  22. mblt_vision/datasets/cityscapes.yaml +54 -0
  23. mblt_vision/datasets/coco.yaml +178 -0
  24. mblt_vision/datasets/dotav1.yaml +28 -0
  25. mblt_vision/datasets/imagenet.yaml +1019 -0
  26. mblt_vision/datasets/imagenet_synsets.txt +1000 -0
  27. mblt_vision/datasets/nyu-depth.yaml +7 -0
  28. mblt_vision/datasets/registry.py +158 -0
  29. mblt_vision/datasets/widerface.yaml +9 -0
  30. mblt_vision/depth_estimation/__init__.py +19 -0
  31. mblt_vision/face_detection/__init__.py +43 -0
  32. mblt_vision/image_classification/__init__.py +311 -0
  33. mblt_vision/instance_segmentation/__init__.py +65 -0
  34. mblt_vision/models/AlexNet.yaml +23 -0
  35. mblt_vision/models/CAFormer_B36.yaml +22 -0
  36. mblt_vision/models/CAFormer_M36.yaml +22 -0
  37. mblt_vision/models/CAFormer_S18.yaml +22 -0
  38. mblt_vision/models/CAFormer_S36.yaml +22 -0
  39. mblt_vision/models/CoAtNet_0_RW_224.yaml +22 -0
  40. mblt_vision/models/CoAtNet_1_RW_224.yaml +22 -0
  41. mblt_vision/models/CoAtNet_2_RW_224.yaml +22 -0
  42. mblt_vision/models/ConvFormer_B36.yaml +22 -0
  43. mblt_vision/models/ConvFormer_M36.yaml +22 -0
  44. mblt_vision/models/ConvFormer_S18.yaml +22 -0
  45. mblt_vision/models/ConvFormer_S36.yaml +22 -0
  46. mblt_vision/models/ConvNext_Base.yaml +23 -0
  47. mblt_vision/models/ConvNext_Large.yaml +23 -0
  48. mblt_vision/models/ConvNext_Small.yaml +23 -0
  49. mblt_vision/models/ConvNext_Tiny.yaml +23 -0
  50. mblt_vision/models/DeiT3_Base_Patch16_224.yaml +22 -0
  51. mblt_vision/models/DeiT3_Base_Patch16_384.yaml +22 -0
  52. mblt_vision/models/DeiT3_Large_Patch16_224.yaml +22 -0
  53. mblt_vision/models/DeiT3_Large_Patch16_384.yaml +22 -0
  54. mblt_vision/models/DeiT3_Medium_Patch16_224.yaml +22 -0
  55. mblt_vision/models/DeiT3_Small_Patch16_224.yaml +22 -0
  56. mblt_vision/models/DeiT3_Small_Patch16_384.yaml +22 -0
  57. mblt_vision/models/DeiT_Base_Patch16_224.yaml +22 -0
  58. mblt_vision/models/DeiT_Base_Patch16_384.yaml +22 -0
  59. mblt_vision/models/DeiT_Small_Patch16_224.yaml +22 -0
  60. mblt_vision/models/DeiT_Tiny_Patch16_224.yaml +22 -0
  61. mblt_vision/models/DenseNet121.yaml +23 -0
  62. mblt_vision/models/DenseNet161.yaml +23 -0
  63. mblt_vision/models/DenseNet169.yaml +23 -0
  64. mblt_vision/models/DenseNet201.yaml +23 -0
  65. mblt_vision/models/EfficientFormer_L1.yaml +22 -0
  66. mblt_vision/models/EfficientFormer_L3.yaml +22 -0
  67. mblt_vision/models/EfficientFormer_L7.yaml +22 -0
  68. mblt_vision/models/EfficientNet_B0.yaml +23 -0
  69. mblt_vision/models/EfficientNet_B1.yaml +32 -0
  70. mblt_vision/models/EfficientNet_B2.yaml +23 -0
  71. mblt_vision/models/EfficientNet_B3.yaml +23 -0
  72. mblt_vision/models/EfficientNet_B4.yaml +23 -0
  73. mblt_vision/models/EfficientNet_B5.yaml +23 -0
  74. mblt_vision/models/EfficientNet_B6.yaml +23 -0
  75. mblt_vision/models/EfficientNet_B7.yaml +23 -0
  76. mblt_vision/models/EfficientNet_V2_L.yaml +23 -0
  77. mblt_vision/models/EfficientNet_V2_M.yaml +23 -0
  78. mblt_vision/models/EfficientNet_V2_S.yaml +23 -0
  79. mblt_vision/models/FlexiVit_Base.yaml +22 -0
  80. mblt_vision/models/FlexiVit_Large.yaml +22 -0
  81. mblt_vision/models/FlexiVit_Small.yaml +22 -0
  82. mblt_vision/models/GELANc-seg.yaml +29 -0
  83. mblt_vision/models/GELANc.yaml +28 -0
  84. mblt_vision/models/GELANe.yaml +28 -0
  85. mblt_vision/models/GELANm.yaml +28 -0
  86. mblt_vision/models/GELANs.yaml +28 -0
  87. mblt_vision/models/GoogLeNet.yaml +23 -0
  88. mblt_vision/models/Inception_V3.yaml +23 -0
  89. mblt_vision/models/LeViT_Conv_128.yaml +22 -0
  90. mblt_vision/models/LeViT_Conv_128S.yaml +22 -0
  91. mblt_vision/models/LeViT_Conv_192.yaml +22 -0
  92. mblt_vision/models/LeViT_Conv_256.yaml +22 -0
  93. mblt_vision/models/LeViT_Conv_384.yaml +22 -0
  94. mblt_vision/models/MNASNet0_5.yaml +23 -0
  95. mblt_vision/models/MNASNet0_75.yaml +23 -0
  96. mblt_vision/models/MNASNet1_0.yaml +23 -0
  97. mblt_vision/models/MNASNet1_3.yaml +23 -0
  98. mblt_vision/models/MobileNet_V2.yaml +32 -0
  99. mblt_vision/models/MobileNet_V3_Large.yaml +32 -0
  100. mblt_vision/models/MobileNet_V3_Small.yaml +23 -0
  101. mblt_vision/models/RegNet_X_16GF.yaml +32 -0
  102. mblt_vision/models/RegNet_X_1_6GF.yaml +32 -0
  103. mblt_vision/models/RegNet_X_32GF.yaml +32 -0
  104. mblt_vision/models/RegNet_X_3_2GF.yaml +32 -0
  105. mblt_vision/models/RegNet_X_400MF.yaml +32 -0
  106. mblt_vision/models/RegNet_X_800MF.yaml +32 -0
  107. mblt_vision/models/RegNet_X_8GF.yaml +32 -0
  108. mblt_vision/models/RegNet_Y_16GF.yaml +76 -0
  109. mblt_vision/models/RegNet_Y_1_6GF.yaml +32 -0
  110. mblt_vision/models/RegNet_Y_32GF.yaml +76 -0
  111. mblt_vision/models/RegNet_Y_3_2GF.yaml +32 -0
  112. mblt_vision/models/RegNet_Y_400MF.yaml +32 -0
  113. mblt_vision/models/RegNet_Y_800MF.yaml +32 -0
  114. mblt_vision/models/RegNet_Y_8GF.yaml +32 -0
  115. mblt_vision/models/RepViT_M0_9.yaml +22 -0
  116. mblt_vision/models/RepViT_M1.yaml +22 -0
  117. mblt_vision/models/RepViT_M1_0.yaml +22 -0
  118. mblt_vision/models/RepViT_M1_1.yaml +22 -0
  119. mblt_vision/models/RepViT_M1_5.yaml +22 -0
  120. mblt_vision/models/RepViT_M2.yaml +22 -0
  121. mblt_vision/models/RepViT_M2_3.yaml +22 -0
  122. mblt_vision/models/RepViT_M3.yaml +22 -0
  123. mblt_vision/models/ResNeXt101_32X8D.yaml +32 -0
  124. mblt_vision/models/ResNeXt101_64X4D.yaml +23 -0
  125. mblt_vision/models/ResNeXt50_32X4D.yaml +32 -0
  126. mblt_vision/models/ResNet101.yaml +32 -0
  127. mblt_vision/models/ResNet152.yaml +32 -0
  128. mblt_vision/models/ResNet18.yaml +23 -0
  129. mblt_vision/models/ResNet34.yaml +23 -0
  130. mblt_vision/models/ResNet50.yaml +32 -0
  131. mblt_vision/models/ShuffleNet_V2_X0_5.yaml +23 -0
  132. mblt_vision/models/ShuffleNet_V2_X1_0.yaml +23 -0
  133. mblt_vision/models/ShuffleNet_V2_X1_5.yaml +23 -0
  134. mblt_vision/models/ShuffleNet_V2_X2_0.yaml +23 -0
  135. mblt_vision/models/SqueezeNet1_0.yaml +23 -0
  136. mblt_vision/models/SqueezeNet1_1.yaml +23 -0
  137. mblt_vision/models/Swin_B.yaml +23 -0
  138. mblt_vision/models/Swin_S.yaml +23 -0
  139. mblt_vision/models/Swin_T.yaml +23 -0
  140. mblt_vision/models/VGG11.yaml +23 -0
  141. mblt_vision/models/VGG11_BN.yaml +23 -0
  142. mblt_vision/models/VGG13.yaml +23 -0
  143. mblt_vision/models/VGG13_BN.yaml +23 -0
  144. mblt_vision/models/VGG16.yaml +23 -0
  145. mblt_vision/models/VGG16_BN.yaml +23 -0
  146. mblt_vision/models/VGG19.yaml +23 -0
  147. mblt_vision/models/VGG19_BN.yaml +23 -0
  148. mblt_vision/models/ViT_B_16.yaml +67 -0
  149. mblt_vision/models/ViT_B_32.yaml +23 -0
  150. mblt_vision/models/ViT_Base_Patch16_224.yaml +22 -0
  151. mblt_vision/models/ViT_Base_Patch16_384.yaml +22 -0
  152. mblt_vision/models/ViT_Base_Patch32_224.yaml +22 -0
  153. mblt_vision/models/ViT_Base_Patch32_384.yaml +22 -0
  154. mblt_vision/models/ViT_Base_Patch8_224.yaml +22 -0
  155. mblt_vision/models/ViT_L_16.yaml +67 -0
  156. mblt_vision/models/ViT_L_32.yaml +23 -0
  157. mblt_vision/models/ViT_Large_Patch16_224.yaml +22 -0
  158. mblt_vision/models/ViT_Large_Patch16_384.yaml +22 -0
  159. mblt_vision/models/ViT_Large_Patch32_384.yaml +22 -0
  160. mblt_vision/models/ViT_Small_Patch16_224.yaml +22 -0
  161. mblt_vision/models/ViT_Small_Patch16_384.yaml +22 -0
  162. mblt_vision/models/ViT_Small_Patch32_224.yaml +22 -0
  163. mblt_vision/models/ViT_Small_Patch32_384.yaml +22 -0
  164. mblt_vision/models/ViT_Tiny_Patch16_224.yaml +22 -0
  165. mblt_vision/models/ViT_Tiny_Patch16_384.yaml +22 -0
  166. mblt_vision/models/VisFormer_Small.yaml +22 -0
  167. mblt_vision/models/VisFormer_Tiny.yaml +22 -0
  168. mblt_vision/models/Wide_ResNet101_2.yaml +32 -0
  169. mblt_vision/models/Wide_ResNet50_2.yaml +32 -0
  170. mblt_vision/models/YOLO11l-cls.yaml +23 -0
  171. mblt_vision/models/YOLO11l-face.yaml +27 -0
  172. mblt_vision/models/YOLO11l-obb.yaml +28 -0
  173. mblt_vision/models/YOLO11l-pose.yaml +28 -0
  174. mblt_vision/models/YOLO11l-seg.yaml +28 -0
  175. mblt_vision/models/YOLO11l.yaml +27 -0
  176. mblt_vision/models/YOLO11m-cls.yaml +23 -0
  177. mblt_vision/models/YOLO11m-face.yaml +27 -0
  178. mblt_vision/models/YOLO11m-obb.yaml +28 -0
  179. mblt_vision/models/YOLO11m-pose.yaml +28 -0
  180. mblt_vision/models/YOLO11m-seg.yaml +28 -0
  181. mblt_vision/models/YOLO11m.yaml +27 -0
  182. mblt_vision/models/YOLO11n-cls.yaml +23 -0
  183. mblt_vision/models/YOLO11n-face.yaml +27 -0
  184. mblt_vision/models/YOLO11n-obb.yaml +28 -0
  185. mblt_vision/models/YOLO11n-pose.yaml +28 -0
  186. mblt_vision/models/YOLO11n-seg.yaml +28 -0
  187. mblt_vision/models/YOLO11n.yaml +27 -0
  188. mblt_vision/models/YOLO11s-cls.yaml +23 -0
  189. mblt_vision/models/YOLO11s-face.yaml +27 -0
  190. mblt_vision/models/YOLO11s-obb.yaml +28 -0
  191. mblt_vision/models/YOLO11s-pose.yaml +28 -0
  192. mblt_vision/models/YOLO11s-seg.yaml +28 -0
  193. mblt_vision/models/YOLO11s.yaml +27 -0
  194. mblt_vision/models/YOLO11x-cls.yaml +23 -0
  195. mblt_vision/models/YOLO11x-obb.yaml +28 -0
  196. mblt_vision/models/YOLO11x-pose.yaml +28 -0
  197. mblt_vision/models/YOLO11x-seg.yaml +28 -0
  198. mblt_vision/models/YOLO11x.yaml +27 -0
  199. mblt_vision/models/YOLO12l-face.yaml +27 -0
  200. mblt_vision/models/YOLO12l-seg.yaml +28 -0
  201. mblt_vision/models/YOLO12l.yaml +27 -0
  202. mblt_vision/models/YOLO12m-face.yaml +27 -0
  203. mblt_vision/models/YOLO12m-seg.yaml +28 -0
  204. mblt_vision/models/YOLO12m.yaml +27 -0
  205. mblt_vision/models/YOLO12n-face.yaml +27 -0
  206. mblt_vision/models/YOLO12n-seg.yaml +28 -0
  207. mblt_vision/models/YOLO12n.yaml +27 -0
  208. mblt_vision/models/YOLO12s-face.yaml +27 -0
  209. mblt_vision/models/YOLO12s-seg.yaml +28 -0
  210. mblt_vision/models/YOLO12s.yaml +27 -0
  211. mblt_vision/models/YOLO12x-seg.yaml +28 -0
  212. mblt_vision/models/YOLO12x.yaml +27 -0
  213. mblt_vision/models/YOLO26l-cls.yaml +23 -0
  214. mblt_vision/models/YOLO26l-depth.yaml +17 -0
  215. mblt_vision/models/YOLO26l-distill.yaml +27 -0
  216. mblt_vision/models/YOLO26l-obb.yaml +28 -0
  217. mblt_vision/models/YOLO26l-pose.yaml +28 -0
  218. mblt_vision/models/YOLO26l-seg.yaml +28 -0
  219. mblt_vision/models/YOLO26l-sem-ade20k.yaml +17 -0
  220. mblt_vision/models/YOLO26l-sem.yaml +17 -0
  221. mblt_vision/models/YOLO26l.yaml +27 -0
  222. mblt_vision/models/YOLO26m-cls.yaml +23 -0
  223. mblt_vision/models/YOLO26m-depth.yaml +17 -0
  224. mblt_vision/models/YOLO26m-distill.yaml +27 -0
  225. mblt_vision/models/YOLO26m-obb.yaml +28 -0
  226. mblt_vision/models/YOLO26m-pose.yaml +28 -0
  227. mblt_vision/models/YOLO26m-seg.yaml +28 -0
  228. mblt_vision/models/YOLO26m-sem-ade20k.yaml +17 -0
  229. mblt_vision/models/YOLO26m-sem.yaml +17 -0
  230. mblt_vision/models/YOLO26m.yaml +27 -0
  231. mblt_vision/models/YOLO26n-cls.yaml +23 -0
  232. mblt_vision/models/YOLO26n-depth.yaml +17 -0
  233. mblt_vision/models/YOLO26n-distill.yaml +27 -0
  234. mblt_vision/models/YOLO26n-obb.yaml +28 -0
  235. mblt_vision/models/YOLO26n-pose.yaml +28 -0
  236. mblt_vision/models/YOLO26n-seg.yaml +28 -0
  237. mblt_vision/models/YOLO26n-sem-ade20k.yaml +17 -0
  238. mblt_vision/models/YOLO26n-sem.yaml +17 -0
  239. mblt_vision/models/YOLO26n.yaml +27 -0
  240. mblt_vision/models/YOLO26s-cls.yaml +23 -0
  241. mblt_vision/models/YOLO26s-depth.yaml +17 -0
  242. mblt_vision/models/YOLO26s-distill.yaml +27 -0
  243. mblt_vision/models/YOLO26s-obb.yaml +28 -0
  244. mblt_vision/models/YOLO26s-pose.yaml +28 -0
  245. mblt_vision/models/YOLO26s-seg.yaml +28 -0
  246. mblt_vision/models/YOLO26s-sem-ade20k.yaml +17 -0
  247. mblt_vision/models/YOLO26s-sem.yaml +17 -0
  248. mblt_vision/models/YOLO26s.yaml +27 -0
  249. mblt_vision/models/YOLO26x-cls.yaml +23 -0
  250. mblt_vision/models/YOLO26x-depth.yaml +17 -0
  251. mblt_vision/models/YOLO26x-distill.yaml +27 -0
  252. mblt_vision/models/YOLO26x-obb.yaml +28 -0
  253. mblt_vision/models/YOLO26x-pose.yaml +28 -0
  254. mblt_vision/models/YOLO26x-seg.yaml +28 -0
  255. mblt_vision/models/YOLO26x-sem-ade20k.yaml +17 -0
  256. mblt_vision/models/YOLO26x-sem.yaml +17 -0
  257. mblt_vision/models/YOLO26x.yaml +27 -0
  258. mblt_vision/models/YOLOv10b.yaml +28 -0
  259. mblt_vision/models/YOLOv10l-face.yaml +28 -0
  260. mblt_vision/models/YOLOv10l.yaml +28 -0
  261. mblt_vision/models/YOLOv10m-face.yaml +28 -0
  262. mblt_vision/models/YOLOv10m.yaml +28 -0
  263. mblt_vision/models/YOLOv10n-face.yaml +28 -0
  264. mblt_vision/models/YOLOv10n.yaml +28 -0
  265. mblt_vision/models/YOLOv10s-face.yaml +28 -0
  266. mblt_vision/models/YOLOv10s.yaml +28 -0
  267. mblt_vision/models/YOLOv10x.yaml +28 -0
  268. mblt_vision/models/YOLOv3-spp.yaml +44 -0
  269. mblt_vision/models/YOLOv3-sppu.yaml +27 -0
  270. mblt_vision/models/YOLOv3-tiny.yaml +38 -0
  271. mblt_vision/models/YOLOv3-tinyu.yaml +27 -0
  272. mblt_vision/models/YOLOv3.yaml +44 -0
  273. mblt_vision/models/YOLOv3u.yaml +27 -0
  274. mblt_vision/models/YOLOv5l-cls.yaml +22 -0
  275. mblt_vision/models/YOLOv5l-seg.yaml +45 -0
  276. mblt_vision/models/YOLOv5l.yaml +44 -0
  277. mblt_vision/models/YOLOv5l6.yaml +50 -0
  278. mblt_vision/models/YOLOv5l6u.yaml +27 -0
  279. mblt_vision/models/YOLOv5lu.yaml +27 -0
  280. mblt_vision/models/YOLOv5m-cls.yaml +22 -0
  281. mblt_vision/models/YOLOv5m-seg.yaml +45 -0
  282. mblt_vision/models/YOLOv5m.yaml +45 -0
  283. mblt_vision/models/YOLOv5m6.yaml +50 -0
  284. mblt_vision/models/YOLOv5m6u.yaml +27 -0
  285. mblt_vision/models/YOLOv5mu.yaml +27 -0
  286. mblt_vision/models/YOLOv5n-cls.yaml +22 -0
  287. mblt_vision/models/YOLOv5n-seg.yaml +45 -0
  288. mblt_vision/models/YOLOv5n.yaml +44 -0
  289. mblt_vision/models/YOLOv5n6.yaml +50 -0
  290. mblt_vision/models/YOLOv5n6u.yaml +27 -0
  291. mblt_vision/models/YOLOv5nu.yaml +27 -0
  292. mblt_vision/models/YOLOv5s-cls.yaml +22 -0
  293. mblt_vision/models/YOLOv5s-seg.yaml +45 -0
  294. mblt_vision/models/YOLOv5s.yaml +44 -0
  295. mblt_vision/models/YOLOv5s6.yaml +50 -0
  296. mblt_vision/models/YOLOv5s6u.yaml +27 -0
  297. mblt_vision/models/YOLOv5su.yaml +27 -0
  298. mblt_vision/models/YOLOv5x-cls.yaml +22 -0
  299. mblt_vision/models/YOLOv5x-seg.yaml +45 -0
  300. mblt_vision/models/YOLOv5x.yaml +44 -0
  301. mblt_vision/models/YOLOv5x6.yaml +50 -0
  302. mblt_vision/models/YOLOv5x6u.yaml +27 -0
  303. mblt_vision/models/YOLOv5xu.yaml +27 -0
  304. mblt_vision/models/YOLOv6m-face.yaml +27 -0
  305. mblt_vision/models/YOLOv6n-face.yaml +27 -0
  306. mblt_vision/models/YOLOv7.yaml +44 -0
  307. mblt_vision/models/YOLOv7d6.yaml +50 -0
  308. mblt_vision/models/YOLOv7e6.yaml +50 -0
  309. mblt_vision/models/YOLOv7e6e.yaml +50 -0
  310. mblt_vision/models/YOLOv7w6.yaml +50 -0
  311. mblt_vision/models/YOLOv7x.yaml +44 -0
  312. mblt_vision/models/YOLOv8l-cls.yaml +23 -0
  313. mblt_vision/models/YOLOv8l-face.yaml +27 -0
  314. mblt_vision/models/YOLOv8l-obb.yaml +28 -0
  315. mblt_vision/models/YOLOv8l-pose.yaml +28 -0
  316. mblt_vision/models/YOLOv8l-seg.yaml +28 -0
  317. mblt_vision/models/YOLOv8l.yaml +27 -0
  318. mblt_vision/models/YOLOv8m-cls.yaml +23 -0
  319. mblt_vision/models/YOLOv8m-face.yaml +27 -0
  320. mblt_vision/models/YOLOv8m-obb.yaml +28 -0
  321. mblt_vision/models/YOLOv8m-pose.yaml +28 -0
  322. mblt_vision/models/YOLOv8m-seg.yaml +28 -0
  323. mblt_vision/models/YOLOv8m.yaml +27 -0
  324. mblt_vision/models/YOLOv8n-cls.yaml +23 -0
  325. mblt_vision/models/YOLOv8n-face.yaml +27 -0
  326. mblt_vision/models/YOLOv8n-obb.yaml +28 -0
  327. mblt_vision/models/YOLOv8n-pose.yaml +28 -0
  328. mblt_vision/models/YOLOv8n-seg.yaml +28 -0
  329. mblt_vision/models/YOLOv8n.yaml +27 -0
  330. mblt_vision/models/YOLOv8s-cls.yaml +23 -0
  331. mblt_vision/models/YOLOv8s-obb.yaml +28 -0
  332. mblt_vision/models/YOLOv8s-pose.yaml +28 -0
  333. mblt_vision/models/YOLOv8s-seg.yaml +28 -0
  334. mblt_vision/models/YOLOv8s.yaml +27 -0
  335. mblt_vision/models/YOLOv8x-cls.yaml +23 -0
  336. mblt_vision/models/YOLOv8x-obb.yaml +28 -0
  337. mblt_vision/models/YOLOv8x-pose-p6.yaml +28 -0
  338. mblt_vision/models/YOLOv8x-pose.yaml +28 -0
  339. mblt_vision/models/YOLOv8x-seg.yaml +28 -0
  340. mblt_vision/models/YOLOv8x.yaml +27 -0
  341. mblt_vision/models/YOLOv9c-seg.yaml +28 -0
  342. mblt_vision/models/YOLOv9c.yaml +27 -0
  343. mblt_vision/models/YOLOv9e-seg.yaml +28 -0
  344. mblt_vision/models/YOLOv9e.yaml +27 -0
  345. mblt_vision/models/YOLOv9m.yaml +27 -0
  346. mblt_vision/models/YOLOv9s.yaml +27 -0
  347. mblt_vision/models/YOLOv9t.yaml +27 -0
  348. mblt_vision/obb/__init__.py +39 -0
  349. mblt_vision/object_detection/__init__.py +153 -0
  350. mblt_vision/pose_estimation/__init__.py +41 -0
  351. mblt_vision/py.typed +1 -0
  352. mblt_vision/semantic_segmentation/__init__.py +29 -0
  353. mblt_vision/utils/__init__.py +0 -0
  354. mblt_vision/utils/datasets/__init__.py +85 -0
  355. mblt_vision/utils/datasets/ade20k.py +41 -0
  356. mblt_vision/utils/datasets/cityscapes.py +66 -0
  357. mblt_vision/utils/datasets/coco.py +243 -0
  358. mblt_vision/utils/datasets/dataloader.py +1016 -0
  359. mblt_vision/utils/datasets/dotav1.py +59 -0
  360. mblt_vision/utils/datasets/imagenet.py +29 -0
  361. mblt_vision/utils/datasets/organizer.py +2145 -0
  362. mblt_vision/utils/datasets/readiness.py +1063 -0
  363. mblt_vision/utils/evaluation/__init__.py +51 -0
  364. mblt_vision/utils/evaluation/_result.py +22 -0
  365. mblt_vision/utils/evaluation/eval_ade20k.py +302 -0
  366. mblt_vision/utils/evaluation/eval_cityscapes.py +32 -0
  367. mblt_vision/utils/evaluation/eval_coco.py +465 -0
  368. mblt_vision/utils/evaluation/eval_dota.py +878 -0
  369. mblt_vision/utils/evaluation/eval_imagenet.py +151 -0
  370. mblt_vision/utils/evaluation/eval_nyu_depth.py +189 -0
  371. mblt_vision/utils/evaluation/eval_widerface.py +440 -0
  372. mblt_vision/utils/letterbox.py +118 -0
  373. mblt_vision/utils/postprocess/__init__.py +9 -0
  374. mblt_vision/utils/postprocess/_letterbox.py +102 -0
  375. mblt_vision/utils/postprocess/base.py +743 -0
  376. mblt_vision/utils/postprocess/build_post.py +120 -0
  377. mblt_vision/utils/postprocess/cls_post.py +117 -0
  378. mblt_vision/utils/postprocess/common.py +1571 -0
  379. mblt_vision/utils/postprocess/depth_post.py +106 -0
  380. mblt_vision/utils/postprocess/semantic_seg_post.py +192 -0
  381. mblt_vision/utils/postprocess/yolo_anchor_post.py +469 -0
  382. mblt_vision/utils/postprocess/yolo_anchorless_post.py +952 -0
  383. mblt_vision/utils/postprocess/yolo_dflfree_post.py +983 -0
  384. mblt_vision/utils/postprocess/yolo_nmsfree_post.py +176 -0
  385. mblt_vision/utils/preprocess/__init__.py +27 -0
  386. mblt_vision/utils/preprocess/_validation.py +66 -0
  387. mblt_vision/utils/preprocess/base.py +157 -0
  388. mblt_vision/utils/preprocess/build_pre.py +48 -0
  389. mblt_vision/utils/preprocess/center_crop.py +75 -0
  390. mblt_vision/utils/preprocess/letterbox.py +123 -0
  391. mblt_vision/utils/preprocess/normalize.py +94 -0
  392. mblt_vision/utils/preprocess/order.py +74 -0
  393. mblt_vision/utils/preprocess/reader.py +94 -0
  394. mblt_vision/utils/preprocess/resize.py +202 -0
  395. mblt_vision/utils/preprocess/yolo_pre.py +7 -0
  396. mblt_vision/utils/results.py +725 -0
  397. mblt_vision/utils/types.py +15 -0
  398. mblt_vision/wrapper.py +1121 -0
  399. mblt_vision_python-0.0.0.dist-info/METADATA +152 -0
  400. mblt_vision_python-0.0.0.dist-info/RECORD +404 -0
  401. mblt_vision_python-0.0.0.dist-info/WHEEL +5 -0
  402. mblt_vision_python-0.0.0.dist-info/entry_points.txt +2 -0
  403. mblt_vision_python-0.0.0.dist-info/licenses/LICENSE +28 -0
  404. 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)