Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
05a4cc2
working on perturbation detectors
rabah-khalek Aug 8, 2024
cf2a8c4
Refactor HF ppl model to convert numpy array to PIL image
Inokinoki Aug 5, 2024
7ba35b6
Allow to set global mode for an HF ppl model for PIL conversion
Inokinoki Aug 5, 2024
6c3ffc9
mode switch in hf models
rabah-khalek Aug 8, 2024
7e14795
supporting gray scale
rabah-khalek Aug 8, 2024
f3d8e32
Merge branch 'main' into perturbation-detectors
rabah-khalek Aug 8, 2024
c30dade
Merge branch 'main' into perturbation-detectors
rabah-khalek Aug 10, 2024
98baa6d
added missing predict_rgb_image
rabah-khalek Aug 12, 2024
a9fa22f
ensuring backward compatibility with predict_image
rabah-khalek Aug 12, 2024
e9198ce
updating detectors
rabah-khalek Aug 12, 2024
4dd46b4
Adding noise perturbation detector with Gaussian noise (#52)
bmalezieux Aug 12, 2024
e547d4d
updating detectors
rabah-khalek Aug 12, 2024
a44399d
refactoring detectors
rabah-khalek Aug 13, 2024
fe26272
small updates
rabah-khalek Aug 13, 2024
c359c9c
refactored spec setting
rabah-khalek Aug 13, 2024
6dca401
fixed import in object_detection dataloader
rabah-khalek Aug 13, 2024
99d98dd
renaming pert detectors
rabah-khalek Aug 13, 2024
6601ecb
Merge branch 'main' into perturbation-detectors
rabah-khalek Aug 13, 2024
14de1fa
Merge branch 'perturbation-detectors' into refactoring-detectors
rabah-khalek Aug 13, 2024
182731c
fixed import
rabah-khalek Aug 13, 2024
6ba1994
fixing get_scan_results args
rabah-khalek Aug 13, 2024
ffbb425
Merge pull request #53 from Giskard-AI/refactoring-detectors
rabah-khalek Aug 13, 2024
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
working on perturbation detectors
  • Loading branch information
rabah-khalek committed Aug 8, 2024
commit 05a4cc2170c5521d281838a0b81d79bd29bd473d
9 changes: 9 additions & 0 deletions giskard_vision/core/detectors/metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from giskard_vision.image_classification.tests.performance import Accuracy
from giskard_vision.landmark_detection.tests.performance import NMEMean
from giskard_vision.object_detection.tests.performance import IoU

detector_metrics = {
"image_classification": Accuracy,
"landmark": NMEMean,
"object_detection": IoU,
}
114 changes: 114 additions & 0 deletions giskard_vision/core/detectors/perturbation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import os
from abc import abstractmethod
from pathlib import Path
from typing import Any, Sequence

import cv2

from giskard_vision.core.dataloaders.wrappers import FilteredDataLoader
from giskard_vision.core.detectors.base import (
DetectorVisionBase,
IssueGroup,
ScanResult,
)
from giskard_vision.landmark_detection.tests.base import TestDiff
from giskard_vision.utils.errors import GiskardImportError

from .metrics import detector_metrics

Cropping = IssueGroup(
"Cropping", description="Cropping involves evaluating the landmark detection model on specific face areas."
)

Ethical = IssueGroup(
"Ethical",
description="The data are filtered by ethnicity to detect ethical biases in the landmark detection model.",
)

Pose = IssueGroup(
"Head Pose",
description="The data are filtered by head pose to detect biases in the landmark detection model.",
)

Robustness = IssueGroup(
"Robustness",
description="Images from the dataset are blurred, recolored and resized to test the robustness of the model to transformations.",
)


class PerturbationBaseDetector(DetectorVisionBase):
"""
Abstract class for Landmark Detection Detectors

Methods:
get_dataloaders(dataset: Any) -> Sequence[Any]:
Abstract method that returns a list of dataloaders corresponding to
slices or transformations

get_results(model: Any, dataset: Any) -> Sequence[ScanResult]:
Returns a list of ScanResult containing the evaluation results

get_scan_result(self, test_result) -> ScanResult:
Convert TestResult to ScanResult
"""

@abstractmethod
def get_dataloaders(self, dataset: Any) -> Sequence[Any]: ...

def get_results(self, model: Any, dataset: Any) -> Sequence[ScanResult]:
dataloaders = self.get_dataloaders(dataset)

results = []
for dl in dataloaders:
test_result = TestDiff(metric=detector_metrics[model.model_type], threshold=1).run(
model=model,
dataloader=dl,
dataloader_ref=dataset,
)

# Save example images from dataloader and dataset
current_path = str(Path())
os.makedirs(f"{current_path}/examples_images", exist_ok=True)
filename_examples = []

index_worst = 0 if test_result.indexes_examples is None else test_result.indexes_examples[0]

if isinstance(dl, FilteredDataLoader):
filename_example_dataloader_ref = str(Path() / "examples_images" / f"{dataset.name}_{index_worst}.png")
cv2.imwrite(
filename_example_dataloader_ref, cv2.resize(dataset[index_worst][0][0], (0, 0), fx=0.3, fy=0.3)
)
filename_examples.append(filename_example_dataloader_ref)

filename_example_dataloader = str(Path() / "examples_images" / f"{dl.name}_{index_worst}.png")
cv2.imwrite(filename_example_dataloader, cv2.resize(dl[index_worst][0][0], (0, 0), fx=0.3, fy=0.3))
filename_examples.append(filename_example_dataloader)
results.append(self.get_scan_result(test_result, filename_examples, dl.name, len(dl)))

return results

def get_scan_result(self, test_result, filename_examples, name, size_data) -> ScanResult:
try:
from giskard.scanner.issues import IssueLevel
except (ImportError, ModuleNotFoundError) as e:
raise GiskardImportError(["giskard"]) from e

relative_delta = (test_result.metric_value_test - test_result.metric_value_ref) / test_result.metric_value_ref
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use what we implemented in the meta data detector for this function, for instance here the metric can be absolute or relative

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

taken care of in #53


if relative_delta > self.issue_level_threshold + self.deviation_threshold:
issue_level = IssueLevel.MAJOR
elif relative_delta > self.issue_level_threshold:
issue_level = IssueLevel.MEDIUM
else:
issue_level = IssueLevel.MINOR

return ScanResult(
name=name,
metric_name=test_result.metric_name,
metric_value=test_result.metric_value_test,
metric_reference_value=test_result.metric_value_ref,
issue_level=issue_level,
slice_size=size_data,
filename_examples=filename_examples,
relative_delta=relative_delta,
)
24 changes: 24 additions & 0 deletions giskard_vision/core/detectors/transformation_blurring_detector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from giskard_vision.core.dataloaders.wrappers import BlurredDataLoader

from ...core.detectors.decorator import maybe_detector
from .perturbation import PerturbationBaseDetector, Robustness


@maybe_detector("blurring", tags=["vision", "robustness", "image_classification", "landmark", "object_detection"])
class TransformationBlurringDetectorLandmark(PerturbationBaseDetector):
"""
Detector that evaluates models performance on blurred images
"""

issue_group = Robustness

def __init__(self, kernel_size=(11, 11), sigma=(3, 3)):
self.kernel_size = kernel_size
self.sigma = sigma

def get_dataloaders(self, dataset):
dl = BlurredDataLoader(dataset, self.kernel_size, self.sigma)

dls = [dl]

return dls
20 changes: 20 additions & 0 deletions giskard_vision/core/detectors/transformation_color_detector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from giskard_vision.core.dataloaders.wrappers import ColoredDataLoader

from ...core.detectors.decorator import maybe_detector
from .perturbation import PerturbationBaseDetector, Robustness


@maybe_detector("coloring", tags=["vision", "robustness", "image_classification", "landmark", "object_detection"])
class TransformationColorDetectorLandmark(PerturbationBaseDetector):
"""
Detector that evaluates models performance depending on images in grayscale
"""

issue_group = Robustness

def get_dataloaders(self, dataset):
dl = ColoredDataLoader(dataset)

dls = [dl]

return dls
2 changes: 1 addition & 1 deletion giskard_vision/core/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def predict_batch(self, idx: int, images: List[np.ndarray]) -> np.ndarray:
except Exception:
res.append(None)
logger.warning(
f"{self.__class__.__name__}: Face not detected in processed image of batch {idx} and index {i}."
f"{self.__class__.__name__}: Prediction failed in processed image of batch {idx} and index {i}."
)
# logger.warning(e) # OpenCV's exception is very misleading

Expand Down
6 changes: 3 additions & 3 deletions giskard_vision/image_classification/dataloaders/loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def get_image(self, idx: int) -> np.ndarray:
Returns:
np.ndarray: The image data.
"""
return self.get_row(idx)["image"]
return np.array(self.get_row(idx)["image"])

def get_labels(self, idx: int) -> Optional[np.ndarray]:
"""
Expand Down Expand Up @@ -124,7 +124,7 @@ def get_image(self, idx: int) -> Any:
Returns:
np.ndarray: The image data.
"""
return self.ds[idx]["image"]
return np.array(self.ds[idx]["image"])

def get_labels(self, idx: int) -> Optional[np.ndarray]:
"""
Expand Down Expand Up @@ -201,7 +201,7 @@ def get_image(self, idx: int) -> Any:
Returns:
np.ndarray: The image data.
"""
return self.ds[idx]["img"]
return np.array(self.ds[idx]["img"])

def get_labels(self, idx: int) -> Optional[np.ndarray]:
"""
Expand Down