lattice-sub 1.2.2__tar.gz → 1.3.1__tar.gz

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 (29) hide show
  1. {lattice_sub-1.2.2/src/lattice_sub.egg-info → lattice_sub-1.3.1}/PKG-INFO +15 -3
  2. {lattice_sub-1.2.2 → lattice_sub-1.3.1}/README.md +14 -2
  3. lattice_sub-1.3.1/docs/images/example_comparison.png +0 -0
  4. {lattice_sub-1.2.2 → lattice_sub-1.3.1}/examples/config.yaml +2 -2
  5. {lattice_sub-1.2.2 → lattice_sub-1.3.1}/pyproject.toml +1 -1
  6. {lattice_sub-1.2.2 → lattice_sub-1.3.1/src/lattice_sub.egg-info}/PKG-INFO +15 -3
  7. {lattice_sub-1.2.2 → lattice_sub-1.3.1}/src/lattice_subtraction/__init__.py +1 -1
  8. {lattice_sub-1.2.2 → lattice_sub-1.3.1}/src/lattice_subtraction/cli.py +15 -2
  9. lattice_sub-1.3.1/src/lattice_subtraction/visualization.py +370 -0
  10. lattice_sub-1.2.2/docs/images/example_comparison.png +0 -0
  11. lattice_sub-1.2.2/src/lattice_subtraction/visualization.py +0 -199
  12. {lattice_sub-1.2.2 → lattice_sub-1.3.1}/LICENSE +0 -0
  13. {lattice_sub-1.2.2 → lattice_sub-1.3.1}/MANIFEST.in +0 -0
  14. {lattice_sub-1.2.2 → lattice_sub-1.3.1}/docs/images/threshold_analysis.png +0 -0
  15. {lattice_sub-1.2.2 → lattice_sub-1.3.1}/examples/converted_params.yaml +0 -0
  16. {lattice_sub-1.2.2 → lattice_sub-1.3.1}/setup.cfg +0 -0
  17. {lattice_sub-1.2.2 → lattice_sub-1.3.1}/src/lattice_sub.egg-info/SOURCES.txt +0 -0
  18. {lattice_sub-1.2.2 → lattice_sub-1.3.1}/src/lattice_sub.egg-info/dependency_links.txt +0 -0
  19. {lattice_sub-1.2.2 → lattice_sub-1.3.1}/src/lattice_sub.egg-info/entry_points.txt +0 -0
  20. {lattice_sub-1.2.2 → lattice_sub-1.3.1}/src/lattice_sub.egg-info/requires.txt +0 -0
  21. {lattice_sub-1.2.2 → lattice_sub-1.3.1}/src/lattice_sub.egg-info/top_level.txt +0 -0
  22. {lattice_sub-1.2.2 → lattice_sub-1.3.1}/src/lattice_subtraction/batch.py +0 -0
  23. {lattice_sub-1.2.2 → lattice_sub-1.3.1}/src/lattice_subtraction/config.py +0 -0
  24. {lattice_sub-1.2.2 → lattice_sub-1.3.1}/src/lattice_subtraction/core.py +0 -0
  25. {lattice_sub-1.2.2 → lattice_sub-1.3.1}/src/lattice_subtraction/io.py +0 -0
  26. {lattice_sub-1.2.2 → lattice_sub-1.3.1}/src/lattice_subtraction/masks.py +0 -0
  27. {lattice_sub-1.2.2 → lattice_sub-1.3.1}/src/lattice_subtraction/processing.py +0 -0
  28. {lattice_sub-1.2.2 → lattice_sub-1.3.1}/src/lattice_subtraction/threshold_optimizer.py +0 -0
  29. {lattice_sub-1.2.2 → lattice_sub-1.3.1}/src/lattice_subtraction/ui.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lattice-sub
3
- Version: 1.2.2
3
+ Version: 1.3.1
4
4
  Summary: Lattice subtraction for cryo-EM micrographs - removes periodic crystal signals to reveal non-periodic features
5
5
  Author-email: George Stephenson <george.stephenson@colorado.edu>, Vignesh Kasinath <vignesh.kasinath@colorado.edu>
6
6
  License: MIT
@@ -94,7 +94,17 @@ lattice-sub batch input_folder/ output_folder/ --pixel-size 0.56
94
94
  lattice-sub batch input_folder/ output_folder/ --pixel-size 0.56 --vis comparisons/
