From 788bc317fa66dd326b31c97a6c48c95e9541d9bb Mon Sep 17 00:00:00 2001
From: Adonai Vera <45982251+AdonaiVera@users.noreply.github.com>
Date: Thu, 1 Feb 2024 17:53:15 -0500
Subject: [PATCH] Improvements in thread #818: documentation, handle unit test,
improvements pattern directory
---
docs/detection/tools/csv_sink.md | 8 -
docs/detection/tools/save_detections.md | 12 +
mkdocs.yml | 2 +-
supervision/detection/tools/csv_sink.py | 148 +++++++---
test/detection/test_csv.py | 374 ++++++++++++++++++------
5 files changed, 402 insertions(+), 142 deletions(-)
delete mode 100644 docs/detection/tools/csv_sink.md
create mode 100644 docs/detection/tools/save_detections.md
diff --git a/docs/detection/tools/csv_sink.md b/docs/detection/tools/csv_sink.md
deleted file mode 100644
index 613e6f3d..00000000
--- a/docs/detection/tools/csv_sink.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-comments: true
-status: new
----
-
-## Save CSV Detection
-
-:::supervision.detection.tools.csv_sink.CSVSink
diff --git a/docs/detection/tools/save_detections.md b/docs/detection/tools/save_detections.md
new file mode 100644
index 00000000..e49bc129
--- /dev/null
+++ b/docs/detection/tools/save_detections.md
@@ -0,0 +1,12 @@
+---
+comments: true
+status: new
+---
+
+# Save Detections
+
+
+
CSV Sink
+
+
+:::supervision.detection.tools.csv_sink.CSVSink
\ No newline at end of file
diff --git a/mkdocs.yml b/mkdocs.yml
index 14eec1c7..98feb19a 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -41,7 +41,7 @@ nav:
- Polygon Zone: detection/tools/polygon_zone.md
- Inference Slicer: detection/tools/inference_slicer.md
- Detection Smoother: detection/tools/smoother.md
- - Save CSV Detection: detection/tools/csv_sink.md
+ - Save Detections: detection/tools/save_detections.md
- Annotators: annotators.md
- Trackers: trackers.md
- Datasets: datasets.md
diff --git a/supervision/detection/tools/csv_sink.py b/supervision/detection/tools/csv_sink.py
index 32dd28b6..d80aeea1 100644
--- a/supervision/detection/tools/csv_sink.py
+++ b/supervision/detection/tools/csv_sink.py
@@ -1,8 +1,11 @@
from __future__ import annotations
import csv
+import os
from typing import Any, Dict, List, Optional
+import numpy as np
+
from supervision.detection.core import Detections
BASE_HEADER = [
@@ -15,7 +18,6 @@ BASE_HEADER = [
"tracker_id",
]
-
class CSVSink:
"""
A utility class for saving detection data to a CSV file. This class is designed to
@@ -26,60 +28,50 @@ class CSVSink:
providing flexibility for logging various types of information.
Args:
- filename (str): The name of the CSV file where the detections will be stored.
+ file_name (str): The name of the CSV file where the detections will be stored.
Defaults to 'output.csv'.
Example:
```python
- import numpy as np
+ import cv2
import supervision as sv
from ultralytics import YOLO
- import time
- model = YOLO("yolov8n.pt")
- tracker = sv.ByteTrack()
- box_annotator = sv.BoundingBoxAnnotator()
- label_annotator = sv.LabelAnnotator()
- csv_sink = sv.CSVSink(...)
+ image = cv2.imread()
+ model = YOLO()
- def callback(frame: np.ndarray, _: int) -> np.ndarray:
- start_time = time.time()
- results = model(frame)[0]
- detections = sv.Detections.from_ultralytics(results)
- detections = tracker.update_with_detections(detections)
+ csv_sink = sv.CSVSink()
- labels = [
- f"#{tracker_id} {results.names[class_id]}"
- for class_id, tracker_id
- in zip(detections.class_id, detections.tracker_id)
- ]
- time_frame = (time.time() - start_time)
-
- csv_sink.append(detections, custom_data={"processing_time": time_frame})
-
- annotated_frame = box_annotator.annotate(
- frame.copy(), detections=detections)
- return label_annotator.annotate(
- annotated_frame, detections=detections, labels=labels)
-
- csv_sink.open()
- sv.process_video(
- source_path="people-walking.mp4",
- target_path="result.mp4",
- callback=callback
- )
- csv_sink.close()
+ result = model(image)[0]
+ detections = sv.Detections.from_ultralytics(result)
+ with csv_sink as sink:
+ sink.append(detections, custom_data={'':''})
```
""" # noqa: E501 // docs
- def __init__(self, filename: str = "output.csv"):
- self.filename = filename
+ def __init__(self, file_name: str = "output.csv"):
+ """
+ Initialize the CSVSink instance.
+
+ Args:
+ file_name (str): The name of the CSV file.
+
+ Returns:
+ None
+ """
+ self.file_name = file_name
self.file: Optional[open] = None
self.writer: Optional[csv.writer] = None
self.header_written = False
- self.fieldnames = [] # To keep track of header names
+ self.field_names = []
def __enter__(self) -> CSVSink:
+ """
+ Enter the context manager.
+
+ Returns:
+ CSVSink: The CSVSink instance.
+ """
self.open()
return self
@@ -89,13 +81,40 @@ class CSVSink:
exc_val: Optional[Exception],
exc_tb: Optional[Any],
) -> None:
+ """
+ Exit the context manager.
+
+ Args:
+ exc_type (Optional[type]): The type of exception.
+ exc_val (Optional[Exception]): The exception instance.
+ exc_tb (Optional[Any]): The traceback.
+
+ Returns:
+ None
+ """
self.close()
def open(self) -> None:
- self.file = open(self.filename, "w", newline="")
+ """
+ Open the CSV file for writing.
+
+ Returns:
+ None
+ """
+ parent_directory = os.path.dirname(self.file_name)
+ if parent_directory and not os.path.exists(parent_directory):
+ os.makedirs(parent_directory)
+
+ self.file = open(self.file_name, "w", newline="")
self.writer = csv.writer(self.file)
def close(self) -> None:
+ """
+ Close the CSV file.
+
+ Returns:
+ None
+ """
if self.file:
self.file.close()
@@ -103,6 +122,16 @@ class CSVSink:
def parse_detection_data(
detections: Detections, custom_data: Dict[str, Any] = None
) -> List[Dict[str, Any]]:
+ """
+ Parse detection data into a list of dictionaries.
+
+ Args:
+ detections (Detections): The detection data.
+ custom_data (Dict[str, Any]): Custom data to include.
+
+ Returns:
+ List[Dict[str, Any]]: A list of dictionaries representing the data.
+ """
parsed_rows = []
for i in range(len(detections.xyxy)):
row = {
@@ -110,13 +139,18 @@ class CSVSink:
"y_min": detections.xyxy[i][1],
"x_max": detections.xyxy[i][2],
"y_max": detections.xyxy[i][3],
- "class_id": detections.class_id[i],
- "confidence": detections.confidence[i],
- "tracker_id": detections.tracker_id[i],
+ "class_id": "" if detections.class_id is None else str(detections.class_id[i]),
+ "confidence": "" if detections.confidence is None else str(detections.confidence[i]),
+ "tracker_id": "" if detections.tracker_id is None else str(detections.tracker_id[i]),
}
+
if hasattr(detections, "data"):
for key, value in detections.data.items():
- row[key] = value[i]
+ if value.ndim == 0:
+ row[key] = value
+ else:
+ row[key] = value[i]
+
if custom_data:
row.update(custom_data)
parsed_rows.append(row)
@@ -125,9 +159,19 @@ class CSVSink:
def append(
self, detections: Detections, custom_data: Dict[str, Any] = None
) -> None:
+ """
+ Append detection data to the CSV file.
+
+ Args:
+ detections (Detections): The detection data.
+ custom_data (Dict[str, Any]): Custom data to include.
+
+ Returns:
+ None
+ """
if not self.writer:
raise Exception(
- f"Cannot append to CSV: The file '{self.filename}' is not open."
+ f"Cannot append to CSV: The file '{self.file_name}' is not open."
)
if not self.header_written:
self.write_header(detections, custom_data)
@@ -135,13 +179,23 @@ class CSVSink:
parsed_rows = CSVSink.parse_detection_data(detections, custom_data)
for row in parsed_rows:
self.writer.writerow(
- [row.get(fieldname, "") for fieldname in self.fieldnames]
+ [row.get(field_name, "") for field_name in self.field_names]
)
def write_header(self, detections: Detections, custom_data: Dict[str, Any]) -> None:
+ """
+ Write the CSV header based on the provided detection and custom data.
+
+ Args:
+ detections (Detections): The detection data.
+ custom_data (Dict[str, Any]): Custom data to include in the header.
+
+ Returns:
+ None
+ """
dynamic_header = sorted(
set(custom_data.keys()) | set(getattr(detections, "data", {}).keys())
)
- self.fieldnames = BASE_HEADER + dynamic_header
- self.writer.writerow(self.fieldnames)
+ self.field_names = BASE_HEADER + dynamic_header
+ self.writer.writerow(self.field_names)
self.header_written = True
diff --git a/test/detection/test_csv.py b/test/detection/test_csv.py
index 9af60b20..7b9f13f6 100644
--- a/test/detection/test_csv.py
+++ b/test/detection/test_csv.py
@@ -1,111 +1,313 @@
import csv
import os
+from contextlib import ExitStack as DoesNotRaise
+from test.test_utils import mock_detections
+from typing import Any, Dict, List
-import numpy as np
import pytest
import supervision as sv
-from supervision.detection.core import Detections
-@pytest.fixture(scope="module")
-def detection_instances():
- # Setup detection instances as per the provided example
- detections = Detections(
- xyxy=np.array([[10, 20, 30, 40], [50, 60, 70, 80]]),
- confidence=np.array([0.7, 0.8]),
- class_id=np.array([0, 0]),
- tracker_id=np.array([0, 1]),
- data={"class_name": np.array(["person", "person"])},
- )
+@pytest.mark.parametrize(
+ "detections, custom_data, second_detections, second_custom_data, file_name, expected_result",
+ [
+ (
+ mock_detections(
+ xyxy=[[10, 20, 30, 40], [50, 60, 70, 80]],
+ confidence=[0.7, 0.8],
+ class_id=[0, 0],
+ tracker_id=[0, 1],
+ data={"class_name": ["person", "person"]}
+ ),
+ {"frame_number": 42},
+ mock_detections(
+ xyxy=[[15, 25, 35, 45], [55, 65, 75, 85]],
+ confidence=[0.6, 0.9],
+ class_id=[1, 1],
+ tracker_id=[2, 3],
+ data={"class_name": ["car", "car"]}
+ ),
+ {"frame_number": 43},
+ "test_detections.csv",
+ [
+ [
+ "x_min",
+ "y_min",
+ "x_max",
+ "y_max",
+ "class_id",
+ "confidence",
+ "tracker_id",
+ "class_name",
+ "frame_number",
+ ],
+ ["10.0", "20.0", "30.0", "40.0", "0", "0.7", "0", "person", "42"],
+ ["50.0", "60.0", "70.0", "80.0", "0", "0.8", "1", "person", "42"],
+ ["15.0", "25.0", "35.0", "45.0", "1", "0.6", "2", "car", "43"],
+ ["55.0", "65.0", "75.0", "85.0", "1", "0.9", "3", "car", "43"],
+ ]
+ ), # multiple detections
+ (
+ mock_detections(
+ xyxy=[[60, 70, 80, 90], [100, 110, 120, 130]],
+ tracker_id=[4, 5],
+ data={"class_name": ["bike", "dog"]}
+ ),
+ {"frame_number": 44},
+ mock_detections(
+ xyxy=[[65, 75, 85, 95], [105, 115, 125, 135]],
+ confidence=[0.5, 0.4],
+ data={"class_name": ["tree", "cat"]}
+ ),
+ {"frame_number": 45},
+ "test_detections_missing_fields.csv",
+ [
+ [
+ "x_min",
+ "y_min",
+ "x_max",
+ "y_max",
+ "class_id",
+ "confidence",
+ "tracker_id",
+ "class_name",
+ "frame_number",
+ ],
+ ["60.0", "70.0", "80.0", "90.0", "", "", "4", "bike", "44"],
+ ["100.0", "110.0", "120.0", "130.0", "", "", "5", "dog", "44"],
+ ["65.0", "75.0", "85.0", "95.0", "", "0.5", "", "tree", "45"],
+ ["105.0", "115.0", "125.0", "135.0", "", "0.4", "", "cat", "45"],
+ ]
+ ), # missing fields
+ (
+ mock_detections(
+ xyxy=[[10, 11, 12, 13]],
+ confidence=[0.95],
+ data={"class_name": "unknown", "is_detected": True, "score": 1}
+ ),
+ {"frame_number": 46},
+ mock_detections(
+ xyxy=[[14, 15, 16, 17]],
+ data={"class_name": "artifact", "is_detected": False, "score": 0.85}
+ ),
+ {"frame_number": 47},
+ "test_detections_varied_data.csv",
+ [
+ [
+ "x_min",
+ "y_min",
+ "x_max",
+ "y_max",
+ "class_id",
+ "confidence",
+ "tracker_id",
+ "class_name",
+ "frame_number",
+ "is_detected",
+ "score",
+ ],
+ ["10.0", "11.0", "12.0", "13.0", "", "0.95", "", "unknown", "46", "True", "1"],
+ ["14.0", "15.0", "16.0", "17.0", "", "", "", "artifact", "47", "False", "0.85"],
+ ]
+ ), # Inconsistent Data Types
+ (
+ mock_detections(
+ xyxy=[[20, 21, 22, 23]],
+ ),
+ {"metadata": {"sensor_id": 101, "location": "north"}, "tags": ["urgent", "review"]},
+ mock_detections(
+ xyxy=[[14, 15, 16, 17]],
+ ),
+ {"metadata": {"sensor_id": 104, "location": "west"}, "tags": ["not-urgent", "done"]},
+ "test_detections_complex_data.csv",
+ [
+ [
+ "x_min",
+ "y_min",
+ "x_max",
+ "y_max",
+ "class_id",
+ "confidence",
+ "tracker_id",
+ "metadata",
+ "tags",
+ ],
+ ["20.0", "21.0", "22.0", "23.0", "", "", "", "{'sensor_id': 101, 'location': 'north'}", "['urgent', 'review']"],
+ ["14.0", "15.0", "16.0", "17.0", "", "", "", "{'sensor_id': 104, 'location': 'west'}", "['not-urgent', 'done']"],
+ ]
+ ), # Complex Data
+ ],
+)
- second_detections = Detections(
- xyxy=np.array([[15, 25, 35, 45], [55, 65, 75, 85]]),
- confidence=np.array([0.6, 0.9]),
- class_id=np.array([1, 1]),
- tracker_id=np.array([2, 3]),
- data={"class_name": np.array(["car", "car"])},
- )
+def test_csv_sink(
+ detections: mock_detections,
+ custom_data: Dict[str, Any],
+ second_detections: mock_detections,
+ second_custom_data: Dict[str, Any],
+ file_name: str,
+ expected_result: List[List[Any]]
+) -> None:
- custom_data = {"frame_number": 42}
- second_custom_data = {"frame_number": 43}
-
- return detections, custom_data, second_detections, second_custom_data
-
-
-def test_csv_sink(detection_instances):
- detections, custom_data, second_detections, second_custom_data = detection_instances
- csv_filename = "test_detections.csv"
- expected_rows = [
- [
- "x_min",
- "y_min",
- "x_max",
- "y_max",
- "class_id",
- "confidence",
- "tracker_id",
- "class_name",
- "frame_number",
- ],
- [10, 20, 30, 40, 0, 0.7, 0, "person", 42],
- [50, 60, 70, 80, 0, 0.8, 1, "person", 42],
- [15, 25, 35, 45, 1, 0.6, 2, "car", 43],
- [55, 65, 75, 85, 1, 0.9, 3, "car", 43],
- ]
-
- # Using the CSVSink class to write the detection data to a CSV file
- with sv.CSVSink(filename=csv_filename) as sink:
+ with sv.CSVSink(file_name) as sink:
sink.append(detections, custom_data)
sink.append(second_detections, second_custom_data)
- # Read back the CSV file and verify its contents
- with open(csv_filename, mode="r", newline="") as file:
- reader = csv.reader(file)
- for i, row in enumerate(reader):
- assert (
- [str(item) for item in expected_rows[i]] == row
- ), f"Row in CSV didn't match expected output: {row} != {expected_rows[i]}"
+ assert_csv_equal(file_name, expected_result)
- # Clean up by removing the test CSV file
- os.remove(csv_filename)
+@pytest.mark.parametrize(
+ "detections, custom_data, second_detections, second_custom_data, file_name, expected_result",
+ [
+ (
+ mock_detections(
+ xyxy=[[10, 20, 30, 40], [50, 60, 70, 80]],
+ confidence=[0.7, 0.8],
+ class_id=[0, 0],
+ tracker_id=[0, 1],
+ data={"class_name": ["person", "person"]}
+ ),
+ {"frame_number": 42},
+ mock_detections(
+ xyxy=[[15, 25, 35, 45], [55, 65, 75, 85]],
+ confidence=[0.6, 0.9],
+ class_id=[1, 1],
+ tracker_id=[2, 3],
+ data={"class_name": ["car", "car"]}
+ ),
+ {"frame_number": 43},
+ "test_detections.csv",
+ [
+ [
+ "x_min",
+ "y_min",
+ "x_max",
+ "y_max",
+ "class_id",
+ "confidence",
+ "tracker_id",
+ "class_name",
+ "frame_number",
+ ],
+ ["10.0", "20.0", "30.0", "40.0", "0", "0.7", "0", "person", "42"],
+ ["50.0", "60.0", "70.0", "80.0", "0", "0.8", "1", "person", "42"],
+ ["15.0", "25.0", "35.0", "45.0", "1", "0.6", "2", "car", "43"],
+ ["55.0", "65.0", "75.0", "85.0", "1", "0.9", "3", "car", "43"],
+ ]
+ ), # multiple detections
+ (
+ mock_detections(
+ xyxy=[[60, 70, 80, 90], [100, 110, 120, 130]],
+ tracker_id=[4, 5],
+ data={"class_name": ["bike", "dog"]}
+ ),
+ {"frame_number": 44},
+ mock_detections(
+ xyxy=[[65, 75, 85, 95], [105, 115, 125, 135]],
+ confidence=[0.5, 0.4],
+ data={"class_name": ["tree", "cat"]}
+ ),
+ {"frame_number": 45},
+ "test_detections_missing_fields.csv",
+ [
+ [
+ "x_min",
+ "y_min",
+ "x_max",
+ "y_max",
+ "class_id",
+ "confidence",
+ "tracker_id",
+ "class_name",
+ "frame_number",
+ ],
+ ["60.0", "70.0", "80.0", "90.0", "", "", "4", "bike", "44"],
+ ["100.0", "110.0", "120.0", "130.0", "", "", "5", "dog", "44"],
+ ["65.0", "75.0", "85.0", "95.0", "", "0.5", "", "tree", "45"],
+ ["105.0", "115.0", "125.0", "135.0", "", "0.4", "", "cat", "45"],
+ ]
+ ), # missing fields
+ (
+ mock_detections(
+ xyxy=[[10, 11, 12, 13]],
+ confidence=[0.95],
+ data={"class_name": "unknown", "is_detected": True, "score": 1}
+ ),
+ {"frame_number": 46},
+ mock_detections(
+ xyxy=[[14, 15, 16, 17]],
+ data={"class_name": "artifact", "is_detected": False, "score": 0.85}
+ ),
+ {"frame_number": 47},
+ "test_detections_varied_data.csv",
+ [
+ [
+ "x_min",
+ "y_min",
+ "x_max",
+ "y_max",
+ "class_id",
+ "confidence",
+ "tracker_id",
+ "class_name",
+ "frame_number",
+ "is_detected",
+ "score",
+ ],
+ ["10.0", "11.0", "12.0", "13.0", "", "0.95", "", "unknown", "46", "True", "1"],
+ ["14.0", "15.0", "16.0", "17.0", "", "", "", "artifact", "47", "False", "0.85"],
+ ]
+ ), # Inconsistent Data Types
+ (
+ mock_detections(
+ xyxy=[[20, 21, 22, 23]],
+ ),
+ {"metadata": {"sensor_id": 101, "location": "north"}, "tags": ["urgent", "review"]},
+ mock_detections(
+ xyxy=[[14, 15, 16, 17]],
+ ),
+ {"metadata": {"sensor_id": 104, "location": "west"}, "tags": ["not-urgent", "done"]},
+ "test_detections_complex_data.csv",
+ [
+ [
+ "x_min",
+ "y_min",
+ "x_max",
+ "y_max",
+ "class_id",
+ "confidence",
+ "tracker_id",
+ "metadata",
+ "tags",
+ ],
+ ["20.0", "21.0", "22.0", "23.0", "", "", "", "{'sensor_id': 101, 'location': 'north'}", "['urgent', 'review']"],
+ ["14.0", "15.0", "16.0", "17.0", "", "", "", "{'sensor_id': 104, 'location': 'west'}", "['not-urgent', 'done']"],
+ ]
+ ), # Complex Data
+ ],
+)
-
-def test_csv_sink_manual(detection_instances):
- detections, custom_data, second_detections, second_custom_data = detection_instances
- csv_filename = "test_detections.csv"
- expected_rows = [
- [
- "x_min",
- "y_min",
- "x_max",
- "y_max",
- "class_id",
- "confidence",
- "tracker_id",
- "class_name",
- "frame_number",
- ],
- [10, 20, 30, 40, 0, 0.7, 0, "person", 42],
- [50, 60, 70, 80, 0, 0.8, 1, "person", 42],
- [15, 25, 35, 45, 1, 0.6, 2, "car", 43],
- [55, 65, 75, 85, 1, 0.9, 3, "car", 43],
- ]
-
- # Using the CSVSink class to write the detection data to a CSV file
- sink = sv.CSVSink(filename=csv_filename)
+def test_csv_sink_manual(
+ detections: mock_detections,
+ custom_data: Dict[str, Any],
+ second_detections: mock_detections,
+ second_custom_data: Dict[str, Any],
+ file_name: str,
+ expected_result: List[List[Any]],
+) -> None:
+ sink = sv.CSVSink(file_name)
sink.open()
sink.append(detections, custom_data)
sink.append(second_detections, second_custom_data)
sink.close()
- # Read back the CSV file and verify its contents
- with open(csv_filename, mode="r", newline="") as file:
+ assert_csv_equal(file_name, expected_result)
+
+def assert_csv_equal(file_name, expected_rows):
+ with open(file_name, mode="r", newline="") as file:
reader = csv.reader(file)
for i, row in enumerate(reader):
assert (
[str(item) for item in expected_rows[i]] == row
), f"Row in CSV didn't match expected output: {row} != {expected_rows[i]}"
-
- # Clean up by removing the test CSV file
- os.remove(csv_filename)
+
+ #os.remove(file_name)
\ No newline at end of file