Spaces:
Running
Running
GitHub Action commited on
Commit ·
4e9afb0
1
Parent(s): 188604c
Auto-deploy from GitHub Actions
Browse files- app.py +8 -5
- coverage.xml +75 -85
- magiceye_solve/solver.py +103 -90
app.py
CHANGED
|
@@ -11,6 +11,9 @@ from typing import Tuple
|
|
| 11 |
import warnings
|
| 12 |
warnings.filterwarnings("ignore", message=".*Trying to detect.*share=True.*")
|
| 13 |
|
|
|
|
|
|
|
|
|
|
| 14 |
def create_autocorrelation_plot(autocorrelation_curve: np.ndarray, current_slider_offset: int, solver_calculated_offset: int, peak_diffs: np.ndarray, peak_indices: np.ndarray = None) -> plt.Figure:
|
| 15 |
"""
|
| 16 |
Generates a matplotlib plot of the autocorrelation curve with vertical lines
|
|
@@ -67,9 +70,6 @@ def create_autocorrelation_plot(autocorrelation_curve: np.ndarray, current_slide
|
|
| 67 |
colors[selected_peak_idx] = 'r' # Highlight the selected peak in red
|
| 68 |
sizes[selected_peak_idx] = 60 # Make the selected peak larger
|
| 69 |
|
| 70 |
-
# Plot all peaks
|
| 71 |
-
#ax.scatter(shifted_peak_indices, peak_y_values, c=colors, s=sizes, marker='o', label='Detected Peaks')
|
| 72 |
-
|
| 73 |
# Specifically label the peak used for the selected offset
|
| 74 |
if selected_peak_idx is not None:
|
| 75 |
peak_idx = peak_indices[selected_peak_idx]
|
|
@@ -102,7 +102,9 @@ def create_autocorrelation_plot(autocorrelation_curve: np.ndarray, current_slide
|
|
| 102 |
|
| 103 |
# Set appropriate x-axis limits to focus on relevant part of the curve
|
| 104 |
# Use this same limit for the slider's range
|
| 105 |
-
x_limit
|
|
|
|
|
|
|
| 106 |
ax.set_xlim([0, x_limit])
|
| 107 |
|
| 108 |
# Add title and labels
|
|
@@ -163,7 +165,8 @@ def process_image(uploaded_image: np.ndarray):
|
|
| 163 |
solver = InteractiveSolver(uploaded_image)
|
| 164 |
|
| 165 |
min_offset = 1
|
| 166 |
-
max_offset
|
|
|
|
| 167 |
default_offset = solver.default_offset
|
| 168 |
default_offset = max(min_offset, min(default_offset, max_offset))
|
| 169 |
|
|
|
|
| 11 |
import warnings
|
| 12 |
warnings.filterwarnings("ignore", message=".*Trying to detect.*share=True.*")
|
| 13 |
|
| 14 |
+
# constants
|
| 15 |
+
MAX_OFFSET_DISPLAY_LIMIT = 700 # max value for display and slider range for offset
|
| 16 |
+
|
| 17 |
def create_autocorrelation_plot(autocorrelation_curve: np.ndarray, current_slider_offset: int, solver_calculated_offset: int, peak_diffs: np.ndarray, peak_indices: np.ndarray = None) -> plt.Figure:
|
| 18 |
"""
|
| 19 |
Generates a matplotlib plot of the autocorrelation curve with vertical lines
|
|
|
|
| 70 |
colors[selected_peak_idx] = 'r' # Highlight the selected peak in red
|
| 71 |
sizes[selected_peak_idx] = 60 # Make the selected peak larger
|
| 72 |
|
|
|
|
|
|
|
|
|
|
| 73 |
# Specifically label the peak used for the selected offset
|
| 74 |
if selected_peak_idx is not None:
|
| 75 |
peak_idx = peak_indices[selected_peak_idx]
|
|
|
|
| 102 |
|
| 103 |
# Set appropriate x-axis limits to focus on relevant part of the curve
|
| 104 |
# Use this same limit for the slider's range
|
| 105 |
+
# ensure x_limit is at least a small positive number if offsets are 0 or very small
|
| 106 |
+
relevant_offset_max = max(10, current_slider_offset * 2, solver_calculated_offset * 2) # ensure a minimum sensible view
|
| 107 |
+
x_limit = min(MAX_OFFSET_DISPLAY_LIMIT, relevant_offset_max)
|
| 108 |
ax.set_xlim([0, x_limit])
|
| 109 |
|
| 110 |
# Add title and labels
|
|
|
|
| 165 |
solver = InteractiveSolver(uploaded_image)
|
| 166 |
|
| 167 |
min_offset = 1
|
| 168 |
+
# ensure max_offset is at least min_offset and considers solver's suggestion
|
| 169 |
+
max_offset = min(MAX_OFFSET_DISPLAY_LIMIT, max(min_offset +1 , solver.default_offset * 2))
|
| 170 |
default_offset = solver.default_offset
|
| 171 |
default_offset = max(min_offset, min(default_offset, max_offset))
|
| 172 |
|
coverage.xml
CHANGED
|
@@ -1,12 +1,12 @@
|
|
| 1 |
<?xml version="1.0" ?>
|
| 2 |
-
<coverage version="7.8.2" timestamp="
|
| 3 |
<!-- Generated by coverage.py: https://coverage.readthedocs.io/en/7.8.2 -->
|
| 4 |
<!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd -->
|
| 5 |
<sources>
|
| 6 |
<source>/home/runner/work/magiceye-solver/magiceye-solver</source>
|
| 7 |
</sources>
|
| 8 |
<packages>
|
| 9 |
-
<package name="magiceye_solve" line-rate="0.
|
| 10 |
<classes>
|
| 11 |
<class name="__init__.py" filename="magiceye_solve/__init__.py" complexity="0" line-rate="1" branch-rate="0">
|
| 12 |
<methods/>
|
|
@@ -14,7 +14,7 @@
|
|
| 14 |
<line number="1" hits="1"/>
|
| 15 |
</lines>
|
| 16 |
</class>
|
| 17 |
-
<class name="solver.py" filename="magiceye_solve/solver.py" complexity="0" line-rate="0.
|
| 18 |
<methods/>
|
| 19 |
<lines>
|
| 20 |
<line number="1" hits="1"/>
|
|
@@ -33,83 +33,81 @@
|
|
| 33 |
<line number="15" hits="0"/>
|
| 34 |
<line number="16" hits="0"/>
|
| 35 |
<line number="18" hits="1"/>
|
| 36 |
-
<line number="
|
| 37 |
-
<line number="
|
| 38 |
-
<line number="
|
| 39 |
-
<line number="
|
| 40 |
-
<line number="
|
| 41 |
<line number="31" hits="1"/>
|
| 42 |
-
<line number="32" hits="0"/>
|
| 43 |
<line number="33" hits="1"/>
|
| 44 |
-
<line number="
|
| 45 |
<line number="36" hits="1"/>
|
| 46 |
-
<line number="
|
| 47 |
<line number="39" hits="1"/>
|
| 48 |
-
<line number="
|
| 49 |
<line number="42" hits="1"/>
|
| 50 |
-
<line number="
|
| 51 |
<line number="45" hits="1"/>
|
| 52 |
-
<line number="46" hits="
|
| 53 |
-
<line number="47" hits="
|
|
|
|
|
|
|
| 54 |
<line number="50" hits="1"/>
|
| 55 |
<line number="51" hits="1"/>
|
| 56 |
-
<line number="
|
| 57 |
-
<line number="
|
|
|
|
| 58 |
<line number="56" hits="1"/>
|
| 59 |
-
<line number="57" hits="1"/>
|
| 60 |
<line number="58" hits="1"/>
|
| 61 |
-
<line number="59" hits="
|
| 62 |
-
<line number="
|
| 63 |
-
<line number="
|
| 64 |
-
<line number="63" hits="0"/>
|
| 65 |
<line number="64" hits="0"/>
|
| 66 |
-
<line number="
|
| 67 |
-
<line number="
|
| 68 |
<line number="68" hits="0"/>
|
| 69 |
<line number="69" hits="0"/>
|
| 70 |
-
<line number="
|
| 71 |
-
<line number="
|
| 72 |
-
<line number="
|
| 73 |
-
<line number="75" hits="
|
| 74 |
-
<line number="
|
| 75 |
-
<line number="78" hits="1"/>
|
| 76 |
<line number="80" hits="1"/>
|
| 77 |
<line number="82" hits="1"/>
|
| 78 |
-
<line number="
|
| 79 |
-
<line number="87" hits="1"/>
|
| 80 |
<line number="88" hits="1"/>
|
| 81 |
<line number="89" hits="1"/>
|
| 82 |
<line number="90" hits="1"/>
|
|
|
|
| 83 |
<line number="92" hits="1"/>
|
| 84 |
-
<line number="93" hits="1"/>
|
| 85 |
<line number="94" hits="1"/>
|
| 86 |
<line number="95" hits="1"/>
|
|
|
|
| 87 |
<line number="97" hits="1"/>
|
| 88 |
-
<line number="
|
| 89 |
-
<line number="102" hits="1"/>
|
| 90 |
<line number="103" hits="1"/>
|
| 91 |
<line number="104" hits="1"/>
|
| 92 |
<line number="105" hits="1"/>
|
| 93 |
<line number="106" hits="1"/>
|
| 94 |
<line number="107" hits="1"/>
|
| 95 |
<line number="108" hits="1"/>
|
| 96 |
-
<line number="109" hits="
|
| 97 |
-
<line number="110" hits="
|
| 98 |
-
<line number="
|
| 99 |
-
<line number="
|
| 100 |
-
<line number="
|
| 101 |
-
<line number="
|
| 102 |
<line number="126" hits="1"/>
|
| 103 |
-
<line number="127" hits="
|
| 104 |
-
<line number="
|
| 105 |
-
<line number="
|
| 106 |
-
<line number="
|
| 107 |
-
<line number="
|
| 108 |
<line number="134" hits="1"/>
|
| 109 |
-
<line number="
|
| 110 |
-
<line number="
|
|
|
|
| 111 |
<line number="140" hits="1"/>
|
| 112 |
-
<line number="141" hits="1"/>
|
| 113 |
<line number="142" hits="1"/>
|
| 114 |
<line number="143" hits="1"/>
|
| 115 |
<line number="144" hits="1"/>
|
|
@@ -117,67 +115,59 @@
|
|
| 117 |
<line number="146" hits="1"/>
|
| 118 |
<line number="147" hits="1"/>
|
| 119 |
<line number="148" hits="1"/>
|
| 120 |
-
<line number="
|
| 121 |
-
<line number="
|
| 122 |
-
<line number="
|
| 123 |
<line number="154" hits="1"/>
|
|
|
|
| 124 |
<line number="156" hits="1"/>
|
| 125 |
-
<line number="157" hits="1"/>
|
| 126 |
<line number="158" hits="1"/>
|
| 127 |
<line number="159" hits="1"/>
|
|
|
|
| 128 |
<line number="161" hits="1"/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
<line number="174" hits="1"/>
|
| 130 |
<line number="175" hits="1"/>
|
| 131 |
<line number="176" hits="1"/>
|
| 132 |
-
<line number="
|
| 133 |
-
<line number="180" hits="
|
| 134 |
<line number="181" hits="1"/>
|
| 135 |
-
<line number="
|
| 136 |
-
<line number="184" hits="
|
| 137 |
<line number="185" hits="1"/>
|
| 138 |
<line number="186" hits="1"/>
|
| 139 |
<line number="187" hits="1"/>
|
| 140 |
-
<line number="188" hits="1"/>
|
| 141 |
<line number="189" hits="1"/>
|
| 142 |
-
<line number="190" hits="1"/>
|
| 143 |
-
<line number="191" hits="1"/>
|
| 144 |
-
<line number="192" hits="1"/>
|
| 145 |
-
<line number="193" hits="1"/>
|
| 146 |
-
<line number="194" hits="1"/>
|
| 147 |
-
<line number="195" hits="1"/>
|
| 148 |
-
<line number="196" hits="1"/>
|
| 149 |
-
<line number="197" hits="1"/>
|
| 150 |
-
<line number="198" hits="1"/>
|
| 151 |
-
<line number="200" hits="1"/>
|
| 152 |
-
<line number="201" hits="1"/>
|
| 153 |
<line number="202" hits="1"/>
|
| 154 |
-
<line number="203" hits="1"/>
|
| 155 |
-
<line number="204" hits="1"/>
|
| 156 |
-
<line number="205" hits="0"/>
|
| 157 |
-
<line number="206" hits="1"/>
|
| 158 |
-
<line number="207" hits="0"/>
|
| 159 |
<line number="208" hits="1"/>
|
| 160 |
-
<line number="209" hits="1"/>
|
| 161 |
<line number="210" hits="1"/>
|
| 162 |
<line number="211" hits="1"/>
|
| 163 |
-
<line number="212" hits="1"/>
|
| 164 |
<line number="213" hits="1"/>
|
| 165 |
<line number="214" hits="1"/>
|
| 166 |
<line number="215" hits="1"/>
|
| 167 |
-
<line number="216" hits="1"/>
|
| 168 |
<line number="217" hits="1"/>
|
| 169 |
-
<line number="
|
| 170 |
-
<line number="
|
| 171 |
-
<line number="220" hits="0"/>
|
| 172 |
-
<line number="221" hits="0"/>
|
| 173 |
-
<line number="222" hits="1"/>
|
| 174 |
<line number="223" hits="1"/>
|
| 175 |
<line number="224" hits="1"/>
|
| 176 |
-
<line number="
|
| 177 |
-
<line number="
|
| 178 |
-
<line number="227" hits="0"/>
|
| 179 |
<line number="228" hits="1"/>
|
|
|
|
| 180 |
<line number="231" hits="1"/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
</lines>
|
| 182 |
</class>
|
| 183 |
</classes>
|
|
|
|
| 1 |
<?xml version="1.0" ?>
|
| 2 |
+
<coverage version="7.8.2" timestamp="1748050735477" lines-valid="394" lines-covered="367" line-rate="0.9315" branches-covered="0" branches-valid="0" branch-rate="0" complexity="0">
|
| 3 |
<!-- Generated by coverage.py: https://coverage.readthedocs.io/en/7.8.2 -->
|
| 4 |
<!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd -->
|
| 5 |
<sources>
|
| 6 |
<source>/home/runner/work/magiceye-solver/magiceye-solver</source>
|
| 7 |
</sources>
|
| 8 |
<packages>
|
| 9 |
+
<package name="magiceye_solve" line-rate="0.8421" branch-rate="0" complexity="0">
|
| 10 |
<classes>
|
| 11 |
<class name="__init__.py" filename="magiceye_solve/__init__.py" complexity="0" line-rate="1" branch-rate="0">
|
| 12 |
<methods/>
|
|
|
|
| 14 |
<line number="1" hits="1"/>
|
| 15 |
</lines>
|
| 16 |
</class>
|
| 17 |
+
<class name="solver.py" filename="magiceye_solve/solver.py" complexity="0" line-rate="0.8411" branch-rate="0">
|
| 18 |
<methods/>
|
| 19 |
<lines>
|
| 20 |
<line number="1" hits="1"/>
|
|
|
|
| 33 |
<line number="15" hits="0"/>
|
| 34 |
<line number="16" hits="0"/>
|
| 35 |
<line number="18" hits="1"/>
|
| 36 |
+
<line number="21" hits="1"/>
|
| 37 |
+
<line number="22" hits="1"/>
|
| 38 |
+
<line number="23" hits="1"/>
|
| 39 |
+
<line number="25" hits="1"/>
|
| 40 |
+
<line number="30" hits="1"/>
|
| 41 |
<line number="31" hits="1"/>
|
|
|
|
| 42 |
<line number="33" hits="1"/>
|
| 43 |
+
<line number="34" hits="1"/>
|
| 44 |
<line number="36" hits="1"/>
|
| 45 |
+
<line number="37" hits="0"/>
|
| 46 |
<line number="39" hits="1"/>
|
| 47 |
+
<line number="41" hits="1"/>
|
| 48 |
<line number="42" hits="1"/>
|
| 49 |
+
<line number="44" hits="1"/>
|
| 50 |
<line number="45" hits="1"/>
|
| 51 |
+
<line number="46" hits="1"/>
|
| 52 |
+
<line number="47" hits="1"/>
|
| 53 |
+
<line number="48" hits="1"/>
|
| 54 |
+
<line number="49" hits="1"/>
|
| 55 |
<line number="50" hits="1"/>
|
| 56 |
<line number="51" hits="1"/>
|
| 57 |
+
<line number="52" hits="0"/>
|
| 58 |
+
<line number="53" hits="0"/>
|
| 59 |
+
<line number="55" hits="1"/>
|
| 60 |
<line number="56" hits="1"/>
|
|
|
|
| 61 |
<line number="58" hits="1"/>
|
| 62 |
+
<line number="59" hits="1"/>
|
| 63 |
+
<line number="61" hits="1"/>
|
| 64 |
+
<line number="63" hits="1"/>
|
|
|
|
| 65 |
<line number="64" hits="0"/>
|
| 66 |
+
<line number="65" hits="0"/>
|
| 67 |
+
<line number="66" hits="0"/>
|
| 68 |
<line number="68" hits="0"/>
|
| 69 |
<line number="69" hits="0"/>
|
| 70 |
+
<line number="72" hits="1"/>
|
| 71 |
+
<line number="73" hits="0"/>
|
| 72 |
+
<line number="74" hits="0"/>
|
| 73 |
+
<line number="75" hits="0"/>
|
| 74 |
+
<line number="79" hits="1"/>
|
|
|
|
| 75 |
<line number="80" hits="1"/>
|
| 76 |
<line number="82" hits="1"/>
|
| 77 |
+
<line number="84" hits="1"/>
|
|
|
|
| 78 |
<line number="88" hits="1"/>
|
| 79 |
<line number="89" hits="1"/>
|
| 80 |
<line number="90" hits="1"/>
|
| 81 |
+
<line number="91" hits="1"/>
|
| 82 |
<line number="92" hits="1"/>
|
|
|
|
| 83 |
<line number="94" hits="1"/>
|
| 84 |
<line number="95" hits="1"/>
|
| 85 |
+
<line number="96" hits="1"/>
|
| 86 |
<line number="97" hits="1"/>
|
| 87 |
+
<line number="99" hits="1"/>
|
|
|
|
| 88 |
<line number="103" hits="1"/>
|
| 89 |
<line number="104" hits="1"/>
|
| 90 |
<line number="105" hits="1"/>
|
| 91 |
<line number="106" hits="1"/>
|
| 92 |
<line number="107" hits="1"/>
|
| 93 |
<line number="108" hits="1"/>
|
| 94 |
+
<line number="109" hits="1"/>
|
| 95 |
+
<line number="110" hits="1"/>
|
| 96 |
+
<line number="111" hits="0"/>
|
| 97 |
+
<line number="112" hits="0"/>
|
| 98 |
+
<line number="114" hits="1"/>
|
| 99 |
+
<line number="118" hits="1"/>
|
| 100 |
<line number="126" hits="1"/>
|
| 101 |
+
<line number="127" hits="1"/>
|
| 102 |
+
<line number="128" hits="1"/>
|
| 103 |
+
<line number="129" hits="0"/>
|
| 104 |
+
<line number="131" hits="1"/>
|
| 105 |
+
<line number="132" hits="0"/>
|
| 106 |
<line number="134" hits="1"/>
|
| 107 |
+
<line number="135" hits="1"/>
|
| 108 |
+
<line number="136" hits="1"/>
|
| 109 |
+
<line number="139" hits="1"/>
|
| 110 |
<line number="140" hits="1"/>
|
|
|
|
| 111 |
<line number="142" hits="1"/>
|
| 112 |
<line number="143" hits="1"/>
|
| 113 |
<line number="144" hits="1"/>
|
|
|
|
| 115 |
<line number="146" hits="1"/>
|
| 116 |
<line number="147" hits="1"/>
|
| 117 |
<line number="148" hits="1"/>
|
| 118 |
+
<line number="149" hits="1"/>
|
| 119 |
+
<line number="150" hits="1"/>
|
| 120 |
+
<line number="152" hits="0"/>
|
| 121 |
<line number="154" hits="1"/>
|
| 122 |
+
<line number="155" hits="1"/>
|
| 123 |
<line number="156" hits="1"/>
|
|
|
|
| 124 |
<line number="158" hits="1"/>
|
| 125 |
<line number="159" hits="1"/>
|
| 126 |
+
<line number="160" hits="1"/>
|
| 127 |
<line number="161" hits="1"/>
|
| 128 |
+
<line number="163" hits="1"/>
|
| 129 |
+
<line number="165" hits="1"/>
|
| 130 |
+
<line number="166" hits="1"/>
|
| 131 |
+
<line number="168" hits="1"/>
|
| 132 |
+
<line number="169" hits="1"/>
|
| 133 |
+
<line number="170" hits="1"/>
|
| 134 |
+
<line number="172" hits="1"/>
|
| 135 |
+
<line number="173" hits="1"/>
|
| 136 |
<line number="174" hits="1"/>
|
| 137 |
<line number="175" hits="1"/>
|
| 138 |
<line number="176" hits="1"/>
|
| 139 |
+
<line number="179" hits="1"/>
|
| 140 |
+
<line number="180" hits="0"/>
|
| 141 |
<line number="181" hits="1"/>
|
| 142 |
+
<line number="183" hits="0"/>
|
| 143 |
+
<line number="184" hits="0"/>
|
| 144 |
<line number="185" hits="1"/>
|
| 145 |
<line number="186" hits="1"/>
|
| 146 |
<line number="187" hits="1"/>
|
|
|
|
| 147 |
<line number="189" hits="1"/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
<line number="202" hits="1"/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
<line number="208" hits="1"/>
|
|
|
|
| 150 |
<line number="210" hits="1"/>
|
| 151 |
<line number="211" hits="1"/>
|
|
|
|
| 152 |
<line number="213" hits="1"/>
|
| 153 |
<line number="214" hits="1"/>
|
| 154 |
<line number="215" hits="1"/>
|
|
|
|
| 155 |
<line number="217" hits="1"/>
|
| 156 |
+
<line number="220" hits="1"/>
|
| 157 |
+
<line number="221" hits="1"/>
|
|
|
|
|
|
|
|
|
|
| 158 |
<line number="223" hits="1"/>
|
| 159 |
<line number="224" hits="1"/>
|
| 160 |
+
<line number="226" hits="1"/>
|
| 161 |
+
<line number="227" hits="1"/>
|
|
|
|
| 162 |
<line number="228" hits="1"/>
|
| 163 |
+
<line number="229" hits="0"/>
|
| 164 |
<line number="231" hits="1"/>
|
| 165 |
+
<line number="234" hits="1"/>
|
| 166 |
+
<line number="235" hits="1"/>
|
| 167 |
+
<line number="236" hits="1"/>
|
| 168 |
+
<line number="237" hits="1"/>
|
| 169 |
+
<line number="240" hits="1"/>
|
| 170 |
+
<line number="244" hits="1"/>
|
| 171 |
</lines>
|
| 172 |
</class>
|
| 173 |
</classes>
|
magiceye_solve/solver.py
CHANGED
|
@@ -17,65 +17,67 @@ except ImportError:
|
|
| 17 |
|
| 18 |
from scipy.signal import fftconvolve
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
def offset(img: np.ndarray) -> Tuple[int, np.ndarray, np.ndarray, np.ndarray]:
|
| 21 |
"""
|
| 22 |
Calculates the offset that defines the stereoscopic effect.
|
| 23 |
-
|
| 24 |
"""
|
| 25 |
-
# Handle empty or 1D arrays gracefully
|
| 26 |
if img.size == 0 or img.ndim < 2:
|
| 27 |
-
return 0, np.array([]), np.array([]), np.array([])
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
| 31 |
if ac.ndim < 1 or ac.shape[0] < 1:
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
| 35 |
if ac_center_row.size == 0 or ac_center_row.std() == 0:
|
| 36 |
-
|
| 37 |
|
|
|
|
| 38 |
try:
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
idx: np.ndarray = np.where(ac_center_row - median_val > threshold)[0]
|
| 46 |
except (ValueError, FloatingPointError):
|
| 47 |
-
idx
|
| 48 |
|
| 49 |
-
# peak diffs for viz
|
| 50 |
diffs: np.ndarray = np.ediff1d(idx)
|
| 51 |
-
raw_offset: int = img.shape[1]
|
| 52 |
|
| 53 |
if idx.size > 0:
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
final_offset = max(1, min(raw_offset, img.shape[1]))
|
| 77 |
-
else:
|
| 78 |
-
final_offset = max(10, min(raw_offset, img.shape[1]))
|
| 79 |
|
| 80 |
return final_offset, ac_center_row, idx, diffs
|
| 81 |
|
|
@@ -158,6 +160,32 @@ class InteractiveSolver:
|
|
| 158 |
self.autocorrelation_peak_indices: np.ndarray = np.array([])
|
| 159 |
self.autocorrelation_peak_diffs: np.ndarray = np.array([])
|
| 160 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
def solve_with_offset(self, user_offset: int, channel_mode: str = 'separate') -> np.ndarray:
|
| 162 |
"""
|
| 163 |
Solves the autostereogram using a specified offset and channel handling mode.
|
|
@@ -172,60 +200,45 @@ class InteractiveSolver:
|
|
| 172 |
The solved image as a NumPy array.
|
| 173 |
"""
|
| 174 |
if user_offset <= 0:
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
|
|
|
| 182 |
final_width_per_channel = max(0, self.n - effective_gap)
|
| 183 |
|
| 184 |
if channel_mode == 'average':
|
| 185 |
img_to_process = np.mean(self.image, axis=2) if self.color_image else self.image
|
| 186 |
-
|
| 187 |
-
return np.zeros((self.m, final_width_per_channel), dtype=float)
|
| 188 |
-
shifted: np.ndarray = shift_pic(img_to_process, effective_gap)
|
| 189 |
-
if shifted.size == 0:
|
| 190 |
-
return np.zeros((self.m, 0), dtype=float)
|
| 191 |
-
try:
|
| 192 |
-
filt_1: np.ndarray = ndimage.prewitt(shifted)
|
| 193 |
-
filt_2: np.ndarray = ndimage.uniform_filter(filt_1, size=(5, 5))
|
| 194 |
-
if _SKIMAGE_AVAILABLE:
|
| 195 |
-
filt_2 = post_process(filt_2)
|
| 196 |
-
return filt_2
|
| 197 |
-
except Exception:
|
| 198 |
-
return np.zeros((self.m, final_width_per_channel), dtype=float)
|
| 199 |
|
| 200 |
elif channel_mode == 'separate':
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
for i in range(self.c):
|
| 203 |
-
|
| 204 |
-
if not self.color_image and i > 0:
|
| 205 |
-
break
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
try:
|
| 212 |
-
filt_1: np.ndarray = ndimage.prewitt(shifted)
|
| 213 |
-
filt_2: np.ndarray = ndimage.uniform_filter(filt_1, size=(5, 5))
|
| 214 |
-
if _SKIMAGE_AVAILABLE:
|
| 215 |
-
filt_2 = post_process(filt_2)
|
| 216 |
-
filt_m, filt_n = filt_2.shape
|
| 217 |
-
if filt_n != final_width_per_channel:
|
| 218 |
-
if filt_n < final_width_per_channel:
|
| 219 |
-
continue
|
| 220 |
-
filt_2 = filt_2[:, :final_width_per_channel]
|
| 221 |
-
filt_n = final_width_per_channel
|
| 222 |
start_col = i * final_width_per_channel
|
| 223 |
end_col = start_col + final_width_per_channel
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
continue
|
| 228 |
return solution
|
| 229 |
|
| 230 |
-
else:
|
|
|
|
| 231 |
return np.zeros((self.m, final_width_per_channel * self.c), dtype=float)
|
|
|
|
| 17 |
|
| 18 |
from scipy.signal import fftconvolve
|
| 19 |
|
| 20 |
+
# constants
|
| 21 |
+
MIN_OFFSET_THRESHOLD = 10 # min offset for images wider than this
|
| 22 |
+
MIN_PEAK_DISTANCE_FROM_CENTER = 20 # min distance of a peak from the center to be considered valid
|
| 23 |
+
AUTOCORRELATION_STD_FACTOR = 3 # factor for threshold calculation in autocorrelation
|
| 24 |
+
|
| 25 |
def offset(img: np.ndarray) -> Tuple[int, np.ndarray, np.ndarray, np.ndarray]:
|
| 26 |
"""
|
| 27 |
Calculates the offset that defines the stereoscopic effect.
|
| 28 |
+
Selects the offset corresponding to the highest peak in the autocorrelation curve.
|
| 29 |
"""
|
|
|
|
| 30 |
if img.size == 0 or img.ndim < 2:
|
| 31 |
+
return 0, np.array([]), np.array([]), np.array([]) # return 0 for invalid input
|
| 32 |
+
|
| 33 |
+
img_mean_subtracted = img - img.mean()
|
| 34 |
+
ac: np.ndarray = fftconvolve(img_mean_subtracted, np.flipud(np.fliplr(img_mean_subtracted)), mode='same')
|
| 35 |
+
|
| 36 |
if ac.ndim < 1 or ac.shape[0] < 1:
|
| 37 |
+
return img.shape[1], np.array([]), np.array([]), np.array([]) # fallback to image width
|
| 38 |
+
|
| 39 |
+
ac_center_row = ac[ac.shape[0] // 2]
|
| 40 |
+
|
| 41 |
if ac_center_row.size == 0 or ac_center_row.std() == 0:
|
| 42 |
+
return img.shape[1], np.array([]), np.array([]), np.array([]) # fallback if center row is invalid
|
| 43 |
|
| 44 |
+
idx = np.array([])
|
| 45 |
try:
|
| 46 |
+
std_dev = ac_center_row.std()
|
| 47 |
+
if std_dev > 0: # ensure std_dev is positive
|
| 48 |
+
threshold = AUTOCORRELATION_STD_FACTOR * std_dev
|
| 49 |
+
median_val = np.median(ac_center_row)
|
| 50 |
+
if np.isfinite(threshold): # ensure threshold is a valid number
|
| 51 |
+
idx = np.where(ac_center_row - median_val > threshold)[0]
|
|
|
|
| 52 |
except (ValueError, FloatingPointError):
|
| 53 |
+
pass # idx remains empty if calculation fails
|
| 54 |
|
|
|
|
| 55 |
diffs: np.ndarray = np.ediff1d(idx)
|
| 56 |
+
raw_offset: int = img.shape[1] # default to image width
|
| 57 |
|
| 58 |
if idx.size > 0:
|
| 59 |
+
center_idx = len(ac_center_row) // 2
|
| 60 |
+
# consider peaks sufficiently far from the center
|
| 61 |
+
valid_peaks_mask = np.abs(idx - center_idx) > MIN_PEAK_DISTANCE_FROM_CENTER
|
| 62 |
+
|
| 63 |
+
if np.any(valid_peaks_mask):
|
| 64 |
+
valid_peaks = idx[valid_peaks_mask]
|
| 65 |
+
valid_peaks_values = ac_center_row[valid_peaks]
|
| 66 |
+
if valid_peaks.size > 0:
|
| 67 |
+
# offset from the highest valid peak
|
| 68 |
+
highest_peak_idx = valid_peaks[np.argmax(valid_peaks_values)]
|
| 69 |
+
raw_offset = abs(highest_peak_idx - center_idx)
|
| 70 |
+
|
| 71 |
+
# fallback to max difference between peaks if current raw_offset is too small
|
| 72 |
+
if diffs.size > 0 and raw_offset < MIN_OFFSET_THRESHOLD:
|
| 73 |
+
max_diff = np.max(diffs)
|
| 74 |
+
if max_diff >= MIN_OFFSET_THRESHOLD:
|
| 75 |
+
raw_offset = max_diff
|
| 76 |
+
# if max_diff is also small, it implies no strong periodic pattern, keep img.shape[1] as fallback
|
| 77 |
+
|
| 78 |
+
# apply min offset constraint based on image width
|
| 79 |
+
min_practical_offset = MIN_OFFSET_THRESHOLD if img.shape[1] >= MIN_OFFSET_THRESHOLD else 1
|
| 80 |
+
final_offset = max(min_practical_offset, min(raw_offset, img.shape[1]))
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
return final_offset, ac_center_row, idx, diffs
|
| 83 |
|
|
|
|
| 160 |
self.autocorrelation_peak_indices: np.ndarray = np.array([])
|
| 161 |
self.autocorrelation_peak_diffs: np.ndarray = np.array([])
|
| 162 |
|
| 163 |
+
def _process_single_channel(self, channel_data: np.ndarray, gap: int, target_width: int) -> np.ndarray:
|
| 164 |
+
"""Helper to process a single image channel."""
|
| 165 |
+
if channel_data.size == 0 or channel_data.std() == 0.0:
|
| 166 |
+
return np.zeros((self.m, target_width), dtype=float)
|
| 167 |
+
|
| 168 |
+
shifted = shift_pic(channel_data, gap)
|
| 169 |
+
if shifted.size == 0 or shifted.shape[1] == 0: # check if shifted result is empty or has zero width
|
| 170 |
+
return np.zeros((self.m, target_width), dtype=float)
|
| 171 |
+
|
| 172 |
+
try:
|
| 173 |
+
filt_1 = ndimage.prewitt(shifted)
|
| 174 |
+
filt_2 = ndimage.uniform_filter(filt_1, size=(5, 5))
|
| 175 |
+
if _SKIMAGE_AVAILABLE:
|
| 176 |
+
filt_2 = post_process(filt_2)
|
| 177 |
+
|
| 178 |
+
# ensure correct width
|
| 179 |
+
if filt_2.shape[1] > target_width:
|
| 180 |
+
filt_2 = filt_2[:, :target_width]
|
| 181 |
+
elif filt_2.shape[1] < target_width:
|
| 182 |
+
# pad if narrower, though shift_pic should handle this by design
|
| 183 |
+
padding = np.zeros((filt_2.shape[0], target_width - filt_2.shape[1]), dtype=filt_2.dtype)
|
| 184 |
+
filt_2 = np.concatenate((filt_2, padding), axis=1)
|
| 185 |
+
return filt_2
|
| 186 |
+
except Exception:
|
| 187 |
+
return np.zeros((self.m, target_width), dtype=float)
|
| 188 |
+
|
| 189 |
def solve_with_offset(self, user_offset: int, channel_mode: str = 'separate') -> np.ndarray:
|
| 190 |
"""
|
| 191 |
Solves the autostereogram using a specified offset and channel handling mode.
|
|
|
|
| 200 |
The solved image as a NumPy array.
|
| 201 |
"""
|
| 202 |
if user_offset <= 0:
|
| 203 |
+
# determine expected shape for zero offset based on mode
|
| 204 |
+
# for average mode, it's single channel, original width
|
| 205 |
+
# for separate mode, it's multi-channel, original width per channel
|
| 206 |
+
# however, shift_pic with 0 gap returns original image, so width isn't reduced
|
| 207 |
+
# thus, n or n*c is appropriate.
|
| 208 |
+
return np.zeros((self.m, self.n if channel_mode == 'average' else self.n * self.c), dtype=float)
|
| 209 |
+
|
| 210 |
+
effective_gap = min(int(user_offset), self.n)
|
| 211 |
final_width_per_channel = max(0, self.n - effective_gap)
|
| 212 |
|
| 213 |
if channel_mode == 'average':
|
| 214 |
img_to_process = np.mean(self.image, axis=2) if self.color_image else self.image
|
| 215 |
+
return self._process_single_channel(img_to_process, effective_gap, final_width_per_channel)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
|
| 217 |
elif channel_mode == 'separate':
|
| 218 |
+
# prepare an empty array for the full solution
|
| 219 |
+
# if final_width_per_channel is 0, the solution will also have 0 width for all channels
|
| 220 |
+
solution_shape = (self.m, final_width_per_channel * self.c)
|
| 221 |
+
solution: np.ndarray = np.zeros(solution_shape, dtype=float)
|
| 222 |
+
|
| 223 |
+
if final_width_per_channel == 0: # if no width, return empty solution early
|
| 224 |
+
return solution
|
| 225 |
+
|
| 226 |
for i in range(self.c):
|
| 227 |
+
channel_data: np.ndarray = self.image[:, :, i] if self.color_image else self.image
|
| 228 |
+
if not self.color_image and i > 0: # only process once for grayscale
|
| 229 |
+
break
|
| 230 |
+
|
| 231 |
+
processed_channel = self._process_single_channel(channel_data, effective_gap, final_width_per_channel)
|
| 232 |
+
|
| 233 |
+
# ensure processed_channel has the correct dimensions before assignment
|
| 234 |
+
if processed_channel.shape[0] == self.m and processed_channel.shape[1] == final_width_per_channel:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
start_col = i * final_width_per_channel
|
| 236 |
end_col = start_col + final_width_per_channel
|
| 237 |
+
solution[:, start_col:end_col] = processed_channel
|
| 238 |
+
# if dimensions mismatch (e.g. due to an error in _process_single_channel returning wrong shape),
|
| 239 |
+
# that part of the solution remains zeros, which is a graceful fallback.
|
|
|
|
| 240 |
return solution
|
| 241 |
|
| 242 |
+
else: # unknown channel_mode
|
| 243 |
+
# fallback to a zero array with expected dimensions if mode is invalid
|
| 244 |
return np.zeros((self.m, final_width_per_channel * self.c), dtype=float)
|