95
95
  ```
96
96
 
97
- This creates side-by-side PNG images showing before/after/difference for each micrograph.
97
+ This creates 4-panel PNG comparison images for each micrograph showing:
98
+ 1. **Original** - Input micrograph
99
+ 2. **Subtracted** - Lattice-removed result
100
+ 3. **Difference** - What was removed (5x amplified)
101
+ 4. **Threshold Curve** - Threshold vs lattice removal efficacy
102
+
103
+ **Limit the number of visualizations:**
104
+ ```bash
105
+ # Generate visualizations for first 10 images only
106
+ lattice-sub batch input_folder/ output_folder/ -p 0.56 --vis comparisons/ -n 10
107
+ ```
98
108
 
99
109
  ---
100
110
 
@@ -105,6 +115,8 @@ This creates side-by-side PNG images showing before/after/difference for each mi
105
115
  | `-p, --pixel-size` | **Required.** Pixel size in Ångstroms |
106
116
  | `-o, --output` | Output file path (default: `sub_<input>`) |
107
117
  | `-t, --threshold` | Peak detection sensitivity (default: **auto** - optimized per image) |
118
+ | `--vis DIR` | Generate 4-panel comparison PNGs in DIR |
119
+ | `-n, --num-vis N` | Limit visualizations to first N images |
108
120
  | `--cpu` | Force CPU processing (GPU is used by default) |
109
121
  | `-q, --quiet` | Hide the banner and progress messages |
110
122
  | `-v, --verbose` | Show detailed processing information |
@@ -207,7 +219,7 @@ lattice-sub batch input/ output/ -p 0.56
207
219
 
208
220
  **Output:**
209
221
  ```
210
- Phase-preserving FFT inpainting for cryo-EM | v1.2.2
222
+ Phase-preserving FFT inpainting for cryo-EM | v1.3.0
211
223
 
212
224
  Configuration
213
225
  -------------
@@ -53,7 +53,17 @@ lattice-sub batch input_folder/ output_folder/ --pixel-size 0.56
53
53
  lattice-sub batch input_folder/ output_folder/ --pixel-size 0.56 --vis comparisons/
54
54
  ```
55
55
 
56
- This creates side-by-side PNG images showing before/after/difference for each micrograph.
56
+ This creates 4-panel PNG comparison images for each micrograph showing:
57
+ 1. **Original** - Input micrograph
58
+ 2. **Subtracted** - Lattice-removed result
59
+ 3. **Difference** - What was removed (5x amplified)
60
+ 4. **Threshold Curve** - Threshold vs lattice removal efficacy
61
+
62
+ **Limit the number of visualizations:**
63
+ ```bash
64
+ # Generate visualizations for first 10 images only
65
+ lattice-sub batch input_folder/ output_folder/ -p 0.56 --vis comparisons/ -n 10
66
+ ```
57
67
 
58
68
  ---
59
69
 
@@ -64,6 +74,8 @@ This creates side-by-side PNG images showing before/after/difference for each mi
64
74
  | `-p, --pixel-size` | **Required.** Pixel size in Ångstroms |
65
75
  | `-o, --output` | Output file path (default: `sub_<input>`) |
66
76
  | `-t, --threshold` | Peak detection sensitivity (default: **auto** - optimized per image) |
77
+ | `--vis DIR` | Generate 4-panel comparison PNGs in DIR |
78
+ | `-n, --num-vis N` | Limit visualizations to first N images |
67
79
  | `--cpu` | Force CPU processing (GPU is used by default) |
68
80
  | `-q, --quiet` | Hide the banner and progress messages |
69
81
  | `-v, --verbose` | Show detailed processing information |
@@ -166,7 +178,7 @@ lattice-sub batch input/ output/ -p 0.56
166
178
 
167
179
  **Output:**
168
180
  ```
169
- Phase-preserving FFT inpainting for cryo-EM | v1.2.2
181
+ Phase-preserving FFT inpainting for cryo-EM | v1.3.0
170
182
 
171
183
  Configuration
172
184
  -------------
@@ -1,9 +1,9 @@
1
- # Lattice Subtraction - Example Configuration (v1.1.0)
1
+ # Lattice Subtraction - Example Configuration
2
2
  #
3
3
  # This file contains all available configuration options.
4
4
  # Copy and modify for your specific dataset.
5
5
  #
6
- # NEW in v1.1.0: Auto-threshold and Kornia GPU acceleration are now defaults!
6
+ # Auto-threshold and Kornia GPU acceleration are defaults.
7
7
  # Just run `lattice-sub process image.mrc -p 0.56` for optimal results.
