|
39 | 39 | # Copyright (C) 2024 Intel Corporation
|
40 | 40 | # SPDX-License-Identifier: Apache-2.0
|
41 | 41 |
|
42 |
| -import json |
43 | 42 | import logging
|
44 | 43 | from collections.abc import Sequence
|
45 | 44 | from dataclasses import dataclass, field
|
46 |
| -from pathlib import Path |
47 | 45 |
|
48 | 46 | import torch
|
49 | 47 | from torch import Tensor
|
50 | 48 | from torchmetrics import Metric
|
51 | 49 |
|
52 |
| -from anomalib.data.utils.image import duplicate_filename |
53 | 50 | from anomalib.data.utils.path import validate_path
|
54 | 51 |
|
55 |
| -from . import _validate, pimo_numpy, utils |
56 |
| -from .utils import StatsOutliersPolicy, StatsRepeatedPolicy |
| 52 | +from . import _validate, pimo_numpy |
57 | 53 |
|
58 | 54 | logger = logging.getLogger(__name__)
|
59 | 55 |
|
60 |
| -# =========================================== AUX =========================================== |
61 |
| - |
62 | 56 |
|
63 | 57 | def _images_classes_from_masks(masks: Tensor) -> Tensor:
|
64 | 58 | masks = torch.concat(masks, dim=0)
|
@@ -256,60 +250,6 @@ def thresh_at(self, fpr_level: float) -> tuple[int, float, float]:
|
256 | 250 | fpr_level,
|
257 | 251 | )
|
258 | 252 |
|
259 |
| - def to_dict(self) -> dict[str, Tensor | str]: |
260 |
| - """Return a dictionary with the result object's attributes.""" |
261 |
| - dic = { |
262 |
| - "threshs": self.threshs, |
263 |
| - "shared_fpr": self.shared_fpr, |
264 |
| - "per_image_tprs": self.per_image_tprs, |
265 |
| - } |
266 |
| - if self.paths is not None: |
267 |
| - dic["paths"] = self.paths |
268 |
| - return dic |
269 |
| - |
270 |
| - @classmethod |
271 |
| - def from_dict(cls: type["PIMOResult"], dic: dict[str, Tensor | str | list[str]]) -> "PIMOResult": |
272 |
| - """Return a result object from a dictionary.""" |
273 |
| - try: |
274 |
| - return cls(**dic) # type: ignore[arg-type] |
275 |
| - |
276 |
| - except TypeError as ex: |
277 |
| - msg = f"Invalid input dictionary for {cls.__name__} object. Cause: {ex}." |
278 |
| - raise TypeError(msg) from ex |
279 |
| - |
280 |
| - def save(self, file_path: str | Path) -> None: |
281 |
| - """Save to a `.pt` file. |
282 |
| -
|
283 |
| - Args: |
284 |
| - file_path: path to the `.pt` file where to save the PIMO result. |
285 |
| - If the file already exists, a numerical suffix is added to the filename. |
286 |
| - """ |
287 |
| - validate_path(file_path, should_exist=False, accepted_extensions=(".pt",)) |
288 |
| - file_path = duplicate_filename(file_path) |
289 |
| - payload = self.to_dict() |
290 |
| - torch.save(payload, file_path) |
291 |
| - |
292 |
| - @classmethod |
293 |
| - def load(cls: type["PIMOResult"], file_path: str | Path) -> "PIMOResult": |
294 |
| - """Load from a `.pt` file. |
295 |
| -
|
296 |
| - Args: |
297 |
| - file_path: path to the `.pt` file where to load the PIMO result. |
298 |
| - """ |
299 |
| - validate_path(file_path, accepted_extensions=(".pt",)) |
300 |
| - payload = torch.load(file_path) |
301 |
| - if not isinstance(payload, dict): |
302 |
| - msg = f"Invalid content in file {file_path}. Must be a dictionary." |
303 |
| - raise TypeError(msg) |
304 |
| - # for compatibility with the original code |
305 |
| - if "shared_fpr_metric" in payload: |
306 |
| - del payload["shared_fpr_metric"] |
307 |
| - try: |
308 |
| - return cls.from_dict(payload) |
309 |
| - except TypeError as ex: |
310 |
| - msg = f"Invalid content in file {file_path}. Cause: {ex}." |
311 |
| - raise TypeError(msg) from ex |
312 |
| - |
313 | 253 |
|
314 | 254 | @dataclass
|
315 | 255 | class AUPIMOResult:
|
@@ -448,97 +388,8 @@ def from_pimoresult(
|
448 | 388 | paths=paths,
|
449 | 389 | )
|
450 | 390 |
|
451 |
| - def to_dict(self) -> dict[str, Tensor | str | float | int]: |
452 |
| - """Return a dictionary with the result object's attributes.""" |
453 |
| - dic = { |
454 |
| - "fpr_lower_bound": self.fpr_lower_bound, |
455 |
| - "fpr_upper_bound": self.fpr_upper_bound, |
456 |
| - "num_threshs": self.num_threshs, |
457 |
| - "thresh_lower_bound": self.thresh_lower_bound, |
458 |
| - "thresh_upper_bound": self.thresh_upper_bound, |
459 |
| - "aupimos": self.aupimos, |
460 |
| - } |
461 |
| - if self.paths is not None: |
462 |
| - dic["paths"] = self.paths |
463 |
| - return dic |
464 |
| - |
465 |
| - @classmethod |
466 |
| - def from_dict(cls: type["AUPIMOResult"], dic: dict[str, Tensor | str | float | int | list[str]]) -> "AUPIMOResult": |
467 |
| - """Return a result object from a dictionary.""" |
468 |
| - try: |
469 |
| - return cls(**dic) # type: ignore[arg-type] |
470 |
| - |
471 |
| - except TypeError as ex: |
472 |
| - msg = f"Invalid input dictionary for {cls.__name__} object. Cause: {ex}." |
473 |
| - raise TypeError(msg) from ex |
474 |
| - |
475 |
| - def save(self, file_path: str | Path) -> None: |
476 |
| - """Save to a `.json` file. |
477 |
| -
|
478 |
| - Args: |
479 |
| - file_path: path to the `.json` file where to save the AUPIMO result. |
480 |
| - If the file already exists, a numerical suffix is added to the filename. |
481 |
| - """ |
482 |
| - validate_path(file_path, should_exist=False, accepted_extensions=(".json",)) |
483 |
| - file_path = duplicate_filename(file_path) |
484 |
| - file_path = Path(file_path) |
485 |
| - payload = self.to_dict() |
486 |
| - aupimos: Tensor = payload["aupimos"] |
487 |
| - payload["aupimos"] = aupimos.numpy().tolist() |
488 |
| - with file_path.open("w") as f: |
489 |
| - json.dump(payload, f, indent=4) |
490 |
| - |
491 |
| - @classmethod |
492 |
| - def load(cls: type["AUPIMOResult"], file_path: str | Path) -> "AUPIMOResult": |
493 |
| - """Load from a `.json` file. |
494 |
| -
|
495 |
| - Args: |
496 |
| - file_path: path to the `.json` file where to load the AUPIMO result. |
497 |
| - """ |
498 |
| - validate_path(file_path, accepted_extensions=(".json",)) |
499 |
| - file_path = Path(file_path) |
500 |
| - with file_path.open("r") as f: |
501 |
| - payload = json.load(f) |
502 |
| - if not isinstance(payload, dict): |
503 |
| - file_path = str(file_path) |
504 |
| - msg = f"Invalid payload in file {file_path}. Must be a dictionary." |
505 |
| - raise TypeError(msg) |
506 |
| - payload["aupimos"] = torch.tensor(payload["aupimos"], dtype=torch.float64) |
507 |
| - # for compatibility with the original code |
508 |
| - if "shared_fpr_metric" in payload: |
509 |
| - del payload["shared_fpr_metric"] |
510 |
| - try: |
511 |
| - return cls.from_dict(payload) |
512 |
| - except (TypeError, ValueError) as ex: |
513 |
| - msg = f"Invalid payload in file {file_path}. Cause: {ex}." |
514 |
| - raise TypeError(msg) from ex |
515 |
| - |
516 |
| - def stats( |
517 |
| - self, |
518 |
| - outliers_policy: str | StatsOutliersPolicy = StatsOutliersPolicy.NONE.value, |
519 |
| - repeated_policy: str | StatsRepeatedPolicy = StatsRepeatedPolicy.AVOID.value, |
520 |
| - repeated_replacement_atol: float = 1e-2, |
521 |
| - ) -> list[dict[str, str | int | float]]: |
522 |
| - """Return the AUPIMO statistics. |
523 |
| -
|
524 |
| - See `anomalib.metrics.per_image.utils.per_image_scores_stats` for details. |
525 |
| -
|
526 |
| - Returns: |
527 |
| - list[dict[str, str | int | float]]: AUPIMO statistics |
528 |
| - """ |
529 |
| - return utils.per_image_scores_stats( |
530 |
| - self.aupimos, |
531 |
| - self.image_classes, |
532 |
| - only_class=1, |
533 |
| - outliers_policy=outliers_policy, |
534 |
| - repeated_policy=repeated_policy, |
535 |
| - repeated_replacement_atol=repeated_replacement_atol, |
536 |
| - ) |
537 |
| - |
538 | 391 |
|
539 | 392 | # =========================================== FUNCTIONAL ===========================================
|
540 |
| - |
541 |
| - |
542 | 393 | def pimo_curves(
|
543 | 394 | anomaly_maps: Tensor,
|
544 | 395 | masks: Tensor,
|
@@ -857,23 +708,6 @@ def normalizing_factor(fpr_bounds: tuple[float, float]) -> float:
|
857 | 708 | """
|
858 | 709 | return pimo_numpy.aupimo_normalizing_factor(fpr_bounds)
|
859 | 710 |
|
860 |
| - @staticmethod |
861 |
| - def random_model_score(fpr_bounds: tuple[float, float]) -> float: |
862 |
| - """AUPIMO of a theoretical random model. |
863 |
| -
|
864 |
| - "Random model" means that there is no discrimination between normal and anomalous pixels/patches/images. |
865 |
| - It corresponds to assuming the functions T = F. |
866 |
| -
|
867 |
| - For the FPR bounds (1e-5, 1e-4), the random model AUPIMO is ~4e-5. |
868 |
| -
|
869 |
| - Args: |
870 |
| - fpr_bounds: lower and upper bounds of the FPR integration range. |
871 |
| -
|
872 |
| - Returns: |
873 |
| - float: the AUPIMO score. |
874 |
| - """ |
875 |
| - return pimo_numpy.aupimo_random_model_score(fpr_bounds) |
876 |
| - |
877 | 711 | def __repr__(self) -> str:
|
878 | 712 | """Show the metric name and its integration bounds."""
|
879 | 713 | lower, upper = self.fpr_bounds
|
|
0 commit comments