photo-stack-finder 0.1.7__py3-none-any.whl → 0.1.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. orchestrator/__init__.py +2 -2
  2. orchestrator/app.py +6 -11
  3. orchestrator/build_pipeline.py +19 -21
  4. orchestrator/orchestrator_runner.py +11 -8
  5. orchestrator/pipeline_builder.py +126 -126
  6. orchestrator/pipeline_orchestrator.py +604 -604
  7. orchestrator/review_persistence.py +162 -162
  8. orchestrator/static/orchestrator.css +76 -76
  9. orchestrator/static/orchestrator.html +11 -5
  10. orchestrator/static/orchestrator.js +3 -1
  11. overlap_metrics/__init__.py +1 -1
  12. overlap_metrics/config.py +135 -135
  13. overlap_metrics/core.py +284 -284
  14. overlap_metrics/estimators.py +292 -292
  15. overlap_metrics/metrics.py +307 -307
  16. overlap_metrics/registry.py +99 -99
  17. overlap_metrics/utils.py +104 -104
  18. photo_compare/__init__.py +1 -1
  19. photo_compare/base.py +285 -285
  20. photo_compare/config.py +225 -225
  21. photo_compare/distance.py +15 -15
  22. photo_compare/feature_methods.py +173 -173
  23. photo_compare/file_hash.py +29 -29
  24. photo_compare/hash_methods.py +99 -99
  25. photo_compare/histogram_methods.py +118 -118
  26. photo_compare/pixel_methods.py +58 -58
  27. photo_compare/structural_methods.py +104 -104
  28. photo_compare/types.py +28 -28
  29. {photo_stack_finder-0.1.7.dist-info → photo_stack_finder-0.1.8.dist-info}/METADATA +21 -22
  30. photo_stack_finder-0.1.8.dist-info/RECORD +75 -0
  31. scripts/orchestrate.py +12 -10
  32. utils/__init__.py +4 -3
  33. utils/base_pipeline_stage.py +171 -171
  34. utils/base_ports.py +176 -176
  35. utils/benchmark_utils.py +823 -823
  36. utils/channel.py +74 -74
  37. utils/comparison_gates.py +40 -21
  38. utils/compute_benchmarks.py +355 -355
  39. utils/compute_identical.py +94 -24
  40. utils/compute_indices.py +235 -235
  41. utils/compute_perceptual_hash.py +127 -127
  42. utils/compute_perceptual_match.py +240 -240
  43. utils/compute_sha_bins.py +64 -20
  44. utils/compute_template_similarity.py +1 -1
  45. utils/compute_versions.py +483 -483
  46. utils/config.py +8 -5
  47. utils/data_io.py +83 -83
  48. utils/graph_context.py +44 -44
  49. utils/logger.py +2 -2
  50. utils/models.py +2 -2
  51. utils/photo_file.py +90 -91
  52. utils/pipeline_graph.py +334 -334
  53. utils/pipeline_stage.py +408 -408
  54. utils/plot_helpers.py +123 -123
  55. utils/ports.py +136 -136
  56. utils/progress.py +415 -415
  57. utils/report_builder.py +139 -139
  58. utils/review_types.py +55 -55
  59. utils/review_utils.py +10 -19
  60. utils/sequence.py +10 -8
  61. utils/sequence_clustering.py +1 -1
  62. utils/template.py +57 -57
  63. utils/template_parsing.py +71 -0
  64. photo_stack_finder-0.1.7.dist-info/RECORD +0 -74
  65. {photo_stack_finder-0.1.7.dist-info → photo_stack_finder-0.1.8.dist-info}/WHEEL +0 -0
  66. {photo_stack_finder-0.1.7.dist-info → photo_stack_finder-0.1.8.dist-info}/entry_points.txt +0 -0
  67. {photo_stack_finder-0.1.7.dist-info → photo_stack_finder-0.1.8.dist-info}/licenses/LICENSE +0 -0
  68. {photo_stack_finder-0.1.7.dist-info → photo_stack_finder-0.1.8.dist-info}/top_level.txt +0 -0