8
8
 
9
9
  # ============================================
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lattice-sub"
7
- version = "1.2.2"
7
+ version = "1.3.1"
8
8
  description = "Lattice subtraction for cryo-EM micrographs - removes periodic crystal signals to reveal non-periodic features"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lattice-sub
3
- Version: 1.2.2
3
+ Version: 1.3.1
4
4
  Summary: Lattice subtraction for cryo-EM micrographs - removes periodic crystal signals to reveal non-periodic features
5
5
  Author-email: George Stephenson <george.stephenson@colorado.edu>, Vignesh Kasinath <vignesh.kasinath@colorado.edu>
6
6
  License: MIT
@@ -94,7 +94,17 @@ lattice-sub batch input_folder/ output_folder/ --pixel-size 0.56
94
94
  lattice-sub batch input_folder/ output_folder/ --pixel-size 0.56 --vis comparisons/
95
95
  ```
96
96
 
97
- This creates side-by-side PNG images showing before/after/difference for each micrograph.
97
+ This creates 4-panel PNG comparison images for each micrograph showing:
98
+ 1. **Original** - Input micrograph
99
+ 2. **Subtracted** - Lattice-removed result
100
+ 3. **Difference** - What was removed (5x amplified)
101
+ 4. **Threshold Curve** - Threshold vs lattice removal efficacy
102
+
103
+ **Limit the number of visualizations:**
104
+ ```bash
105
+ # Generate visualizations for first 10 images only
106
+ lattice-sub batch input_folder/ output_folder/ -p 0.56 --vis comparisons/ -n 10
107
+ ```
98
108
 
99
109
  ---
100
110
 
@@ -105,6 +115,8 @@ This creates side-by-side PNG images showing before/after/difference for each mi
105
115
  | `-p, --pixel-size` | **Required.** Pixel size in Ångstroms |
106
116
  | `-o, --output` | Output file path (default: `sub_<input>`) |
107
117
  | `-t, --threshold` | Peak detection sensitivity (default: **auto** - optimized per image) |
118
+ | `--vis DIR` | Generate 4-panel comparison PNGs in DIR |
119
+ | `-n, --num-vis N` | Limit visualizations to first N images |
108
120
  | `--cpu` | Force CPU processing (GPU is used by default) |
109
121
  | `-q, --quiet` | Hide the banner and progress messages |
110
122
  | `-v, --verbose` | Show detailed processing information |
@@ -207,7 +219,7 @@ lattice-sub batch input/ output/ -p 0.56
207
219
 
208
220
  **Output:**
209
221
  ```
210
- Phase-preserving FFT inpainting for cryo-EM | v1.2.2
222
+ Phase-preserving FFT inpainting for cryo-EM | v1.3.0
211
223
 
212
224
  Configuration
213
225
  -------------
@@ -19,7 +19,7 @@ Example:
19
19
  >>> result.save("output.mrc")
