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.
- orchestrator/__init__.py +2 -2
- orchestrator/app.py +6 -11
- orchestrator/build_pipeline.py +19 -21
- orchestrator/orchestrator_runner.py +11 -8
- orchestrator/pipeline_builder.py +126 -126
- orchestrator/pipeline_orchestrator.py +604 -604
- orchestrator/review_persistence.py +162 -162
- orchestrator/static/orchestrator.css +76 -76
- orchestrator/static/orchestrator.html +11 -5
- orchestrator/static/orchestrator.js +3 -1
- overlap_metrics/__init__.py +1 -1
- overlap_metrics/config.py +135 -135
- overlap_metrics/core.py +284 -284
- overlap_metrics/estimators.py +292 -292
- overlap_metrics/metrics.py +307 -307
- overlap_metrics/registry.py +99 -99
- overlap_metrics/utils.py +104 -104
- photo_compare/__init__.py +1 -1
- photo_compare/base.py +285 -285
- photo_compare/config.py +225 -225
- photo_compare/distance.py +15 -15
- photo_compare/feature_methods.py +173 -173
- photo_compare/file_hash.py +29 -29
- photo_compare/hash_methods.py +99 -99
- photo_compare/histogram_methods.py +118 -118
- photo_compare/pixel_methods.py +58 -58
- photo_compare/structural_methods.py +104 -104
- photo_compare/types.py +28 -28
- {photo_stack_finder-0.1.7.dist-info → photo_stack_finder-0.1.8.dist-info}/METADATA +21 -22
- photo_stack_finder-0.1.8.dist-info/RECORD +75 -0
- scripts/orchestrate.py +12 -10
- utils/__init__.py +4 -3
- utils/base_pipeline_stage.py +171 -171
- utils/base_ports.py +176 -176
- utils/benchmark_utils.py +823 -823
- utils/channel.py +74 -74
- utils/comparison_gates.py +40 -21
- utils/compute_benchmarks.py +355 -355
- utils/compute_identical.py +94 -24
- utils/compute_indices.py +235 -235
- utils/compute_perceptual_hash.py +127 -127
- utils/compute_perceptual_match.py +240 -240
- utils/compute_sha_bins.py +64 -20
- utils/compute_template_similarity.py +1 -1
- utils/compute_versions.py +483 -483
- utils/config.py +8 -5
- utils/data_io.py +83 -83
- utils/graph_context.py +44 -44
- utils/logger.py +2 -2
- utils/models.py +2 -2
- utils/photo_file.py +90 -91
- utils/pipeline_graph.py +334 -334
- utils/pipeline_stage.py +408 -408
- utils/plot_helpers.py +123 -123
- utils/ports.py +136 -136
- utils/progress.py +415 -415
- utils/report_builder.py +139 -139
- utils/review_types.py +55 -55
- utils/review_utils.py +10 -19
- utils/sequence.py +10 -8
- utils/sequence_clustering.py +1 -1
- utils/template.py +57 -57
- utils/template_parsing.py +71 -0
- photo_stack_finder-0.1.7.dist-info/RECORD +0 -74
- {photo_stack_finder-0.1.7.dist-info → photo_stack_finder-0.1.8.dist-info}/WHEEL +0 -0
- {photo_stack_finder-0.1.7.dist-info → photo_stack_finder-0.1.8.dist-info}/entry_points.txt +0 -0
- {photo_stack_finder-0.1.7.dist-info → photo_stack_finder-0.1.8.dist-info}/licenses/LICENSE +0 -0
- {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)
|
photo_compare/pixel_methods.py
CHANGED
|
@@ -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]
|