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