Skip to content

Commit 677cf94

Browse files
Get basic image metadata from image properties (#49)
* Add utilities to get image metadata * Add meta auto based on basic meta data of images * Refactor HF dataset to always have numpy array instead of PIL image * Refactor HF ppl model to convert numpy array to PIL image * Allow to set global mode for an HF ppl model for PIL conversion * Correct height and width in np array * Add brightness * Add average color and contrast * Add entropy * Fix test due to missing raw HF image in PIL format * Adapt object detection models with image mode to convert PIL image * Rewrite `get_meta` to concatenate basic metadata * Fix import of issues * Fix name masking in ffhq * Expand dicts in ffhq face detection * Should database be integer instead of float? * Use uint8 for image numpy test data * Convert and assume RGB uint8 image as input * Fix cached dataloader counter after calling `get_image` in `get_meta` * Unify issue groups * Save images from `get_raw_hf_image` to get image path in HF * Recover issue meta groups as in `main` * Clarify the dataloader access time changes * Use AttributesIssueMeta for basic meta * Use AttributesIssueMeta for depth basic meta * Fix format * Fix the issue group of brightness --------- Co-authored-by: Rabah Khalek <[email protected]>
1 parent 145d5d4 commit 677cf94

File tree

15 files changed

+366
-71
lines changed

15 files changed

+366
-71
lines changed

giskard_vision/core/dataloaders/base.py

+48-3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@
22
from abc import ABC, abstractmethod
33
from typing import List, Optional
44

5+
import cv2
56
import numpy as np
67

7-
from giskard_vision.core.dataloaders.meta import MetaData
8+
from giskard_vision.core.dataloaders.meta import (
9+
MetaData,
10+
get_brightness,
11+
get_entropy,
12+
get_image_channel_number,
13+
get_image_size,
14+
)
815
from giskard_vision.core.detectors.base import IssueGroup
916

1017
from ..types import TypesBase
@@ -17,6 +24,10 @@
1724
"Performance",
1825
description="The data are filtered by metadata like emotion, head pose, or exposure value to detect performance issues.",
1926
)
27+
AttributesIssueMeta = IssueGroup(
28+
"Attributes",
29+
description="The data are filtered by the image attributes like width, height, or brightness value to detect issues.",
30+
)
2031

2132

2233
class DataIteratorBase(ABC):
@@ -120,7 +131,7 @@ def meta_none(self) -> Optional[TypesBase.meta]:
120131
Returns default for meta data if it is None.
121132
122133
Returns:
123-
Optional[np.ndarray]: Default for meta data.
134+
Optional[TypesBase.meta]: Default for meta data.
124135
"""
125136
return None
126137

@@ -146,7 +157,41 @@ def get_meta(self, idx: int) -> Optional[TypesBase.meta]:
146157
Returns:
147158
Optional[TypesBase.meta]: Meta information for the given index.
148159
"""
149-
return None
160+
img = self.get_image(idx)
161+
if img.dtype != np.uint8:
162+
# Normalize image to 0-255 range with uint8
163+
img = (img * 255 % 255).astype(np.uint8)
164+
165+
gray_img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
166+
size = get_image_size(img)
167+
nb_channels = get_image_channel_number(img)
168+
avg_color = np.mean(img, axis=(0, 1))
169+
170+
return MetaData(
171+
data={
172+
"height": size[0],
173+
"width": size[1],
174+
"nb_channels": nb_channels,
175+
"brightness": get_brightness(img),
176+
"average_color_r": avg_color[0],
177+
"average_color_g": avg_color[1] if avg_color.shape[0] > 0 else avg_color[0],
178+
"average_color_b": avg_color[2] if avg_color.shape[0] > 0 else avg_color[0],
179+
"contrast": np.max(gray_img) - np.min(gray_img),
180+
"entropy": get_entropy(img),
181+
},
182+
categories=["nb_channels"],
183+
issue_groups={
184+
"width": AttributesIssueMeta,
185+
"height": AttributesIssueMeta,
186+
"nb_channels": AttributesIssueMeta,
187+
"brightness": AttributesIssueMeta,
188+
"average_color_r": AttributesIssueMeta,
189+
"average_color_g": AttributesIssueMeta,
190+
"average_color_b": AttributesIssueMeta,
191+
"contrast": AttributesIssueMeta,
192+
"entropy": AttributesIssueMeta,
193+
},
194+
)
150195

151196
def get_labels_with_default(self, idx: int) -> np.ndarray:
152197
"""

giskard_vision/core/dataloaders/hf.py

+39-3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22
import os
33
import shutil
44
import tempfile
5+
from abc import abstractmethod
56
from typing import Optional
67

7-
from giskard_vision.core.dataloaders.base import DataIteratorBase
8+
from PIL.Image import Image as PILImage
9+
10+
from giskard_vision.core.dataloaders.base import AttributesIssueMeta, DataIteratorBase
11+
from giskard_vision.core.dataloaders.meta import MetaData, get_pil_image_depth
812
from giskard_vision.utils.errors import GiskardError, GiskardImportError
913

1014

@@ -31,7 +35,11 @@ class HFDataLoader(DataIteratorBase):
3135
"""
3236

3337
def __init__(
34-
self, hf_id: str, hf_config: Optional[str] = None, hf_split: str = "test", name: Optional[str] = None
38+
self,
39+
hf_id: str,
40+
hf_config: Optional[str] = None,
41+
hf_split: str = "test",
42+
name: Optional[str] = None,
3543
) -> None:
3644
"""
3745
Initializes the general HuggingFace Datasets instance.
@@ -90,7 +98,7 @@ def get_image_path(self, idx: int) -> str:
9098
str: Image path
9199
"""
92100

93-
image = self.ds[idx]["image"]
101+
image = self.get_raw_hf_image(idx)
94102
image_path = os.path.join(self.temp_folder, f"image_{idx}.png")
95103
image.save(image_path)
96104

