From 4e881f5585a13375c0815b4c8b1b90ac9027db45 Mon Sep 17 00:00:00 2001 From: Niklas Schmolenski Date: Sun, 31 Aug 2025 16:58:24 +0200 Subject: [PATCH 1/8] feat: support complex slicing conditions --- doleus/datasets/base.py | 45 +++++++++++++++++++++ tests/datasets/test_datasets.py | 71 +++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/doleus/datasets/base.py b/doleus/datasets/base.py index d458519..2710efc 100644 --- a/doleus/datasets/base.py +++ b/doleus/datasets/base.py @@ -391,3 +391,48 @@ def slice_by_groundtruth_class( slice_name = create_filename(self.name, "class", "==", class_str) return self._create_new_instance(self.dataset, filtered_indices, slice_name) + + def slice_by_conditions( + self, + conditions: List[tuple], + logical_operator: str = "AND", + slice_name: Optional[str] = None, + ): + """Create a slice based on multiple conditions. + + Parameters + ---------- + conditions : List[tuple] + List of conditions in format (metadata_key, operator_str, value). + logical_operator : str, optional + How to combine conditions: "AND" or "OR", by default "AND". + slice_name : str, optional + Name for the slice. If None, a name will be generated, by default None. + + Returns + ------- + Slice + A new slice containing datapoints that meet the criteria. + """ + if logical_operator.upper() not in ["AND", "OR"]: + raise ValueError("logical_operator must be 'AND' or 'OR'") + + indices = [] + for i in range(len(self.dataset)): + if logical_operator.upper() == "AND": + if all( + OPERATOR_DICT[op](self.metadata_store.get_metadata(i, key), val) + for key, op, val in conditions + ): + indices.append(i) + else: + if any( + OPERATOR_DICT[op](self.metadata_store.get_metadata(i, key), val) + for key, op, val in conditions + ): + indices.append(i) + + if slice_name is None: + slice_name = f"{self.name}_filtered_{len(conditions)}conditions_{logical_operator.lower()}" + + return self._create_new_instance(self.dataset, indices, slice_name) diff --git a/tests/datasets/test_datasets.py b/tests/datasets/test_datasets.py index 87093dd..ac5646e 100644 --- a/tests/datasets/test_datasets.py +++ b/tests/datasets/test_datasets.py @@ -4,6 +4,7 @@ import numpy as np import pandas as pd import torch + from doleus.utils import Task, TaskType @@ -210,3 +211,73 @@ def test_chained_slicing( high_conf_validated.metadata_store.get_metadata(i, "confidence_score") >= 0.9 ) + + +def test_slice_and_operator( + doleus_binary_classification_dataset, basic_metadata, numeric_metadata +): + dataset = doleus_binary_classification_dataset + dataset.add_metadata_from_list(basic_metadata) + dataset.add_metadata_from_list(numeric_metadata) + + conditions = [("validated", "==", True), ("confidence_score", ">=", 0.9)] + + filtered_slice = dataset.slice_by_conditions( + conditions, logical_operator="AND", slice_name="validated_high_conf" + ) + + assert len(filtered_slice) == 3 + assert filtered_slice.name == "validated_high_conf" + + for i in range(len(filtered_slice)): + assert filtered_slice.metadata_store.get_metadata(i, "validated") == True + assert filtered_slice.metadata_store.get_metadata(i, "confidence_score") >= 0.9 + + +def test_slice_or_operator( + doleus_binary_classification_dataset, basic_metadata, numeric_metadata +): + dataset = doleus_binary_classification_dataset + dataset.add_metadata_from_list(basic_metadata) + dataset.add_metadata_from_list(numeric_metadata) + + conditions = [("batch_id", "==", 1), ("batch_id", "==", 2)] + + filtered_slice = dataset.slice_by_conditions( + conditions, logical_operator="OR", slice_name="batch_1_or_2" + ) + + assert len(filtered_slice) == 6 + assert filtered_slice.name == "batch_1_or_2" + + for i in range(len(filtered_slice)): + batch_id = filtered_slice.metadata_store.get_metadata(i, "batch_id") + assert batch_id in [1, 2] + + +def test_complex_slicing( + doleus_binary_classification_dataset, basic_metadata, numeric_metadata +): + dataset = doleus_binary_classification_dataset + dataset.add_metadata_from_list(basic_metadata) + dataset.add_metadata_from_list(numeric_metadata) + + new_method = dataset.slice_by_conditions( + [("validated", "==", True), ("confidence_score", ">=", 0.9)], + logical_operator="AND", + ) + + chained_method = dataset.slice_by_value("validated", "==", True) + chained_method = chained_method.slice_by_value("confidence_score", ">=", 0.9) + + assert len(new_method) == len(chained_method) + + for i in range(len(new_method)): + new_validated = new_method.metadata_store.get_metadata(i, "validated") + new_conf = new_method.metadata_store.get_metadata(i, "confidence_score") + + chained_validated = chained_method.metadata_store.get_metadata(i, "validated") + chained_conf = chained_method.metadata_store.get_metadata(i, "confidence_score") + + assert new_validated == chained_validated == True + assert new_conf == chained_conf >= 0.9 From 75fa5041539aa61bf0f22b049669f51a6e69c778 Mon Sep 17 00:00:00 2001 From: Niklas Schmolenski Date: Sun, 31 Aug 2025 19:52:29 +0200 Subject: [PATCH 2/8] fix: add operators --- doleus/utils/data.py | 5 ++++ tests/datasets/test_datasets.py | 50 +++++++++++++++++---------------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/doleus/utils/data.py b/doleus/utils/data.py index 09fad88..020ae08 100644 --- a/doleus/utils/data.py +++ b/doleus/utils/data.py @@ -12,6 +12,11 @@ "==": op.eq, "=": op.eq, "!=": op.ne, + "in": lambda x, y: x in y, + "not_in": lambda x, y: x not in y, + "between": lambda x, y: ( + y[0] <= x <= y[1] if isinstance(y, (list, tuple)) and len(y) == 2 else False + ), } diff --git a/tests/datasets/test_datasets.py b/tests/datasets/test_datasets.py index ac5646e..3dc0cbf 100644 --- a/tests/datasets/test_datasets.py +++ b/tests/datasets/test_datasets.py @@ -233,6 +233,21 @@ def test_slice_and_operator( assert filtered_slice.metadata_store.get_metadata(i, "validated") == True assert filtered_slice.metadata_store.get_metadata(i, "confidence_score") >= 0.9 + camera_conditions = [ + ("source", "in", ["camera_a"]), + ("confidence_score", "between", [0.85, 1.0]), + ] + + camera_slice = dataset.slice_by_conditions( + camera_conditions, logical_operator="AND", slice_name="camera_a_high_conf" + ) + + assert len(camera_slice) == 4 + for i in range(len(camera_slice)): + assert camera_slice.metadata_store.get_metadata(i, "source") == "camera_a" + score = camera_slice.metadata_store.get_metadata(i, "confidence_score") + assert 0.85 <= score <= 1.0 + def test_slice_or_operator( doleus_binary_classification_dataset, basic_metadata, numeric_metadata @@ -254,30 +269,17 @@ def test_slice_or_operator( batch_id = filtered_slice.metadata_store.get_metadata(i, "batch_id") assert batch_id in [1, 2] - -def test_complex_slicing( - doleus_binary_classification_dataset, basic_metadata, numeric_metadata -): - dataset = doleus_binary_classification_dataset - dataset.add_metadata_from_list(basic_metadata) - dataset.add_metadata_from_list(numeric_metadata) - - new_method = dataset.slice_by_conditions( - [("validated", "==", True), ("confidence_score", ">=", 0.9)], - logical_operator="AND", + source_slice = dataset.slice_by_value( + "source", "in", ["camera_a", "camera_b"], "all_sources" ) + assert len(source_slice) == 10 - chained_method = dataset.slice_by_value("validated", "==", True) - chained_method = chained_method.slice_by_value("confidence_score", ">=", 0.9) - - assert len(new_method) == len(chained_method) - - for i in range(len(new_method)): - new_validated = new_method.metadata_store.get_metadata(i, "validated") - new_conf = new_method.metadata_store.get_metadata(i, "confidence_score") - - chained_validated = chained_method.metadata_store.get_metadata(i, "validated") - chained_conf = chained_method.metadata_store.get_metadata(i, "confidence_score") + excluded_batch = dataset.slice_by_value( + "batch_id", "not_in", [1], "excluded_batch_1" + ) + assert len(excluded_batch) == 7 - assert new_validated == chained_validated == True - assert new_conf == chained_conf >= 0.9 + confidence_range = dataset.slice_by_value( + "confidence_score", "between", [0.8, 0.95], "mid_confidence" + ) + assert len(confidence_range) == 6 From 5f6b51cf4d1f0479b370d77e15ee7eda48385039 Mon Sep 17 00:00:00 2001 From: iamheinrich <76793837+iamheinrich@users.noreply.github.com> Date: Thu, 2 Oct 2025 08:49:10 +0200 Subject: [PATCH 3/8] build: replaced open-cv with open-cv-headless --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c9c5f0a..03b4fd4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,9 +14,9 @@ dependencies = [ "torchvision (>=0.22.1,<0.23.0)", "tqdm (>=4.67.1,<5.0.0)", "torchmetrics (>=1.7.3,<2.0.0)", - "opencv-python (>=4.11.0.86,<5.0.0.0)", + "opencv-python-headless (>=4.11.0.86,<5.0.0.0)", "pytz (>=2025.2,<2026.0)", - "numpy (>=2.3.1,<3.0.0)", + "numpy (>=2.0.0,<3.0.0)", "pillow (>=11.2.1,<12.0.0)" ] From 65c10c3e7dbe433c60e75f9f2577e85413a69362 Mon Sep 17 00:00:00 2001 From: iamheinrich <76793837+iamheinrich@users.noreply.github.com> Date: Thu, 2 Oct 2025 08:50:25 +0200 Subject: [PATCH 4/8] test: Added tests for automatically created metadata --- tests/utils/__init__.py | 3 + tests/utils/test_image_metadata.py | 222 +++++++++++++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 tests/utils/__init__.py create mode 100644 tests/utils/test_image_metadata.py diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 0000000..ace00ba --- /dev/null +++ b/tests/utils/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2025 Doleus contributors +# SPDX-License-Identifier: Apache-2.0 + diff --git a/tests/utils/test_image_metadata.py b/tests/utils/test_image_metadata.py new file mode 100644 index 0000000..750026d --- /dev/null +++ b/tests/utils/test_image_metadata.py @@ -0,0 +1,222 @@ +# SPDX-FileCopyrightText: 2025 Doleus contributors +# SPDX-License-Identifier: Apache-2.0 + +import numpy as np +import pytest +from doleus.utils.image_metadata import ( + ATTRIBUTE_FUNCTIONS, + compute_brightness, + compute_contrast, + compute_image_metadata, + compute_resolution, + compute_saturation, +) + + +class TestComputeBrightness: + """Test cases for compute_brightness function.""" + + def test_black_image(self): + """Test brightness of completely black image.""" + image = np.zeros((100, 100, 3), dtype=np.uint8) + brightness = compute_brightness(image) + assert brightness == 0.0 + + def test_white_image(self): + """Test brightness of completely white image.""" + image = np.ones((100, 100, 3), dtype=np.uint8) * 255 + brightness = compute_brightness(image) + assert brightness == 255.0 + + def test_gray_image(self): + """Test brightness of uniform gray image.""" + image = np.ones((100, 100, 3), dtype=np.uint8) * 128 + brightness = compute_brightness(image) + assert brightness == pytest.approx(128.0, abs=1.0) + + def test_returns_float(self): + """Test that brightness returns a float value.""" + image = np.random.randint(0, 256, (50, 50, 3), dtype=np.uint8) + brightness = compute_brightness(image) + assert isinstance(brightness, float) + + def test_brightness_range(self): + """Test that brightness is within valid range.""" + image = np.random.randint(0, 256, (100, 100, 3), dtype=np.uint8) + brightness = compute_brightness(image) + assert 0.0 <= brightness <= 255.0 + + +class TestComputeContrast: + """Test cases for compute_contrast function.""" + + def test_uniform_image_zero_contrast(self): + """Test contrast of uniform image is zero.""" + image = np.ones((100, 100, 3), dtype=np.uint8) * 128 + contrast = compute_contrast(image) + assert contrast == 0.0 + + def test_high_contrast_image(self): + """Test contrast of black and white checkerboard.""" + image = np.zeros((100, 100, 3), dtype=np.uint8) + image[::2, ::2] = 255 + image[1::2, 1::2] = 255 + contrast = compute_contrast(image) + assert contrast > 0.0 + + def test_returns_float(self): + """Test that contrast returns a float value.""" + image = np.random.randint(0, 256, (50, 50, 3), dtype=np.uint8) + contrast = compute_contrast(image) + assert isinstance(contrast, float) + + def test_contrast_non_negative(self): + """Test that contrast is always non-negative.""" + image = np.random.randint(0, 256, (100, 100, 3), dtype=np.uint8) + contrast = compute_contrast(image) + assert contrast >= 0.0 + + +class TestComputeSaturation: + """Test cases for compute_saturation function.""" + + def test_grayscale_image_zero_saturation(self): + """Test saturation of grayscale image is zero.""" + gray_value = 128 + image = np.ones((100, 100, 3), dtype=np.uint8) * gray_value + saturation = compute_saturation(image) + assert saturation == 0.0 + + def test_fully_saturated_red(self): + """Test saturation of fully saturated red image.""" + image = np.zeros((100, 100, 3), dtype=np.uint8) + image[:, :, 2] = 255 # BGR format, so red is channel 2 + saturation = compute_saturation(image) + assert saturation == 255.0 + + def test_returns_float(self): + """Test that saturation returns a float value.""" + image = np.random.randint(0, 256, (50, 50, 3), dtype=np.uint8) + saturation = compute_saturation(image) + assert isinstance(saturation, float) + + def test_saturation_range(self): + """Test that saturation is within valid range.""" + image = np.random.randint(0, 256, (100, 100, 3), dtype=np.uint8) + saturation = compute_saturation(image) + assert 0.0 <= saturation <= 255.0 + + +class TestComputeResolution: + """Test cases for compute_resolution function.""" + + def test_resolution_calculation(self): + """Test resolution calculation for known dimensions.""" + image = np.zeros((100, 200, 3), dtype=np.uint8) + resolution = compute_resolution(image) + assert resolution == 20000 + + def test_square_image(self): + """Test resolution of square image.""" + image = np.zeros((50, 50, 3), dtype=np.uint8) + resolution = compute_resolution(image) + assert resolution == 2500 + + def test_single_pixel(self): + """Test resolution of single pixel image.""" + image = np.zeros((1, 1, 3), dtype=np.uint8) + resolution = compute_resolution(image) + assert resolution == 1 + + def test_returns_int(self): + """Test that resolution returns an integer value.""" + image = np.zeros((100, 100, 3), dtype=np.uint8) + resolution = compute_resolution(image) + assert isinstance(resolution, int) + + def test_grayscale_image_resolution(self): + """Test resolution calculation for grayscale image.""" + image = np.zeros((100, 200), dtype=np.uint8) + resolution = compute_resolution(image) + assert resolution == 20000 + + +class TestComputeImageMetadata: + """Test cases for compute_image_metadata function.""" + + def test_returns_dict(self): + """Test that compute_image_metadata returns a dictionary.""" + image = np.random.randint(0, 256, (100, 100, 3), dtype=np.uint8) + metadata = compute_image_metadata(image) + assert isinstance(metadata, dict) + + def test_contains_all_attributes(self): + """Test that all expected attributes are present.""" + image = np.random.randint(0, 256, (100, 100, 3), dtype=np.uint8) + metadata = compute_image_metadata(image) + expected_keys = ["brightness", "contrast", "saturation", "resolution"] + assert set(metadata.keys()) == set(expected_keys) + + def test_all_values_are_numeric(self): + """Test that all metadata values are numeric.""" + image = np.random.randint(0, 256, (100, 100, 3), dtype=np.uint8) + metadata = compute_image_metadata(image) + for key, value in metadata.items(): + assert isinstance(value, (int, float)) + + def test_black_image_metadata(self): + """Test metadata values for completely black image.""" + image = np.zeros((100, 100, 3), dtype=np.uint8) + metadata = compute_image_metadata(image) + assert metadata["brightness"] == 0.0 + assert metadata["contrast"] == 0.0 + assert metadata["saturation"] == 0.0 + assert metadata["resolution"] == 10000 + + def test_white_image_metadata(self): + """Test metadata values for completely white image.""" + image = np.ones((100, 100, 3), dtype=np.uint8) * 255 + metadata = compute_image_metadata(image) + assert metadata["brightness"] == 255.0 + assert metadata["contrast"] == 0.0 + assert metadata["saturation"] == 0.0 + assert metadata["resolution"] == 10000 + + +class TestAttributeFunctions: + """Test cases for ATTRIBUTE_FUNCTIONS dictionary.""" + + def test_contains_all_functions(self): + """Test that ATTRIBUTE_FUNCTIONS contains all expected functions.""" + expected_keys = ["brightness", "contrast", "saturation", "resolution"] + assert set(ATTRIBUTE_FUNCTIONS.keys()) == set(expected_keys) + + def test_functions_are_callable(self): + """Test that all functions in ATTRIBUTE_FUNCTIONS are callable.""" + for key, func in ATTRIBUTE_FUNCTIONS.items(): + assert callable(func) + + def test_brightness_function_mapping(self): + """Test that brightness function is correctly mapped.""" + assert ATTRIBUTE_FUNCTIONS["brightness"] == compute_brightness + + def test_contrast_function_mapping(self): + """Test that contrast function is correctly mapped.""" + assert ATTRIBUTE_FUNCTIONS["contrast"] == compute_contrast + + def test_saturation_function_mapping(self): + """Test that saturation function is correctly mapped.""" + assert ATTRIBUTE_FUNCTIONS["saturation"] == compute_saturation + + def test_resolution_function_mapping(self): + """Test that resolution function is correctly mapped.""" + assert ATTRIBUTE_FUNCTIONS["resolution"] == compute_resolution + + def test_functions_work_through_dictionary(self): + """Test that functions can be called through the dictionary.""" + image = np.random.randint(0, 256, (100, 100, 3), dtype=np.uint8) + for key, func in ATTRIBUTE_FUNCTIONS.items(): + result = func(image) + assert result is not None + assert isinstance(result, (int, float)) + From a4d34424bdbac7be3edb972ff7c973db84642bb4 Mon Sep 17 00:00:00 2001 From: iamheinrich <76793837+iamheinrich@users.noreply.github.com> Date: Sat, 4 Oct 2025 13:30:36 +0200 Subject: [PATCH 5/8] feat: added not_between operator --- doleus/utils/data.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doleus/utils/data.py b/doleus/utils/data.py index 020ae08..93b944d 100644 --- a/doleus/utils/data.py +++ b/doleus/utils/data.py @@ -17,6 +17,9 @@ "between": lambda x, y: ( y[0] <= x <= y[1] if isinstance(y, (list, tuple)) and len(y) == 2 else False ), + "not_between": lambda x, y: ( + not (y[0] <= x <= y[1]) if isinstance(y, (list, tuple)) and len(y) == 2 else False + ), } From 7cec696f55e6cfff41165195844ceb7df36e6bbf Mon Sep 17 00:00:00 2001 From: iamheinrich <76793837+iamheinrich@users.noreply.github.com> Date: Sat, 4 Oct 2025 13:31:07 +0200 Subject: [PATCH 6/8] test: test for not_bewtween operator --- tests/datasets/test_datasets.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/datasets/test_datasets.py b/tests/datasets/test_datasets.py index 3dc0cbf..0cf1e4b 100644 --- a/tests/datasets/test_datasets.py +++ b/tests/datasets/test_datasets.py @@ -283,3 +283,8 @@ def test_slice_or_operator( "confidence_score", "between", [0.8, 0.95], "mid_confidence" ) assert len(confidence_range) == 6 + + confidence_range_not = dataset.slice_by_value( + "confidence_score", "not_between", [0.8, 0.95], "not_mid_confidence" + ) + assert len(confidence_range_not) == 4 From 50f77d0e166ce328b7961ddabbed8df9009e7a81 Mon Sep 17 00:00:00 2001 From: iamheinrich <76793837+iamheinrich@users.noreply.github.com> Date: Sat, 4 Oct 2025 13:41:26 +0200 Subject: [PATCH 7/8] docs: added docs for new slicing methods --- README.md | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 14734d3..bf8ada9 100644 --- a/README.md +++ b/README.md @@ -347,8 +347,84 @@ Subsets of your data filtered by metadata: - `slice_by_value("weather_condition", "==", "fog")` → Only foggy conditions - `slice_by_groundtruth_class(class_names=["pedestrian", "cyclist"])` → Specific object classes -> [!NOTE] -> **Slicing Method**: Use `slice_by_value("metadata_key", "==", "value")` for categorical filtering. In theory, all comparison operators are supported: `>`, `<`, `>=`, `<=`, `==`, `!=`. +#### **Available Operators** + +Doleus supports a comprehensive set of operators for flexible data slicing: + +**Comparison operators:** + +- `>`, `<`, `>=`, `<=`, `==`, `!=` - Standard comparisons + +**Membership operators:** + +- `in` - Check if value is in a list: `slice_by_value("source", "in", ["camera_a", "camera_b"])` +- `not_in` - Check if value is not in a list: `slice_by_value("batch_id", "not_in", [1, 2])` + +**Range operators:** + +- `between` - Check if value falls within range (inclusive): `slice_by_value("confidence", "between", [0.8, 0.95])` +- `not_between` - Check if value falls outside range: `slice_by_value("temperature", "not_between", [20, 30])` + +#### **Combining Multiple Conditions** + +For more complex filtering, use `slice_by_conditions()` to combine multiple criteria with logical operators: + +```python +# AND: All conditions must be true +conditions = [ + ("validated", "==", True), + ("confidence_score", ">=", 0.9), + ("source", "in", ["camera_a", "camera_b"]) +] +high_quality = doleus_dataset.slice_by_conditions( + conditions, + logical_operator="AND", + slice_name="high_quality_validated" +) + +# OR: Any condition must be true +conditions = [ + ("weather", "==", "fog"), + ("weather", "==", "rain"), + ("visibility_meters", "<", 50) +] +challenging_weather = doleus_dataset.slice_by_conditions( + conditions, + logical_operator="OR", + slice_name="challenging_conditions" +) +``` + +**Practical Example - Manufacturing Quality Control:** + +```python +# Find defects that are either very small OR on reflective surfaces with low confidence +defect_conditions = [ + ("defect_area_mm2", "<=", 1.0), # Small defects + ("surface_reflectivity", ">=", 0.8), # Highly reflective + ("detection_confidence", "between", [0.5, 0.7]) # Medium confidence +] + +# OR logic: catches small defects OR reflective surfaces with medium confidence +at_risk_detections = doleus_dataset.slice_by_conditions( + defect_conditions, + logical_operator="OR", + slice_name="at_risk_detections" +) + +# AND logic: only small defects on reflective surfaces with medium confidence +high_risk_combination = doleus_dataset.slice_by_conditions( + defect_conditions, + logical_operator="AND", + slice_name="high_risk_cases" +) +``` + +> [!TIP] **When to Use What**: +> +> - Use `slice_by_value()` for single-condition filters +> - Use `slice_by_conditions()` with `AND` when all conditions must be met (narrow down) +> - Use `slice_by_conditions()` with `OR` when any condition suffices (cast wider net) ### **Checks** @@ -359,11 +435,9 @@ Tests that compute metrics on slices: Checks become tests when you add pass/fail conditions (operator and value). Without these conditions, checks simply evaluate and report metric values. -> [!NOTE] -> **Prediction Format**: Doleus uses [torchmetrics](https://torchmetrics.readthedocs.io/) to compute metrics and expects the same prediction formats that torchmetrics functions require. +> [!NOTE] > **Prediction Format**: Doleus uses [torchmetrics](https://torchmetrics.readthedocs.io/) to compute metrics and expects the same prediction formats that torchmetrics functions require. -> [!IMPORTANT] -> **Macro Averaging Default**: Doleus uses **macro averaging** as the default for classification metrics (Accuracy, Precision, Recall, F1) to avoid known bugs in torchmetrics' micro averaging implementation (see [GitHub issue #2280](https://github.com/Lightning-AI/torchmetrics/issues/2280)). +> [!IMPORTANT] > **Macro Averaging Default**: Doleus uses **macro averaging** as the default for classification metrics (Accuracy, Precision, Recall, F1) to avoid known bugs in torchmetrics' micro averaging implementation (see [GitHub issue #2280](https://github.com/Lightning-AI/torchmetrics/issues/2280)). > > You can override this by setting `metric_parameters={"average": "micro"}` in your checks if needed. From d3cfb98b9a90eef9eb62d9830afa6a358d55fd73 Mon Sep 17 00:00:00 2001 From: iamheinrich <76793837+iamheinrich@users.noreply.github.com> Date: Sat, 4 Oct 2025 16:44:32 +0200 Subject: [PATCH 8/8] chore: bump version to v0.2.0 and update changelog --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a45d06..ca4fa4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,40 @@ # CHANGELOG +## v0.2.0 (2025-10-04) + +### Build + +- Replaced opencv-python with opencv-python-headless + ([`5f6b51c`](https://github.com/Doleus/doleus/commit/5f6b51c)) + +### Features + +- Support complex slicing conditions with AND/OR logic + ([`4e881f5`](https://github.com/Doleus/doleus/commit/4e881f5)) + +- Added slicing operators: in, not_in, between + ([`75fa504`](https://github.com/Doleus/doleus/commit/75fa504)) + +- Added not_between operator + ([`a4d3442`](https://github.com/Doleus/doleus/commit/a4d3442)) + +### Documentation + +- Added docs for new slicing methods + ([`50f77d0`](https://github.com/Doleus/doleus/commit/50f77d0)) + +### Testing + +- Added tests for complex slicing conditions (AND/OR operators) + ([`4e881f5`](https://github.com/Doleus/doleus/commit/4e881f5)) + +- Added tests for automatically created metadata + ([`65c10c3`](https://github.com/Doleus/doleus/commit/65c10c3)) + +- Test for not_between operator + ([`7cec696`](https://github.com/Doleus/doleus/commit/7cec696)) + ## v0.1.1 (2025-03-10) ### Bug Fixes diff --git a/pyproject.toml b/pyproject.toml index 03b4fd4..d5f753a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "doleus" -version = "0.1.0" +version = "0.2.0" description = "" authors = [ {name = "Hendrik Schulze Bröring"},