Skip to content
Snippets Groups Projects
Commit 6fd482d0 authored by André Anjos's avatar André Anjos :speech_balloon: Committed by Daniel CARRON
Browse files

[pyproject,conda] Add scikit-image dependence

parent 5a314b5c
No related branches found
No related tags found
1 merge request!12Adds grad-cam support on classifiers
...@@ -30,6 +30,7 @@ requirements: ...@@ -30,6 +30,7 @@ requirements:
- pillow {{ pillow }} - pillow {{ pillow }}
- psutil {{ psutil }} - psutil {{ psutil }}
- pytorch {{ pytorch }} - pytorch {{ pytorch }}
- scikit-image {{ scikit_image }}
- scikit-learn {{ scikit_learn }} - scikit-learn {{ scikit_learn }}
- scipy {{ scipy }} - scipy {{ scipy }}
- tabulate {{ tabulate }} - tabulate {{ tabulate }}
...@@ -46,6 +47,7 @@ requirements: ...@@ -46,6 +47,7 @@ requirements:
- {{ pin_compatible('pillow') }} - {{ pin_compatible('pillow') }}
- {{ pin_compatible('psutil') }} - {{ pin_compatible('psutil') }}
- {{ pin_compatible('pytorch') }} - {{ pin_compatible('pytorch') }}
- {{ pin_compatible('scikit-image') }}
- {{ pin_compatible('scikit-learn') }} - {{ pin_compatible('scikit-learn') }}
- {{ pin_compatible('scipy') }} - {{ pin_compatible('scipy') }}
- {{ pin_compatible('tabulate') }} - {{ pin_compatible('tabulate') }}
......
...@@ -31,6 +31,7 @@ dependencies = [ ...@@ -31,6 +31,7 @@ dependencies = [
"click", "click",
"numpy", "numpy",
"scipy", "scipy",
"scikit-image",
"scikit-learn", "scikit-learn",
"tqdm", "tqdm",
"psutil", "psutil",
......
...@@ -6,10 +6,10 @@ import logging ...@@ -6,10 +6,10 @@ import logging
import pathlib import pathlib
import typing import typing
import cv2
import lightning.pytorch import lightning.pytorch
import numpy import numpy
import numpy.typing import numpy.typing
import skimage.measure
import torch import torch
import torchvision.ops import torchvision.ops
...@@ -69,14 +69,8 @@ def _ordered_connected_components( ...@@ -69,14 +69,8 @@ def _ordered_connected_components(
if not numpy.any(thresholded_mask): if not numpy.any(thresholded_mask):
return [] return []
# opencv implementation: labelled, n = skimage.measure.label(thresholded_mask, return_num=True) # type: ignore
n, labelled = cv2.connectedComponents(thresholded_mask, connectivity=8) retval = [labelled == k for k in range(1, n + 1)]
retval = [labelled == k for k in range(1, n)]
# scikit-image implementation
# import skimage.measure
# labelled, n = skimage.measure.label(thresholded_mask, return_num=True)
# retval = [labelled == k for k in range(1, n+1)]
return sorted(retval, key=lambda x: x.sum(), reverse=True) return sorted(retval, key=lambda x: x.sum(), reverse=True)
...@@ -138,7 +132,7 @@ def _compute_max_iou_and_ioda( ...@@ -138,7 +132,7 @@ def _compute_max_iou_and_ioda(
return iou, ioda return iou, ioda
def get_largest_bounding_boxes( def _get_largest_bounding_boxes(
saliency_map: typing.Sequence[typing.Sequence[float]] saliency_map: typing.Sequence[typing.Sequence[float]]
| numpy.typing.NDArray[numpy.double], | numpy.typing.NDArray[numpy.double],
n: int, n: int,
...@@ -269,7 +263,7 @@ def _compute_proportional_energy( ...@@ -269,7 +263,7 @@ def _compute_proportional_energy(
def _process_sample( def _process_sample(
gt_bboxes: BoundingBoxes, gt_bboxes: BoundingBoxes,
saliency_map: numpy.typing.NDArray[numpy.double], saliency_map: numpy.typing.NDArray[numpy.double],
) -> tuple[float, float, float, float, tuple[int, int, int, int]]: ) -> tuple[float, float]:
"""Calculates the metrics for a single sample. """Calculates the metrics for a single sample.
Parameters Parameters
...@@ -289,13 +283,13 @@ def _process_sample( ...@@ -289,13 +283,13 @@ def _process_sample(
* Largest detected bounding box * Largest detected bounding box
""" """
largest_bbox = get_largest_bounding_boxes(saliency_map, n=1, threshold=0.2) # largest_bbox = _get_largest_bounding_boxes(saliency_map, n=1, threshold=0.2)
detected_box = ( # detected_box = (
largest_bbox[0] if largest_bbox else BoundingBox(-1, 0, 0, 0, 0) # largest_bbox[0] if largest_bbox else BoundingBox(-1, 0, 0, 0, 0)
) # )
#
# Calculate localization metrics # # Calculate localization metrics
iou, ioda = _compute_max_iou_and_ioda(detected_box, gt_bboxes) # iou, ioda = _compute_max_iou_and_ioda(detected_box, gt_bboxes)
# The binary_mask will be ON/True where the gt boxes are located # The binary_mask will be ON/True where the gt boxes are located
binary_mask = numpy.zeros_like(saliency_map, dtype=numpy.bool_) binary_mask = numpy.zeros_like(saliency_map, dtype=numpy.bool_)
...@@ -306,16 +300,16 @@ def _process_sample( ...@@ -306,16 +300,16 @@ def _process_sample(
] = True ] = True
return ( return (
iou, # iou,
ioda, # ioda,
_compute_proportional_energy(saliency_map, binary_mask), _compute_proportional_energy(saliency_map, binary_mask),
_compute_avg_saliency_focus(saliency_map, binary_mask), _compute_avg_saliency_focus(saliency_map, binary_mask),
( # (
detected_box.xmin, # detected_box.xmin,
detected_box.ymin, # detected_box.ymin,
detected_box.width, # detected_box.width,
detected_box.height, # detected_box.height,
), # ),
) )
...@@ -348,11 +342,8 @@ def run( ...@@ -348,11 +342,8 @@ def run(
* Sample name (str) * Sample name (str)
* Sample target class (int) * Sample target class (int)
* IoU (float)
* IoDA (float)
* Proportional energy (float) * Proportional energy (float)
* Average saliency focus (float) * Average saliency focus (float)
* Largest detected bounding box (x, y, width, height) (4 x int)
""" """
retval: dict[str, list[typing.Any]] = {} retval: dict[str, list[typing.Any]] = {}
......
...@@ -94,7 +94,7 @@ def interpretability( ...@@ -94,7 +94,7 @@ def interpretability(
.. note:: .. note::
For obvious reasons, this evaluation is limited to databases that For obvious reasons, this evaluation is limited to datasets that
contain built-in annotations which corroborate classification. contain built-in annotations which corroborate classification.
...@@ -102,11 +102,6 @@ def interpretability( ...@@ -102,11 +102,6 @@ def interpretability(
that resembles the original datamodule, with added information containing that resembles the original datamodule, with added information containing
the following measures, for each sample: the following measures, for each sample:
* IoU: The intersection of the (thresholded) saliency maps with
the annotation the most overlaps, over the union of both areas.
* IoDA: The intersection of the (thresholded) saliency maps with
the annotation that most overlaps, over area of (thresholded) saliency
maps.
* Proportional Energy: A measure that compares (UNthresholed) saliency maps * Proportional Energy: A measure that compares (UNthresholed) saliency maps
with annotations (based on [SCORECAM-2020]_). It estimates how much with annotations (based on [SCORECAM-2020]_). It estimates how much
activation lies within the ground truth boxes compared to the total sum activation lies within the ground truth boxes compared to the total sum
...@@ -115,21 +110,6 @@ def interpretability( ...@@ -115,21 +110,6 @@ def interpretability(
boxes area is covered by the activations. It is similar to the boxes area is covered by the activations. It is similar to the
proportional energy measure in the sense it does not need explicit proportional energy measure in the sense it does not need explicit
thresholding. thresholding.
.. important::
The thresholding algorithm used to evaluate IoU and IoDA measures is
based on the process done by the original CAM paper [GRADCAM-2015]_. It
keeps all points from the saliency map that are above the 20% of its
maximum value.
It then calculates a **single** bounding box for largest connected
component. This bounding box represents detected elements on the
original sample that corroborate the classification outcome.
IoU and IoDA are only evaluated for a single ground-truth bounding box
per sample (the one with the highest overlap). Any other bounding box
marked on the sample is ignored in the present implementation.
""" """
import json import json
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment