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
utils/report_builder.py
CHANGED
|
@@ -1,139 +1,139 @@
|
|
|
1
|
-
"""Report generation utility with fluent API for consistent formatting.
|
|
2
|
-
|
|
3
|
-
This module provides a ReportBuilder class that simplifies the creation of
|
|
4
|
-
text-based reports with consistent formatting across the codebase.
|
|
5
|
-
|
|
6
|
-
Usage:
|
|
7
|
-
report = (
|
|
8
|
-
ReportBuilder()
|
|
9
|
-
.add_title("Benchmark Results")
|
|
10
|
-
.add_section("Performance Metrics")
|
|
11
|
-
.add_metric("Accuracy", 0.95, ".2%")
|
|
12
|
-
.add_blank_line()
|
|
13
|
-
.build()
|
|
14
|
-
)
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
from typing import Any, Self
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class ReportBuilder:
|
|
22
|
-
"""Fluent API for building text reports with consistent formatting.
|
|
23
|
-
|
|
24
|
-
Provides a chainable interface for adding formatted sections, metrics,
|
|
25
|
-
and separators to text reports. All methods return self to enable
|
|
26
|
-
method chaining.
|
|
27
|
-
|
|
28
|
-
Example:
|
|
29
|
-
>>> report = (
|
|
30
|
-
... ReportBuilder()
|
|
31
|
-
... .add_title("Results")
|
|
32
|
-
... .add_metric("Score", 0.85, ".2f")
|
|
33
|
-
... .build()
|
|
34
|
-
... )
|
|
35
|
-
"""
|
|
36
|
-
|
|
37
|
-
def __init__(self) -> None:
|
|
38
|
-
"""Initialize an empty report builder."""
|
|
39
|
-
self._lines: list[str] = []
|
|
40
|
-
|
|
41
|
-
def add_title(self, text: str, width: int = 80) -> Self:
|
|
42
|
-
"""Add a centered title with separator lines above and below.
|
|
43
|
-
|
|
44
|
-
Args:
|
|
45
|
-
text: The title text to display
|
|
46
|
-
width: Width of the separator lines (default: 80)
|
|
47
|
-
|
|
48
|
-
Returns:
|
|
49
|
-
Self for method chaining
|
|
50
|
-
"""
|
|
51
|
-
self._lines.append("=" * width)
|
|
52
|
-
self._lines.append(text.center(width))
|
|
53
|
-
self._lines.append("=" * width)
|
|
54
|
-
return self
|
|
55
|
-
|
|
56
|
-
def add_section(self, header: str) -> Self:
|
|
57
|
-
"""Add a section header with a single underline.
|
|
58
|
-
|
|
59
|
-
Args:
|
|
60
|
-
header: The section header text
|
|
61
|
-
|
|
62
|
-
Returns:
|
|
63
|
-
Self for method chaining
|
|
64
|
-
"""
|
|
65
|
-
self._lines.append("")
|
|
66
|
-
self._lines.append(header)
|
|
67
|
-
self._lines.append("-" * len(header))
|
|
68
|
-
return self
|
|
69
|
-
|
|
70
|
-
def add_metric(self, name: str, value: Any, format_spec: str = "") -> Self:
|
|
71
|
-
"""Add a formatted metric line.
|
|
72
|
-
|
|
73
|
-
Args:
|
|
74
|
-
name: The metric name (will be left-aligned)
|
|
75
|
-
value: The metric value to format
|
|
76
|
-
format_spec: Optional format specification (e.g., ".2f", ".2%")
|
|
77
|
-
|
|
78
|
-
Returns:
|
|
79
|
-
Self for method chaining
|
|
80
|
-
|
|
81
|
-
Example:
|
|
82
|
-
>>> builder.add_metric("Accuracy", 0.95, ".2%") # "Accuracy: 95.00%"
|
|
83
|
-
"""
|
|
84
|
-
if format_spec:
|
|
85
|
-
formatted_value = f"{value:{format_spec}}"
|
|
86
|
-
else:
|
|
87
|
-
formatted_value = str(value)
|
|
88
|
-
self._lines.append(f"{name}: {formatted_value}")
|
|
89
|
-
return self
|
|
90
|
-
|
|
91
|
-
def add_blank_line(self) -> Self:
|
|
92
|
-
"""Add a blank line for spacing.
|
|
93
|
-
|
|
94
|
-
Returns:
|
|
95
|
-
Self for method chaining
|
|
96
|
-
"""
|
|
97
|
-
self._lines.append("")
|
|
98
|
-
return self
|
|
99
|
-
|
|
100
|
-
def add_separator(self, char: str = "=", width: int = 80) -> Self:
|
|
101
|
-
"""Add a separator line.
|
|
102
|
-
|
|
103
|
-
Args:
|
|
104
|
-
char: The character to use for the separator (default: "=")
|
|
105
|
-
width: Width of the separator line (default: 80)
|
|
106
|
-
|
|
107
|
-
Returns:
|
|
108
|
-
Self for method chaining
|
|
109
|
-
"""
|
|
110
|
-
self._lines.append(char * width)
|
|
111
|
-
return self
|
|
112
|
-
|
|
113
|
-
def add_text(self, text: str) -> Self:
|
|
114
|
-
"""Add arbitrary text (can be multi-line).
|
|
115
|
-
|
|
116
|
-
Args:
|
|
117
|
-
text: The text to add (newlines will be preserved)
|
|
118
|
-
|
|
119
|
-
Returns:
|
|
120
|
-
Self for method chaining
|
|
121
|
-
"""
|
|
122
|
-
self._lines.append(text)
|
|
123
|
-
return self
|
|
124
|
-
|
|
125
|
-
def build(self) -> str:
|
|
126
|
-
"""Build and return the complete report as a string.
|
|
127
|
-
|
|
128
|
-
Returns:
|
|
129
|
-
The formatted report with newline-separated lines
|
|
130
|
-
"""
|
|
131
|
-
return "\n".join(self._lines)
|
|
132
|
-
|
|
133
|
-
def save(self, path: Path) -> None:
|
|
134
|
-
"""Build the report and save it to a file.
|
|
135
|
-
|
|
136
|
-
Args:
|
|
137
|
-
path: The file path where the report should be saved
|
|
138
|
-
"""
|
|
139
|
-
path.write_text(self.build(), encoding="utf-8")
|
|
1
|
+
"""Report generation utility with fluent API for consistent formatting.
|
|
2
|
+
|
|
3
|
+
This module provides a ReportBuilder class that simplifies the creation of
|
|
4
|
+
text-based reports with consistent formatting across the codebase.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
report = (
|
|
8
|
+
ReportBuilder()
|
|
9
|
+
.add_title("Benchmark Results")
|
|
10
|
+
.add_section("Performance Metrics")
|
|
11
|
+
.add_metric("Accuracy", 0.95, ".2%")
|
|
12
|
+
.add_blank_line()
|
|
13
|
+
.build()
|
|
14
|
+
)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Self
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ReportBuilder:
|
|
22
|
+
"""Fluent API for building text reports with consistent formatting.
|
|
23
|
+
|
|
24
|
+
Provides a chainable interface for adding formatted sections, metrics,
|
|
25
|
+
and separators to text reports. All methods return self to enable
|
|
26
|
+
method chaining.
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
>>> report = (
|
|
30
|
+
... ReportBuilder()
|
|
31
|
+
... .add_title("Results")
|
|
32
|
+
... .add_metric("Score", 0.85, ".2f")
|
|
33
|
+
... .build()
|
|
34
|
+
... )
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self) -> None:
|
|
38
|
+
"""Initialize an empty report builder."""
|
|
39
|
+
self._lines: list[str] = []
|
|
40
|
+
|
|
41
|
+
def add_title(self, text: str, width: int = 80) -> Self:
|
|
42
|
+
"""Add a centered title with separator lines above and below.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
text: The title text to display
|
|
46
|
+
width: Width of the separator lines (default: 80)
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Self for method chaining
|
|
50
|
+
"""
|
|
51
|
+
self._lines.append("=" * width)
|
|
52
|
+
self._lines.append(text.center(width))
|
|
53
|
+
self._lines.append("=" * width)
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
def add_section(self, header: str) -> Self:
|
|
57
|
+
"""Add a section header with a single underline.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
header: The section header text
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Self for method chaining
|
|
64
|
+
"""
|
|
65
|
+
self._lines.append("")
|
|
66
|
+
self._lines.append(header)
|
|
67
|
+
self._lines.append("-" * len(header))
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def add_metric(self, name: str, value: Any, format_spec: str = "") -> Self:
|
|
71
|
+
"""Add a formatted metric line.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
name: The metric name (will be left-aligned)
|
|
75
|
+
value: The metric value to format
|
|
76
|
+
format_spec: Optional format specification (e.g., ".2f", ".2%")
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Self for method chaining
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
>>> builder.add_metric("Accuracy", 0.95, ".2%") # "Accuracy: 95.00%"
|
|
83
|
+
"""
|
|
84
|
+
if format_spec:
|
|
85
|
+
formatted_value = f"{value:{format_spec}}"
|
|
86
|
+
else:
|
|
87
|
+
formatted_value = str(value)
|
|
88
|
+
self._lines.append(f"{name}: {formatted_value}")
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
def add_blank_line(self) -> Self:
|
|
92
|
+
"""Add a blank line for spacing.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Self for method chaining
|
|
96
|
+
"""
|
|
97
|
+
self._lines.append("")
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
def add_separator(self, char: str = "=", width: int = 80) -> Self:
|
|
101
|
+
"""Add a separator line.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
char: The character to use for the separator (default: "=")
|
|
105
|
+
width: Width of the separator line (default: 80)
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Self for method chaining
|
|
109
|
+
"""
|
|
110
|
+
self._lines.append(char * width)
|
|
111
|
+
return self
|
|
112
|
+
|
|
113
|
+
def add_text(self, text: str) -> Self:
|
|
114
|
+
"""Add arbitrary text (can be multi-line).
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
text: The text to add (newlines will be preserved)
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Self for method chaining
|
|
121
|
+
"""
|
|
122
|
+
self._lines.append(text)
|
|
123
|
+
return self
|
|
124
|
+
|
|
125
|
+
def build(self) -> str:
|
|
126
|
+
"""Build and return the complete report as a string.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
The formatted report with newline-separated lines
|
|
130
|
+
"""
|
|
131
|
+
return "\n".join(self._lines)
|
|
132
|
+
|
|
133
|
+
def save(self, path: Path) -> None:
|
|
134
|
+
"""Build the report and save it to a file.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
path: The file path where the report should be saved
|
|
138
|
+
"""
|
|
139
|
+
path.write_text(self.build(), encoding="utf-8")
|
utils/review_types.py
CHANGED
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
"""Type definitions for review decision persistence."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Literal, TypedDict
|
|
6
|
-
|
|
7
|
-
# Photo identifier using content hash + path (stable across runs)
|
|
8
|
-
PhotoIdentifier = tuple[str, str] # (sha256, relative_path)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class IdenticalDecision(TypedDict):
|
|
12
|
-
"""Decision for an identical group."""
|
|
13
|
-
|
|
14
|
-
type: Literal["identical"]
|
|
15
|
-
group_id: str # SHA256 of sorted photo sha256s
|
|
16
|
-
timestamp: str # ISO format
|
|
17
|
-
user: str
|
|
18
|
-
action: Literal["keep_all", "keep_exemplar", "delete_all", "custom"]
|
|
19
|
-
kept_photos: list[PhotoIdentifier] # Photos to keep (empty if keep_all)
|
|
20
|
-
deleted_photos: list[PhotoIdentifier] # Photos to delete (empty if keep_all)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class SequenceDecision(TypedDict):
|
|
24
|
-
"""Decision for a sequence similarity group."""
|
|
25
|
-
|
|
26
|
-
type: Literal["sequences"]
|
|
27
|
-
group_id: str # SHA256 of template + sorted photo sha256s
|
|
28
|
-
timestamp: str # ISO format
|
|
29
|
-
user: str
|
|
30
|
-
action: Literal["approved", "rejected"]
|
|
31
|
-
sequence_selections: dict[str, bool] # sequence_name -> included
|
|
32
|
-
deleted_photos: list[PhotoIdentifier] # Individual photos marked for deletion
|
|
33
|
-
deleted_rows: list[int] # Row positions marked for deletion
|
|
34
|
-
deleted_sequences: list[int] # Sequence indices marked for deletion
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class ReviewIndexEntry(TypedDict):
|
|
38
|
-
"""Entry in the review index (loaded from JSONL)."""
|
|
39
|
-
|
|
40
|
-
group_id: str
|
|
41
|
-
decision_type: Literal["identical", "sequences"]
|
|
42
|
-
action: str
|
|
43
|
-
timestamp: str
|
|
44
|
-
user: str
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class DeletionIndexEntry(TypedDict):
|
|
48
|
-
"""Entry in the deletion index."""
|
|
49
|
-
|
|
50
|
-
sha256: str
|
|
51
|
-
path: str
|
|
52
|
-
reason: str # "identical_group", "sequence_group", "individual"
|
|
53
|
-
group_id: str
|
|
54
|
-
timestamp: str
|
|
55
|
-
user: str
|
|
1
|
+
"""Type definitions for review decision persistence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal, TypedDict
|
|
6
|
+
|
|
7
|
+
# Photo identifier using content hash + path (stable across runs)
|
|
8
|
+
PhotoIdentifier = tuple[str, str] # (sha256, relative_path)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class IdenticalDecision(TypedDict):
|
|
12
|
+
"""Decision for an identical group."""
|
|
13
|
+
|
|
14
|
+
type: Literal["identical"]
|
|
15
|
+
group_id: str # SHA256 of sorted photo sha256s
|
|
16
|
+
timestamp: str # ISO format
|
|
17
|
+
user: str
|
|
18
|
+
action: Literal["keep_all", "keep_exemplar", "delete_all", "custom"]
|
|
19
|
+
kept_photos: list[PhotoIdentifier] # Photos to keep (empty if keep_all)
|
|
20
|
+
deleted_photos: list[PhotoIdentifier] # Photos to delete (empty if keep_all)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SequenceDecision(TypedDict):
|
|
24
|
+
"""Decision for a sequence similarity group."""
|
|
25
|
+
|
|
26
|
+
type: Literal["sequences"]
|
|
27
|
+
group_id: str # SHA256 of template + sorted photo sha256s
|
|
28
|
+
timestamp: str # ISO format
|
|
29
|
+
user: str
|
|
30
|
+
action: Literal["approved", "rejected"]
|
|
31
|
+
sequence_selections: dict[str, bool] # sequence_name -> included
|
|
32
|
+
deleted_photos: list[PhotoIdentifier] # Individual photos marked for deletion
|
|
33
|
+
deleted_rows: list[int] # Row positions marked for deletion
|
|
34
|
+
deleted_sequences: list[int] # Sequence indices marked for deletion
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ReviewIndexEntry(TypedDict):
|
|
38
|
+
"""Entry in the review index (loaded from JSONL)."""
|
|
39
|
+
|
|
40
|
+
group_id: str
|
|
41
|
+
decision_type: Literal["identical", "sequences"]
|
|
42
|
+
action: str
|
|
43
|
+
timestamp: str
|
|
44
|
+
user: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DeletionIndexEntry(TypedDict):
|
|
48
|
+
"""Entry in the deletion index."""
|
|
49
|
+
|
|
50
|
+
sha256: str
|
|
51
|
+
path: str
|
|
52
|
+
reason: str # "identical_group", "sequence_group", "individual"
|
|
53
|
+
group_id: str
|
|
54
|
+
timestamp: str
|
|
55
|
+
user: str
|
utils/review_utils.py
CHANGED
|
@@ -32,18 +32,16 @@ from .sequence import (
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
def build_identical_group(eq_class: list[PhotoFile], exemplar_id: int) -> IdenticalGroup:
|
|
35
|
-
"""Create review data structure from a list of identical photos.
|
|
35
|
+
"""Create review data structure from a list of identical photos.
|
|
36
|
+
|
|
37
|
+
Uses pre-computed dimensions from PhotoFile (no file I/O required).
|
|
38
|
+
"""
|
|
36
39
|
# Create stable group_id from sorted photo IDs
|
|
37
40
|
photo_ids: list[int] = sorted([pf.id for pf in eq_class])
|
|
38
41
|
group_id: str = hashlib.sha256("".join(str(id) for id in photo_ids).encode()).hexdigest()
|
|
39
42
|
|
|
40
43
|
photos: list[IdenticalPhoto] = []
|
|
41
44
|
for pf in eq_class:
|
|
42
|
-
# Get canonical dimensions (may trigger rotation detection if not cached)
|
|
43
|
-
with pf.image_data() as img:
|
|
44
|
-
width = img.get_width()
|
|
45
|
-
height = img.get_height()
|
|
46
|
-
|
|
47
45
|
# Production code: path should never be None
|
|
48
46
|
assert pf.path is not None, f"Photo {pf.id} has None path in production code"
|
|
49
47
|
|
|
@@ -52,10 +50,10 @@ def build_identical_group(eq_class: list[PhotoFile], exemplar_id: int) -> Identi
|
|
|
52
50
|
id=pf.id,
|
|
53
51
|
path=str(pf.path),
|
|
54
52
|
filename=pf.path.name,
|
|
55
|
-
is_exemplar="IDENTICAL" in pf.cache,
|
|
53
|
+
is_exemplar="IDENTICAL" not in pf.cache,
|
|
56
54
|
file_size=pf.size_bytes,
|
|
57
|
-
width=width,
|
|
58
|
-
height=height,
|
|
55
|
+
width=pf.width, # Use pre-computed dimension
|
|
56
|
+
height=pf.height, # Use pre-computed dimension
|
|
59
57
|
)
|
|
60
58
|
)
|
|
61
59
|
|
|
@@ -183,16 +181,9 @@ def _dataframe_to_group_dict(df: pd.DataFrame, reference: PhotoFileSeries) -> Se
|
|
|
183
181
|
# In production, path is never None, but test fixtures need this
|
|
184
182
|
filename = photo.path.name if photo.path is not None else f"test_photo_{photo.id}.jpg"
|
|
185
183
|
|
|
186
|
-
#
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
with photo.image_data() as img:
|
|
190
|
-
width = img.get_width()
|
|
191
|
-
height = img.get_height()
|
|
192
|
-
else:
|
|
193
|
-
# Test fixture: estimate dimensions from pixels assuming square
|
|
194
|
-
width = int(math.sqrt(photo.pixels))
|
|
195
|
-
height = int(math.sqrt(photo.pixels))
|
|
184
|
+
# Use pre-computed dimensions (loaded during PhotoFile.__init__)
|
|
185
|
+
width = photo.width
|
|
186
|
+
height = photo.height
|
|
196
187
|
|
|
197
188
|
photos.append(
|
|
198
189
|
SequencePhoto(
|
utils/sequence.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
"""Sequence data structures for photo
|
|
1
|
+
"""Sequence data structures for photo stack finding pipeline.
|
|
2
2
|
|
|
3
3
|
This module provides dict-based data structures that replace pandas for type safety
|
|
4
4
|
and pickle reliability, while maintaining minimal pandas usage for specific algorithms.
|
|
5
5
|
|
|
6
6
|
Core Types:
|
|
7
|
-
INDEX_T: Type alias for tuple[str, ...], used as multi-field index keys
|
|
7
|
+
INDEX_T: Type alias for tuple[str, ...] (from template_parsing), used as multi-field index keys
|
|
8
8
|
PhotoFileSeries: dict[INDEX_T, PhotoFile] with template name and pd.Series-like API
|
|
9
9
|
PhotoSequence: Hierarchical forest structure containing reference + similar sequences
|
|
10
10
|
|
|
@@ -54,12 +54,15 @@ import pandas as pd
|
|
|
54
54
|
from .comparison_gates import GateSequence
|
|
55
55
|
from .config import CONFIG
|
|
56
56
|
from .photo_file import PhotoFile
|
|
57
|
+
from .template_parsing import INDEX_T
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
__all__ = [
|
|
60
|
+
"INDEX_T",
|
|
61
|
+
"PhotoFileSeries",
|
|
62
|
+
"PhotoSequence",
|
|
63
|
+
"count_forest_ref_photos",
|
|
64
|
+
"count_forest_total_photos",
|
|
65
|
+
]
|
|
63
66
|
|
|
64
67
|
|
|
65
68
|
class PhotoFileSeries(dict[INDEX_T, PhotoFile]):
|
|
@@ -844,6 +847,5 @@ def extend_reference_sequence(
|
|
|
844
847
|
return result
|
|
845
848
|
|
|
846
849
|
|
|
847
|
-
|
|
848
850
|
# Moved to sequence_clustering.py to avoid circular import with review_utils
|
|
849
851
|
# Import from sequence_clustering instead: from .sequence_clustering import cluster_similar_sequences
|
utils/sequence_clustering.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Sequence clustering algorithm for photo
|
|
1
|
+
"""Sequence clustering algorithm for photo stack finding pipeline.
|
|
2
2
|
|
|
3
3
|
This module provides the core clustering algorithm used by multiple pipeline stages
|
|
4
4
|
(ComputeIndices, ComputeTemplateSimilarity, ComputePerceptualMatch) to group similar
|
utils/template.py
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
"""Template utilities for partial string substitution.
|
|
2
|
-
|
|
3
|
-
Provides template extraction and partial substitution using str.format_map().
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class DefaultDict(dict[str, str]):
|
|
10
|
-
"""Dictionary that returns placeholder for missing keys during format_map().
|
|
11
|
-
|
|
12
|
-
This allows partial substitution of template strings, leaving unmatched
|
|
13
|
-
placeholders in their original form.
|
|
14
|
-
|
|
15
|
-
Example:
|
|
16
|
-
>>> template = "IMG_{P0}_{P1}_{P2}"
|
|
17
|
-
>>> values = {'P0': '1234', 'P1': '5678'}
|
|
18
|
-
>>> result = template.format_map(DefaultDict(values))
|
|
19
|
-
>>> print(result)
|
|
20
|
-
IMG_1234_5678_{P2}
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
def __missing__(self, key: str) -> str:
|
|
24
|
-
"""Return the key wrapped in braces for missing keys.
|
|
25
|
-
|
|
26
|
-
Args:
|
|
27
|
-
key: The missing key
|
|
28
|
-
|
|
29
|
-
Returns:
|
|
30
|
-
String of the form "{key}"
|
|
31
|
-
"""
|
|
32
|
-
return f"{{{key}}}"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def partial_format(template: str, values: dict[str, str]) -> str:
|
|
36
|
-
"""Perform partial string substitution on a template.
|
|
37
|
-
|
|
38
|
-
Substitutes available values and leaves missing placeholders unchanged.
|
|
39
|
-
|
|
40
|
-
Args:
|
|
41
|
-
template: Template string with {P0}, {P1}, etc. placeholders
|
|
42
|
-
values: Dictionary of values to substitute
|
|
43
|
-
|
|
44
|
-
Returns:
|
|
45
|
-
Partially formatted string
|
|
46
|
-
|
|
47
|
-
Example:
|
|
48
|
-
>>> partial_format("IMG_{P0}_{P1}_{P2}", {'P0': '1234', 'P1': '5678'})
|
|
49
|
-
'IMG_1234_5678_{P2}'
|
|
50
|
-
|
|
51
|
-
>>> partial_format("IMG_{P0}_{P1}", {'P0': '1234', 'P1': '5678'})
|
|
52
|
-
'IMG_1234_5678'
|
|
53
|
-
|
|
54
|
-
>>> partial_format("IMG_{P0}_{P1}_{P2}", {})
|
|
55
|
-
'IMG_{P0}_{P1}_{P2}'
|
|
56
|
-
"""
|
|
57
|
-
return template.format_map(DefaultDict(values))
|
|
1
|
+
"""Template utilities for partial string substitution.
|
|
2
|
+
|
|
3
|
+
Provides template extraction and partial substitution using str.format_map().
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DefaultDict(dict[str, str]):
|
|
10
|
+
"""Dictionary that returns placeholder for missing keys during format_map().
|
|
11
|
+
|
|
12
|
+
This allows partial substitution of template strings, leaving unmatched
|
|
13
|
+
placeholders in their original form.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
>>> template = "IMG_{P0}_{P1}_{P2}"
|
|
17
|
+
>>> values = {'P0': '1234', 'P1': '5678'}
|
|
18
|
+
>>> result = template.format_map(DefaultDict(values))
|
|
19
|
+
>>> print(result)
|
|
20
|
+
IMG_1234_5678_{P2}
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __missing__(self, key: str) -> str:
|
|
24
|
+
"""Return the key wrapped in braces for missing keys.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
key: The missing key
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
String of the form "{key}"
|
|
31
|
+
"""
|
|
32
|
+
return f"{{{key}}}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def partial_format(template: str, values: dict[str, str]) -> str:
|
|
36
|
+
"""Perform partial string substitution on a template.
|
|
37
|
+
|
|
38
|
+
Substitutes available values and leaves missing placeholders unchanged.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
template: Template string with {P0}, {P1}, etc. placeholders
|
|
42
|
+
values: Dictionary of values to substitute
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Partially formatted string
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
>>> partial_format("IMG_{P0}_{P1}_{P2}", {'P0': '1234', 'P1': '5678'})
|
|
49
|
+
'IMG_1234_5678_{P2}'
|
|
50
|
+
|
|
51
|
+
>>> partial_format("IMG_{P0}_{P1}", {'P0': '1234', 'P1': '5678'})
|
|
52
|
+
'IMG_1234_5678'
|
|
53
|
+
|
|
54
|
+
>>> partial_format("IMG_{P0}_{P1}_{P2}", {})
|
|
55
|
+
'IMG_{P0}_{P1}_{P2}'
|
|
56
|
+
"""
|
|
57
|
+
return template.format_map(DefaultDict(values))
|