20
20
  """
21
21
 
22
- __version__ = "1.2.2"
22
+ __version__ = "1.3.1"
23
23
  __author__ = "George Stephenson & Vignesh Kasinath"
24
24
 
25
25
  from .config import Config
@@ -153,9 +153,12 @@ def main():
153
153
  # Batch process directory (GPU handles parallelism)
154
154
  lattice-sub batch input_dir/ output_dir/ --pixel-size 0.56
155
155
 
156
- # Batch with visualizations
156
+ # Batch with visualizations (4-panel with threshold curve)
157
157
  lattice-sub batch input_dir/ output_dir/ -p 0.56 --vis viz_dir/
158
158
 
159
+ # Batch with limited visualizations (only first 10)
160
+ lattice-sub batch input_dir/ output_dir/ -p 0.56 --vis viz_dir/ -n 10
161
+
159
162
  # CPU batch with parallel workers (use -j only with --cpu)
160
163
  lattice-sub batch input_dir/ output_dir/ -p 0.56 --cpu -j 8
161
164
 
@@ -502,6 +505,12 @@ def process(
502
505
  default=None,
503
506
  help="Generate comparison visualizations in this directory",
504
507
  )
508
+ @click.option(
509
+ "-n", "--num-vis",
510
+ type=int,
511
+ default=None,
512
+ help="Number of visualizations to generate (default: all)",
513
+ )
505
514
  @click.option(
506
515
  "-v", "--verbose",
507
516
  is_flag=True,
@@ -528,6 +537,7 @@ def batch(
528
537
  config: Optional[str],
529
538
  recursive: bool,
530
539
  vis: Optional[str],
540
+ num_vis: Optional[int],
531
541
  verbose: bool,
532
542
  quiet: bool,
533
543
  cpu: bool,
@@ -617,7 +627,8 @@ def batch(
617
627
 
618
628
  # Generate visualizations if requested
619
629
  if vis:
620
- logger.info(f"Generating visualizations in: {vis}")
630
+ limit_msg = f" (limit: {num_vis})" if num_vis else ""
631
+ logger.info(f"Generating visualizations in: {vis}{limit_msg}")
621
632
  viz_success, viz_total = generate_visualizations(
622
633
  input_dir=input_dir,
623
634
  output_dir=output_dir,
@@ -625,6 +636,8 @@ def batch(
625
636
  prefix=prefix,
626
637
  pattern=pattern,
627
638
  show_progress=True,
639
+ limit=num_vis,
640
+ config=cfg,
628
641
  )
629
642
  logger.info(f"Visualizations: {viz_success}/{viz_total} created")
630
643
 
@@ -0,0 +1,370 @@
1
+ """
2
+ Visualization utilities for lattice subtraction results.
3
+
4
+ This module provides functions to create comparison visualizations
5
+ showing original, processed, difference images, and threshold optimization curves.
6
+ """
7
+
8
+ import logging
9
+ from pathlib import Path
10
+ from typing import Optional, Tuple, List
11
+
12
+ import numpy as np
13
+
14
+ # Silence matplotlib's verbose debug logging
15
+ logging.getLogger('matplotlib').setLevel(logging.WARNING)
16
+ logging.getLogger('matplotlib.font_manager').setLevel(logging.WARNING)
17
+
18
+ # Silence PIL/Pillow debug logging (PNG chunk messages)
19
+ logging.getLogger('PIL').setLevel(logging.WARNING)
20
+ logging.getLogger('PIL.PngImagePlugin').setLevel(logging.WARNING)
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def compute_threshold_curve(
26
+ image: np.ndarray,
27
+ config,
28
+ n_points: int = 21,
29
+ ) -> Tuple[np.ndarray, np.ndarray, float, float]:
30
+ """
31
+ Compute quality scores across a range of thresholds.
32
+
33
+ Args:
34
+ image: Original image array
35
+ config: Config object with processing parameters
36
+ n_points: Number of threshold points to evaluate
37
+
38
+ Returns:
39
+ Tuple of (thresholds, quality_scores, optimal_threshold, optimal_quality)
40
+ """
41
+ from .threshold_optimizer import ThresholdOptimizer
42
+
43
+ optimizer = ThresholdOptimizer(config)
44
+
45
+ # Prepare FFT data once
46
+ subtracted, radial_mask, box_size = optimizer._prepare_fft_data(image)
47
+
48
+ # Evaluate across threshold range
49
+ thresholds = np.linspace(
50
+ optimizer.min_threshold,
51
+ optimizer.max_threshold,
52
+ n_points
53
+ )
54
+
55
+ # Use GPU batch if available
56
+ if optimizer.use_gpu:
57
+ qualities, peak_counts = optimizer._compute_quality_batch_gpu(
58
+ subtracted, radial_mask, thresholds
59
+ )
60
+ else:
61
+ qualities = []
62
+ for t in thresholds:
63
+ q, _ = optimizer._compute_quality(subtracted, radial_mask, t)
64
+ qualities.append(q)
65
+ qualities = np.array(qualities)
66
+
67
+ # Find optimal
68
+ best_idx = np.argmax(qualities)
69
+ optimal_threshold = thresholds[best_idx]
70
+ optimal_quality = qualities[best_idx]
71
+
72
+ return thresholds, qualities, optimal_threshold, optimal_quality
73
+
74
+
75
+ def create_comparison_figure(
76
+ original: np.ndarray,
77
+ processed: np.ndarray,
78
+ title: str = "Lattice Subtraction Comparison",
79
+ figsize: Tuple[int, int] = (18, 6),
80
+ dpi: int = 150,
81
+ ):
82
+ """
83
+ Create a comparison figure showing original, processed, and difference images.
84
+
85
+ Args:
86
+ original: Original image array
87
+ processed: Processed (lattice-subtracted) image array
88
+ title: Figure title
89
+ figsize: Figure size in inches (width, height)
90
+ dpi: Resolution for saving
91
+
92
+ Returns:
93
+ matplotlib Figure object
94
+ """
95
+ import matplotlib.pyplot as plt
96
+
97
+ # Compute difference
98
+ difference = original - processed
99
+
100
+ # Create figure
101
+ fig, axes = plt.subplots(1, 3, figsize=figsize)
102
+
103
+ # Contrast limits from original
104
+ vmin, vmax = np.percentile(original, [1, 99])
105
+
106
+ # Original
107
+ axes[0].imshow(original, cmap='gray', vmin=vmin, vmax=vmax)
108
+ axes[0].set_title(f'Original\n{original.shape}')
109
+ axes[0].axis('off')
110
+
111
+ # Lattice Subtracted
112
+ axes[1].imshow(processed, cmap='gray', vmin=vmin, vmax=vmax)
113
+ axes[1].set_title(f'Lattice Subtracted\n{processed.shape}')
114
+ axes[1].axis('off')
115
+
116
+ # Difference (removed lattice)
117
+ diff_std = np.std(difference)
118
+ axes[2].imshow(
119
+ difference,
120
+ cmap='RdBu_r',
121
+ vmin=-diff_std * 3,
122
+ vmax=diff_std * 3
123
+ )
124
+ axes[2].set_title('Difference (Removed Lattice)')
125
+ axes[2].axis('off')
126
+
127
+ # Title
128
+ plt.suptitle(title, fontsize=14)
129
+ plt.tight_layout()
130
+
131
+ return fig
132
+
133
+
134
+ def create_comparison_figure_with_threshold(
135
+ original: np.ndarray,
136
+ processed: np.ndarray,
137
+ thresholds: np.ndarray,
138
+ quality_scores: np.ndarray,
139
+ optimal_threshold: float,
140
+ optimal_quality: float,
141
+ title: str = "Lattice Subtraction Comparison",
142
+ figsize: Tuple[int, int] = (14, 12),
143
+ dpi: int = 150,
144
+ ):
145
+ """
146
+ Create a 4-panel comparison figure with threshold optimization curve.
147
+
148
+ Layout (2x2 grid):
149
+ [Original] [Subtracted]
150
+ [Difference] [Threshold Curve]
151
+
152
+ Args:
153
+ original: Original image array
154
+ processed: Processed (lattice-subtracted) image array
155
+ thresholds: Array of threshold values tested
156
+ quality_scores: Array of quality scores for each threshold
157
+ optimal_threshold: The optimal threshold that was selected
158
+ optimal_quality: Quality score at optimal threshold
159
+ title: Figure title
160
+ figsize: Figure size in inches (width, height)
161
+ dpi: Resolution for saving
162
+
163
+ Returns:
164
+ matplotlib Figure object
165
+ """
166
+ import matplotlib.pyplot as plt
167
+
168
+ # Compute difference
169
+ difference = original - processed
170
+
171
+ # Create figure with 4 panels (2 rows, 2 columns)
172
+ fig, axes = plt.subplots(2, 2, figsize=figsize)
173
+
174
+ # Contrast limits from original
175
+ vmin, vmax = np.percentile(original, [1, 99])
176
+
177
+ # Panel 1 (top-left): Original
178
+ axes[0, 0].imshow(original, cmap='gray', vmin=vmin, vmax=vmax)
179
+ axes[0, 0].set_title(f'Original\n{original.shape}')
180
+ axes[0, 0].axis('off')
181
+
182
+ # Panel 2 (top-right): Lattice Subtracted
183
+ axes[0, 1].imshow(processed, cmap='gray', vmin=vmin, vmax=vmax)
184
+ axes[0, 1].set_title(f'Lattice Subtracted\n{processed.shape}')
185
+ axes[0, 1].axis('off')
186
+
187
+ # Panel 3 (bottom-left): Difference (removed lattice)
188
+ diff_std = np.std(difference)
189
+ axes[1, 0].imshow(
190
+ difference,
191
+ cmap='RdBu_r',
192
+ vmin=-diff_std * 3,
193
+ vmax=diff_std * 3
194
+ )
195
+ axes[1, 0].set_title('Difference (Removed Lattice)')
196
+ axes[1, 0].axis('off')
197
+
198
+ # Panel 4 (bottom-right): Threshold vs Quality Score curve
199
+ axes[1, 1].plot(thresholds, quality_scores, 'b-', linewidth=2, label='Quality Score')
200
+ axes[1, 1].axvline(x=optimal_threshold, color='r', linestyle='--', linewidth=2,
201
+ label=f'Optimal: {optimal_threshold:.3f}')
202
+ axes[1, 1].scatter([optimal_threshold], [optimal_quality], color='r', s=100, zorder=5)
203
+ axes[1, 1].set_xlabel('Threshold', fontsize=11)
204
+ axes[1, 1].set_ylabel('Lattice Removal Efficacy', fontsize=11)
205
+ axes[1, 1].set_title(f'Threshold Optimization\nOptimal = {optimal_threshold:.3f}')
206
+ axes[1, 1].legend(loc='best', fontsize=9)
207
+ axes[1, 1].grid(True, alpha=0.3)
208
+ axes[1, 1].set_xlim(thresholds.min(), thresholds.max())
209
+
210
+ # Title
211
+ plt.suptitle(title, fontsize=14)
212
+ plt.tight_layout()
213
+
214
+ return fig
215
+
216
+
217
+ def save_comparison_visualization(
218
+ original_path: Path,
219
+ processed_path: Path,
220
+ output_path: Path,
221
+ config = None,
222
+ dpi: int = 150,
223
+ ) -> None:
224
+ """
225
+ Create and save a 4-panel comparison visualization for a single image pair.
226
+
227
+ Includes threshold optimization curve showing how the optimal threshold
228
+ was selected based on lattice removal efficacy.
229
+
230
+ Args:
231
+ original_path: Path to original MRC file
232
+ processed_path: Path to processed MRC file
233
+ output_path: Path for output PNG file
234
+ config: Config object for threshold computation (optional, will use defaults)
235
+ dpi: Resolution for output images
236
+ """
237
+ import matplotlib.pyplot as plt
238
+ import mrcfile
239
+
240
+ # Load images
241
+ with mrcfile.open(original_path, 'r') as f:
242
+ original = f.data.copy()
243
+ with mrcfile.open(processed_path, 'r') as f:
244
+ processed = f.data.copy()
245
+
246
+ # Create title
247
+ name = original_path.name
248
+ short_name = name[:50] + "..." if len(name) > 50 else name
249
+ title = f"Lattice Subtraction: {short_name}"
250
+
251
+ # Try to compute threshold curve if config available
252
+ try:
253
+ if config is None:
254
+ # Create default config for threshold computation
255
+ from .config import Config
256
+ config = Config(pixel_ang=0.56) # Default K3 pixel size
257
+
258
+ # Compute threshold optimization curve
259
+ thresholds, quality_scores, optimal_threshold, optimal_quality = \
260
+ compute_threshold_curve(original, config)
261
+
262
+ # Create 4-panel figure with threshold curve
263
+ fig = create_comparison_figure_with_threshold(
264
+ original, processed,
265
+ thresholds, quality_scores,
266
+ optimal_threshold, optimal_quality,
267
+ title=title, dpi=dpi
268
+ )
269
+ except Exception as e:
270
+ # Fallback to 3-panel if threshold computation fails
271
+ logger.debug(f"Could not compute threshold curve: {e}, using 3-panel view")
272
+ fig = create_comparison_figure(original, processed, title=title, dpi=dpi)
273
+
274
+ output_path.parent.mkdir(parents=True, exist_ok=True)
275
+ fig.savefig(output_path, dpi=dpi, bbox_inches='tight')
276
+ plt.close(fig)
277
+
278
+
279
+ def generate_visualizations(
280
+ input_dir: Path,
281
+ output_dir: Path,
282
+ viz_dir: Path,
283
+ prefix: str = "sub_",
284
+ pattern: str = "*.mrc",
285
+ dpi: int = 150,
286
+ show_progress: bool = True,
287
+ limit: Optional[int] = None,
288
+ config = None,
289
+ ) -> Tuple[int, int]:
290
+ """
291
+ Generate comparison visualizations for processed images in a directory.
292
+
293
+ Args:
294
+ input_dir: Directory containing original MRC files
295
+ output_dir: Directory containing processed MRC files
296
+ viz_dir: Directory for output visualization PNG files
297
+ prefix: Prefix used for processed files (default: "sub_")
298
+ pattern: Glob pattern for finding processed files
299
+ dpi: Resolution for output images
300
+ show_progress: Show progress bar
301
+ limit: Maximum number of visualizations to generate (None = all)
302
+ config: Config object for threshold computation (optional)
303
+
304
+ Returns:
305
+ Tuple of (successful_count, total_count)
306
+ """
307
+ import matplotlib.pyplot as plt
308
+
309
+ viz_dir = Path(viz_dir)
310
+ viz_dir.mkdir(parents=True, exist_ok=True)
311
+
312
+ # Find all processed files
313
+ output_files = sorted(Path(output_dir).glob(f"{prefix}{pattern}"))
314
+
315
+ if not output_files:
316
+ logger.warning(f"No processed files found matching '{prefix}{pattern}' in {output_dir}")
317
+ return 0, 0
318
+
319
+ # Apply limit if specified
320
+ total_available = len(output_files)
321
+ if limit is not None and limit > 0:
322
+ output_files = output_files[:limit]
323
+ logger.info(f"Limiting to {limit} visualizations (of {total_available} available)")
324
+
325
+ successful = 0
326
+ total = len(output_files)
327
+
328
+ # Setup iterator with optional progress bar
329
+ if show_progress:
330
+ try:
331
+ from tqdm import tqdm
332
+ iterator = tqdm(output_files, desc="Generating visualizations", unit="file")
333
+ except ImportError:
334
+ iterator = output_files
335
+ else:
336
+ iterator = output_files
337
+
338
+ for processed_path in iterator:
339
+ try:
340
+ # Get corresponding input file
341
+ input_name = processed_path.name.replace(prefix, "", 1)
342
+ input_path = Path(input_dir) / input_name
343
+
344
+ if not input_path.exists():
345
+ logger.debug(f"Original not found: {input_path}")
346
+ continue
347
+
348
+ # Output path
349
+ viz_name = input_name.replace(".mrc", ".png")
350
+ viz_path = viz_dir / viz_name
351
+
352
+ # Skip if already exists
353
+ if viz_path.exists():
354
+ successful += 1
355
+ continue
356
+
357
+ # Generate visualization with 4-panel layout
358
+ save_comparison_visualization(
359
+ original_path=input_path,
360
+ processed_path=processed_path,
361
+ output_path=viz_path,
362
+ config=config,
363
+ dpi=dpi,
364
+ )
365
+ successful += 1
366
+
367
+ except Exception as e:
368
+ logger.error(f"Failed to create visualization for {processed_path.name}: {e}")
369
+
370
+ return successful, total
@@ -1,199 +0,0 @@
1
- """
2
- Visualization utilities for lattice subtraction results.
3
-
4
- This module provides functions to create comparison visualizations
5
- showing original, processed, and difference images.
6
- """
7
-
8
- import logging
9
- from pathlib import Path
10
- from typing import Optional, Tuple
11
-
12
- import numpy as np
13
-
14
- # Silence matplotlib's verbose debug logging
15
- logging.getLogger('matplotlib').setLevel(logging.WARNING)
16
- logging.getLogger('matplotlib.font_manager').setLevel(logging.WARNING)
17
-
18
- # Silence PIL/Pillow debug logging (PNG chunk messages)
19
- logging.getLogger('PIL').setLevel(logging.WARNING)
20
- logging.getLogger('PIL.PngImagePlugin').setLevel(logging.WARNING)
21
-
22
- logger = logging.getLogger(__name__)
23
-
24
-
25
- def create_comparison_figure(
26
- original: np.ndarray,
27
- processed: np.ndarray,
28
- title: str = "Lattice Subtraction Comparison",
29
- figsize: Tuple[int, int] = (18, 6),
30
- dpi: int = 150,
31
- ):
32
- """
33
- Create a comparison figure showing original, processed, and difference images.
34
-
35
- Args:
36
- original: Original image array
37
- processed: Processed (lattice-subtracted) image array
38
- title: Figure title
39
- figsize: Figure size in inches (width, height)
40
- dpi: Resolution for saving
41
-
42
- Returns:
43
- matplotlib Figure object
44
- """
45
- import matplotlib.pyplot as plt
46
-
47
- # Compute difference
48
- difference = original - processed
49
-
50
- # Create figure
51
- fig, axes = plt.subplots(1, 3, figsize=figsize)
52
-
53
- # Contrast limits from original
54
- vmin, vmax = np.percentile(original, [1, 99])
55
-
56
- # Original
57
- axes[0].imshow(original, cmap='gray', vmin=vmin, vmax=vmax)
58
- axes[0].set_title(f'Original\n{original.shape}')
59
- axes[0].axis('off')
60
-
61
- # Lattice Subtracted
62
- axes[1].imshow(processed, cmap='gray', vmin=vmin, vmax=vmax)
63
- axes[1].set_title(f'Lattice Subtracted\n{processed.shape}')
64
- axes[1].axis('off')
65
-
66
- # Difference (removed lattice)
67
- diff_std = np.std(difference)
68
- axes[2].imshow(
69
- difference,
70
- cmap='RdBu_r',
71
- vmin=-diff_std * 3,
72
- vmax=diff_std * 3
73
- )
74
- axes[2].set_title('Difference (Removed Lattice)')
75
- axes[2].axis('off')
76
-
77
- # Title
78
- plt.suptitle(title, fontsize=14)
79
- plt.tight_layout()
80
-
81
- return fig
82
-
83
-
84
- def save_comparison_visualization(
85
- original_path: Path,
86
- processed_path: Path,
87
- output_path: Path,
88
- dpi: int = 150,
89
- ) -> None:
90
- """
91
- Create and save a comparison visualization for a single image pair.
92
-
93
- Args:
94
- original_path: Path to original MRC file
95
- processed_path: Path to processed MRC file
96
- output_path: Path for output PNG file
97
- """
98
- import matplotlib.pyplot as plt
99
- import mrcfile
100
-
101
- # Load images
102
- with mrcfile.open(original_path, 'r') as f:
103
- original = f.data.copy()
104
- with mrcfile.open(processed_path, 'r') as f:
105
- processed = f.data.copy()
106
-
107
- # Create title
108
- name = original_path.name
109
- short_name = name[:60] + "..." if len(name) > 60 else name
110
- title = f"Lattice Subtraction Comparison: {short_name}"
111
-
112
- # Create and save figure
113
- fig = create_comparison_figure(original, processed, title=title, dpi=dpi)
114
- output_path.parent.mkdir(parents=True, exist_ok=True)
115
- fig.savefig(output_path, dpi=dpi, bbox_inches='tight')
116
- plt.close(fig)
117
-
118
-
119
- def generate_visualizations(
120
- input_dir: Path,
121
- output_dir: Path,
122
- viz_dir: Path,
123
- prefix: str = "sub_",
124
- pattern: str = "*.mrc",
125
- dpi: int = 150,
126
- show_progress: bool = True,
127
- ) -> Tuple[int, int]:
128
- """
129
- Generate comparison visualizations for all processed images in a directory.
130
-
131
- Args:
132
- input_dir: Directory containing original MRC files
133
- output_dir: Directory containing processed MRC files
134
- viz_dir: Directory for output visualization PNG files
135
- prefix: Prefix used for processed files (default: "sub_")
136
- pattern: Glob pattern for finding processed files
137
- dpi: Resolution for output images
138
- show_progress: Show progress bar
139
-
140
- Returns:
141
- Tuple of (successful_count, total_count)
142
- """
143
- import matplotlib.pyplot as plt
144
-
145
- viz_dir = Path(viz_dir)
146
- viz_dir.mkdir(parents=True, exist_ok=True)
147
-
148
- # Find all processed files
149
- output_files = sorted(Path(output_dir).glob(f"{prefix}{pattern}"))
150
-
151
- if not output_files:
152
- logger.warning(f"No processed files found matching '{prefix}{pattern}' in {output_dir}")
153
- return 0, 0
154
-
155
- successful = 0
156
- total = len(output_files)
157
-
158
- # Setup iterator with optional progress bar
159
- if show_progress:
160
- try:
161
- from tqdm import tqdm
162
- iterator = tqdm(output_files, desc="Generating visualizations", unit="file")
163
- except ImportError:
164
- iterator = output_files
165
- else:
166
- iterator = output_files
167
-
168
- for processed_path in iterator:
169
- try:
170
- # Get corresponding input file
171
- input_name = processed_path.name.replace(prefix, "", 1)
172
- input_path = Path(input_dir) / input_name
173
-
174
- if not input_path.exists():
175
- logger.debug(f"Original not found: {input_path}")
176
- continue
177
-
178
- # Output path
179
- viz_name = input_name.replace(".mrc", ".png")
180
- viz_path = viz_dir / viz_name
181
-
182
- # Skip if already exists
183
- if viz_path.exists():
184
- successful += 1
185
- continue
186
-
187
- # Generate visualization
188
- save_comparison_visualization(
189
- original_path=input_path,
190
- processed_path=processed_path,
191
- output_path=viz_path,
192
- dpi=dpi,
193
- )
194
- successful += 1
195
-
196
- except Exception as e:
197
- logger.error(f"Failed to create visualization for {processed_path.name}: {e}")
198
-
199
- return successful, total
File without changes
File without changes
File without changes