Skip to content

Commit

Permalink
Merge pull request #162 from XAITK/update-to-v0.9.1
Browse files Browse the repository at this point in the history
Update to v0.9.1
  • Loading branch information
bjrichardwebster authored Nov 14, 2024
2 parents 60a510f + 22f42b4 commit 5da9fd4
Show file tree
Hide file tree
Showing 9 changed files with 143 additions and 54 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
[![badge-unittests](https://github.com/xaitk/xaitk-saliency/actions/workflows/ci-unittests.yml/badge.svg)](https://github.com/XAITK/xaitk-saliency/actions/workflows/ci-unittests.yml)
[![badge-notebooks](https://github.com/xaitk/xaitk-saliency/actions/workflows/ci-example-notebooks.yml/badge.svg)](https://github.com/XAITK/xaitk-saliency/actions/workflows/ci-example-notebooks.yml)
[![codecov](https://codecov.io/gh/XAITK/xaitk-saliency/branch/master/graph/badge.svg?token=VHRNXYCNCG)](https://codecov.io/gh/XAITK/xaitk-saliency)
[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/XAITK/xaitk-saliency.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/XAITK/xaitk-saliency/context:python)

# XAITK - Saliency
The `xaitk-saliency` package is an open source, Explainable AI (XAI) framework
Expand Down
1 change: 1 addition & 0 deletions docs/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ Release Notes
release_notes/v0.8.2
release_notes/v0.8.3
release_notes/v0.9.0
release_notes/v0.9.1
20 changes: 20 additions & 0 deletions docs/release_notes/v0.9.1.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
v0.9.1
======

Fixed a bug where if no detections were found in an image, then the generator would fail.

Updates / New Features
----------------------

Documentation

* Removed a deprecated badge from the README.

Implementations

* Added a check to exit early if no detections were found in `PerturbationOcclusion`

* Added a check to exit early if no saliency maps were generated in `GenerateObjectDetectorBlackboxSaliency`

Fixes
-----
14 changes: 14 additions & 0 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ name = "xaitk_saliency"
# REMEMBER: `distutils.version.*Version` types can be used to compare versions
# from strings like this.
# This package prefers to use the strict numbering standard when possible.
version = "0.9.0"
version = "0.9.1"
description = """\
Visual saliency map generation interfaces and baseline implementations \
for explainable AI."""
Expand All @@ -25,7 +25,6 @@ classifiers = [
'Operating System :: MacOS :: MacOS X',
'Operating System :: Unix',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
Expand Down
113 changes: 62 additions & 51 deletions tests/impls/gen_object_detector_blackbox_sal/test_occlusion_based.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
from xaitk_saliency.utils.masking import occlude_image_batch


def _perturb(ref_image: np.ndarray) -> np.ndarray:
return np.ones((6, *ref_image.shape[:2]), dtype=bool)


class TestPerturbationOcclusion:

def teardown(self) -> None:
Expand Down Expand Up @@ -62,70 +66,40 @@ def test_generate_success(self) -> None:
Test successfully invoking _generate().
"""

class StubPI (PerturbImage):
"""
Stub perturber that returns masks of ones.
"""

def perturb(self, ref_image: np.ndarray) -> np.ndarray:
return np.ones((6, *ref_image.shape[:2]), dtype=bool)

get_config = None # type: ignore

class StubGen (GenerateDetectorProposalSaliency):
"""
Stub saliency generator that returns zeros with correct shape.
"""

def generate(
self,
ref_dets: np.ndarray,
pert_dets: np.ndarray,
pert_masks: np.ndarray
) -> np.ndarray:
return np.zeros((ref_dets.shape[0], *pert_masks.shape[1:]), dtype=np.float16)

get_config = None # type: ignore

class StubDetector (DetectImageObjects):
"""
Stub object detector that returns known detections.
"""

def detect_objects(
self,
img_iter: Iterable[np.ndarray]
) -> Iterable[Iterable[Tuple[AxisAlignedBoundingBox, Dict[Hashable, float]]]]:
for i, _ in enumerate(img_iter):
# Return different number of detections for each image to
# test padding functinality
yield [(
AxisAlignedBoundingBox((0, 0), (1, 1)),
{'class0': 0.0, 'class1': 0.9}
) for _ in range(i)]

get_config = None # type: ignore

test_pi = StubPI()
test_gen = StubGen()
test_detector = StubDetector()
def detect_objects(
img_iter: Iterable[np.ndarray]
) -> Iterable[Iterable[Tuple[AxisAlignedBoundingBox, Dict[Hashable, float]]]]:
for i, _ in enumerate(img_iter):
# Return different number of detections for each image to
# test padding functinality
yield [(
AxisAlignedBoundingBox((0, 0), (1, 1)),
{'class0': 0.0, 'class1': 0.9}
) for _ in range(i)]

test_image = np.ones((64, 64, 3), dtype=np.uint8)

test_bboxes = np.ones((3, 4))
test_scores = np.ones((3, 2))

m_perturb = mock.Mock(spec=PerturbImage)
m_perturb.return_value = _perturb(test_image)
m_gen = mock.Mock(spec=GenerateDetectorProposalSaliency)
m_gen.return_value = np.zeros((3, 64, 64))
m_detector = mock.Mock(spec=DetectImageObjects)
m_detector.detect_objects = detect_objects

# Call with default fill
with mock.patch(
'xaitk_saliency.impls.gen_object_detector_blackbox_sal.occlusion_based.occlude_image_batch',
wraps=occlude_image_batch
) as m_occ_img:
inst = PerturbationOcclusion(test_pi, test_gen)
inst = PerturbationOcclusion(m_perturb, m_gen)
test_result = inst._generate(
test_image,
test_bboxes,
test_scores,
test_detector
m_detector,
)

assert test_result.shape == (3, 64, 64)
Expand All @@ -143,13 +117,13 @@ def detect_objects(
'xaitk_saliency.impls.gen_object_detector_blackbox_sal.occlusion_based.occlude_image_batch',
wraps=occlude_image_batch
) as m_occ_img:
inst = PerturbationOcclusion(test_pi, test_gen)
inst = PerturbationOcclusion(m_perturb, m_gen)
inst.fill = test_fill
test_result = inst._generate(
test_image,
test_bboxes,
test_scores,
test_detector
m_detector,
)

assert test_result.shape == (3, 64, 64)
Expand All @@ -160,3 +134,40 @@ def detect_objects(
m_kwargs = m_occ_img.call_args[-1]
assert "fill" in m_kwargs
assert m_kwargs['fill'] == test_fill

def test_empty_detections(self) -> None:
"""
Test invoking _generate() with empty detections.
"""

def detect_objects(
img_iter: Iterable[np.ndarray]
) -> Iterable[Iterable[Tuple[AxisAlignedBoundingBox, Dict[Hashable, float]]]]:
for i, _ in enumerate(img_iter):
# Return 0 detections for each image
yield []

m_detector = mock.Mock(spec=DetectImageObjects)
m_detector.detect_objects = detect_objects

test_image = np.ones((64, 64, 3), dtype=np.uint8)

test_bboxes = np.ones((3, 4))
test_scores = np.ones((3, 2))

m_perturb = mock.Mock(spec=PerturbImage)
m_perturb.return_value = _perturb(test_image)
m_gen = mock.Mock(spec=GenerateDetectorProposalSaliency)
m_gen.return_value = np.zeros((3, 64, 64))
m_detector = mock.Mock(spec=DetectImageObjects)
m_detector.detect_objects = detect_objects

inst = PerturbationOcclusion(m_perturb, m_gen)
test_result = inst._generate(
test_image,
test_bboxes,
test_scores,
m_detector,
)

assert len(test_result) == 0
35 changes: 35 additions & 0 deletions tests/interfaces/test_gen_object_detector_blackbox_sal.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,3 +301,38 @@ def test_call_alias() -> None:
None # no objectness passed
)
assert test_ret == expected_return


def test_return_empty_map() -> None:
"""
Test that an empty array of maps is returned properly
"""
m_impl = mock.Mock(spec=GenerateObjectDetectorBlackboxSaliency)
m_detector = mock.Mock(spec=DetectImageObjects)

# test reference detections inputs with matching lengths
test_bboxes = np.ones((5, 4), dtype=float)
test_scores = np.ones((5, 3), dtype=float)

# 2-channel image as just HxW should work
test_image = np.ones((256, 256), dtype=np.uint8)

expected_return = np.array([])
m_impl._generate.return_value = expected_return

test_ret = GenerateObjectDetectorBlackboxSaliency.generate(
m_impl,
test_image,
test_bboxes,
test_scores,
m_detector,
)

m_impl._generate.assert_called_with(
test_image,
test_bboxes,
test_scores,
m_detector,
None # no objectness passed
)
assert len(test_ret) == 0
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ def _generate(

pert_dets_mat = _dets_to_formatted_mat(pert_dets)

if pert_dets_mat.shape[1] == 0:
return np.array([])

return self._generator(
ref_dets_mat,
pert_dets_mat,
Expand Down
7 changes: 7 additions & 0 deletions xaitk_saliency/interfaces/gen_object_detector_blackbox_sal.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import logging

import numpy as np
import abc
from typing import Optional
Expand All @@ -6,6 +8,7 @@
from smqtk_detection import DetectImageObjects

from xaitk_saliency.exceptions import ShapeMismatchError
logger = logging.getLogger(__name__)


class GenerateObjectDetectorBlackboxSaliency (Plugfigurable):
Expand Down Expand Up @@ -144,6 +147,10 @@ def generate(
objectness,
)

if len(output) == 0:
logging.info("No detections found for image. Check DetectImageObjects and saliency configuation")
return output

# Check that the saliency heatmaps' shape matches the reference image.
if output.shape[1:] != ref_image.shape[:2]:
raise ShapeMismatchError(
Expand Down

0 comments on commit 5da9fd4

Please sign in to comment.