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

[scripts] Streamline saliency map generation and analysis

parent 1b311779
No related branches found
No related tags found
1 merge request!12Adds grad-cam support on classifiers
...@@ -592,7 +592,8 @@ class ConcatDataModule(lightning.LightningDataModule): ...@@ -592,7 +592,8 @@ class ConcatDataModule(lightning.LightningDataModule):
] = multiprocessing.get_context("spawn") ] = multiprocessing.get_context("spawn")
# keep workers hanging around if we have multiple # keep workers hanging around if we have multiple
self._dataloader_multiproc["persistent_workers"] = True if value >= 0:
self._dataloader_multiproc["persistent_workers"] = True
@property @property
def model_transforms(self) -> list[Transform] | None: def model_transforms(self) -> list[Transform] | None:
......
...@@ -101,7 +101,7 @@ def run( ...@@ -101,7 +101,7 @@ def run(
model: lightning.pytorch.LightningModule, model: lightning.pytorch.LightningModule,
datamodule: lightning.pytorch.LightningDataModule, datamodule: lightning.pytorch.LightningDataModule,
device_manager: DeviceManager, device_manager: DeviceManager,
saliency_map_algorithms: typing.Sequence[SaliencyMapAlgorithm], saliency_map_algorithm: SaliencyMapAlgorithm,
target_class: typing.Literal["highest", "all"], target_class: typing.Literal["highest", "all"],
positive_only: bool, positive_only: bool,
output_folder: pathlib.Path, output_folder: pathlib.Path,
...@@ -119,8 +119,8 @@ def run( ...@@ -119,8 +119,8 @@ def run(
An internal device representation, to be used for training and An internal device representation, to be used for training and
validation. This representation can be converted into a pytorch device validation. This representation can be converted into a pytorch device
or a torch lightning accelerator setup. or a torch lightning accelerator setup.
saliency_map_algorithms saliency_map_algorithm
The algorithms for saliency map estimation to use. The algorithm to use for saliency map estimation.
target_class target_class
(Use only with multi-label models) Which class to target for CAM (Use only with multi-label models) Which class to target for CAM
calculation. Can be either set to "all" or "highest". "highest" is calculation. Can be either set to "all" or "highest". "highest" is
...@@ -140,7 +140,7 @@ def run( ...@@ -140,7 +140,7 @@ def run(
from ...models.pasa import Pasa from ...models.pasa import Pasa
if isinstance(model, Pasa): if isinstance(model, Pasa):
if "fullgrad" in saliency_map_algorithms: if saliency_map_algorithm == "fullgrad":
raise ValueError( raise ValueError(
"Fullgrad saliency map algorithm is not supported for the " "Fullgrad saliency map algorithm is not supported for the "
"Pasa model." "Pasa model."
...@@ -160,71 +160,63 @@ def run( ...@@ -160,71 +160,63 @@ def run(
model = model.to(device) model = model.to(device)
model.eval() model.eval()
for algo_type in saliency_map_algorithms: saliency_map_callable = _create_saliency_map_callable(
saliency_map_callable = _create_saliency_map_callable( saliency_map_algorithm,
algo_type, model,
model, target_layers, # type: ignore
target_layers, # type: ignore use_cuda,
use_cuda, )
for k, v in datamodule.predict_dataloader().items():
logger.info(
f"Generating saliency maps for dataset `{k}` via `{saliency_map_algorithm}`..."
) )
for k, v in datamodule.predict_dataloader().items(): for sample in tqdm.tqdm(v, desc="samples", leave=False, disable=None):
logger.info( name = sample[1]["name"][0]
f"Generating saliency maps for dataset `{k}` via `{algo_type}`..." label = sample[1]["label"].item()
image = sample[0].to(
device=device, non_blocking=torch.cuda.is_available()
) )
for sample in tqdm.tqdm( # in binary classification systems, negative labels may be skipped
v, desc="samples", leave=False, disable=None if positive_only and (model.num_classes == 1) and (label == 0):
): continue
name = sample[1]["name"][0]
label = sample[1]["label"].item() # chooses target outputs to generate saliency maps for
image = sample[0].to( if model.num_classes > 1:
device=device, non_blocking=torch.cuda.is_available() if target_class == "all":
) # just blindly generate saliency maps for all outputs
# - make one directory for every target output and lay
# in binary classification systems, negative labels may be skipped # images there like in the original dataset.
if positive_only and (model.num_classes == 1) and (label == 0): for output_num in range(model.num_classes):
continue use_folder = output_folder / str(output_num)
# chooses target outputs to generate saliency maps for
if model.num_classes > 1:
if target_class == "all":
# just blindly generate saliency maps for all outputs
# - make one directory for every target output and lay
# images there like in the original dataset.
for output_num in range(model.num_classes):
use_folder = (
output_folder / algo_type / str(output_num)
)
saliency_map = saliency_map_callable(
input_tensor=image,
targets=[ClassifierOutputTarget(output_num)], # type: ignore
)
_save_saliency_map(use_folder, name, saliency_map) # type: ignore
else:
# pytorch-grad-cam will figure out the output with the
# highest value and produce a saliency map for it - we
# will save it to disk.
use_folder = (
output_folder / algo_type / "highest-output"
)
saliency_map = saliency_map_callable( saliency_map = saliency_map_callable(
input_tensor=image, input_tensor=image,
# setting `targets=None` will set target to the targets=[ClassifierOutputTarget(output_num)], # type: ignore
# maximum output index using
# ClassifierOutputTarget(max_output_index)
targets=None, # type: ignore
) )
_save_saliency_map(use_folder, name, saliency_map) # type: ignore _save_saliency_map(use_folder, name, saliency_map) # type: ignore
else: else:
# binary classification model with a single output - just # pytorch-grad-cam will figure out the output with the
# lay all cams uniformily like the original dataset # highest value and produce a saliency map for it - we
use_folder = output_folder / algo_type # will save it to disk.
use_folder = output_folder / "highest-output"
saliency_map = saliency_map_callable( saliency_map = saliency_map_callable(
input_tensor=image, input_tensor=image,
targets=[ # setting `targets=None` will set target to the
ClassifierOutputTarget(0), # type: ignore # maximum output index using
], # ClassifierOutputTarget(max_output_index)
targets=None, # type: ignore
) )
_save_saliency_map(use_folder, name, saliency_map) # type: ignore _save_saliency_map(use_folder, name, saliency_map) # type: ignore
else:
# binary classification model with a single output - just
# lay all cams uniformily like the original dataset
saliency_map = saliency_map_callable(
input_tensor=image,
targets=[
ClassifierOutputTarget(0), # type: ignore
],
)
_save_saliency_map(output_folder, name, saliency_map) # type: ignore
...@@ -269,7 +269,7 @@ def _compute_proportional_energy( ...@@ -269,7 +269,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, BoundingBox]: ) -> tuple[float, float, float, float, tuple[int, int, int, int]]:
"""Calculates the metrics for a single sample. """Calculates the metrics for a single sample.
Parameters Parameters
...@@ -310,7 +310,12 @@ def _process_sample( ...@@ -310,7 +310,12 @@ def _process_sample(
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, (
detected_box.xmin,
detected_box.ymin,
detected_box.width,
detected_box.height,
),
) )
...@@ -396,7 +401,7 @@ def run( ...@@ -396,7 +401,7 @@ def run(
name, name,
label, label,
*_process_sample( *_process_sample(
bboxes, bboxes[0],
numpy.load( numpy.load(
input_folder input_folder
/ pathlib.Path(name).with_suffix(".npy") / pathlib.Path(name).with_suffix(".npy")
......
...@@ -111,11 +111,6 @@ def experiment( ...@@ -111,11 +111,6 @@ def experiment(
logger.info("Started predicting") logger.info("Started predicting")
from ..utils.checkpointer import get_checkpoint_to_run_inference
model_file = get_checkpoint_to_run_inference(train_output_folder)
logger.info(f"Found `{str(model_file)}`. Continuing...")
from .predict import predict from .predict import predict
predictions_output = output_folder / "predictions.json" predictions_output = output_folder / "predictions.json"
...@@ -126,7 +121,7 @@ def experiment( ...@@ -126,7 +121,7 @@ def experiment(
model=model, model=model,
datamodule=datamodule, datamodule=datamodule,
device=device, device=device,
weight=model_file, weight=train_output_folder,
batch_size=batch_size, batch_size=batch_size,
parallel=parallel, parallel=parallel,
) )
......
...@@ -23,13 +23,13 @@ logger = setup(__name__.split(".")[0], format="%(levelname)s: %(message)s") ...@@ -23,13 +23,13 @@ logger = setup(__name__.split(".")[0], format="%(levelname)s: %(message)s")
.. code:: sh .. code:: sh
ptbench predict -vv pasa montgomery --weight=path/to/model.ckpt --output=path/to/predictions.json ptbench predict -vv pasa montgomery --weight=path/to/model-at-lowest-validation-loss.ckpt --output=path/to/predictions.json
2. Enables multi-processing data loading with 6 processes: 2. Enables multi-processing data loading with 6 processes:
.. code:: sh .. code:: sh
ptbench predict -vv pasa montgomery --parallel=6 --weight=path/to/model.ckpt --output=path/to/predictions.json ptbench predict -vv pasa montgomery --parallel=6 --weight=path/to/model-at-lowest-validation-loss.ckpt --output=path/to/predictions.json
""", """,
) )
...@@ -88,10 +88,18 @@ logger = setup(__name__.split(".")[0], format="%(levelname)s: %(message)s") ...@@ -88,10 +88,18 @@ logger = setup(__name__.split(".")[0], format="%(levelname)s: %(message)s")
"--weight", "--weight",
"-w", "-w",
help="""Path or URL to pretrained model file (`.ckpt` extension), help="""Path or URL to pretrained model file (`.ckpt` extension),
corresponding to the architecture set with `--model`.""", corresponding to the architecture set with `--model`. Optionally, you may
also pass a directory containing the result of a training session, in which
case either the best (lowest validation) or latest model will be loaded.""",
required=True, required=True,
cls=ResourceOption, cls=ResourceOption,
type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True), type=click.Path(
exists=True,
file_okay=True,
dir_okay=True,
readable=True,
path_type=pathlib.Path,
),
) )
@click.option( @click.option(
"--parallel", "--parallel",
...@@ -125,6 +133,7 @@ def predict( ...@@ -125,6 +133,7 @@ def predict(
from ..engine.device import DeviceManager from ..engine.device import DeviceManager
from ..engine.predictor import run from ..engine.predictor import run
from ..utils.checkpointer import get_checkpoint_to_run_inference
datamodule.set_chunk_size(batch_size, 1) datamodule.set_chunk_size(batch_size, 1)
datamodule.parallel = parallel datamodule.parallel = parallel
...@@ -133,6 +142,9 @@ def predict( ...@@ -133,6 +142,9 @@ def predict(
datamodule.prepare_data() datamodule.prepare_data()
datamodule.setup(stage="predict") datamodule.setup(stage="predict")
if weight.is_dir():
weight = get_checkpoint_to_run_inference(weight)
logger.info(f"Loading checkpoint from `{weight}`...") logger.info(f"Loading checkpoint from `{weight}`...")
model = type(model).load_from_checkpoint(weight, strict=False) model = type(model).load_from_checkpoint(weight, strict=False)
......
...@@ -25,7 +25,7 @@ logger = setup(__name__.split(".")[0], format="%(levelname)s: %(message)s") ...@@ -25,7 +25,7 @@ logger = setup(__name__.split(".")[0], format="%(levelname)s: %(message)s")
.. code:: sh .. code:: sh
ptbench saliency completeness -vv pasa tbx11k-v1-healthy-vs-atb --device="cuda" --weight=path/to/model-at-lowest-validation-loss.ckpt --output-folder=path/to/completeness-scores/ ptbench saliency completeness -vv pasa tbx11k-v1-healthy-vs-atb --device="cuda" --weight=path/to/model-at-lowest-validation-loss.ckpt --output-json=path/to/completeness-scores.json
""", """,
) )
...@@ -49,18 +49,17 @@ logger = setup(__name__.split(".")[0], format="%(levelname)s: %(message)s") ...@@ -49,18 +49,17 @@ logger = setup(__name__.split(".")[0], format="%(levelname)s: %(message)s")
cls=ResourceOption, cls=ResourceOption,
) )
@click.option( @click.option(
"--output-folder", "--output-json",
"-o", "-o",
help="Path where to store saliency maps (created if does not exist)", help="""Path where to store the output JSON file containing all
measures.""",
required=True, required=True,
type=click.Path( type=click.Path(
exists=False, file_okay=True,
file_okay=False, dir_okay=False,
dir_okay=True,
writable=True,
path_type=pathlib.Path, path_type=pathlib.Path,
), ),
default="saliency-maps", default="saliency-interpretability.json",
cls=ResourceOption, cls=ResourceOption,
) )
@click.option( @click.option(
...@@ -85,10 +84,18 @@ logger = setup(__name__.split(".")[0], format="%(levelname)s: %(message)s") ...@@ -85,10 +84,18 @@ logger = setup(__name__.split(".")[0], format="%(levelname)s: %(message)s")
"--weight", "--weight",
"-w", "-w",
help="""Path or URL to pretrained model file (`.ckpt` extension), help="""Path or URL to pretrained model file (`.ckpt` extension),
corresponding to the architecture set with `--model`.""", corresponding to the architecture set with `--model`. Optionally, you may
also pass a directory containing the result of a training session, in which
case either the best (lowest validation) or latest model will be loaded.""",
required=True, required=True,
cls=ResourceOption, cls=ResourceOption,
type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True), type=click.Path(
exists=True,
file_okay=True,
dir_okay=True,
readable=True,
path_type=pathlib.Path,
),
) )
@click.option( @click.option(
"--parallel", "--parallel",
...@@ -108,13 +115,11 @@ logger = setup(__name__.split(".")[0], format="%(levelname)s: %(message)s") ...@@ -108,13 +115,11 @@ logger = setup(__name__.split(".")[0], format="%(levelname)s: %(message)s")
@click.option( @click.option(
"--saliency-map-algorithm", "--saliency-map-algorithm",
"-s", "-s",
help="""Saliency map algorithm(s) to be used. Can be called multiple times help="""Saliency map algorithm to be used.""",
with different techniques.""",
type=click.Choice( type=click.Choice(
typing.get_args(SaliencyMapAlgorithm), case_sensitive=False typing.get_args(SaliencyMapAlgorithm), case_sensitive=False
), ),
multiple=True, default="gradcam",
default=["gradcam"],
show_default=True, show_default=True,
cls=ResourceOption, cls=ResourceOption,
) )
...@@ -157,7 +162,7 @@ logger = setup(__name__.split(".")[0], format="%(levelname)s: %(message)s") ...@@ -157,7 +162,7 @@ logger = setup(__name__.split(".")[0], format="%(levelname)s: %(message)s")
def completeness( def completeness(
model, model,
datamodule, datamodule,
output_folder, output_json,
device, device,
cache_samples, cache_samples,
weight, weight,
...@@ -171,7 +176,7 @@ def completeness( ...@@ -171,7 +176,7 @@ def completeness(
"""Evaluates saliency map algorithm completeness using RemOve And Debias """Evaluates saliency map algorithm completeness using RemOve And Debias
(ROAD). (ROAD).
For each selected saliency map algorithm, evaluates the completeness of For the selected saliency map algorithm, evaluates the completeness of
explanations using the RemOve And Debias (ROAD) algorithm. The ROAD explanations using the RemOve And Debias (ROAD) algorithm. The ROAD
algorithm was first described at [ROAD-2022]_. It estimates explainability algorithm was first described at [ROAD-2022]_. It estimates explainability
(in the completeness sense) of saliency mapping algorithms by substituting (in the completeness sense) of saliency mapping algorithms by substituting
...@@ -185,12 +190,9 @@ def completeness( ...@@ -185,12 +190,9 @@ def completeness(
This program outputs a JSON file containing the ROAD evaluations (using This program outputs a JSON file containing the ROAD evaluations (using
most-relevant-first, or MoRF, and least-relevant-first, or LeRF for each most-relevant-first, or MoRF, and least-relevant-first, or LeRF for each
sample in the datamodule, and per saliency-mapping algorithm. Each sample in the datamodule. Values for MoRF and LeRF represent averages by
saliency-mapping algorithm yields a single JSON file with the target removing 20, 40, 60 and 80% of most or least relevant pixels respectively
algorithm name on the ``output-folder``. Values for MoRF and LeRF represent from the image, and averaging results for all these percentiles.
averages by removing 20, 40, 60 and 80% of most or least relevant pixels
respectively from the image, and averaging results for all these
percentiles.
.. note:: .. note::
...@@ -201,9 +203,7 @@ def completeness( ...@@ -201,9 +203,7 @@ def completeness(
from ...engine.device import DeviceManager from ...engine.device import DeviceManager
from ...engine.saliency.completeness import run from ...engine.saliency.completeness import run
from ...utils.checkpointer import get_checkpoint_to_run_inference
logger.info(f"Output folder: {output_folder}")
output_folder.mkdir(parents=True, exist_ok=True)
if device in ("cuda", "mps") and (parallel == 0 or parallel > 1): if device in ("cuda", "mps") and (parallel == 0 or parallel > 1):
raise RuntimeError( raise RuntimeError(
...@@ -226,27 +226,29 @@ def completeness( ...@@ -226,27 +226,29 @@ def completeness(
datamodule.prepare_data() datamodule.prepare_data()
datamodule.setup(stage="predict") datamodule.setup(stage="predict")
logger.info(f"Loading checkpoint from `{weight}`...") if weight.is_dir():
model = model.load_from_checkpoint(weight, strict=False) weight = get_checkpoint_to_run_inference(weight)
for algo in saliency_map_algorithm:
logger.info(
f"Evaluating RemOve And Debias (ROAD) average scores for "
f"algorithm `{algo}` with percentiles "
f"`{', '.join([str(k) for k in percentile])}`..."
)
results = run(
model=model,
datamodule=datamodule,
device_manager=device_manager,
saliency_map_algorithm=algo,
target_class=target_class,
positive_only=positive_only,
percentiles=percentile,
parallel=parallel,
)
output_json = output_folder / (algo + ".json") logger.info(f"Loading checkpoint from `{weight}`...")
with output_json.open("w") as f: model = type(model).load_from_checkpoint(weight, strict=False)
logger.info(f"Saving output file to `{str(output_json)}`...")
json.dump(results, f, indent=2) logger.info(
f"Evaluating RemOve And Debias (ROAD) average scores for "
f"algorithm `{saliency_map_algorithm}` with percentiles "
f"`{', '.join([str(k) for k in percentile])}`..."
)
results = run(
model=model,
datamodule=datamodule,
device_manager=device_manager,
saliency_map_algorithm=saliency_map_algorithm,
target_class=target_class,
positive_only=positive_only,
percentiles=percentile,
parallel=parallel,
)
output_json.parent.mkdir(parents=True, exist_ok=True)
with output_json.open("w") as f:
logger.info(f"Saving output file to `{str(output_json)}`...")
json.dump(results, f, indent=2)
...@@ -87,10 +87,18 @@ logger = setup(__name__.split(".")[0], format="%(levelname)s: %(message)s") ...@@ -87,10 +87,18 @@ logger = setup(__name__.split(".")[0], format="%(levelname)s: %(message)s")
"--weight", "--weight",
"-w", "-w",
help="""Path or URL to pretrained model file (`.ckpt` extension), help="""Path or URL to pretrained model file (`.ckpt` extension),
corresponding to the architecture set with `--model`.""", corresponding to the architecture set with `--model`. Optionally, you may
also pass a directory containing the result of a training session, in which
case either the best (lowest validation) or latest model will be loaded.""",
required=True, required=True,
cls=ResourceOption, cls=ResourceOption,
type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True), type=click.Path(
exists=True,
file_okay=True,
dir_okay=True,
readable=True,
path_type=pathlib.Path,
),
) )
@click.option( @click.option(
"--parallel", "--parallel",
...@@ -108,13 +116,11 @@ logger = setup(__name__.split(".")[0], format="%(levelname)s: %(message)s") ...@@ -108,13 +116,11 @@ logger = setup(__name__.split(".")[0], format="%(levelname)s: %(message)s")
@click.option( @click.option(
"--saliency-map-algorithm", "--saliency-map-algorithm",
"-s", "-s",
help="""Saliency map algorithm(s) to be used. Can be called multiple times help="""Saliency map algorithm to be used.""",
with different techniques.""",
type=click.Choice( type=click.Choice(
typing.get_args(SaliencyMapAlgorithm), case_sensitive=False typing.get_args(SaliencyMapAlgorithm), case_sensitive=False
), ),
multiple=True, default="gradcam",
default=["gradcam"],
show_default=True, show_default=True,
cls=ResourceOption, cls=ResourceOption,
) )
...@@ -165,6 +171,7 @@ def generate( ...@@ -165,6 +171,7 @@ def generate(
from ...engine.device import DeviceManager from ...engine.device import DeviceManager
from ...engine.saliency.generator import run from ...engine.saliency.generator import run
from ...utils.checkpointer import get_checkpoint_to_run_inference
logger.info(f"Output folder: {output_folder}") logger.info(f"Output folder: {output_folder}")
output_folder.mkdir(parents=True, exist_ok=True) output_folder.mkdir(parents=True, exist_ok=True)
...@@ -181,14 +188,17 @@ def generate( ...@@ -181,14 +188,17 @@ def generate(
datamodule.prepare_data() datamodule.prepare_data()
datamodule.setup(stage="predict") datamodule.setup(stage="predict")
if weight.is_dir():
weight = get_checkpoint_to_run_inference(weight)
logger.info(f"Loading checkpoint from `{weight}`...") logger.info(f"Loading checkpoint from `{weight}`...")
model = model.load_from_checkpoint(weight, strict=False) model = type(model).load_from_checkpoint(weight, strict=False)
run( run(
model=model, model=model,
datamodule=datamodule, datamodule=datamodule,
device_manager=device_manager, device_manager=device_manager,
saliency_map_algorithms=saliency_map_algorithm, saliency_map_algorithm=saliency_map_algorithm,
target_class=target_class, target_class=target_class,
positive_only=positive_only, positive_only=positive_only,
output_folder=output_folder, output_folder=output_folder,
......
...@@ -23,7 +23,7 @@ logger = setup(__name__.split(".")[0], format="%(levelname)s: %(message)s") ...@@ -23,7 +23,7 @@ logger = setup(__name__.split(".")[0], format="%(levelname)s: %(message)s")
.. code:: sh .. code:: sh
ptbench saliency interpretability -vv tbx11k-v1-healthy-vs-atb --input-folder=parent_folder/gradcam/ --output-json=parent_folder/gradcam/tbx11k-v1-interp.json ptbench saliency interpretability -vv tbx11k-v1-healthy-vs-atb --input-folder=parent-folder/saliencies/ --output-json=path/to/interpretability-scores.json
""", """,
) )
......
...@@ -139,7 +139,7 @@ def get_checkpoint_to_run_inference( ...@@ -139,7 +139,7 @@ def get_checkpoint_to_run_inference(
""" """
try: try:
_get_checkpoint_from_alias(path, "best") return _get_checkpoint_from_alias(path, "best")
except FileNotFoundError: except FileNotFoundError:
logger.error( logger.error(
"Did not find lowest-validation-loss model to run inference " "Did not find lowest-validation-loss model to run inference "
......
...@@ -3,19 +3,17 @@ ...@@ -3,19 +3,17 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
"""Tests for the cam_utils script.""" """Tests for the cam_utils script."""
import cv2
import numpy as np import numpy as np
import pandas as pd
import pytest import pytest
from ptbench.utils.cam_utils import ( # from ptbench.utils.cam_utils import (
_calculate_stats_over_dataset, # _calculate_stats_over_dataset,
calculate_metrics_avg_for_every_class, # calculate_metrics_avg_for_every_class,
draw_boxes_on_image, # draw_boxes_on_image,
draw_largest_component_bbox_on_image, # draw_largest_component_bbox_on_image,
show_cam_on_image, # show_cam_on_image,
visualize_road_scores, # visualize_road_scores,
) # )
def test_calculate_stats_over_dataset(datadir): def test_calculate_stats_over_dataset(datadir):
......
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