@@ -1,118 +1,118 @@
1
- """Histogram-based similarity methods with caching support."""
2
-
3
- from __future__ import annotations
4
-
5
- from abc import abstractmethod
6
-
7
- import cv2 as cv
8
- import numpy as np
9
- import numpy.typing as npt
10
- from PIL import Image
11
-
12
- from .base import ComparisonMethodName, SimilarityMethod
13
-
14
-
15
- class HistogramMethodBase(SimilarityMethod[npt.NDArray[np.float32]]):
16
- """Base class for histogram-based similarity methods."""
17
-
18
- def __init__(self, method_name: ComparisonMethodName, comparison_method: str) -> None:
19
- super().__init__(method_name)
20
- self.comparison_method = comparison_method
21
-
22
- @abstractmethod
23
- def _prepare_single(self, pixels: npt.NDArray[np.uint8]) -> npt.NDArray[np.float32]:
24
- """Implement the actual histogram preparation logic.
25
-
26
- Args:
27
- pixels: RGB pixel array with shape (height, width, 3), dtype uint8.
28
- EXIF orientation already applied. Full resolution.
29
-
30
- Returns:
31
- Normalized histogram as float32 array
32
- """
33
- ...
34
-
35
- def _compare_histograms(self, hist1: npt.NDArray[np.float32], hist2: npt.NDArray[np.float32]) -> float:
36
- """Compare histograms using OpenCV methods."""
37
- methods: dict[str, int] = {
38
- "correlation": cv.HISTCMP_CORREL,
39
- "chi_square": cv.HISTCMP_CHISQR,
40
- "intersection": cv.HISTCMP_INTERSECT,
41
- "bhattacharyya": cv.HISTCMP_BHATTACHARYYA,
42
- }
43
-
44
- method: int = methods.get(self.comparison_method, cv.HISTCMP_CORREL)
45
- similarity: float = cv.compareHist(hist1.astype(np.float32), hist2.astype(np.float32), method)
46
-
47
- # Normalize to 0-1 scale where higher is more similar
48
- if self.comparison_method == "chi_square":
49
- return float(1.0 / (1.0 + similarity))
50
- if self.comparison_method == "bhattacharyya":
51
- return float(1.0 - similarity)
52
- return float(max(0.0, similarity))
53
-
54
-
55
- class ColorHistogramMethod(HistogramMethodBase):
56
- """RGB colour histogram method for colour-based similarity."""
57
-
58
- def __init__(self, bins: int, comparison_method: str) -> None:
59
- super().__init__("colour_histogram", comparison_method)
60
- self.bins = bins
61
-
62
- def _prepare_single(self, pixels: npt.NDArray[np.uint8]) -> npt.NDArray[np.float32]:
63
- """Prepare RGB color histograms for the image."""
64
- img = Image.fromarray(pixels, mode="RGB")
65
- img = img.resize((256, 256), Image.Resampling.LANCZOS)
66
- arr = np.array(img)
67
-
68
- hist_r: npt.NDArray[np.float32] = cv.calcHist([arr], [0], None, [self.bins], [0, 256]).flatten()
69
- hist_g: npt.NDArray[np.float32] = cv.calcHist([arr], [1], None, [self.bins], [0, 256]).flatten()
70
- hist_b: npt.NDArray[np.float32] = cv.calcHist([arr], [2], None, [self.bins], [0, 256]).flatten()
71
-
72
- # Normalize
73
- hist_r = hist_r / (hist_r.sum() + 1e-10)
74
- hist_g = hist_g / (hist_g.sum() + 1e-10)
75
- hist_b = hist_b / (hist_b.sum() + 1e-10)
76
-
77
- # Concatenate into single array for consistent interface
78
- return np.concatenate([hist_r, hist_g, hist_b])
79
-
80
- def _compare_prepared(self, prep1: npt.NDArray[np.float32], prep2: npt.NDArray[np.float32]) -> float:
81
- """Compare RGB histograms by averaging across channels."""
82
- # Split back into R, G, B channels
83
- bins: int = len(prep1) // 3
84
- r1: npt.NDArray[np.float32] = prep1[:bins]
85
- g1: npt.NDArray[np.float32] = prep1[bins : 2 * bins]
86
- b1: npt.NDArray[np.float32] = prep1[2 * bins :]
87
- r2: npt.NDArray[np.float32] = prep2[:bins]
88
- g2: npt.NDArray[np.float32] = prep2[bins : 2 * bins]
89
- b2: npt.NDArray[np.float32] = prep2[2 * bins :]
90
-
91
- r_sim: float = self._compare_histograms(r1, r2)
92
- g_sim: float = self._compare_histograms(g1, g2)
93
- b_sim: float = self._compare_histograms(b1, b2)
94
- return (r_sim + g_sim + b_sim) / 3.0
95
-
96
-
97
- class HSVHistogramMethod(HistogramMethodBase):
98
- """HSV colour histogram method - often better than RGB for colour similarity."""
99
-
100
- def __init__(self, bins: tuple[int, int, int], comparison_method: str) -> None:
101
- super().__init__("hsv_histogram", comparison_method)
102
- self.bins = bins
103
-
104
- def _prepare_single(self, pixels: npt.NDArray[np.uint8]) -> npt.NDArray[np.float32]:
105
- """Prepare HSV color histogram for the image."""
106
- img = Image.fromarray(pixels, mode="RGB")
107
- img = img.resize((256, 256), Image.Resampling.LANCZOS)
108
- arr = np.array(img)
109
- hsv = cv.cvtColor(arr, cv.COLOR_RGB2HSV)
110
-
111
- hist: npt.NDArray[np.float32] = cv.calcHist([hsv], [0, 1, 2], None, list(self.bins), [0, 180, 0, 256, 0, 256])
112
- hist = hist.flatten()
113
- normalized: npt.NDArray[np.float32] = (hist / (hist.sum() + 1e-10)).astype(np.float32)
114
- return normalized
115
-
116
- def _compare_prepared(self, prep1: npt.NDArray[np.float32], prep2: npt.NDArray[np.float32]) -> float:
117
- """Compare HSV histograms using specified method."""
118
- return self._compare_histograms(prep1, prep2)
1
+ """Histogram-based similarity methods with caching support."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import abstractmethod
6
+
7
+ import cv2 as cv
8
+ import numpy as np
9
+ import numpy.typing as npt
10
+ from PIL import Image
11
+
12
+ from .base import ComparisonMethodName, SimilarityMethod
13
+
14
+
15
+ class HistogramMethodBase(SimilarityMethod[npt.NDArray[np.float32]]):
16
+ """Base class for histogram-based similarity methods."""
17
+
18
+ def __init__(self, method_name: ComparisonMethodName, comparison_method: str) -> None:
19
+ super().__init__(method_name)
20
+ self.comparison_method = comparison_method
21
+
22
+ @abstractmethod
23
+ def _prepare_single(self, pixels: npt.NDArray[np.uint8]) -> npt.NDArray[np.float32]:
24
+ """Implement the actual histogram preparation logic.
25
+
26
+ Args:
27
+ pixels: RGB pixel array with shape (height, width, 3), dtype uint8.
28
+ EXIF orientation already applied. Full resolution.
29
+
30
+ Returns:
31
+ Normalized histogram as float32 array
32
+ """
33
+ ...
34
+
35
+ def _compare_histograms(self, hist1: npt.NDArray[np.float32], hist2: npt.NDArray[np.float32]) -> float:
36
+ """Compare histograms using OpenCV methods."""
37
+ methods: dict[str, int] = {
38
+ "correlation": cv.HISTCMP_CORREL,
39
+ "chi_square": cv.HISTCMP_CHISQR,
40
+ "intersection": cv.HISTCMP_INTERSECT,
41
+ "bhattacharyya": cv.HISTCMP_BHATTACHARYYA,
42
+ }
43
+
44
+ method: int = methods.get(self.comparison_method, cv.HISTCMP_CORREL)
45
+ similarity: float = cv.compareHist(hist1.astype(np.float32), hist2.astype(np.float32), method)
46
+
47
+ # Normalize to 0-1 scale where higher is more similar
48
+ if self.comparison_method == "chi_square":
49
+ return float(1.0 / (1.0 + similarity))
50
+ if self.comparison_method == "bhattacharyya":
51
+ return float(1.0 - similarity)
52
+ return float(max(0.0, similarity))
53
+
54
+
55
+ class ColorHistogramMethod(HistogramMethodBase):
56
+ """RGB colour histogram method for colour-based similarity."""
57
+
58
+ def __init__(self, bins: int, comparison_method: str) -> None:
59
+ super().__init__("colour_histogram", comparison_method)
60
+ self.bins = bins
61
+
62
+ def _prepare_single(self, pixels: npt.NDArray[np.uint8]) -> npt.NDArray[np.float32]:
63
+ """Prepare RGB color histograms for the image."""
64
+ img = Image.fromarray(pixels, mode="RGB")
65
+ img = img.resize((256, 256), Image.Resampling.LANCZOS)
66
+ arr = np.array(img)
67
+
68
+ hist_r: npt.NDArray[np.float32] = cv.calcHist([arr], [0], None, [self.bins], [0, 256]).flatten()
69
+ hist_g: npt.NDArray[np.float32] = cv.calcHist([arr], [1], None, [self.bins], [0, 256]).flatten()
70
+ hist_b: npt.NDArray[np.float32] = cv.calcHist([arr], [2], None, [self.bins], [0, 256]).flatten()
71
+
72
+ # Normalize
73
+ hist_r = hist_r / (hist_r.sum() + 1e-10)
74
+ hist_g = hist_g / (hist_g.sum() + 1e-10)
75
+ hist_b = hist_b / (hist_b.sum() + 1e-10)
76
+
77
+ # Concatenate into single array for consistent interface
78
+ return np.concatenate([hist_r, hist_g, hist_b])
79
+
80
+ def _compare_prepared(self, prep1: npt.NDArray[np.float32], prep2: npt.NDArray[np.float32]) -> float:
81
+ """Compare RGB histograms by averaging across channels."""
82
+ # Split back into R, G, B channels
83
+ bins: int = len(prep1) // 3
84
+ r1: npt.NDArray[np.float32] = prep1[:bins]
85
+ g1: npt.NDArray[np.float32] = prep1[bins : 2 * bins]
86
+ b1: npt.NDArray[np.float32] = prep1[2 * bins :]
87
+ r2: npt.NDArray[np.float32] = prep2[:bins]
88
+ g2: npt.NDArray[np.float32] = prep2[bins : 2 * bins]
89
+ b2: npt.NDArray[np.float32] = prep2[2 * bins :]
90
+
91
+ r_sim: float = self._compare_histograms(r1, r2)
92
+ g_sim: float = self._compare_histograms(g1, g2)
93
+ b_sim: float = self._compare_histograms(b1, b2)
94
+ return (r_sim + g_sim + b_sim) / 3.0
95
+
96
+
97
+ class HSVHistogramMethod(HistogramMethodBase):
98
+ """HSV colour histogram method - often better than RGB for colour similarity."""
99
+
100
+ def __init__(self, bins: tuple[int, int, int], comparison_method: str) -> None:
101
+ super().__init__("hsv_histogram", comparison_method)
102
+ self.bins = bins
103
+
104
+ def _prepare_single(self, pixels: npt.NDArray[np.uint8]) -> npt.NDArray[np.float32]:
105
+ """Prepare HSV color histogram for the image."""
106
+ img = Image.fromarray(pixels, mode="RGB")
107
+ img = img.resize((256, 256), Image.Resampling.LANCZOS)
108
+ arr = np.array(img)
109
+ hsv = cv.cvtColor(arr, cv.COLOR_RGB2HSV)
110
+
111
+ hist: npt.NDArray[np.float32] = cv.calcHist([hsv], [0, 1, 2], None, list(self.bins), [0, 180, 0, 256, 0, 256])
112
+ hist = hist.flatten()
113
+ normalized: npt.NDArray[np.float32] = (hist / (hist.sum() + 1e-10)).astype(np.float32)
114
+ return normalized
115
+
116
+ def _compare_prepared(self, prep1: npt.NDArray[np.float32], prep2: npt.NDArray[np.float32]) -> float:
117
+ """Compare HSV histograms using specified method."""
118
+ return self._compare_histograms(prep1, prep2)
@@ -1,58 +1,58 @@
1
- """Pixel-based similarity methods with caching support."""
2
-
3
- from __future__ import annotations
4
-
5
- import numpy as np
6
- import numpy.typing as npt
7
- from PIL import Image
8
-
9
- from .base import SimilarityMethod
10
-
11
-
12
- class MSEMethod(SimilarityMethod[npt.NDArray[np.float32]]):
13
- """Mean Squared Error method for pixel-level comparison."""
14
-
15
- def __init__(self, image_size: int) -> None:
16
- super().__init__("mse")
17
- self.image_size = image_size
18
-
19
- def _prepare_single(self, pixels: npt.NDArray[np.uint8]) -> npt.NDArray[np.float32]:
20
- """Prepare standardized image for MSE comparison."""
21
- img = Image.fromarray(pixels, mode="RGB")
22
- img = img.convert("L")
23
- img = img.resize((self.image_size, self.image_size), Image.Resampling.LANCZOS)
24
- return np.array(img, dtype=np.float32)
25
-
26
- def _compare_prepared(self, prep1: npt.NDArray[np.float32], prep2: npt.NDArray[np.float32]) -> float:
27
- """Compare images using MSE converted to similarity score."""
28
- mse = float(np.mean((prep1 - prep2) ** 2))
29
- # Convert MSE to similarity score using exponential decay
30
- return float(np.exp(-mse / 1000.0))
31
-
32
-
33
- class PSNRMethod(SimilarityMethod[npt.NDArray[np.float32]]):
34
- """Peak Signal-to-Noise Ratio method for image quality comparison."""
35
-
36
- def __init__(self, image_size: int, max_value: float) -> None:
37
- super().__init__("psnr")
38
- self.image_size = image_size
39
- self.max_value = max_value
40
-
41
- def _prepare_single(self, pixels: npt.NDArray[np.uint8]) -> npt.NDArray[np.float32]:
42
- """Prepare standardized image for PSNR comparison."""
43
- img = Image.fromarray(pixels, mode="RGB")
44
- img = img.convert("L")
45
- img = img.resize((self.image_size, self.image_size), Image.Resampling.LANCZOS)
46
- return np.array(img, dtype=np.float32)
47
-
48
- def _compare_prepared(self, prep1: npt.NDArray[np.float32], prep2: npt.NDArray[np.float32]) -> float:
49
- """Compare images using PSNR converted to similarity score."""
50
- mse = float(np.mean((prep1 - prep2) ** 2))
51
-
52
- if mse == 0:
53
- return 1.0 # Perfect similarity
54
-
55
- psnr = 20 * np.log10(self.max_value / np.sqrt(mse))
56
- # Convert PSNR to similarity score (0-1)
57
- # PSNR typically ranges from ~10 (poor) to ~50+ (excellent)
58
- return float(min(1.0, max(0.0, (psnr - 10.0) / 40.0)))
1
+ """Pixel-based similarity methods with caching support."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+ import numpy.typing as npt
7
+ from PIL import Image
8
+
9
+ from .base import SimilarityMethod
10
+
11
+
12
+ class MSEMethod(SimilarityMethod[npt.NDArray[np.float32]]):
13
+ """Mean Squared Error method for pixel-level comparison."""
14
+
15
+ def __init__(self, image_size: int) -> None:
16
+ super().__init__("mse")
17
+ self.image_size = image_size
18
+
19
+ def _prepare_single(self, pixels: npt.NDArray[np.uint8]) -> npt.NDArray[np.float32]:
20
+ """Prepare standardized image for MSE comparison."""
21
+ img = Image.fromarray(pixels, mode="RGB")
22
+ img = img.convert("L")
23
+ img = img.resize((self.image_size, self.image_size), Image.Resampling.LANCZOS)
24
+ return np.array(img, dtype=np.float32)
25
+
26
+ def _compare_prepared(self, prep1: npt.NDArray[np.float32], prep2: npt.NDArray[np.float32]) -> float:
27
+ """Compare images using MSE converted to similarity score."""
28
+ mse = float(np.mean((prep1 - prep2) ** 2))
29
+ # Convert MSE to similarity score using exponential decay
30
+ return float(np.exp(-mse / 1000.0))
31
+
32
+
33
+ class PSNRMethod(SimilarityMethod[npt.NDArray[np.float32]]):
34
+ """Peak Signal-to-Noise Ratio method for image quality comparison."""
35
+
36
+ def __init__(self, image_size: int, max_value: float) -> None:
37
+ super().__init__("psnr")
38
+ self.image_size = image_size
39
+ self.max_value = max_value
40
+
41
+ def _prepare_single(self, pixels: npt.NDArray[np.uint8]) -> npt.NDArray[np.float32]:
42
+ """Prepare standardized image for PSNR comparison."""
43
+ img = Image.fromarray(pixels, mode="RGB")
44
+ img = img.convert("L")
45
+ img = img.resize((self.image_size, self.image_size), Image.Resampling.LANCZOS)
46
+ return np.array(img, dtype=np.float32)
47
+
48
+ def _compare_prepared(self, prep1: npt.NDArray[np.float32], prep2: npt.NDArray[np.float32]) -> float:
49
+ """Compare images using PSNR converted to similarity score."""
50
+ mse = float(np.mean((prep1 - prep2) ** 2))
51
+
52
+ if mse == 0:
53
+ return 1.0 # Perfect similarity
54
+
55
+ psnr = 20 * np.log10(self.max_value / np.sqrt(mse))
56
+ # Convert PSNR to similarity score (0-1)
57
+ # PSNR typically ranges from ~10 (poor) to ~50+ (excellent)
58
+ return float(min(1.0, max(0.0, (psnr - 10.0) / 40.0)))
@@ -1,104 +1,104 @@
1
- """Structural similarity methods with caching support."""
2
-
3
- from __future__ import annotations
4
-
5
- from collections.abc import Sequence
6
-
7
- import cv2 as cv
8
- import numpy as np
9
- import numpy.typing as npt
10
- from PIL import Image
11
- from skimage.feature import hog
12
- from skimage.metrics import structural_similarity as ssim
13
-
14
- from .base import SimilarityMethod
15
-
16
-
17
- class SSIMMethod(SimilarityMethod[npt.NDArray[np.float32]]):
18
- """SSIM (Structural Similarity Index) method using scikit-image."""
19
-
20
- def __init__(self, image_size: int) -> None:
21
- super().__init__("ssim")
22
- self.image_size = image_size
23
-
24
- def _prepare_single(self, pixels: npt.NDArray[np.uint8]) -> npt.NDArray[np.float32]:
25
- """Prepare image for SSIM comparison."""
26
- img = Image.fromarray(pixels, mode="RGB")
27
- img = img.convert("L")
28
- img = img.resize((self.image_size, self.image_size), Image.Resampling.LANCZOS)
29
- return np.array(img, dtype=np.float32) / 255.0
30
-
31
- def _compare_prepared(self, prep1: npt.NDArray[np.float32], prep2: npt.NDArray[np.float32]) -> float:
32
- """Compare prepared images using SSIM."""
33
- return float(ssim(prep1, prep2, data_range=1.0))
34
-
35
-
36
- class MultiScaleSSIMMethod(SimilarityMethod[npt.NDArray[np.float32]]):
37
- """Multi-scale SSIM method for more robust comparison."""
38
-
39
- def __init__(self, image_size: int, scales: Sequence[float]) -> None:
40
- super().__init__("ms_ssim")
41
- self.image_size = image_size
42
- self.scales = scales
43
-
44
- def _prepare_single(self, pixels: npt.NDArray[np.uint8]) -> npt.NDArray[np.float32]:
45
- """Prepare image for multi-scale SSIM comparison."""
46
- img = Image.fromarray(pixels, mode="RGB")
47
- img = img.convert("L")
48
- img = img.resize((self.image_size, self.image_size), Image.Resampling.LANCZOS)
49
- return np.array(img, dtype=np.float32) / 255.0
50
-
51
- def _compare_prepared(self, prep1: npt.NDArray[np.float32], prep2: npt.NDArray[np.float32]) -> float:
52
- """Compare prepared images using multi-scale SSIM."""
53
- scores: list[float] = []
54
- for scale in self.scales:
55
- if scale == 1.0:
56
- score: float = float(ssim(prep1, prep2, data_range=1.0))
57
- else:
58
- h: int
59
- w: int
60
- h, w = prep1.shape
61
- new_h: int = max(1, int(h * scale))
62
- new_w: int = max(1, int(w * scale))
63
- prep1_scaled: npt.NDArray[np.float32] = cv.resize(prep1, (new_w, new_h), interpolation=cv.INTER_AREA)
64
- prep2_scaled: npt.NDArray[np.float32] = cv.resize(prep2, (new_w, new_h), interpolation=cv.INTER_AREA)
65
- score = float(ssim(prep1_scaled, prep2_scaled, data_range=1.0))
66
- scores.append(score)
67
- return float(np.mean(scores))
68
-
69
-
70
- class HOGMethod(SimilarityMethod[npt.NDArray[np.float32]]):
71
- """HOG (Histogram of Oriented Gradients) feature method using scikit-image."""
72
-
73
- def __init__(self, orientations: int, pixels_per_cell: tuple[int, int]) -> None:
74
- super().__init__("hog")
75
- self.orientations = orientations
76
- self.pixels_per_cell = pixels_per_cell
77
-
78
- def _prepare_single(self, pixels: npt.NDArray[np.uint8]) -> npt.NDArray[np.float32]:
79
- """Prepare HOG features for the image."""
80
- img = Image.fromarray(pixels, mode="RGB")
81
- img = img.convert("L")
82
- img = img.resize((128, 128), Image.Resampling.LANCZOS)
83
- img_array: npt.NDArray[np.float32] = np.array(img, dtype=np.float32) / 255.0
84
-
85
- features: npt.NDArray[np.float32] = hog(
86
- img_array,
87
- orientations=self.orientations,
88
- pixels_per_cell=self.pixels_per_cell,
89
- cells_per_block=(2, 2),
90
- block_norm="L2-Hys",
91
- feature_vector=True,
92
- )
93
- return features.astype(np.float32)
94
-
95
- def _compare_prepared(self, prep1: npt.NDArray[np.float32], prep2: npt.NDArray[np.float32]) -> float:
96
- """Compare HOG features using cosine similarity."""
97
- dot_product = float(np.dot(prep1, prep2))
98
- norm_a = float(np.linalg.norm(prep1))
99
- norm_b = float(np.linalg.norm(prep2))
100
-
101
- if norm_a == 0.0 or norm_b == 0.0:
102
- return 0.0
103
-
104
- return dot_product / (norm_a * norm_b)
1
+ """Structural similarity methods with caching support."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+
7
+ import cv2 as cv
8
+ import numpy as np
9
+ import numpy.typing as npt
10
+ from PIL import Image
11
+ from skimage.feature import hog
12
+ from skimage.metrics import structural_similarity as ssim
13
+
14
+ from .base import SimilarityMethod
15
+
16
+
17
+ class SSIMMethod(SimilarityMethod[npt.NDArray[np.float32]]):
18
+ """SSIM (Structural Similarity Index) method using scikit-image."""
19
+
20
+ def __init__(self, image_size: int) -> None:
21
+ super().__init__("ssim")
22
+ self.image_size = image_size
23
+
24
+ def _prepare_single(self, pixels: npt.NDArray[np.uint8]) -> npt.NDArray[np.float32]:
25
+ """Prepare image for SSIM comparison."""
26
+ img = Image.fromarray(pixels, mode="RGB")
27
+ img = img.convert("L")
28
+ img = img.resize((self.image_size, self.image_size), Image.Resampling.LANCZOS)
29
+ return np.array(img, dtype=np.float32) / 255.0
30
+
31
+ def _compare_prepared(self, prep1: npt.NDArray[np.float32], prep2: npt.NDArray[np.float32]) -> float:
32
+ """Compare prepared images using SSIM."""
33
+ return float(ssim(prep1, prep2, data_range=1.0))
34
+
35
+
36
+ class MultiScaleSSIMMethod(SimilarityMethod[npt.NDArray[np.float32]]):
37
+ """Multi-scale SSIM method for more robust comparison."""
38
+
39
+ def __init__(self, image_size: int, scales: Sequence[float]) -> None:
40
+ super().__init__("ms_ssim")
41
+ self.image_size = image_size
42
+ self.scales = scales
43
+
44
+ def _prepare_single(self, pixels: npt.NDArray[np.uint8]) -> npt.NDArray[np.float32]:
45
+ """Prepare image for multi-scale SSIM comparison."""
46
+ img = Image.fromarray(pixels, mode="RGB")
47
+ img = img.convert("L")
48
+ img = img.resize((self.image_size, self.image_size), Image.Resampling.LANCZOS)
49
+ return np.array(img, dtype=np.float32) / 255.0
50
+
51
+ def _compare_prepared(self, prep1: npt.NDArray[np.float32], prep2: npt.NDArray[np.float32]) -> float:
52
+ """Compare prepared images using multi-scale SSIM."""
53
+ scores: list[float] = []
54
+ for scale in self.scales:
55
+ if scale == 1.0:
56
+ score: float = float(ssim(prep1, prep2, data_range=1.0))
57
+ else:
58
+ h: int
59
+ w: int
60
+ h, w = prep1.shape
61
+ new_h: int = max(1, int(h * scale))
62
+ new_w: int = max(1, int(w * scale))
63
+ prep1_scaled: npt.NDArray[np.float32] = cv.resize(prep1, (new_w, new_h), interpolation=cv.INTER_AREA)
64
+ prep2_scaled: npt.NDArray[np.float32] = cv.resize(prep2, (new_w, new_h), interpolation=cv.INTER_AREA)
65
+ score = float(ssim(prep1_scaled, prep2_scaled, data_range=1.0))
66
+ scores.append(score)
67
+ return float(np.mean(scores))
68
+
69
+
70
+ class HOGMethod(SimilarityMethod[npt.NDArray[np.float32]]):
71
+ """HOG (Histogram of Oriented Gradients) feature method using scikit-image."""
72
+
73
+ def __init__(self, orientations: int, pixels_per_cell: tuple[int, int]) -> None:
74
+ super().__init__("hog")
75
+ self.orientations = orientations
76
+ self.pixels_per_cell = pixels_per_cell
77
+
78
+ def _prepare_single(self, pixels: npt.NDArray[np.uint8]) -> npt.NDArray[np.float32]:
79
+ """Prepare HOG features for the image."""
80
+ img = Image.fromarray(pixels, mode="RGB")
81
+ img = img.convert("L")
82
+ img = img.resize((128, 128), Image.Resampling.LANCZOS)
83
+ img_array: npt.NDArray[np.float32] = np.array(img, dtype=np.float32) / 255.0
84
+
85
+ features: npt.NDArray[np.float32] = hog(
86
+ img_array,
87
+ orientations=self.orientations,
88
+ pixels_per_cell=self.pixels_per_cell,
89
+ cells_per_block=(2, 2),
90
+ block_norm="L2-Hys",
91
+ feature_vector=True,
92
+ )
93
+ return features.astype(np.float32)
94
+
95
+ def _compare_prepared(self, prep1: npt.NDArray[np.float32], prep2: npt.NDArray[np.float32]) -> float:
96
+ """Compare HOG features using cosine similarity."""
97
+ dot_product = float(np.dot(prep1, prep2))
98
+ norm_a = float(np.linalg.norm(prep1))
99
+ norm_b = float(np.linalg.norm(prep2))
100
+
101
+ if norm_a == 0.0 or norm_b == 0.0:
102
+ return 0.0
103
+
104
+ return dot_product / (norm_a * norm_b)
photo_compare/types.py CHANGED
@@ -1,28 +1,28 @@
1
- """Type aliases for photo comparison arrays.
2
-
3
- This module defines common numpy array type aliases used throughout the
4
- photo comparison subsystem. Using these aliases makes type signatures
5
- more readable while maintaining strict type safety.
6
- """
7
-
8
- import numpy as np
9
- import numpy.typing as npt
10
-
11
- # Image arrays (H, W, C) - typically uint8 RGB images
12
- ImageArray = npt.NDArray[np.uint8]
13
-
14
- # Grayscale images (H, W) - typically uint8 single channel
15
- GrayscaleArray = npt.NDArray[np.uint8]
16
-
17
- # Histogram arrays (1D) - typically float64 normalized histograms
18
- HistogramArray = npt.NDArray[np.float64]
19
-
20
- # Feature descriptor arrays (N, D) - typically float32 feature vectors
21
- # where N is the number of keypoints and D is the descriptor dimension
22
- DescriptorArray = npt.NDArray[np.float32]
23
-
24
- # Hash arrays - boolean arrays representing perceptual hashes
25
- HashArray = npt.NDArray[np.bool_]
26
-
27
- # Structural comparison arrays (H, W) - float64 SSIM maps
28
- StructuralArray = npt.NDArray[np.float64]
1
+ """Type aliases for photo comparison arrays.
2
+
3
+ This module defines common numpy array type aliases used throughout the
4
+ photo comparison subsystem. Using these aliases makes type signatures
5
+ more readable while maintaining strict type safety.
6
+ """
7
+
8
+ import numpy as np
9
+ import numpy.typing as npt
10
+
11
+ # Image arrays (H, W, C) - typically uint8 RGB images
12
+ ImageArray = npt.NDArray[np.uint8]
13
+
14
+ # Grayscale images (H, W) - typically uint8 single channel
15
+ GrayscaleArray = npt.NDArray[np.uint8]
16
+
17
+ # Histogram arrays (1D) - typically float64 normalized histograms
18
+ HistogramArray = npt.NDArray[np.float64]
19
+
20
+ # Feature descriptor arrays (N, D) - typically float32 feature vectors
21
+ # where N is the number of keypoints and D is the descriptor dimension
22
+ DescriptorArray = npt.NDArray[np.float32]
23
+
24
+ # Hash arrays - boolean arrays representing perceptual hashes
25
+ HashArray = npt.NDArray[np.bool_]
26
+
27
+ # Structural comparison arrays (H, W) - float64 SSIM maps
28
+ StructuralArray = npt.NDArray[np.float64]