dataeval 0.69.4__py3-none-any.whl → 0.70.1__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.
- dataeval/__init__.py +8 -8
- dataeval/_internal/datasets.py +235 -131
- dataeval/_internal/detectors/clusterer.py +2 -0
- dataeval/_internal/detectors/drift/base.py +7 -8
- dataeval/_internal/detectors/drift/mmd.py +4 -4
- dataeval/_internal/detectors/duplicates.py +64 -45
- dataeval/_internal/detectors/merged_stats.py +23 -54
- dataeval/_internal/detectors/ood/ae.py +8 -6
- dataeval/_internal/detectors/ood/aegmm.py +6 -4
- dataeval/_internal/detectors/ood/base.py +12 -7
- dataeval/_internal/detectors/ood/llr.py +6 -4
- dataeval/_internal/detectors/ood/vae.py +5 -3
- dataeval/_internal/detectors/ood/vaegmm.py +6 -4
- dataeval/_internal/detectors/outliers.py +137 -63
- dataeval/_internal/interop.py +11 -7
- dataeval/_internal/metrics/balance.py +13 -11
- dataeval/_internal/metrics/ber.py +5 -3
- dataeval/_internal/metrics/coverage.py +4 -0
- dataeval/_internal/metrics/divergence.py +9 -5
- dataeval/_internal/metrics/diversity.py +14 -12
- dataeval/_internal/metrics/parity.py +32 -22
- dataeval/_internal/metrics/stats/base.py +231 -0
- dataeval/_internal/metrics/stats/boxratiostats.py +159 -0
- dataeval/_internal/metrics/stats/datasetstats.py +99 -0
- dataeval/_internal/metrics/stats/dimensionstats.py +113 -0
- dataeval/_internal/metrics/stats/hashstats.py +75 -0
- dataeval/_internal/metrics/stats/labelstats.py +125 -0
- dataeval/_internal/metrics/stats/pixelstats.py +119 -0
- dataeval/_internal/metrics/stats/visualstats.py +124 -0
- dataeval/_internal/metrics/uap.py +8 -4
- dataeval/_internal/metrics/utils.py +30 -15
- dataeval/_internal/models/pytorch/autoencoder.py +5 -5
- dataeval/_internal/models/tensorflow/pixelcnn.py +1 -4
- dataeval/_internal/output.py +3 -18
- dataeval/_internal/utils.py +11 -16
- dataeval/_internal/workflows/sufficiency.py +152 -151
- dataeval/detectors/__init__.py +4 -0
- dataeval/detectors/drift/__init__.py +8 -3
- dataeval/detectors/drift/kernels/__init__.py +4 -0
- dataeval/detectors/drift/updates/__init__.py +4 -0
- dataeval/detectors/linters/__init__.py +15 -4
- dataeval/detectors/ood/__init__.py +14 -2
- dataeval/metrics/__init__.py +5 -0
- dataeval/metrics/bias/__init__.py +13 -4
- dataeval/metrics/estimators/__init__.py +8 -8
- dataeval/metrics/stats/__init__.py +25 -3
- dataeval/utils/__init__.py +16 -3
- dataeval/utils/tensorflow/__init__.py +11 -0
- dataeval/utils/torch/__init__.py +12 -0
- dataeval/utils/torch/datasets/__init__.py +7 -0
- dataeval/workflows/__init__.py +6 -2
- {dataeval-0.69.4.dist-info → dataeval-0.70.1.dist-info}/METADATA +12 -4
- dataeval-0.70.1.dist-info/RECORD +80 -0
- {dataeval-0.69.4.dist-info → dataeval-0.70.1.dist-info}/WHEEL +1 -1
- dataeval/_internal/flags.py +0 -77
- dataeval/_internal/metrics/stats.py +0 -397
- dataeval/flags/__init__.py +0 -3
- dataeval/tensorflow/__init__.py +0 -3
- dataeval/torch/__init__.py +0 -3
- dataeval-0.69.4.dist-info/RECORD +0 -74
- /dataeval/{tensorflow → utils/tensorflow}/loss/__init__.py +0 -0
- /dataeval/{tensorflow → utils/tensorflow}/models/__init__.py +0 -0
- /dataeval/{tensorflow → utils/tensorflow}/recon/__init__.py +0 -0
- /dataeval/{torch → utils/torch}/models/__init__.py +0 -0
- /dataeval/{torch → utils/torch}/trainer/__init__.py +0 -0
- {dataeval-0.69.4.dist-info → dataeval-0.70.1.dist-info}/LICENSE.txt +0 -0
@@ -17,6 +17,8 @@ TData = TypeVar("TData", np.float64, NDArray[np.float64])
|
|
17
17
|
@dataclass(frozen=True)
|
18
18
|
class ParityOutput(Generic[TData], OutputMetadata):
|
19
19
|
"""
|
20
|
+
Output class for :func:`parity` and :func:`label_parity` bias metrics
|
21
|
+
|
20
22
|
Attributes
|
21
23
|
----------
|
22
24
|
score : np.float64 | NDArray[np.float64]
|
@@ -62,8 +64,8 @@ def digitize_factor_bins(continuous_values: NDArray, bins: int, factor_name: str
|
|
62
64
|
|
63
65
|
|
64
66
|
def format_discretize_factors(
|
65
|
-
data_factors:
|
66
|
-
) ->
|
67
|
+
data_factors: Mapping[str, NDArray], continuous_factor_bincounts: Mapping[str, int]
|
68
|
+
) -> dict[str, NDArray]:
|
67
69
|
"""
|
68
70
|
Sets up the internal list of metadata factors.
|
69
71
|
|
@@ -80,10 +82,9 @@ def format_discretize_factors(
|
|
80
82
|
|
81
83
|
Returns
|
82
84
|
-------
|
83
|
-
|
85
|
+
Dict[str, NDArray]
|
84
86
|
- Intrinsic per-image metadata information with the formatting that input data_factors uses.
|
85
87
|
Each key is a metadata factor, whose value is the discrete per-image factor values.
|
86
|
-
- Per-image labels, whose ith element is the label for the ith element of the dataset.
|
87
88
|
"""
|
88
89
|
|
89
90
|
invalid_keys = set(continuous_factor_bincounts.keys()) - set(data_factors.keys())
|
@@ -103,8 +104,6 @@ def format_discretize_factors(
|
|
103
104
|
if lengths[1:] != lengths[:-1]:
|
104
105
|
raise ValueError("The lengths of each entry in the dictionary are not equal." f" Found lengths {lengths}")
|
105
106
|
|
106
|
-
labels = data_factors["class"]
|
107
|
-
|
108
107
|
metadata_factors = {
|
109
108
|
name: val
|
110
109
|
if name not in continuous_factor_bincounts
|
@@ -113,7 +112,7 @@ def format_discretize_factors(
|
|
113
112
|
if name != "class"
|
114
113
|
}
|
115
114
|
|
116
|
-
return metadata_factors
|
115
|
+
return metadata_factors
|
117
116
|
|
118
117
|
|
119
118
|
def normalize_expected_dist(expected_dist: NDArray, observed_dist: NDArray) -> NDArray:
|
@@ -140,8 +139,8 @@ def normalize_expected_dist(expected_dist: NDArray, observed_dist: NDArray) -> N
|
|
140
139
|
ValueError
|
141
140
|
If the expected distribution is all zeros.
|
142
141
|
|
143
|
-
|
144
|
-
|
142
|
+
Note
|
143
|
+
----
|
145
144
|
The function ensures that the total number of labels in the expected distribution matches the total
|
146
145
|
number of labels in the observed distribution by scaling the expected distribution.
|
147
146
|
"""
|
@@ -187,7 +186,8 @@ def validate_dist(label_dist: NDArray, label_name: str):
|
|
187
186
|
warnings.warn(
|
188
187
|
f"Labels {np.where(label_dist<5)[0]} in {label_name}"
|
189
188
|
" dataset have frequencies less than 5. This may lead"
|
190
|
-
" to invalid chi-squared evaluation."
|
189
|
+
" to invalid chi-squared evaluation.",
|
190
|
+
UserWarning,
|
191
191
|
)
|
192
192
|
|
193
193
|
|
@@ -226,8 +226,8 @@ def label_parity(
|
|
226
226
|
of unique classes between the observed and expected distributions.
|
227
227
|
|
228
228
|
|
229
|
-
|
230
|
-
|
229
|
+
Note
|
230
|
+
----
|
231
231
|
- Providing ``num_classes`` can be helpful if there are classes with zero instances in one of the distributions.
|
232
232
|
- The function first validates the observed distribution and normalizes the expected distribution so that it
|
233
233
|
has the same total number of labels as the observed distribution.
|
@@ -280,8 +280,9 @@ def label_parity(
|
|
280
280
|
|
281
281
|
@set_metadata("dataeval.metrics")
|
282
282
|
def parity(
|
283
|
+
class_labels: ArrayLike,
|
283
284
|
data_factors: Mapping[str, ArrayLike],
|
284
|
-
continuous_factor_bincounts:
|
285
|
+
continuous_factor_bincounts: Mapping[str, int] | None = None,
|
285
286
|
) -> ParityOutput[NDArray[np.float64]]:
|
286
287
|
"""
|
287
288
|
Calculate chi-square statistics to assess the relationship between multiple factors and class labels.
|
@@ -292,10 +293,12 @@ def parity(
|
|
292
293
|
|
293
294
|
Parameters
|
294
295
|
----------
|
296
|
+
class_labels: ArrayLike
|
297
|
+
List of class labels for each image
|
295
298
|
data_factors: Mapping[str, ArrayLike]
|
296
|
-
The dataset factors, which are per-image attributes
|
299
|
+
The dataset factors, which are per-image metadata attributes.
|
297
300
|
Each key of dataset_factors is a factor, whose value is the per-image factor values.
|
298
|
-
continuous_factor_bincounts :
|
301
|
+
continuous_factor_bincounts : Mapping[str, int] | None, default None
|
299
302
|
A dictionary specifying the number of bins for discretizing the continuous factors.
|
300
303
|
The keys should correspond to the names of continuous factors in `data_factors`,
|
301
304
|
and the values should be the number of bins to use for discretization.
|
@@ -316,8 +319,8 @@ def parity(
|
|
316
319
|
factor values either 0 times or at least 5 times. Alternatively, continuous-valued factors can be digitized
|
317
320
|
into fewer bins.
|
318
321
|
|
319
|
-
|
320
|
-
|
322
|
+
Note
|
323
|
+
----
|
321
324
|
- Each key of the ``continuous_factor_bincounts`` dictionary must occur as a key in data_factors.
|
322
325
|
- A high score with a low p-value suggests that a metadata factor is strongly correlated with a class label.
|
323
326
|
- The function creates a contingency matrix for each factor, where each entry represents the frequency of a
|
@@ -329,21 +332,27 @@ def parity(
|
|
329
332
|
--------
|
330
333
|
Randomly creating some "continuous" and categorical variables using ``np.random.default_rng``
|
331
334
|
|
335
|
+
>>> labels = np_random_gen.choice([0, 1, 2], (100))
|
332
336
|
>>> data_factors = {
|
333
337
|
... "age": np_random_gen.choice([25, 30, 35, 45], (100)),
|
334
338
|
... "income": np_random_gen.choice([50000, 65000, 80000], (100)),
|
335
339
|
... "gender": np_random_gen.choice(["M", "F"], (100)),
|
336
|
-
... "class": np_random_gen.choice([0, 1, 2], (100)),
|
337
340
|
... }
|
338
341
|
>>> continuous_factor_bincounts = {"age": 4, "income": 3}
|
339
|
-
>>> parity(data_factors, continuous_factor_bincounts)
|
340
|
-
ParityOutput(score=array([
|
342
|
+
>>> parity(labels, data_factors, continuous_factor_bincounts)
|
343
|
+
ParityOutput(score=array([7.35731943, 5.46711299, 0.51506212]), p_value=array([0.28906231, 0.24263543, 0.77295762]))
|
341
344
|
"""
|
345
|
+
if len(np.shape(class_labels)) > 1:
|
346
|
+
raise ValueError(
|
347
|
+
f"Got class labels with {len(np.shape(class_labels))}-dimensional",
|
348
|
+
f" shape {np.shape(class_labels)}, but expected a 1-dimensional array.",
|
349
|
+
)
|
342
350
|
|
343
351
|
data_factors_np = {k: to_numpy(v) for k, v in data_factors.items()}
|
344
352
|
continuous_factor_bincounts = continuous_factor_bincounts if continuous_factor_bincounts else {}
|
345
353
|
|
346
|
-
|
354
|
+
labels = to_numpy(class_labels)
|
355
|
+
factors = format_discretize_factors(data_factors_np, continuous_factor_bincounts)
|
347
356
|
|
348
357
|
chi_scores = np.zeros(len(factors))
|
349
358
|
p_values = np.zeros(len(factors))
|
@@ -396,7 +405,8 @@ def parity(
|
|
396
405
|
message = "\n".join(factor_msg)
|
397
406
|
|
398
407
|
warnings.warn(
|
399
|
-
f"The following factors did not meet the recommended 5 occurrences for each value-label combination. \
|
408
|
+
f"The following factors did not meet the recommended 5 occurrences for each value-label combination. \n\
|
409
|
+
Recommend rerunning parity after adjusting the following factor-value-label combinations: \n{message}",
|
400
410
|
UserWarning,
|
401
411
|
)
|
402
412
|
|
@@ -0,0 +1,231 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import re
|
4
|
+
import warnings
|
5
|
+
from dataclasses import dataclass
|
6
|
+
from typing import Any, Callable, Iterable, NamedTuple, Optional, Union
|
7
|
+
|
8
|
+
import numpy as np
|
9
|
+
from numpy.typing import ArrayLike, NDArray
|
10
|
+
|
11
|
+
from dataeval._internal.interop import to_numpy_iter
|
12
|
+
from dataeval._internal.metrics.utils import normalize_box_shape, normalize_image_shape, rescale
|
13
|
+
from dataeval._internal.output import OutputMetadata
|
14
|
+
|
15
|
+
DTYPE_REGEX = re.compile(r"NDArray\[np\.(.*?)\]")
|
16
|
+
SOURCE_INDEX = "source_index"
|
17
|
+
BOX_COUNT = "box_count"
|
18
|
+
|
19
|
+
OptionalRange = Optional[Union[int, Iterable[int]]]
|
20
|
+
|
21
|
+
|
22
|
+
def matches(index: int | None, opt_range: OptionalRange) -> bool:
|
23
|
+
if index is None or opt_range is None:
|
24
|
+
return True
|
25
|
+
return index in opt_range if isinstance(opt_range, Iterable) else index == opt_range
|
26
|
+
|
27
|
+
|
28
|
+
class SourceIndex(NamedTuple):
|
29
|
+
"""
|
30
|
+
Attributes
|
31
|
+
----------
|
32
|
+
image: int
|
33
|
+
Index of the source image
|
34
|
+
box : int | None
|
35
|
+
Index of the box of the source image
|
36
|
+
channel : int | None
|
37
|
+
Index of the channel of the source image
|
38
|
+
"""
|
39
|
+
|
40
|
+
image: int
|
41
|
+
box: int | None
|
42
|
+
channel: int | None
|
43
|
+
|
44
|
+
|
45
|
+
@dataclass(frozen=True)
|
46
|
+
class BaseStatsOutput(OutputMetadata):
|
47
|
+
"""
|
48
|
+
Attributes
|
49
|
+
----------
|
50
|
+
source_index : List[SourceIndex]
|
51
|
+
Mapping from statistic to source image, box and channel index
|
52
|
+
box_count : NDArray[np.uint16]
|
53
|
+
"""
|
54
|
+
|
55
|
+
source_index: list[SourceIndex]
|
56
|
+
box_count: NDArray[np.uint16]
|
57
|
+
|
58
|
+
def get_channel_mask(
|
59
|
+
self,
|
60
|
+
channel_index: OptionalRange,
|
61
|
+
channel_count: OptionalRange = None,
|
62
|
+
) -> list[bool]:
|
63
|
+
"""
|
64
|
+
Boolean mask for results filtered to specified channel index and optionally the count
|
65
|
+
of the channels per image.
|
66
|
+
|
67
|
+
Parameters
|
68
|
+
----------
|
69
|
+
channel_index : int | Iterable[int] | None
|
70
|
+
Index or indices of channel(s) to filter for
|
71
|
+
channel_count : int | Iterable[int] | None
|
72
|
+
Optional count(s) of channels to filter for
|
73
|
+
"""
|
74
|
+
mask: list[bool] = []
|
75
|
+
cur_mask: list[bool] = []
|
76
|
+
cur_image = 0
|
77
|
+
cur_max_channel = 0
|
78
|
+
for source_index in list(self.source_index) + [None]:
|
79
|
+
if source_index is None or source_index.image > cur_image:
|
80
|
+
mask.extend(cur_mask if matches(cur_max_channel + 1, channel_count) else [False for _ in cur_mask])
|
81
|
+
if source_index is None:
|
82
|
+
break
|
83
|
+
cur_image = source_index.image
|
84
|
+
cur_max_channel = 0
|
85
|
+
cur_mask.clear()
|
86
|
+
cur_mask.append(matches(source_index.channel, channel_index))
|
87
|
+
cur_max_channel = max(cur_max_channel, source_index.channel or 0)
|
88
|
+
return mask
|
89
|
+
|
90
|
+
def __len__(self) -> int:
|
91
|
+
return len(self.source_index)
|
92
|
+
|
93
|
+
|
94
|
+
class StatsProcessor:
|
95
|
+
cache_keys: list[str] = []
|
96
|
+
image_function_map: dict[str, Callable[[StatsProcessor], Any]] = {}
|
97
|
+
channel_function_map: dict[str, Callable[[StatsProcessor], Any]] = {}
|
98
|
+
|
99
|
+
def __init__(self, image: NDArray, box: NDArray | None, per_channel: bool):
|
100
|
+
self.raw = image
|
101
|
+
self.width = image.shape[-1]
|
102
|
+
self.height = image.shape[-2]
|
103
|
+
self.box = np.array([0, 0, self.width, self.height]) if box is None else box
|
104
|
+
self.per_channel = per_channel
|
105
|
+
self._image = None
|
106
|
+
self._shape = None
|
107
|
+
self._scaled = None
|
108
|
+
self.cache = {}
|
109
|
+
self.fn_map = self.channel_function_map if per_channel else self.image_function_map
|
110
|
+
self.is_valid_slice = box is None or bool(
|
111
|
+
box[0] >= 0 and box[1] >= 0 and box[2] <= image.shape[-1] and box[3] <= image.shape[-2]
|
112
|
+
)
|
113
|
+
|
114
|
+
def get(self, fn_key: str) -> NDArray:
|
115
|
+
if fn_key in self.cache_keys:
|
116
|
+
if fn_key not in self.cache:
|
117
|
+
self.cache[fn_key] = self.fn_map[fn_key](self)
|
118
|
+
return self.cache[fn_key]
|
119
|
+
else:
|
120
|
+
return self.fn_map[fn_key](self)
|
121
|
+
|
122
|
+
@property
|
123
|
+
def image(self) -> NDArray:
|
124
|
+
if self._image is None:
|
125
|
+
if self.is_valid_slice:
|
126
|
+
norm = normalize_image_shape(self.raw)
|
127
|
+
self._image = norm[:, self.box[1] : self.box[3], self.box[0] : self.box[2]]
|
128
|
+
else:
|
129
|
+
self._image = np.zeros((self.raw.shape[0], self.box[3] - self.box[1], self.box[2] - self.box[0]))
|
130
|
+
return self._image
|
131
|
+
|
132
|
+
@property
|
133
|
+
def shape(self) -> tuple:
|
134
|
+
if self._shape is None:
|
135
|
+
self._shape = self.image.shape
|
136
|
+
return self._shape
|
137
|
+
|
138
|
+
@property
|
139
|
+
def scaled(self) -> NDArray:
|
140
|
+
if self._scaled is None:
|
141
|
+
self._scaled = rescale(self.image)
|
142
|
+
if self.per_channel:
|
143
|
+
self._scaled = self._scaled.reshape(self.image.shape[0], -1)
|
144
|
+
return self._scaled
|
145
|
+
|
146
|
+
|
147
|
+
def run_stats(
|
148
|
+
images: Iterable[ArrayLike],
|
149
|
+
bboxes: Iterable[ArrayLike] | None,
|
150
|
+
per_channel: bool,
|
151
|
+
stats_processor_cls: type,
|
152
|
+
output_cls: type,
|
153
|
+
) -> dict:
|
154
|
+
"""
|
155
|
+
Compute specified statistics on a set of images.
|
156
|
+
|
157
|
+
This function applies a set of statistical operations to each image in the input iterable,
|
158
|
+
based on the specified output class. The function determines which statistics to apply
|
159
|
+
using a function map. It also supports optional image flattening for pixel-wise calculations.
|
160
|
+
|
161
|
+
Parameters
|
162
|
+
----------
|
163
|
+
images : Iterable[ArrayLike]
|
164
|
+
An iterable of images (e.g., list of arrays), where each image is represented as an
|
165
|
+
array-like structure (e.g., NumPy arrays).
|
166
|
+
bboxes : Iterable[ArrayLike]
|
167
|
+
An iterable of bounding boxes (e.g. list of arrays) where each bounding box is represented
|
168
|
+
as an array-like structure in the format of (X0, Y0, X1, Y1). The length of the bounding boxes
|
169
|
+
iterable should match the length of the input images.
|
170
|
+
per_channel : bool
|
171
|
+
A flag which determines if the states should be evaluated on a per-channel basis or not.
|
172
|
+
output_cls : type
|
173
|
+
The output class for which stats values will be calculated.
|
174
|
+
|
175
|
+
Returns
|
176
|
+
-------
|
177
|
+
dict[str, NDArray]]
|
178
|
+
A dictionary containing the computed statistics for each image.
|
179
|
+
The dictionary keys correspond to the names of the statistics, and the values are NumPy arrays
|
180
|
+
with the results of the computations.
|
181
|
+
|
182
|
+
Note
|
183
|
+
----
|
184
|
+
- The function performs image normalization (rescaling the image values)
|
185
|
+
before applying some of the statistics.
|
186
|
+
- Pixel-level statistics (e.g., brightness, entropy) are computed after
|
187
|
+
rescaling and, optionally, flattening the images.
|
188
|
+
- For statistics like histograms and entropy, intermediate results may
|
189
|
+
be reused to avoid redundant computation.
|
190
|
+
"""
|
191
|
+
results_list: list[dict[str, NDArray]] = []
|
192
|
+
output_list = list(output_cls.__annotations__)
|
193
|
+
source_index = []
|
194
|
+
box_count = []
|
195
|
+
bbox_iter = (None for _ in images) if bboxes is None else to_numpy_iter(bboxes)
|
196
|
+
|
197
|
+
for i, (boxes, image) in enumerate(zip(bbox_iter, to_numpy_iter(images))):
|
198
|
+
nboxes = [None] if boxes is None else normalize_box_shape(boxes)
|
199
|
+
for i_b, box in enumerate(nboxes):
|
200
|
+
i_b = None if box is None else i_b
|
201
|
+
processor: StatsProcessor = stats_processor_cls(image, box, per_channel)
|
202
|
+
if not processor.is_valid_slice:
|
203
|
+
warnings.warn(f"Bounding box {i_b}: {box} is out of bounds of image {i}: {image.shape}.")
|
204
|
+
results_list.append({stat: processor.get(stat) for stat in output_list})
|
205
|
+
if per_channel:
|
206
|
+
source_index.extend([SourceIndex(i, i_b, c) for c in range(image.shape[-3])])
|
207
|
+
else:
|
208
|
+
source_index.append(SourceIndex(i, i_b, None))
|
209
|
+
box_count.append(0 if boxes is None else len(boxes))
|
210
|
+
|
211
|
+
output = {}
|
212
|
+
if per_channel:
|
213
|
+
for i, results in enumerate(results_list):
|
214
|
+
for stat, result in results.items():
|
215
|
+
output.setdefault(stat, []).extend(result.tolist())
|
216
|
+
else:
|
217
|
+
for results in results_list:
|
218
|
+
for stat, result in results.items():
|
219
|
+
output.setdefault(stat, []).append(result.tolist() if isinstance(result, np.ndarray) else result)
|
220
|
+
|
221
|
+
for stat in output:
|
222
|
+
stat_type: str = output_cls.__annotations__[stat]
|
223
|
+
|
224
|
+
dtype_match = re.match(DTYPE_REGEX, stat_type)
|
225
|
+
if dtype_match is not None:
|
226
|
+
output[stat] = np.asarray(output[stat], dtype=np.dtype(dtype_match.group(1)))
|
227
|
+
|
228
|
+
output[SOURCE_INDEX] = source_index
|
229
|
+
output[BOX_COUNT] = np.asarray(box_count, dtype=np.uint16)
|
230
|
+
|
231
|
+
return output
|
@@ -0,0 +1,159 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import copy
|
4
|
+
from typing import Callable, Generic, TypeVar, cast
|
5
|
+
|
6
|
+
import numpy as np
|
7
|
+
from numpy.typing import NDArray
|
8
|
+
|
9
|
+
from dataeval._internal.metrics.stats.base import BOX_COUNT, SOURCE_INDEX, BaseStatsOutput
|
10
|
+
from dataeval._internal.metrics.stats.dimensionstats import DimensionStatsOutput
|
11
|
+
from dataeval._internal.output import set_metadata
|
12
|
+
|
13
|
+
TStatOutput = TypeVar("TStatOutput", bound=BaseStatsOutput, contravariant=True)
|
14
|
+
ArraySlice = tuple[int, int]
|
15
|
+
|
16
|
+
|
17
|
+
class BoxImageStatsOutputSlice(Generic[TStatOutput]):
|
18
|
+
class StatSlicer:
|
19
|
+
def __init__(self, stats: TStatOutput, slice: ArraySlice, channels: int = 0) -> None: # noqa: A002
|
20
|
+
self._stats = stats
|
21
|
+
self._slice = slice
|
22
|
+
self._channels = channels
|
23
|
+
|
24
|
+
def __getitem__(self, key: str) -> NDArray[np.float64]:
|
25
|
+
_stat = cast(np.ndarray, getattr(self._stats, key)).astype(np.float64)
|
26
|
+
_shape = _stat[0].shape
|
27
|
+
_slice = _stat[self._slice[0] : self._slice[1]]
|
28
|
+
return _slice.reshape(-1, self._channels, *_shape) if self._channels else _slice.reshape(-1, *_shape)
|
29
|
+
|
30
|
+
box: StatSlicer
|
31
|
+
img: StatSlicer
|
32
|
+
channels: int
|
33
|
+
|
34
|
+
def __init__(
|
35
|
+
self, box_stats: TStatOutput, box_slice: ArraySlice, img_stats: TStatOutput, img_slice: ArraySlice
|
36
|
+
) -> None:
|
37
|
+
self.channels = img_slice[1] - img_slice[0]
|
38
|
+
self.box = self.StatSlicer(box_stats, box_slice, self.channels)
|
39
|
+
self.img = self.StatSlicer(img_stats, img_slice)
|
40
|
+
|
41
|
+
|
42
|
+
RATIOSTATS_OVERRIDE_MAP: dict[type, dict[str, Callable[[BoxImageStatsOutputSlice], NDArray]]] = {
|
43
|
+
DimensionStatsOutput: {
|
44
|
+
"left": lambda x: x.box["left"] / x.img["width"],
|
45
|
+
"top": lambda x: x.box["top"] / x.img["height"],
|
46
|
+
"channels": lambda x: x.box["channels"],
|
47
|
+
"depth": lambda x: x.box["depth"],
|
48
|
+
"distance": lambda x: x.box["distance"],
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
|
53
|
+
def get_index_map(stats: BaseStatsOutput) -> list[int]:
|
54
|
+
index_map: list[int] = []
|
55
|
+
cur_index = -1
|
56
|
+
for i, s in enumerate(stats.source_index):
|
57
|
+
if s.image > cur_index:
|
58
|
+
index_map.append(i)
|
59
|
+
cur_index = s.image
|
60
|
+
return index_map
|
61
|
+
|
62
|
+
|
63
|
+
def calculate_ratios(key: str, box_stats: BaseStatsOutput, img_stats: BaseStatsOutput) -> NDArray:
|
64
|
+
if not hasattr(box_stats, key) or not hasattr(img_stats, key):
|
65
|
+
raise KeyError("Invalid key for provided stats output object.")
|
66
|
+
|
67
|
+
stats = getattr(box_stats, key)
|
68
|
+
|
69
|
+
# Copy over stats index maps and box counts
|
70
|
+
if key in (SOURCE_INDEX):
|
71
|
+
return copy.deepcopy(stats)
|
72
|
+
elif key == BOX_COUNT:
|
73
|
+
return np.copy(stats)
|
74
|
+
|
75
|
+
# Calculate ratios for each stat
|
76
|
+
out_stats: np.ndarray = np.copy(stats).astype(np.float64)
|
77
|
+
|
78
|
+
box_map = get_index_map(box_stats)
|
79
|
+
img_map = get_index_map(img_stats)
|
80
|
+
for i, (box_i, img_i) in enumerate(zip(box_map, img_map)):
|
81
|
+
box_j = len(box_stats) if i == len(box_map) - 1 else box_map[i + 1]
|
82
|
+
img_j = len(img_stats) if i == len(img_map) - 1 else img_map[i + 1]
|
83
|
+
stats = BoxImageStatsOutputSlice(box_stats, (box_i, box_j), img_stats, (img_i, img_j))
|
84
|
+
out_type = type(box_stats)
|
85
|
+
use_override = out_type in RATIOSTATS_OVERRIDE_MAP and key in RATIOSTATS_OVERRIDE_MAP[out_type]
|
86
|
+
ratio = (
|
87
|
+
RATIOSTATS_OVERRIDE_MAP[out_type][key](stats)
|
88
|
+
if use_override
|
89
|
+
else np.nan_to_num(stats.box[key] / stats.img[key])
|
90
|
+
)
|
91
|
+
out_stats[box_i:box_j] = ratio.reshape(-1, *out_stats[box_i].shape)
|
92
|
+
return out_stats
|
93
|
+
|
94
|
+
|
95
|
+
@set_metadata("dataeval.metrics")
|
96
|
+
def boxratiostats(
|
97
|
+
boxstats: TStatOutput,
|
98
|
+
imgstats: TStatOutput,
|
99
|
+
) -> TStatOutput:
|
100
|
+
"""
|
101
|
+
Calculates ratio statistics of box outputs over image outputs
|
102
|
+
|
103
|
+
Parameters
|
104
|
+
----------
|
105
|
+
boxstats : DimensionStatsOutput | PixelStatsOutput | VisualStatsOutput
|
106
|
+
Box statistics outputs to perform calculations on
|
107
|
+
imgstats : DimensionStatsOutput | PixelStatsOutput | VisualStatsOutput
|
108
|
+
Image statistics outputs to perform calculations on
|
109
|
+
|
110
|
+
Returns
|
111
|
+
-------
|
112
|
+
DimensionStatsOutput | PixelStatsOutput | VisualStatsOutput
|
113
|
+
A dictionary-like object containing the computed ratio of the box statistics divided by the
|
114
|
+
image statistics.
|
115
|
+
|
116
|
+
See Also
|
117
|
+
--------
|
118
|
+
dimensionstats, pixelstats, visualstats
|
119
|
+
|
120
|
+
Note
|
121
|
+
----
|
122
|
+
DimensionStatsOutput values for channels, depth and distances are the original values
|
123
|
+
provided by the box outputs
|
124
|
+
|
125
|
+
Examples
|
126
|
+
--------
|
127
|
+
Calculating the box ratio statistics using the dimension stats of the boxes and images
|
128
|
+
|
129
|
+
>>> imagestats = dimensionstats(images)
|
130
|
+
>>> boxstats = dimensionstats(images, bboxes)
|
131
|
+
>>> ratiostats = boxratiostats(boxstats, imagestats)
|
132
|
+
>>> print(ratiostats.aspect_ratio)
|
133
|
+
[ 1.15169271 0.78450521 21.33333333 1.5234375 2.25651042 0.77799479
|
134
|
+
0.88867188 3.40625 1.73307292 1.11132812 0.75018315 0.45018315
|
135
|
+
0.69596354 20. 5.11197917 2.33333333 0.75 0.70019531]
|
136
|
+
>>> print(ratiostats.size)
|
137
|
+
[0.03401693 0.01383464 0.00130208 0.01822917 0.02327474 0.00683594
|
138
|
+
0.01220703 0.0168457 0.01057943 0.00976562 0.00130208 0.01098633
|
139
|
+
0.02246094 0.0012207 0.01123047 0.00911458 0.02636719 0.06835938]
|
140
|
+
"""
|
141
|
+
output_cls = type(boxstats)
|
142
|
+
if type(boxstats) is not type(imgstats):
|
143
|
+
raise TypeError("Must provide stats outputs of the same type.")
|
144
|
+
if boxstats.source_index[-1].image != imgstats.source_index[-1].image:
|
145
|
+
raise ValueError("Stats index_map length mismatch. Check if the correct box and image stats were provided.")
|
146
|
+
if all(count == 0 for count in boxstats.box_count):
|
147
|
+
raise TypeError("Input for boxstats must contain box information.")
|
148
|
+
if any(count != 0 for count in imgstats.box_count):
|
149
|
+
raise TypeError("Input for imgstats must not contain box information.")
|
150
|
+
boxstats_has_channels = any(si.channel is None for si in boxstats.source_index)
|
151
|
+
imgstats_has_channels = any(si.channel is None for si in imgstats.source_index)
|
152
|
+
if boxstats_has_channels != imgstats_has_channels:
|
153
|
+
raise TypeError("Input for boxstats and imgstats must have matching channel information.")
|
154
|
+
|
155
|
+
output_dict = {}
|
156
|
+
for key in boxstats.dict():
|
157
|
+
output_dict[key] = calculate_ratios(key, boxstats, imgstats)
|
158
|
+
|
159
|
+
return output_cls(**output_dict)
|
@@ -0,0 +1,99 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from typing import Iterable
|
5
|
+
|
6
|
+
from numpy.typing import ArrayLike
|
7
|
+
|
8
|
+
from dataeval._internal.metrics.stats.base import BaseStatsOutput
|
9
|
+
from dataeval._internal.metrics.stats.dimensionstats import DimensionStatsOutput, dimensionstats
|
10
|
+
from dataeval._internal.metrics.stats.labelstats import LabelStatsOutput, labelstats
|
11
|
+
from dataeval._internal.metrics.stats.pixelstats import PixelStatsOutput, pixelstats
|
12
|
+
from dataeval._internal.metrics.stats.visualstats import VisualStatsOutput, visualstats
|
13
|
+
from dataeval._internal.output import OutputMetadata, set_metadata
|
14
|
+
|
15
|
+
|
16
|
+
@dataclass(frozen=True)
|
17
|
+
class DatasetStatsOutput(OutputMetadata):
|
18
|
+
"""
|
19
|
+
Output class for :func:`datasetstats` stats metric
|
20
|
+
|
21
|
+
This class represents the outputs of various stats functions against a single
|
22
|
+
dataset, such that each index across all stat outputs are representative of
|
23
|
+
the same source image. Modifying or mixing outputs will result in inaccurate
|
24
|
+
outlier calculations if not created correctly.
|
25
|
+
|
26
|
+
Attributes
|
27
|
+
----------
|
28
|
+
dimensionstats : DimensionStatsOutput or None
|
29
|
+
pixelstats: PixelStatsOutput or None
|
30
|
+
visualstats: VisualStatsOutput or None
|
31
|
+
labelstats: LabelStatsOutput or None, default None
|
32
|
+
"""
|
33
|
+
|
34
|
+
dimensionstats: DimensionStatsOutput | None
|
35
|
+
pixelstats: PixelStatsOutput | None
|
36
|
+
visualstats: VisualStatsOutput | None
|
37
|
+
labelstats: LabelStatsOutput | None = None
|
38
|
+
|
39
|
+
def outputs(self) -> list[BaseStatsOutput]:
|
40
|
+
return [s for s in (self.dimensionstats, self.pixelstats, self.visualstats) if s is not None]
|
41
|
+
|
42
|
+
def __post_init__(self):
|
43
|
+
lengths = [len(s) for s in self.outputs()]
|
44
|
+
if not all(length == lengths[0] for length in lengths):
|
45
|
+
raise ValueError("All StatsOutput classes must contain the same number of image sources.")
|
46
|
+
|
47
|
+
|
48
|
+
@set_metadata("dataeval.metrics")
|
49
|
+
def datasetstats(
|
50
|
+
images: Iterable[ArrayLike],
|
51
|
+
bboxes: Iterable[ArrayLike] | None = None,
|
52
|
+
labels: Iterable[ArrayLike] | None = None,
|
53
|
+
use_dimension: bool = True,
|
54
|
+
use_pixel: bool = True,
|
55
|
+
use_visual: bool = True,
|
56
|
+
) -> DatasetStatsOutput:
|
57
|
+
"""
|
58
|
+
Calculates various statistics for each image
|
59
|
+
|
60
|
+
This function computes dimension, pixel and visual metrics
|
61
|
+
on the images or individual bounding boxes for each image as
|
62
|
+
well as label statistics if provided.
|
63
|
+
|
64
|
+
Parameters
|
65
|
+
----------
|
66
|
+
images : Iterable[ArrayLike]
|
67
|
+
Images to perform calculations on
|
68
|
+
bboxes : Iterable[ArrayLike] or None
|
69
|
+
Bounding boxes in `xyxy` format for each image to perform calculations on
|
70
|
+
labels : Iterable[ArrayLike] or None
|
71
|
+
Labels of images or boxes to perform calculations on
|
72
|
+
|
73
|
+
Returns
|
74
|
+
-------
|
75
|
+
DatasetStatsOutput
|
76
|
+
Output class containing the outputs of various stats functions
|
77
|
+
|
78
|
+
See Also
|
79
|
+
--------
|
80
|
+
dimensionstats, labelstats, pixelstats, visualstats, Outliers
|
81
|
+
|
82
|
+
Examples
|
83
|
+
--------
|
84
|
+
Calculating the dimension, pixel and visual stats for a dataset with bounding boxes
|
85
|
+
|
86
|
+
>>> stats = datasetstats(images, bboxes)
|
87
|
+
>>> print(stats.dimensionstats.aspect_ratio)
|
88
|
+
[ 0.864 0.5884 16. 1.143 1.692 0.5835 0.6665 2.555 1.3
|
89
|
+
0.8335 1. 0.6 0.522 15. 3.834 1.75 0.75 0.7 ]
|
90
|
+
>>> print(stats.visualstats.contrast)
|
91
|
+
[1.744 1.946 0.1164 0.0635 0.0633 0.06274 0.0429 0.0317 0.0317
|
92
|
+
0.02576 0.02081 0.02171 0.01915 0.01767 0.01799 0.01595 0.01433 0.01478]
|
93
|
+
"""
|
94
|
+
return DatasetStatsOutput(
|
95
|
+
dimensionstats(images, bboxes) if use_dimension else None,
|
96
|
+
pixelstats(images, bboxes) if use_pixel else None,
|
97
|
+
visualstats(images, bboxes) if use_visual else None,
|
98
|
+
labelstats(labels) if labels else None,
|
99
|
+
)
|