dataflow-cv 1.2.0__tar.gz → 1.3.0__tar.gz

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 (51) hide show
  1. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/PKG-INFO +33 -20
  2. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/README.md +32 -19
  3. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/__init__.py +1 -1
  4. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/cli/commands/convert.py +3 -2
  5. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/cli/commands/evaluate.py +24 -23
  6. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/convert/base.py +104 -69
  7. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/convert/coco_and_labelme.py +16 -94
  8. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/convert/labelme_and_yolo.py +13 -56
  9. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/convert/utils.py +140 -5
  10. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/convert/yolo_and_coco.py +29 -147
  11. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/evaluate/base.py +7 -3
  12. dataflow_cv-1.3.0/dataflow/evaluate/metrics.py +456 -0
  13. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/evaluate/result.py +5 -0
  14. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/evaluate/utils.py +105 -5
  15. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/label/base.py +29 -3
  16. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/label/coco_handler.py +32 -5
  17. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/util/logging_util.py +67 -0
  18. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/visualize/base.py +16 -21
  19. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow_cv.egg-info/PKG-INFO +33 -20
  20. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/pyproject.toml +1 -1
  21. dataflow_cv-1.2.0/dataflow/evaluate/metrics.py +0 -276
  22. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/LICENSE +0 -0
  23. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/cli/__init__.py +0 -0
  24. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/cli/commands/__init__.py +0 -0
  25. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/cli/commands/utils.py +0 -0
  26. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/cli/commands/visualize.py +0 -0
  27. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/cli/exceptions.py +0 -0
  28. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/cli/main.py +0 -0
  29. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/convert/__init__.py +0 -0
  30. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/convert/rle_converter.py +0 -0
  31. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/evaluate/__init__.py +0 -0
  32. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/evaluate/evaluator.py +0 -0
  33. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/label/__init__.py +0 -0
  34. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/label/labelme_handler.py +0 -0
  35. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/label/models.py +0 -0
  36. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/label/utils.py +0 -0
  37. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/label/yolo_handler.py +0 -0
  38. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/util/__init__.py +0 -0
  39. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/util/file_util.py +0 -0
  40. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/visualize/__init__.py +0 -0
  41. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/visualize/coco_visualizer.py +0 -0
  42. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/visualize/labelme_visualizer.py +0 -0
  43. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/visualize/utils.py +0 -0
  44. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow/visualize/yolo_visualizer.py +0 -0
  45. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow_cv.egg-info/SOURCES.txt +0 -0
  46. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow_cv.egg-info/dependency_links.txt +0 -0
  47. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow_cv.egg-info/entry_points.txt +0 -0
  48. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow_cv.egg-info/not-zip-safe +0 -0
  49. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow_cv.egg-info/requires.txt +0 -0
  50. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/dataflow_cv.egg-info/top_level.txt +0 -0
  51. {dataflow_cv-1.2.0 → dataflow_cv-1.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataflow-cv
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: A computer vision dataset processing library
5
5
  Author-email: DataFlow-CV Team <example@example.com>
6
6
  License: MIT
@@ -73,10 +73,10 @@ graph LR
73
73
 
74
74
  | | | |
75
75
  |:---|:---|:---|
76
- | 🔄 **Format Conversion** | Convert between YOLO, LabelMe, and COCO in any direction — 6 conversion paths, plus prediction file support with confidence scores |
76
+ | 🔄 **Format Conversion** | Convert between YOLO, LabelMe, and COCO in any direction — 6 conversion paths, plus prediction file support (outputs standard list-format COCO predictions) |
77
77
  | 🎯 **Detection & Segmentation** | Handle both object detection (bbox) and instance segmentation (polygon/RLE) annotations |
78
78
  | 🎨 **Visualization** | Render annotations with OpenCV — color-coded classes, semi-transparent masks, display & save modes |
79
- | 📊 **Evaluation** | COCO-standard 12-metric output (mAP, AP50, AP75, AR) via pycocotools, with per-class breakdowns |
79
+ | 📊 **Evaluation** | COCO-standard 12-metric output (mAP, AP50, AP75, AR) via pycocotools, with per-class breakdowns and macro/micro P/R/F1 |
80
80
  | 💻 **Command-line Interface** | Intuitive CLI with `convert`, `visualize`, and `evaluate` subcommands — positional args, rich `--help` |
81
81
  | 🐍 **Python API** | Programmatic access for integration into larger ML pipelines |
82
82
  | 📝 **Verbose Logging** | File-based debug logging with timestamps — toggle with `--verbose` |
@@ -146,7 +146,7 @@ dataflow-cv convert coco2yolo input.json yolo_labels/
146
146
  # COCO → LabelMe
147
147
  dataflow-cv convert coco2labelme input.json labelme_json/
148
148
 
149
- # YOLO predictions → COCO (with confidence scores)
149
+ # YOLO predictions → COCO (output: plain JSON list — prediction format)
150
150
  dataflow-cv convert yolo2coco --prediction images/ yolo_preds/ classes.txt pred.json
151
151
 
152
152
  # Options
@@ -174,10 +174,10 @@ dataflow-cv visualize yolo --verbose --no-display images/ yolo_labels/ classes.t
174
174
 
175
175
  Evaluate object detection and instance segmentation model outputs using COCO-standard metrics. Two COCO-format JSON files are required:
176
176
 
177
- | File | Role | Source |
178
- |------|------|--------|
179
- | **`anno.json`** | Ground Truth (GT) — reference annotations | `yolo2coco` (label mode) |
180
- | **`pred.json`** | Detection (DT) — model predictions with `score` | `yolo2coco --prediction` |
177
+ | File | Role | Format | Source |
178
+ |------|------|--------|--------|
179
+ | **`anno.json`** | Ground Truth (GT) — reference annotations | Full COCO dict (`images`, `annotations`, `categories`) | `yolo2coco` (label mode) |
180
+ | **`pred.json`** | Detection (DT) — model predictions | Plain JSON list of annotation dicts (with `score`) | `yolo2coco --prediction`, Detectron2, MMDetection |
181
181
 
182
182
  ##### ① Preparing Evaluation Data
183
183
 
@@ -195,7 +195,9 @@ dataflow-cv convert yolo2coco images/ yolo_labels/ classes.txt anno.json
195
195
  dataflow-cv convert yolo2coco --prediction images/ yolo_preds/ classes.txt pred.json
196
196
  ```
197
197
 
198
- > ⚠️ **Important**: YOLO label files (GT) use **odd** token counts, while prediction files (DT) use **even** token counts with a trailing `confidence`. The `--prediction` flag is required for DT — it preserves confidence as the COCO `score` field. Mixed label/prediction files in the same directory are not supported.
198
+ > ⚠️ **Important**: YOLO label files (GT) use **odd** token counts, while prediction files (DT) use **even** token counts with a trailing `confidence`. The `--prediction` flag is required for DT — it outputs a **plain JSON list** of annotation dicts (not a full COCO dict with `images`/`categories`). Mixed label/prediction files in the same directory are not supported.
199
+ >
200
+ > ℹ️ **Note**: The `--prediction` flag is **only available for `yolo2coco`**. `labelme2coco` does not support prediction conversion — LabelMe files (.json) have no label vs prediction format distinction, so there is no equivalent prediction source format to convert from.
199
201
 
200
202
  ##### ② Detection vs Segmentation — Format Requirements
201
203
 
@@ -219,9 +221,12 @@ dataflow-cv evaluate detection anno.json pred.json
219
221
  # Verbose per-class breakdown
220
222
  dataflow-cv evaluate detection --verbose anno.json pred.json
221
223
 
222
- # With P/R/F1 at IoU=0.5
224
+ # With P/R/F1 at IoU=0.5 (default: macro averaging)
223
225
  dataflow-cv evaluate detection --prf1 --prf1-iou 0.5 anno.json pred.json
224
226
 
227
+ # With P/R/F1 using micro averaging
228
+ dataflow-cv evaluate detection --prf1 --prf1-method micro anno.json pred.json
229
+
225
230
  # Instance segmentation evaluation (mask IoU)
226
231
  dataflow-cv evaluate segmentation anno.json pred.json
227
232
 
@@ -273,9 +278,17 @@ evaluator = DetectionEvaluator(verbose=True)
273
278
  result = evaluator.evaluate("anno.json", "pred.json")
274
279
  print(f"AP: {result.metrics.ap:.3f}, AP50: {result.metrics.ap50:.3f}")
275
280
 
276
- # Quick P/R/F1 at IoU=0.5
281
+ # Quick P/R/F1 at IoU=0.5 (default: macro averaging, bbox IoU)
277
282
  prf1 = compute_pr_f1("anno.json", "pred.json", iou_threshold=0.5)
278
- print(f"F1: {prf1.overall.f1_score:.3f}")
283
+ print(f"Macro F1: {prf1.overall.f1_score:.3f}")
284
+
285
+ # Micro averaging P/R/F1 (samples weighted equally)
286
+ prf1 = compute_pr_f1("anno.json", "pred.json", method="micro")
287
+ print(f"Micro F1: {prf1.overall.f1_score:.3f}")
288
+
289
+ # Segmentation P/R/F1 (mask IoU)
290
+ prf1 = compute_pr_f1("anno_segm.json", "pred_segm.json", iou_type="segm")
291
+ print(f"Segm F1: {prf1.overall.f1_score:.3f}")
279
292
  ```
280
293
 
281
294
  > 📂 See the `samples/` directory for complete examples: `samples/visualize/` (YOLO, LabelMe, COCO demos), `samples/convert/` (conversion examples).
@@ -301,8 +314,8 @@ print(f"F1: {prf1.overall.f1_score:.3f}")
301
314
  - **Headless Support**: Use `--no-display` for servers/Docker; pair with `--save` to output visualization images without a window.
302
315
  - **Keyboard Shortcuts**: During visualization — `q`/`ESC` to exit, `Enter`/`Space` to advance, any other key to continue.
303
316
  - **Color Management**: Each class ID gets a unique color from an HSV-based palette (up to 1000 classes) for consistent visualization.
304
- - **Evaluation Metrics**: COCO-standard 12-metric output with optional per-class breakdown and P/R/F1 computation.
305
- - **Prediction Files**: YOLO prediction files (6 tokens detection, even tokens segmentation) differ from label files (5/odd tokens). Use `--prediction` flag.
317
+ - **Evaluation Metrics**: COCO-standard 12-metric output with optional per-class breakdown. P/R/F1 supports both macro (default) and micro averaging, and both bbox and mask IoU.
318
+ - **Prediction Files**: YOLO prediction files use 6 tokens (detection) or even tokens (segmentation) vs 5/odd for labels. `--prediction` outputs a plain JSON list of annotation dicts — the standard prediction exchange format compatible with pycocotools `loadRes()`.
306
319
 
307
320
  ---
308
321
 
@@ -312,7 +325,7 @@ For detailed developer guidance including advanced test commands, debugging, and
312
325
 
313
326
  ### 🧪 Testing
314
327
 
315
- **370 tests, 75% code coverage.**
328
+ **405 tests, 76% code coverage (3986 statements).**
316
329
 
317
330
  ```bash
318
331
  pytest # All tests
@@ -326,12 +339,12 @@ pytest tests/evaluate/test_evaluator.py # Single module
326
339
 
327
340
  | Module | Coverage | Highlights |
328
341
  |--------|:--------:|------------|
329
- | `dataflow/label/` | 68% | models (87%), coco_handler (74%), labelme_handler (72%), yolo_handler (57%) |
330
- | `dataflow/convert/` | 84% | yolo_and_coco (89%), labelme_and_yolo (93%), coco_and_labelme (88%), rle (81%) |
331
- | `dataflow/visualize/` | 81% | yolo_vis (97%), labelme_vis (100%), coco_vis (97%), base (80%) |
332
- | `dataflow/evaluate/` | 88% | evaluator (100%), metrics (96%), result (99%), base (91%), utils (69%) |
342
+ | `dataflow/label/` | 68% | models (87%), coco_handler (75%), labelme_handler (70%), yolo_handler (58%) |
343
+ | `dataflow/convert/` | 87% | yolo_and_coco (90%), labelme_and_yolo (86%), coco_and_labelme (87%), rle (80%), base (83%), utils (92%) |
344
+ | `dataflow/visualize/` | 81% | yolo_vis (100%), labelme_vis (100%), coco_vis (97%), base (74%) |
345
+ | `dataflow/evaluate/` | 87% | evaluator (100%), metrics (93%), result (99%), base (91%), utils (68%) |
333
346
  | `dataflow/cli/` | 59% | main (96%), convert cmd (48%), evaluate cmd (24%), visualize cmd (84%), utils (86%) |
334
- | `dataflow/util/` | 93% | logging (99%), file_util (84%) |
347
+ | `dataflow/util/` | 93% | logging (98%), file_util (84%) |
335
348
 
336
349
  </details>
337
350
 
@@ -30,10 +30,10 @@ graph LR
30
30
 
31
31
  | | | |
32
32
  |:---|:---|:---|
33
- | 🔄 **Format Conversion** | Convert between YOLO, LabelMe, and COCO in any direction — 6 conversion paths, plus prediction file support with confidence scores |
33
+ | 🔄 **Format Conversion** | Convert between YOLO, LabelMe, and COCO in any direction — 6 conversion paths, plus prediction file support (outputs standard list-format COCO predictions) |
34
34
  | 🎯 **Detection & Segmentation** | Handle both object detection (bbox) and instance segmentation (polygon/RLE) annotations |
35
35
  | 🎨 **Visualization** | Render annotations with OpenCV — color-coded classes, semi-transparent masks, display & save modes |
36
- | 📊 **Evaluation** | COCO-standard 12-metric output (mAP, AP50, AP75, AR) via pycocotools, with per-class breakdowns |
36
+ | 📊 **Evaluation** | COCO-standard 12-metric output (mAP, AP50, AP75, AR) via pycocotools, with per-class breakdowns and macro/micro P/R/F1 |
37
37
  | 💻 **Command-line Interface** | Intuitive CLI with `convert`, `visualize`, and `evaluate` subcommands — positional args, rich `--help` |
38
38
  | 🐍 **Python API** | Programmatic access for integration into larger ML pipelines |
39
39
  | 📝 **Verbose Logging** | File-based debug logging with timestamps — toggle with `--verbose` |
@@ -103,7 +103,7 @@ dataflow-cv convert coco2yolo input.json yolo_labels/
103
103
  # COCO → LabelMe
104
104
  dataflow-cv convert coco2labelme input.json labelme_json/
105
105
 
106
- # YOLO predictions → COCO (with confidence scores)
106
+ # YOLO predictions → COCO (output: plain JSON list — prediction format)
107
107
  dataflow-cv convert yolo2coco --prediction images/ yolo_preds/ classes.txt pred.json
108
108
 
109
109
  # Options
@@ -131,10 +131,10 @@ dataflow-cv visualize yolo --verbose --no-display images/ yolo_labels/ classes.t
131
131
 
132
132
  Evaluate object detection and instance segmentation model outputs using COCO-standard metrics. Two COCO-format JSON files are required:
133
133
 
134
- | File | Role | Source |
135
- |------|------|--------|
136
- | **`anno.json`** | Ground Truth (GT) — reference annotations | `yolo2coco` (label mode) |
137
- | **`pred.json`** | Detection (DT) — model predictions with `score` | `yolo2coco --prediction` |
134
+ | File | Role | Format | Source |
135
+ |------|------|--------|--------|
136
+ | **`anno.json`** | Ground Truth (GT) — reference annotations | Full COCO dict (`images`, `annotations`, `categories`) | `yolo2coco` (label mode) |
137
+ | **`pred.json`** | Detection (DT) — model predictions | Plain JSON list of annotation dicts (with `score`) | `yolo2coco --prediction`, Detectron2, MMDetection |
138
138
 
139
139
  ##### ① Preparing Evaluation Data
140
140
 
@@ -152,7 +152,9 @@ dataflow-cv convert yolo2coco images/ yolo_labels/ classes.txt anno.json
152
152
  dataflow-cv convert yolo2coco --prediction images/ yolo_preds/ classes.txt pred.json
153
153
  ```
154
154
 
155
- > ⚠️ **Important**: YOLO label files (GT) use **odd** token counts, while prediction files (DT) use **even** token counts with a trailing `confidence`. The `--prediction` flag is required for DT — it preserves confidence as the COCO `score` field. Mixed label/prediction files in the same directory are not supported.
155
+ > ⚠️ **Important**: YOLO label files (GT) use **odd** token counts, while prediction files (DT) use **even** token counts with a trailing `confidence`. The `--prediction` flag is required for DT — it outputs a **plain JSON list** of annotation dicts (not a full COCO dict with `images`/`categories`). Mixed label/prediction files in the same directory are not supported.
156
+ >
157
+ > ℹ️ **Note**: The `--prediction` flag is **only available for `yolo2coco`**. `labelme2coco` does not support prediction conversion — LabelMe files (.json) have no label vs prediction format distinction, so there is no equivalent prediction source format to convert from.
156
158
 
157
159
  ##### ② Detection vs Segmentation — Format Requirements
158
160
 
@@ -176,9 +178,12 @@ dataflow-cv evaluate detection anno.json pred.json
176
178
  # Verbose per-class breakdown
177
179
  dataflow-cv evaluate detection --verbose anno.json pred.json
178
180
 
179
- # With P/R/F1 at IoU=0.5
181
+ # With P/R/F1 at IoU=0.5 (default: macro averaging)
180
182
  dataflow-cv evaluate detection --prf1 --prf1-iou 0.5 anno.json pred.json
181
183
 
184
+ # With P/R/F1 using micro averaging
185
+ dataflow-cv evaluate detection --prf1 --prf1-method micro anno.json pred.json
186
+
182
187
  # Instance segmentation evaluation (mask IoU)
183
188
  dataflow-cv evaluate segmentation anno.json pred.json
184
189
 
@@ -230,9 +235,17 @@ evaluator = DetectionEvaluator(verbose=True)
230
235
  result = evaluator.evaluate("anno.json", "pred.json")
231
236
  print(f"AP: {result.metrics.ap:.3f}, AP50: {result.metrics.ap50:.3f}")
232
237
 
233
- # Quick P/R/F1 at IoU=0.5
238
+ # Quick P/R/F1 at IoU=0.5 (default: macro averaging, bbox IoU)
234
239
  prf1 = compute_pr_f1("anno.json", "pred.json", iou_threshold=0.5)
235
- print(f"F1: {prf1.overall.f1_score:.3f}")
240
+ print(f"Macro F1: {prf1.overall.f1_score:.3f}")
241
+
242
+ # Micro averaging P/R/F1 (samples weighted equally)
243
+ prf1 = compute_pr_f1("anno.json", "pred.json", method="micro")
244
+ print(f"Micro F1: {prf1.overall.f1_score:.3f}")
245
+
246
+ # Segmentation P/R/F1 (mask IoU)
247
+ prf1 = compute_pr_f1("anno_segm.json", "pred_segm.json", iou_type="segm")
248
+ print(f"Segm F1: {prf1.overall.f1_score:.3f}")
236
249
  ```
237
250
 
238
251
  > 📂 See the `samples/` directory for complete examples: `samples/visualize/` (YOLO, LabelMe, COCO demos), `samples/convert/` (conversion examples).
@@ -258,8 +271,8 @@ print(f"F1: {prf1.overall.f1_score:.3f}")
258
271
  - **Headless Support**: Use `--no-display` for servers/Docker; pair with `--save` to output visualization images without a window.
259
272
  - **Keyboard Shortcuts**: During visualization — `q`/`ESC` to exit, `Enter`/`Space` to advance, any other key to continue.
260
273
  - **Color Management**: Each class ID gets a unique color from an HSV-based palette (up to 1000 classes) for consistent visualization.
261
- - **Evaluation Metrics**: COCO-standard 12-metric output with optional per-class breakdown and P/R/F1 computation.
262
- - **Prediction Files**: YOLO prediction files (6 tokens detection, even tokens segmentation) differ from label files (5/odd tokens). Use `--prediction` flag.
274
+ - **Evaluation Metrics**: COCO-standard 12-metric output with optional per-class breakdown. P/R/F1 supports both macro (default) and micro averaging, and both bbox and mask IoU.
275
+ - **Prediction Files**: YOLO prediction files use 6 tokens (detection) or even tokens (segmentation) vs 5/odd for labels. `--prediction` outputs a plain JSON list of annotation dicts — the standard prediction exchange format compatible with pycocotools `loadRes()`.
263
276
 
264
277
  ---
265
278
 
@@ -269,7 +282,7 @@ For detailed developer guidance including advanced test commands, debugging, and
269
282
 
270
283
  ### 🧪 Testing
271
284
 
272
- **370 tests, 75% code coverage.**
285
+ **405 tests, 76% code coverage (3986 statements).**
273
286
 
274
287
  ```bash
275
288
  pytest # All tests
@@ -283,12 +296,12 @@ pytest tests/evaluate/test_evaluator.py # Single module
283
296
 
284
297
  | Module | Coverage | Highlights |
285
298
  |--------|:--------:|------------|
286
- | `dataflow/label/` | 68% | models (87%), coco_handler (74%), labelme_handler (72%), yolo_handler (57%) |
287
- | `dataflow/convert/` | 84% | yolo_and_coco (89%), labelme_and_yolo (93%), coco_and_labelme (88%), rle (81%) |
288
- | `dataflow/visualize/` | 81% | yolo_vis (97%), labelme_vis (100%), coco_vis (97%), base (80%) |
289
- | `dataflow/evaluate/` | 88% | evaluator (100%), metrics (96%), result (99%), base (91%), utils (69%) |
299
+ | `dataflow/label/` | 68% | models (87%), coco_handler (75%), labelme_handler (70%), yolo_handler (58%) |
300
+ | `dataflow/convert/` | 87% | yolo_and_coco (90%), labelme_and_yolo (86%), coco_and_labelme (87%), rle (80%), base (83%), utils (92%) |
301
+ | `dataflow/visualize/` | 81% | yolo_vis (100%), labelme_vis (100%), coco_vis (97%), base (74%) |
302
+ | `dataflow/evaluate/` | 87% | evaluator (100%), metrics (93%), result (99%), base (91%), utils (68%) |
290
303
  | `dataflow/cli/` | 59% | main (96%), convert cmd (48%), evaluate cmd (24%), visualize cmd (84%), utils (86%) |
291
- | `dataflow/util/` | 93% | logging (99%), file_util (84%) |
304
+ | `dataflow/util/` | 93% | logging (98%), file_util (84%) |
292
305
 
293
306
  </details>
294
307
 
@@ -1,4 +1,4 @@
1
1
  """DataFlow-CV: A computer vision dataset processing library."""
2
2
 
3
- __version__ = "1.2.0"
3
+ __version__ = "1.3.0"
4
4
  __author__ = "DataFlow-CV Team"
@@ -28,8 +28,9 @@ def convert_group():
28
28
  @click.option(
29
29
  "--prediction",
30
30
  is_flag=True,
31
- help="Treat input as prediction format (with confidence scores). "
32
- "Output COCO JSON will include 'score' fields.",
31
+ help="Treat input as prediction format (YOLO: 6 tokens for detection, "
32
+ "even tokens for segmentation). Output COCO as plain JSON list of "
33
+ "annotation dicts (prediction format, Variant B).",
33
34
  )
34
35
  def yolo2coco(
35
36
  ctx,
@@ -48,6 +48,13 @@ def add_evaluate_options(func):
48
48
  show_default=True,
49
49
  help="Confidence threshold for P/R/F1 calculation",
50
50
  )
51
+ @click.option(
52
+ "--prf1-method",
53
+ type=click.Choice(["macro", "micro"], case_sensitive=False),
54
+ default="macro",
55
+ show_default=True,
56
+ help="Aggregation method for overall P/R/F1",
57
+ )
51
58
  @click.option(
52
59
  "--output",
53
60
  "-o",
@@ -98,7 +105,7 @@ def evaluate_group():
98
105
  "dt_json",
99
106
  type=click.Path(exists=True, path_type=Path),
100
107
  )
101
- def detection(ctx, gt_json, dt_json, verbose, prf1, prf1_iou, prf1_conf, output):
108
+ def detection(ctx, gt_json, dt_json, verbose, prf1, prf1_iou, prf1_conf, prf1_method, output):
102
109
  """Evaluate object detection results (bbox IoU).
103
110
 
104
111
  GT_JSON: COCO format Ground Truth JSON file.
@@ -140,7 +147,7 @@ def detection(ctx, gt_json, dt_json, verbose, prf1, prf1_iou, prf1_conf, output)
140
147
 
141
148
  # Print results
142
149
  if result.success and result.metrics is not None:
143
- _print_detection_result(result, verbose, prf1, prf1_iou, prf1_conf, gt_json, dt_json, logger)
150
+ _print_detection_result(result, verbose, prf1, prf1_iou, prf1_conf, prf1_method, gt_json, dt_json, logger)
144
151
 
145
152
  if output:
146
153
  _save_result_json(result, output)
@@ -171,7 +178,7 @@ def detection(ctx, gt_json, dt_json, verbose, prf1, prf1_iou, prf1_conf, output)
171
178
  "dt_json",
172
179
  type=click.Path(exists=True, path_type=Path),
173
180
  )
174
- def segmentation(ctx, gt_json, dt_json, verbose, prf1, prf1_iou, prf1_conf, output):
181
+ def segmentation(ctx, gt_json, dt_json, verbose, prf1, prf1_iou, prf1_conf, prf1_method, output):
175
182
  """Evaluate instance segmentation results (mask IoU).
176
183
 
177
184
  GT_JSON: COCO format Ground Truth JSON file (annotations must include 'segmentation').
@@ -213,7 +220,7 @@ def segmentation(ctx, gt_json, dt_json, verbose, prf1, prf1_iou, prf1_conf, outp
213
220
 
214
221
  # Print results
215
222
  if result.success and result.metrics is not None:
216
- _print_segmentation_result(result, verbose, prf1, prf1_iou, prf1_conf, gt_json, dt_json, logger)
223
+ _print_segmentation_result(result, verbose, prf1, prf1_iou, prf1_conf, prf1_method, gt_json, dt_json, logger)
217
224
 
218
225
  if output:
219
226
  _save_result_json(result, output)
@@ -234,7 +241,7 @@ def segmentation(ctx, gt_json, dt_json, verbose, prf1, prf1_iou, prf1_conf, outp
234
241
  # Output helpers
235
242
  # ---------------------------------------------------------------------------
236
243
 
237
- def _print_detection_result(result, verbose, prf1, prf1_iou, prf1_conf, gt_json, dt_json, logger):
244
+ def _print_detection_result(result, verbose, prf1, prf1_iou, prf1_conf, prf1_method, gt_json, dt_json, logger):
238
245
  """Print detection evaluation results."""
239
246
  from dataflow.evaluate.utils import format_metric_table, format_per_class_table, format_prf1_output
240
247
 
@@ -262,12 +269,14 @@ def _print_detection_result(result, verbose, prf1, prf1_iou, prf1_conf, gt_json,
262
269
  str(gt_json), str(dt_json),
263
270
  iou_threshold=prf1_iou,
264
271
  confidence_threshold=prf1_conf,
265
- iou_type="bbox", verbose=verbose, logger=logger,
272
+ iou_type="bbox",
273
+ method=prf1_method,
274
+ verbose=verbose, logger=logger,
266
275
  )
267
276
  click.echo(format_prf1_output(prf1_result))
268
277
 
269
278
 
270
- def _print_segmentation_result(result, verbose, prf1, prf1_iou, prf1_conf, gt_json, dt_json, logger):
279
+ def _print_segmentation_result(result, verbose, prf1, prf1_iou, prf1_conf, prf1_method, gt_json, dt_json, logger):
271
280
  """Print segmentation evaluation results."""
272
281
  from dataflow.evaluate.utils import format_metric_table, format_per_class_table, format_prf1_output
273
282
 
@@ -291,22 +300,14 @@ def _print_segmentation_result(result, verbose, prf1, prf1_iou, prf1_conf, gt_js
291
300
  if prf1:
292
301
  from dataflow.evaluate import compute_pr_f1
293
302
  click.echo()
294
- # Manual mask IoU not yet supported — fallback to bbox
295
- try:
296
- prf1_result = compute_pr_f1(
297
- str(gt_json), str(dt_json),
298
- iou_threshold=prf1_iou,
299
- confidence_threshold=prf1_conf,
300
- iou_type="segm", verbose=verbose, logger=logger,
301
- )
302
- except NotImplementedError:
303
- logger.warning("Mask IoU P/R/F1 not yet supported in manual matching. Falling back to bbox IoU.")
304
- prf1_result = compute_pr_f1(
305
- str(gt_json), str(dt_json),
306
- iou_threshold=prf1_iou,
307
- confidence_threshold=prf1_conf,
308
- iou_type="bbox", verbose=verbose, logger=logger,
309
- )
303
+ prf1_result = compute_pr_f1(
304
+ str(gt_json), str(dt_json),
305
+ iou_threshold=prf1_iou,
306
+ confidence_threshold=prf1_conf,
307
+ iou_type="segm",
308
+ method=prf1_method,
309
+ verbose=verbose, logger=logger,
310
+ )
310
311
  click.echo(format_prf1_output(prf1_result))
311
312
 
312
313
 
@@ -200,6 +200,10 @@ class BaseConverter(ABC):
200
200
  target_path=target_path,
201
201
  )
202
202
 
203
+ # Store source_path for subclasses that need it in
204
+ # create_target_handler() (e.g., image copying in LabelMe→YOLO)
205
+ self._source_path = source_path
206
+
203
207
  num_images = 0
204
208
  num_objects = 0
205
209
 
@@ -267,48 +271,56 @@ class BaseConverter(ABC):
267
271
  return result
268
272
 
269
273
  def convert(self, source_path: str, target_path: str, **kwargs) -> ConversionResult:
270
- """
271
- Convert annotations from source format to target format (enhanced with verbose logging).
274
+ """Convert annotations from source format to target format.
275
+
276
+ Auto-dispatches to the correct pipeline based on target format:
277
+ - COCO target (single JSON) → batch pipeline (``_batch_convert()``)
278
+ - YOLO / LabelMe target (per-file output) → streaming pipeline
279
+ (``stream_convert()``)
280
+
281
+ Subclasses should NOT override this method. Instead, override the
282
+ abstract hooks: ``create_source_handler()``, ``create_target_handler()``,
283
+ ``_convert_single_image()``, ``convert_annotations()``, and optionally
284
+ ``_post_batch_convert()``.
272
285
 
273
286
  Args:
274
- source_path: Path to source annotations
275
- target_path: Path for target annotations
276
- **kwargs: Additional conversion parameters
287
+ source_path: Path to source annotations.
288
+ target_path: Path for target annotations.
289
+ **kwargs: Additional conversion parameters.
277
290
 
278
291
  Returns:
279
- ConversionResult containing conversion status and details
292
+ ConversionResult containing conversion status and details.
280
293
  """
281
- start_time = datetime.datetime.now()
282
- self.conversion_stats["start_time"] = start_time
294
+ if self.target_format == "coco":
295
+ return self._batch_convert(source_path, target_path, **kwargs)
296
+ else:
297
+ return self.stream_convert(source_path, target_path, **kwargs)
283
298
 
284
- # Log start information
285
- if self.verbose:
286
- self.logger.debug(f"Starting conversion: {source_path} -> {target_path}")
287
- self.logger.debug(f"Conversion parameters: {kwargs}")
299
+ def _batch_convert(
300
+ self, source_path: str, target_path: str, **kwargs
301
+ ) -> ConversionResult:
302
+ """Batch pipeline: read ALL → convert ALL → write ALL.
288
303
 
289
- # 1. Validate input parameters
304
+ Used when the target format is a single file (COCO JSON).
305
+ Subclasses should NOT override this — override the abstract
306
+ hooks and the optional ``_post_batch_convert()`` hook instead.
307
+ """
308
+ self._source_annotations_for_target = None
309
+
310
+ # 1. Validate inputs
290
311
  if not self.validate_inputs(source_path, target_path, kwargs):
291
- if self.verbose:
292
- self.logger.error("Input parameter validation failed")
293
312
  return self._create_conversion_result(
294
313
  success=False,
295
314
  source_path=source_path,
296
315
  target_path=target_path,
297
- errors=["Input parameter validation failed"],
316
+ errors=["Input validation failed"],
298
317
  log_file_path=self.log_file_path,
299
318
  )
300
319
 
301
- # 2. Use source handler to read data
320
+ # 2. Read data using source handler
302
321
  source_handler = self.create_source_handler(source_path, kwargs)
303
- if self.verbose:
304
- self.logger.debug(
305
- f"Created source handler: {source_handler.__class__.__name__}"
306
- )
307
-
308
322
  read_result = source_handler.read()
309
323
  if not read_result.success:
310
- if self.verbose:
311
- self.logger.error(f"Failed to read source data: {read_result.errors}")
312
324
  return self._create_conversion_result(
313
325
  success=False,
314
326
  source_path=source_path,
@@ -317,34 +329,33 @@ class BaseConverter(ABC):
317
329
  log_file_path=self.log_file_path,
318
330
  )
319
331
 
320
- # Record read results
332
+ # 3. Convert data
321
333
  annotations = read_result.data
322
334
  if self.verbose:
323
- self.logger.info(f"Read annotations for {annotations.num_images} images")
335
+ self.logger.info(
336
+ f"Read annotations for {annotations.num_images} images"
337
+ )
324
338
  self.logger.debug(f"Category count: {len(annotations.categories)}")
325
339
 
326
- # 3. Convert data (format-specific conversion)
327
- if self.verbose:
328
- self.logger.debug("Starting format-specific conversion")
329
-
330
340
  converted_annotations = self.convert_annotations(annotations, kwargs)
331
- self.conversion_stats["objects_converted"] = converted_annotations.num_objects
332
341
 
333
342
  if self.verbose:
334
343
  self.logger.debug(
335
- f"Conversion completed, object count: {converted_annotations.num_objects}"
344
+ f"Conversion completed, object count: "
345
+ f"{converted_annotations.num_objects}"
336
346
  )
337
347
 
338
- # 4. Use target handler to write data
339
- target_handler = self.create_target_handler(target_path, kwargs)
340
- if self.verbose:
341
- self.logger.debug(
342
- f"Created target handler: {target_handler.__class__.__name__}"
348
+ # 4. Write data (with state cleanup guarantee)
349
+ self._source_annotations_for_target = converted_annotations
350
+ try:
351
+ target_handler = self.create_target_handler(target_path, kwargs)
352
+ write_result = target_handler.write(
353
+ converted_annotations, target_path
343
354
  )
355
+ finally:
356
+ self._source_annotations_for_target = None
344
357
 
345
- write_result = target_handler.write(converted_annotations, target_path)
346
-
347
- # 5. Create result
358
+ # 5. Build result
348
359
  result = self._create_conversion_result(
349
360
  success=write_result.success,
350
361
  source_path=source_path,
@@ -354,32 +365,37 @@ class BaseConverter(ABC):
354
365
  log_file_path=self.log_file_path,
355
366
  )
356
367
 
357
- # Add verbose log entries
358
368
  if self.verbose:
359
- result.add_verbose_log(f"Source format: {self.source_format}")
360
- result.add_verbose_log(f"Target format: {self.target_format}")
361
- result.add_verbose_log(f"Source path: {source_path}")
362
- result.add_verbose_log(f"Target path: {target_path}")
363
- result.add_verbose_log(f"Images processed: {annotations.num_images}")
364
369
  result.add_verbose_log(
365
- f"Objects converted: {converted_annotations.num_objects}"
366
- )
367
-
368
- if write_result.warnings:
369
- for warning in write_result.warnings:
370
- result.add_verbose_log(f"Warning: {warning}")
371
-
372
- # Record conversion statistics
373
- self.conversion_stats["end_time"] = datetime.datetime.now()
374
- duration = (
375
- self.conversion_stats["end_time"] - self.conversion_stats["start_time"]
370
+ f"Images processed: {annotations.num_images}"
376
371
  )
377
372
  result.add_verbose_log(
378
- f"Total duration: {duration.total_seconds():.2f} seconds"
373
+ f"Objects converted: {converted_annotations.num_objects}"
379
374
  )
375
+ if write_result.errors:
376
+ for error in write_result.errors:
377
+ result.add_verbose_log(f"Error: {error}")
378
+
379
+ # 6. Post-processing hook (e.g., RLE warnings)
380
+ self._post_batch_convert(result, source_handler, kwargs)
380
381
 
381
382
  return result
382
383
 
384
+ def _post_batch_convert(
385
+ self,
386
+ result: ConversionResult,
387
+ source_handler: BaseAnnotationHandler,
388
+ kwargs: Dict,
389
+ ) -> None:
390
+ """Optional post-processing hook for batch conversions.
391
+
392
+ Called after a successful batch write. Subclasses override this
393
+ to add format-specific warnings or metadata (e.g., RLE accuracy
394
+ warnings).
395
+
396
+ Default: no-op.
397
+ """
398
+
383
399
  def validate_inputs(self, source_path: str, target_path: str, kwargs: Dict) -> bool:
384
400
  """
385
401
  Validate conversion input parameters.
@@ -442,19 +458,38 @@ class BaseConverter(ABC):
442
458
  def convert_annotations(
443
459
  self, source_annotations: DatasetAnnotations, kwargs: Dict
444
460
  ) -> DatasetAnnotations:
445
- """
446
- Convert annotation data (format-specific conversion).
461
+ """Convert annotation data (format-specific transformation).
462
+
463
+ Subclasses MUST override this to implement the coordinate
464
+ transformation for their conversion direction. There is no
465
+ safe pass-through default because coordinate semantics differ
466
+ between formats.
467
+
468
+ The canonical implementation delegates to
469
+ ``_convert_single_image()`` per image:
470
+
471
+ .. code-block:: python
472
+
473
+ target = DatasetAnnotations(format=target_format)
474
+ target.categories = source_annotations.categories.copy()
475
+ for img in source_annotations.images:
476
+ target.add_image(self._convert_single_image(img, **kwargs))
477
+ return target
447
478
 
448
479
  Args:
449
- source_annotations: Annotations read from source format
450
- kwargs: Additional conversion parameters
480
+ source_annotations: Annotations in source-native coordinates.
481
+ kwargs: Additional conversion parameters.
451
482
 
452
483
  Returns:
453
- Converted DatasetAnnotations ready for writing to target format
484
+ Converted DatasetAnnotations in target-native coordinates.
485
+
486
+ Raises:
487
+ NotImplementedError: Always — subclasses must implement.
454
488
  """
455
- # Default implementation: return as-is (assuming categories and data are already correct)
456
- # Subclasses should override for format-specific conversions like category mapping
457
- return source_annotations
489
+ raise NotImplementedError(
490
+ "Subclass must implement convert_annotations() "
491
+ "for this conversion direction"
492
+ )
458
493
 
459
494
  def _create_conversion_result(
460
495
  self,
@@ -514,6 +549,6 @@ class BaseConverter(ABC):
514
549
 
515
550
  def _log_error(self, message: str):
516
551
  """Log error message, raise exception in strict mode."""
517
- self.logger.error(message)
518
- if self.strict_mode:
519
- raise ValueError(message)
552
+ from dataflow.util.logging_util import logging_error_or_raise
553
+
554
+ logging_error_or_raise(message, self.logger, self.strict_mode)