@@ -101,3 +109,31 @@ def cleanup(self):
101109
Clean the temporary folder
102110
"""
103111
shutil.rmtree(self.temp_folder)
112+
113+
@abstractmethod
114+
def get_raw_hf_image(self, idx: int) -> PILImage:
115+
"""
116+
Retrieves the raw image at the specified index in the HF dataset.
117+
Args:
118+
idx (int): Index of the image
119+
120+
Returns:
121+
PIL.Image.Image: The image instance.
122+
"""
123+
...
124+
125+
def get_meta(self, idx: int) -> MetaData:
126+
meta = super().get_meta(idx)
127+
img = self.get_raw_hf_image(idx)
128+
129+
return MetaData(
130+
data={
131+
**meta.data,
132+
"depth": get_pil_image_depth(img),
133+
},
134+
categories=["depth"] + meta.categories,
135+
issue_groups={
136+
**meta.issue_groups,
137+
"depth": AttributesIssueMeta,
138+
},
139+
)

giskard_vision/core/dataloaders/meta.py

+98-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
from typing import Any, Dict, List, Optional
1+
from typing import Any, Dict, List, Optional, Tuple
2+
3+
import cv2
4+
import numpy as np
5+
from PIL.Image import Image as PILImage
26

37
from giskard_vision.core.detectors.base import IssueGroup
48

@@ -140,3 +144,96 @@ def get_categories(self) -> Optional[List[str]]:
140144
Optional[List[str]]: The categories of the metadata, or None if no categories were provided.
141145
"""
142146
return self.categories
147+
148+
149+
def get_image_size(image: np.ndarray) -> Tuple[int, int]:
150+
"""
151+
Utitlity to create metadata with image size.
152+
153+
Args:
154+
image (np.ndarray): The numpy ndarray representation of an image.
155+
156+
Returns:
157+
Tuple[int, int]: The image size, width and height.
158+
"""
159+
return image.shape[:2]
160+
161+
162+
def get_image_channel_number(image: np.ndarray) -> int:
163+
"""
164+
Utitlity to create metadata with image channel number.
165+
166+
Args:
167+
image (np.ndarray): The numpy ndarray representation of an image.
168+
169+
Returns:
170+
int: The image channel number.
171+
"""
172+
shape = image.shape
173+
return shape[2] if len(shape) > 2 else 1
174+
175+
176+
def get_pil_image_depth(image: PILImage) -> int:
177+
"""
178+
Utitlity to create metadata with image depth.
179+
180+
Args:
181+
image (PILImage): The PIL Image object.
182+
183+
Returns:
184+
int: The image depth.
185+
"""
186+
mode = image.mode
187+
if mode == "1":
188+
return 1
189+
elif mode == "L":
190+
return 8
191+
elif mode == "P":
192+
return 8
193+
elif mode == "RGB":
194+
return 24
195+
elif mode == "RGBA":
196+
return 32
197+
elif mode == "CMYK":
198+
return 32
199+
elif mode == "YCbCr":
200+
return 24
201+
elif mode == "LAB":
202+
return 24
203+
elif mode == "HSV":
204+
return 24
205+
elif mode == "I":
206+
return 32
207+
elif mode == "F":
208+
return 32
209+
return 0
210+
211+
212+
def get_brightness(image: np.ndarray) -> float:
213+
"""
214+
Utitlity to create metadata with image brightness.
215+
216+
Args:
217+
image (np.ndarray): The numpy ndarray representation of an image.
218+
219+
Returns:
220+
float: The image brightness normalized to 1.
221+
"""
222+
hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)
223+
return np.mean(hsv[:, :, 2]) / 255
224+
225+
226+
def get_entropy(image: np.ndarray) -> float:
227+
"""
228+
Utitlity to create metadata with image entropy.
229+
230+
Args:
231+
image (np.ndarray): The numpy ndarray representation of an image.
232+
233+
Returns:
234+
float: The image entropy.
235+
"""
236+
hist = cv2.calcHist([image], [0], None, [256], [0, 256])
237+
hist /= hist.sum()
238+
# Add eps to avoid log(0)
239+
return -np.sum(hist * np.log2(hist + np.finfo(float).eps))

giskard_vision/core/detectors/metadata_scan_detector.py

+3-8
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,8 @@
44
import numpy as np
55
import pandas as pd
66

7-
from giskard_vision.core.detectors.base import (
8-
DetectorVisionBase,
9-
IssueGroup,
10-
ScanResult,
11-
)
7+
from giskard_vision.core.dataloaders.base import PerformanceIssueMeta
8+
from giskard_vision.core.detectors.base import DetectorVisionBase, ScanResult
129
from giskard_vision.core.tests.base import MetricBase
1310
from giskard_vision.utils.errors import GiskardImportError
1411

@@ -46,9 +43,7 @@ class MetaDataScanDetector(DetectorVisionBase):
4643
metric: MetricBase = None
4744
metric_type: str = None
4845
metric_direction: str = "better_lower"
49-
issue_group = IssueGroup(
50-
name="Performance", description="The data are filtered by metadata to detect performance issues."
51-
)
46+
issue_group = PerformanceIssueMeta
5247

5348
def __init__(self) -> None:
5449
super().__init__()

giskard_vision/core/models/hf_pipeline.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ def __init__(
1919
"""init method that accepts a model object, number of landmarks and dimensions
2020
2121
Args:
22-
model_id (str): Hugging Face model ID
23-
name (Optional[str]): name of the model
24-
pipeline_task (HFPipelineTask): HuggingFace pipeline task
22+
model_id (str): Hugging Face model ID.
23+
name (Optional[str]): name of the model.
24+
pipeline_task (HFPipelineTask): HuggingFace pipeline task.
2525
2626
Raises:
2727
GiskardImportError: If there are missing Hugging Face dependencies.

0 commit comments

Comments
 (0)