optimouse 0.1.0__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.
- modules/__init__.py +21 -0
- modules/bruker_concat_tif.py +650 -0
- modules/compute_zcorr.py +2040 -0
- modules/extraction.py +559 -0
- modules/motion_correction.py +982 -0
- modules/utils/plot_zones.py +125 -0
- modules/utils/save_z_motion_args.py +93 -0
- optimouse-0.1.0.dist-info/METADATA +321 -0
- optimouse-0.1.0.dist-info/RECORD +24 -0
- optimouse-0.1.0.dist-info/WHEEL +5 -0
- optimouse-0.1.0.dist-info/licenses/LICENSE.md +21 -0
- optimouse-0.1.0.dist-info/top_level.txt +2 -0
- pipeline/__init__.py +0 -0
- pipeline/configs/config_C57_O1M2_10022023_run1run2.json +107 -0
- pipeline/configs/config_C57_O1M2_10022023_run1run2run3run4.json +114 -0
- pipeline/configs/config_template_cnmf.json +123 -0
- pipeline/configs/config_template_suite2p.json +102 -0
- pipeline/configs/config_test.json +114 -0
- pipeline/pipeline_cnmf.py +263 -0
- pipeline/pipeline_mcorr.py +299 -0
- pipeline/roi_zcorr.py +65 -0
- pipeline/utils/__init__.py +0 -0
- pipeline/utils/config_loader.py +92 -0
- pipeline/utils/pipeline_utils.py +537 -0
modules/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Analysis 2P Pipeline modules
|
|
3
|
+
|
|
4
|
+
This package contains core modules for the Analysis 2P pipeline:
|
|
5
|
+
- bruker_concat_tif: Functions to concatenate Bruker microscopy files
|
|
6
|
+
- motion_correction: Complete motion correction workflow
|
|
7
|
+
- compute_zcorr: Functions for Z-motion correction
|
|
8
|
+
- extraction: Functions for extracting ROIs and traces from 2P data
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
# Version information
|
|
14
|
+
__version__ = "1.1.0"
|
|
15
|
+
__author__ = "Manuel Levy, Vincent Prevosto"
|
|
16
|
+
|
|
17
|
+
# Import core modules
|
|
18
|
+
from . import bruker_concat_tif
|
|
19
|
+
from . import compute_zcorr
|
|
20
|
+
from . import motion_correction
|
|
21
|
+
from . import extraction
|
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
"""bruker_concat_tif.py
|
|
2
|
+
Script to concatenate Bruker 2P ome.tif files into a single BigTIFF file.
|
|
3
|
+
|
|
4
|
+
Example usage, from a python console or notebook:
|
|
5
|
+
import bruker_concat_tif as ct
|
|
6
|
+
ct.concatenate_files(['/path/to/folder1'], '/path/to/output')
|
|
7
|
+
ct.concatenate_files(['/path/to/folder1', '/path/to/folder2'], '/path/to/output', regex='*_Ch1_*.ome.tif')
|
|
8
|
+
|
|
9
|
+
Example usage, from the command line:
|
|
10
|
+
python bruker_concat_tif.py /path/to/folder1 /path/to/output
|
|
11
|
+
"""
|
|
12
|
+
# Written by Vincent Prevosto <prevosto at mit dot edu>
|
|
13
|
+
# CC BY-SA 4.0
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
# Add project root to path
|
|
21
|
+
PROJECT_ROOT = Path(__file__).parent.parent
|
|
22
|
+
if str(PROJECT_ROOT) not in sys.path:
|
|
23
|
+
sys.path.insert(0, str(PROJECT_ROOT))
|
|
24
|
+
|
|
25
|
+
import os, glob, argparse, time
|
|
26
|
+
# from tifftools import tiff_concat
|
|
27
|
+
from tifffile import imread, imwrite, TiffWriter, TiffFile
|
|
28
|
+
from libtiff import TIFF, libtiff_ctypes
|
|
29
|
+
import numpy as np
|
|
30
|
+
import warnings, contextlib
|
|
31
|
+
import psutil
|
|
32
|
+
import json
|
|
33
|
+
import xml.etree.ElementTree as ET
|
|
34
|
+
from datetime import datetime
|
|
35
|
+
# import dask.array as da
|
|
36
|
+
|
|
37
|
+
def extract_pv_metadata(xml_file_path):
|
|
38
|
+
"""
|
|
39
|
+
Extract imaging metadata from Prairie View's PVScan XML file.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
xml_file_path: Path to the XML file
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
dict: Extracted metadata, or None if failed
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
# Parse XML file
|
|
49
|
+
tree = ET.parse(xml_file_path)
|
|
50
|
+
root = tree.getroot()
|
|
51
|
+
|
|
52
|
+
# Extract metadata
|
|
53
|
+
metadata = {}
|
|
54
|
+
|
|
55
|
+
# Find PVStateShard
|
|
56
|
+
pv_state = root.find('.//PVStateShard')
|
|
57
|
+
if pv_state is not None:
|
|
58
|
+
# Extract frame period and calculate frame rate
|
|
59
|
+
frame_period_elem = pv_state.find('.//PVStateValue[@key="framePeriod"]')
|
|
60
|
+
if frame_period_elem is not None:
|
|
61
|
+
frame_period = float(frame_period_elem.get('value'))
|
|
62
|
+
fr = round(1 / frame_period, 1) # Convert to Hz
|
|
63
|
+
metadata['fr'] = fr
|
|
64
|
+
|
|
65
|
+
# Extract optical zoom
|
|
66
|
+
zoom_elem = pv_state.find('.//PVStateValue[@key="opticalZoom"]')
|
|
67
|
+
if zoom_elem is not None:
|
|
68
|
+
metadata['numerical_zoom'] = float(zoom_elem.get('value'))
|
|
69
|
+
|
|
70
|
+
# Extract objective lens
|
|
71
|
+
obj_elem = pv_state.find('.//PVStateValue[@key="objectiveLens"]')
|
|
72
|
+
if obj_elem is not None:
|
|
73
|
+
obj_value = obj_elem.get('value')
|
|
74
|
+
# Extract numeric value from "16X" format
|
|
75
|
+
obj_num = ''.join(filter(str.isdigit, obj_value))
|
|
76
|
+
if obj_num:
|
|
77
|
+
metadata['objective'] = int(obj_num)
|
|
78
|
+
|
|
79
|
+
# Extract laser power (imaging channel)
|
|
80
|
+
laser_power_elem = pv_state.find('.//PVStateValue[@key="laserPower"]')
|
|
81
|
+
if laser_power_elem is not None:
|
|
82
|
+
imaging_power = laser_power_elem.find('.//IndexedValue[@index="0"]')
|
|
83
|
+
if imaging_power is not None:
|
|
84
|
+
metadata['power'] = round(float(imaging_power.get('value')), 1)
|
|
85
|
+
|
|
86
|
+
# Extract laser wavelength
|
|
87
|
+
wavelength_elem = pv_state.find('.//PVStateValue[@key="laserWavelength"]')
|
|
88
|
+
if wavelength_elem is not None:
|
|
89
|
+
wavelength = wavelength_elem.find('.//IndexedValue[@index="0"]')
|
|
90
|
+
if wavelength is not None:
|
|
91
|
+
metadata['wavelength'] = int(wavelength.get('value'))
|
|
92
|
+
|
|
93
|
+
# Extract Z position (Z Focus)
|
|
94
|
+
position_elem = pv_state.find('.//PVStateValue[@key="positionCurrent"]')
|
|
95
|
+
if position_elem is not None:
|
|
96
|
+
# Find Z axis subindexed values
|
|
97
|
+
z_axis = position_elem.find('.//SubindexedValues[@index="ZAxis"]')
|
|
98
|
+
if z_axis is not None:
|
|
99
|
+
z_focus = z_axis.find('.//SubindexedValue[@subindex="0"]')
|
|
100
|
+
if z_focus is not None and z_focus.get('description') == 'Z Focus':
|
|
101
|
+
metadata['depth'] = round(float(z_focus.get('value')), 0)
|
|
102
|
+
|
|
103
|
+
# Extract microns per pixel
|
|
104
|
+
microns_elem = pv_state.find('.//PVStateValue[@key="micronsPerPixel"]')
|
|
105
|
+
if microns_elem is not None:
|
|
106
|
+
x_microns = microns_elem.find('.//IndexedValue[@index="XAxis"]')
|
|
107
|
+
if x_microns is not None:
|
|
108
|
+
metadata['microns_per_pixel'] = round(float(x_microns.get('value')), 2)
|
|
109
|
+
|
|
110
|
+
# Extract pixels per line and lines per frame
|
|
111
|
+
pixels_elem = pv_state.find('.//PVStateValue[@key="pixelsPerLine"]')
|
|
112
|
+
lines_elem = pv_state.find('.//PVStateValue[@key="linesPerFrame"]')
|
|
113
|
+
if pixels_elem is not None and lines_elem is not None:
|
|
114
|
+
metadata['Npixel_x'] = int(pixels_elem.get('value'))
|
|
115
|
+
metadata['Npixel_y'] = int(lines_elem.get('value'))
|
|
116
|
+
|
|
117
|
+
# Extract date from PVScan
|
|
118
|
+
date_attr = root.get('date')
|
|
119
|
+
if date_attr:
|
|
120
|
+
# Convert "1/26/2024 5:26:15 PM" to "01262024" format
|
|
121
|
+
try:
|
|
122
|
+
date_obj = datetime.strptime(date_attr, "%m/%d/%Y %I:%M:%S %p")
|
|
123
|
+
metadata['date'] = date_obj.strftime("%m%d%Y")
|
|
124
|
+
except:
|
|
125
|
+
metadata['date'] = date_attr
|
|
126
|
+
|
|
127
|
+
return metadata
|
|
128
|
+
except Exception as e:
|
|
129
|
+
print(f"Error parsing XML {xml_file_path}: {e}")
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
def update_sidecar_with_xml_metadata(xml_file_path, sidecar_path, append_to_runs=False):
|
|
133
|
+
"""
|
|
134
|
+
Extract imaging metadata from PVScan XML file and add to sidecar JSON.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
xml_file_path: Path to the XML file
|
|
138
|
+
sidecar_path: Path to existing sidecar JSON file
|
|
139
|
+
append_to_runs: If True, append metadata to a 'runs' list in the sidecar
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
bool: True if successful, False otherwise
|
|
143
|
+
"""
|
|
144
|
+
try:
|
|
145
|
+
metadata = extract_pv_metadata(xml_file_path)
|
|
146
|
+
if not metadata:
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
# Load existing sidecar JSON
|
|
150
|
+
with open(sidecar_path, 'r') as f:
|
|
151
|
+
sidecar = json.load(f)
|
|
152
|
+
|
|
153
|
+
if append_to_runs:
|
|
154
|
+
runs = sidecar.get('runs', [])
|
|
155
|
+
# Add run info
|
|
156
|
+
metadata['xml_file'] = Path(xml_file_path).name
|
|
157
|
+
metadata['run_folder'] = Path(xml_file_path).parent.name
|
|
158
|
+
runs.append(metadata)
|
|
159
|
+
sidecar['runs'] = runs
|
|
160
|
+
|
|
161
|
+
# Also update 'imaging' if it's the first run or if 'imaging' is empty, for backward compatibility
|
|
162
|
+
if 'imaging' not in sidecar:
|
|
163
|
+
sidecar['imaging'] = metadata
|
|
164
|
+
else:
|
|
165
|
+
sidecar['imaging'] = metadata
|
|
166
|
+
|
|
167
|
+
# Save updated sidecar
|
|
168
|
+
with open(sidecar_path, 'w') as f:
|
|
169
|
+
json.dump(sidecar, f, indent=2)
|
|
170
|
+
|
|
171
|
+
print(f"Updated sidecar JSON with imaging metadata from {Path(xml_file_path).name}")
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
except Exception as e:
|
|
175
|
+
print(f"Warning: could not update sidecar json: {e}")
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
# Deprecated: alias for backward compatibility until all calls are updated
|
|
179
|
+
extract_xml_metadata_to_sidecar_to_sidecar = update_sidecar_with_xml_metadata
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# Couldn't suppress tiffffile warnings with warnings.filterwarnings('ignore'), so used this instead
|
|
184
|
+
@contextlib.contextmanager
|
|
185
|
+
def suppress_stdout_stderr():
|
|
186
|
+
"""A context manager that redirects stdout and stderr to devnull"""
|
|
187
|
+
with open(os.devnull, 'w') as fnull:
|
|
188
|
+
old_stdout, old_stderr = sys.stdout, sys.stderr
|
|
189
|
+
sys.stdout, sys.stderr = fnull, fnull
|
|
190
|
+
try:
|
|
191
|
+
yield
|
|
192
|
+
finally:
|
|
193
|
+
sys.stdout, sys.stderr = old_stdout, old_stderr
|
|
194
|
+
|
|
195
|
+
@contextlib.contextmanager
|
|
196
|
+
def suppress_c_stderr():
|
|
197
|
+
"""A context manager that redirects C level stderr to devnull"""
|
|
198
|
+
original_stderr_fd = sys.stderr.fileno()
|
|
199
|
+
original_stderr = os.dup(original_stderr_fd)
|
|
200
|
+
devnull = os.open(os.devnull, os.O_WRONLY)
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
os.dup2(devnull, original_stderr_fd)
|
|
204
|
+
yield
|
|
205
|
+
finally:
|
|
206
|
+
os.dup2(original_stderr, original_stderr_fd)
|
|
207
|
+
os.close(devnull)
|
|
208
|
+
os.close(original_stderr)
|
|
209
|
+
|
|
210
|
+
def is_multi_page_tiff(file_path):
|
|
211
|
+
"""
|
|
212
|
+
Check if the TIFF file at file_path is a multi-page TIFF.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
- file_path: Path to the TIFF file.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
- True if the file is a multi-page TIFF, False otherwise.
|
|
219
|
+
"""
|
|
220
|
+
with TiffFile(file_path) as tif:
|
|
221
|
+
# Check if the TIFF file has more than one page
|
|
222
|
+
return len(tif.pages) > 1
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def concat_tiff_files(file_list, output_file, compression=None):
|
|
226
|
+
""" Concatenate a list of tiff files into a multi-page TIFF"""
|
|
227
|
+
# with suppress_c_stderr():
|
|
228
|
+
libtiff_ctypes.suppress_warnings()
|
|
229
|
+
|
|
230
|
+
tif = TIFF.open(file_list[0], 'r')
|
|
231
|
+
first_img = tif.read_image()
|
|
232
|
+
tif.close()
|
|
233
|
+
|
|
234
|
+
# Get the shape of the first image
|
|
235
|
+
# img_shape = first_img.shape
|
|
236
|
+
# print(f"First image shape: {first_img.shape}")
|
|
237
|
+
# Get the data type of the first image
|
|
238
|
+
img_dtype = first_img.dtype
|
|
239
|
+
# Get the range of the first image
|
|
240
|
+
img_range = (first_img.min(), first_img.max())
|
|
241
|
+
|
|
242
|
+
if img_dtype == np.uint16 and img_range[1] <= 4095:
|
|
243
|
+
# Convert range to uint16 by multiplying by 16
|
|
244
|
+
print(f"Images are {img_dtype} with range {img_range}.")
|
|
245
|
+
|
|
246
|
+
# Open the output file as biggiff
|
|
247
|
+
out_tif = TIFF.open(output_file, mode='w8')
|
|
248
|
+
# Write all the images to the output file
|
|
249
|
+
# tiff_concat(file_list, output_file, overwrite=True)
|
|
250
|
+
for i in range(len(file_list)):
|
|
251
|
+
tif = TIFF.open(file_list[i], 'r')
|
|
252
|
+
img = tif.read_image()
|
|
253
|
+
# Flip the image vertically (Original Bruker frames are flipped upside down)
|
|
254
|
+
img = np.flipud(img)
|
|
255
|
+
tif.close()
|
|
256
|
+
out_tif.write_image(img, compression=compression)
|
|
257
|
+
out_tif.close()
|
|
258
|
+
|
|
259
|
+
# # Load all the images first with then save as a multi-page tiff
|
|
260
|
+
# for i in range(len(file_list)):
|
|
261
|
+
# tif = TIFF.open(file_list[i], 'r')
|
|
262
|
+
# img = tif.read_image()
|
|
263
|
+
# tif.close()
|
|
264
|
+
# if i == 0:
|
|
265
|
+
# m = img
|
|
266
|
+
# else:
|
|
267
|
+
# m = np.dstack((m, img))
|
|
268
|
+
|
|
269
|
+
# bigtiff_file = output_file.replace('.tiff', '_bt.tiff')
|
|
270
|
+
# imwrite(bigtiff_file, m, imagej=False, bigtiff=True, metadata=None, dtype=np.uint16, compressionargs={'compression': compression})
|
|
271
|
+
|
|
272
|
+
print(f"Concatenated {len(file_list)} files to multi-page TIFF {output_file}.")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def concat_multi_tiff_files(file_list, output_file, compression=None):
|
|
276
|
+
# with suppress_c_stderr():
|
|
277
|
+
libtiff_ctypes.suppress_warnings()
|
|
278
|
+
|
|
279
|
+
with TiffWriter(output_file, bigtiff=True) as out_tif:
|
|
280
|
+
for file_path in file_list:
|
|
281
|
+
# print(f"Processing {file_path}")
|
|
282
|
+
with TiffFile(file_path) as tif:
|
|
283
|
+
for page in tif.pages:
|
|
284
|
+
img = page.asarray()
|
|
285
|
+
# Flip the image vertically if needed
|
|
286
|
+
img_flipped = np.flipud(img)
|
|
287
|
+
out_tif.write(img_flipped, compression=compression)
|
|
288
|
+
print(f"Concatenated {len(file_list)} files to multi-page TIFF {output_file}.")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def load_multi_page_tiff(input_file):
|
|
292
|
+
"""
|
|
293
|
+
Load a multi-page TIFF file into a NumPy array.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
- input_file: Path to the multi-page TIFF file.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
- loaded_movie: A NumPy array containing the image data from all pages.
|
|
300
|
+
"""
|
|
301
|
+
with TiffFile(input_file) as tif:
|
|
302
|
+
# Allocate a NumPy array to hold the image data
|
|
303
|
+
# Assumes that all pages have the same shape and data type
|
|
304
|
+
num_pages = len(tif.pages)
|
|
305
|
+
if num_pages == 0:
|
|
306
|
+
raise ValueError("The TIFF file contains no pages.")
|
|
307
|
+
|
|
308
|
+
# Read the first page to determine the shape and dtype
|
|
309
|
+
sample_page = tif.pages[0].asarray()
|
|
310
|
+
image_shape = sample_page.shape
|
|
311
|
+
dtype = sample_page.dtype
|
|
312
|
+
|
|
313
|
+
# Create an empty array to store all images
|
|
314
|
+
loaded_movie = np.empty((num_pages,) + image_shape, dtype=dtype)
|
|
315
|
+
|
|
316
|
+
# Load each page into the array
|
|
317
|
+
for i, page in enumerate(tif.pages):
|
|
318
|
+
loaded_movie[i] = page.asarray()
|
|
319
|
+
|
|
320
|
+
return loaded_movie
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def convert_to_bigtiff(input_file, output_file, compression='lzw', scale_range=False, remove_temp=True):
|
|
324
|
+
"""
|
|
325
|
+
Convert a multi-page TIFF to a BigTIFF.
|
|
326
|
+
BigTIFF is required for files larger than 4GB.
|
|
327
|
+
LazyTIFF will read these multi-page TIFFs as 4D
|
|
328
|
+
"""
|
|
329
|
+
try:
|
|
330
|
+
# Check how much memory will be needed to load the file
|
|
331
|
+
file_size_gb = os.path.getsize(input_file) / 1024**3
|
|
332
|
+
print(f"Memory needed to load file: {file_size_gb:.2f} GB")
|
|
333
|
+
|
|
334
|
+
# Check how much memory is available
|
|
335
|
+
available_memory_gb = psutil.virtual_memory().available / 1024**3
|
|
336
|
+
print(f"Memory available: {available_memory_gb:.2f} GB")
|
|
337
|
+
|
|
338
|
+
if file_size_gb > available_memory_gb:
|
|
339
|
+
raise MemoryError(f"Memory needed to load file ({file_size_gb:.2f} GB) is greater than available memory ({available_memory_gb:.2f} GB).")
|
|
340
|
+
# Create a Dask array that lazily represents the TIFF file
|
|
341
|
+
loaded_movie = da.from_array(imread(input_file), chunks=(1, -1, -1))
|
|
342
|
+
else:
|
|
343
|
+
# loaded_movie = imread(input_file) #, outtype=np.uint16)
|
|
344
|
+
|
|
345
|
+
loaded_movie = load_multi_page_tiff(input_file)
|
|
346
|
+
|
|
347
|
+
except Exception as e:
|
|
348
|
+
print(f"Error loading multi-page TIFF file: {e}")
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
# Check data type and data range
|
|
352
|
+
# At this point it is likely uint16 datatype, but with a uint12 range (original data from Bruker is uint12)
|
|
353
|
+
if loaded_movie.dtype == np.uint16 and loaded_movie.max() <= 4095 and scale_range:
|
|
354
|
+
# Convert range to uint16
|
|
355
|
+
print(f"Images are {loaded_movie.dtype} with range {loaded_movie.min()} - {loaded_movie.max()}.")
|
|
356
|
+
print(f"Set -r flag to False to disable conversion from uint12 to uint16.")
|
|
357
|
+
|
|
358
|
+
# loaded_movie = loaded_movie * 16 # Replaced with slightly more accurate method below
|
|
359
|
+
scale_factor = (2**16 - 1) / (2**12 - 1)
|
|
360
|
+
# Check how much memory the scaled array will need, in addition to the original array
|
|
361
|
+
scaled_size_gb = loaded_movie.nbytes * scale_factor / 1024**3
|
|
362
|
+
print(f"Memory needed to scale array, to convert to uint16: {scaled_size_gb:.2f} GB")
|
|
363
|
+
print(f"Total memory needed: {file_size_gb + scaled_size_gb:.2f} GB")
|
|
364
|
+
available_memory_gb = psutil.virtual_memory().available / 1024**3
|
|
365
|
+
# print(f"Memory available: {available_memory_gb:.2f} GB")
|
|
366
|
+
|
|
367
|
+
if file_size_gb + scaled_size_gb > available_memory_gb/2:
|
|
368
|
+
# # Cancel the conversion if there is not enough memory
|
|
369
|
+
# # raise MemoryError(f"Memory needed to scale array ({scaled_size_gb:.2f} GB) is greater than available memory ({available_memory_gb:.2f} GB).")
|
|
370
|
+
# print(f"Memory needed to scale array ({file_size_gb + scaled_size_gb:.2f} GB) is greater than half available memory ({available_memory_gb/2:.2f} GB).")
|
|
371
|
+
# # print(f"Set -r flag to False to disable conversion from uint12 to uint16, or increase available memory.")
|
|
372
|
+
print(f"Scaling by chunks out of caution.")
|
|
373
|
+
|
|
374
|
+
# Define chunk size
|
|
375
|
+
chunk_size = 1000 # TODO: Adjust this value based on the system's memory capacity
|
|
376
|
+
|
|
377
|
+
# Calculate the number of chunks
|
|
378
|
+
num_chunks = (len(loaded_movie) - 1) // chunk_size + 1
|
|
379
|
+
|
|
380
|
+
# Process each chunk
|
|
381
|
+
print(f"Scaling by chunks: {num_chunks} chunks of {chunk_size} frames.")
|
|
382
|
+
# scaled_movie = np.zeros_like(loaded_movie)
|
|
383
|
+
for i in range(num_chunks):
|
|
384
|
+
start = i * chunk_size
|
|
385
|
+
end = start + chunk_size
|
|
386
|
+
print(f"Processing chunk {i+1}/{num_chunks}")
|
|
387
|
+
print(f"Memory needed to scale array: {loaded_movie[start:end].nbytes * scale_factor / 1024**3:.2f} GB")
|
|
388
|
+
loaded_movie[start:end] = (loaded_movie[start:end] * scale_factor).astype(np.uint16)
|
|
389
|
+
|
|
390
|
+
else:
|
|
391
|
+
loaded_movie = (loaded_movie * scale_factor).astype(np.uint16)
|
|
392
|
+
|
|
393
|
+
print(f"Converted data range: {loaded_movie.min()} - {loaded_movie.max()}")
|
|
394
|
+
|
|
395
|
+
else:
|
|
396
|
+
print(f"Images are {loaded_movie.dtype} with range {loaded_movie.min()} - {loaded_movie.max()}.")
|
|
397
|
+
print(f"No scaling of data range requested. \n")
|
|
398
|
+
|
|
399
|
+
# Flip each frame of the movie individually (this is done earlier in concat_tiff_files)
|
|
400
|
+
# flipped_movie = np.array([np.flipud(frame) for frame in loaded_movie])
|
|
401
|
+
|
|
402
|
+
if remove_temp:
|
|
403
|
+
# Remove the original file
|
|
404
|
+
os.remove(input_file)
|
|
405
|
+
print(f"Removed {input_file}")
|
|
406
|
+
|
|
407
|
+
# Save the movie as a bigtiff
|
|
408
|
+
imwrite(output_file, loaded_movie, imagej=False, bigtiff=True, metadata=None, dtype=np.uint16, compressionargs={'compression': compression})
|
|
409
|
+
|
|
410
|
+
# print(f"BigTIFF file saved as {output_file}")
|
|
411
|
+
|
|
412
|
+
def concatenate_tiff_to_bigtiff(tiff_files, export_path, conversion_step='one-step', compression='lzw', scale_range=False, data_path=None):
|
|
413
|
+
""" Concatenate a list of tiff files into a multi-page BigTIFF"""
|
|
414
|
+
if os.path.isdir(export_path):
|
|
415
|
+
# Define output file path
|
|
416
|
+
cattiff_file = os.path.join(export_path, 'cat_tiff.tiff')
|
|
417
|
+
elif os.path.isfile(export_path):
|
|
418
|
+
cattiff_file = export_path
|
|
419
|
+
export_path = os.path.dirname(export_path)
|
|
420
|
+
|
|
421
|
+
multipagetiff_file = cattiff_file.replace('.tiff', '_mpt.tiff')
|
|
422
|
+
bigtiff_file = cattiff_file.replace('.tiff', '_bt.tiff')
|
|
423
|
+
|
|
424
|
+
# ignore warnings for this operation
|
|
425
|
+
with warnings.catch_warnings():
|
|
426
|
+
warnings.simplefilter("ignore")
|
|
427
|
+
# with suppress_stdout_stderr():
|
|
428
|
+
# with suppress_c_stderr():
|
|
429
|
+
|
|
430
|
+
# # if two-step conversion is not needed, write to bigtiff directly
|
|
431
|
+
# First concatenate the ome.tif files into a multipage tiff
|
|
432
|
+
try:
|
|
433
|
+
if(is_multi_page_tiff(tiff_files[0])):
|
|
434
|
+
# input files are multitiff
|
|
435
|
+
print("Input files are multitiff")
|
|
436
|
+
concat_multi_tiff_files(tiff_files, multipagetiff_file)
|
|
437
|
+
else:
|
|
438
|
+
print("Input files are single tiff")
|
|
439
|
+
concat_tiff_files(tiff_files, multipagetiff_file)
|
|
440
|
+
|
|
441
|
+
except Exception as e:
|
|
442
|
+
print(f"Error concatenating files: {e}")
|
|
443
|
+
else:
|
|
444
|
+
print(f"Done with concatenation.")
|
|
445
|
+
|
|
446
|
+
# Then convert the multipage tiff to bigtiff
|
|
447
|
+
try:
|
|
448
|
+
convert_to_bigtiff(multipagetiff_file, bigtiff_file, compression, scale_range)
|
|
449
|
+
except Exception as e:
|
|
450
|
+
print(f"Error converting to BigTIFF: {e}")
|
|
451
|
+
raise
|
|
452
|
+
else:
|
|
453
|
+
print(f"Done with conversion.")
|
|
454
|
+
finally:
|
|
455
|
+
# set warnings back to default
|
|
456
|
+
warnings.resetwarnings()
|
|
457
|
+
|
|
458
|
+
print(f"Concatenated {len(tiff_files)} files to BigTIFF {bigtiff_file}.")
|
|
459
|
+
|
|
460
|
+
# Double check the output
|
|
461
|
+
reloaded_movie = load_multi_page_tiff(bigtiff_file)
|
|
462
|
+
|
|
463
|
+
print(f"Checking BigTIFF file: {bigtiff_file}")
|
|
464
|
+
print(f"Number of pages: {len(reloaded_movie)}")
|
|
465
|
+
print(f"Image shape: {reloaded_movie.shape[1:]}, \n\
|
|
466
|
+
dtype: {reloaded_movie.dtype}, \n\
|
|
467
|
+
range: {reloaded_movie.min()} - {reloaded_movie.max()}\n")
|
|
468
|
+
|
|
469
|
+
# Write a small JSON sidecar next to the concatenated movie
|
|
470
|
+
try:
|
|
471
|
+
nframes = int(reloaded_movie.shape[0])
|
|
472
|
+
Ly = int(reloaded_movie.shape[1])
|
|
473
|
+
Lx = int(reloaded_movie.shape[2])
|
|
474
|
+
sidecar = {
|
|
475
|
+
"nframes": nframes,
|
|
476
|
+
"Ly": Ly,
|
|
477
|
+
"Lx": Lx,
|
|
478
|
+
"dtype": str(reloaded_movie.dtype),
|
|
479
|
+
"source": "concatenate_tiff_to_bigtiff",
|
|
480
|
+
}
|
|
481
|
+
sidecar_path = Path(bigtiff_file).with_name(Path(bigtiff_file).name + ".json")
|
|
482
|
+
with open(sidecar_path, "w") as f:
|
|
483
|
+
json.dump(sidecar, f)
|
|
484
|
+
print(f"Wrote sidecar JSON: {sidecar_path}")
|
|
485
|
+
|
|
486
|
+
# Try to extract XML metadata and add to sidecar
|
|
487
|
+
# Look for XML file in the original data directory (same name as folder)
|
|
488
|
+
if data_path is not None:
|
|
489
|
+
xml_file = Path(data_path) / (Path(data_path).name + ".xml")
|
|
490
|
+
else:
|
|
491
|
+
# Fallback: look in the same directory as the concatenated file
|
|
492
|
+
xml_file = Path(bigtiff_file).parent / (Path(bigtiff_file).parent.name + ".xml")
|
|
493
|
+
if xml_file.exists():
|
|
494
|
+
extract_xml_metadata_to_sidecar_to_sidecar(xml_file, sidecar_path)
|
|
495
|
+
else:
|
|
496
|
+
print(f"XML file not found: {xml_file}")
|
|
497
|
+
|
|
498
|
+
except Exception as e:
|
|
499
|
+
print(f"Warning: could not write sidecar JSON for {bigtiff_file}: {e}")
|
|
500
|
+
|
|
501
|
+
def concatenate_tiff_to_hdf5(tiff_files, export_path, data_path=None):
|
|
502
|
+
import caiman as cm
|
|
503
|
+
|
|
504
|
+
# Check if the export path is a directory
|
|
505
|
+
if os.path.isdir(export_path):
|
|
506
|
+
# Define output file path
|
|
507
|
+
cat_h5_file = os.path.join(export_path, 'cat_tiff.h5')
|
|
508
|
+
elif os.path.isfile(export_path):
|
|
509
|
+
cat_h5_file = export_path
|
|
510
|
+
export_path = os.path.dirname(export_path)
|
|
511
|
+
with warnings.catch_warnings():
|
|
512
|
+
warnings.simplefilter("ignore")
|
|
513
|
+
with suppress_stdout_stderr():
|
|
514
|
+
m = cm.load_movie_chain(tiff_files,outtype=np.ushort)
|
|
515
|
+
# set warnings back to default
|
|
516
|
+
warnings.resetwarnings()
|
|
517
|
+
|
|
518
|
+
m.save(cat_h5_file,to32=False)
|
|
519
|
+
print(f"Concatenated Tiff saved to {cat_h5_file}")
|
|
520
|
+
|
|
521
|
+
# Write a small JSON sidecar next to the concatenated HDF5
|
|
522
|
+
try:
|
|
523
|
+
# caiman Movie has shape (T, Ly, Lx)
|
|
524
|
+
T, Ly, Lx = m.shape # type: ignore[attr-defined]
|
|
525
|
+
sidecar = {
|
|
526
|
+
"nframes": int(T),
|
|
527
|
+
"Ly": int(Ly),
|
|
528
|
+
"Lx": int(Lx),
|
|
529
|
+
"dtype": "uint16", # saved via to32=False above
|
|
530
|
+
"source": "concatenate_tiff_to_hdf5",
|
|
531
|
+
}
|
|
532
|
+
sidecar_path = Path(cat_h5_file).with_name(Path(cat_h5_file).name + ".json")
|
|
533
|
+
with open(sidecar_path, "w") as f:
|
|
534
|
+
json.dump(sidecar, f)
|
|
535
|
+
print(f"Wrote sidecar JSON: {sidecar_path}")
|
|
536
|
+
|
|
537
|
+
# Try to extract XML metadata and add to sidecar
|
|
538
|
+
# Look for XML file in the original data directory (same name as folder)
|
|
539
|
+
if data_path is not None:
|
|
540
|
+
xml_file = Path(data_path) / (Path(data_path).name + ".xml")
|
|
541
|
+
else:
|
|
542
|
+
# Fallback: look in the same directory as the concatenated file
|
|
543
|
+
xml_file = Path(cat_h5_file).parent / (Path(cat_h5_file).parent.name + ".xml")
|
|
544
|
+
if xml_file.exists():
|
|
545
|
+
extract_xml_metadata_to_sidecar_to_sidecar(xml_file, sidecar_path)
|
|
546
|
+
else:
|
|
547
|
+
print(f"XML file not found: {xml_file}")
|
|
548
|
+
|
|
549
|
+
except Exception as e:
|
|
550
|
+
print(f"Warning: could not write sidecar JSON for {cat_h5_file}: {e}")
|
|
551
|
+
|
|
552
|
+
def concatenate_files(input_paths, output_path, regex='*_Ch2_*.ome.tif', method='bigtiff', keep=False, compression='lzw', scale_range=True):
|
|
553
|
+
""" Concatenate a list of tiff files into a multi-page TIFF"""
|
|
554
|
+
if os.path.isdir(input_paths[0]):
|
|
555
|
+
# If the input is a directory, we use the default regex pattern.
|
|
556
|
+
if regex is None:
|
|
557
|
+
print('Default regex pattern is: ' f'{regex}')
|
|
558
|
+
print('Use -p to specify a different pattern.')
|
|
559
|
+
|
|
560
|
+
# If the input is a single directory, get all the tiff files
|
|
561
|
+
if len(input_paths) == 1:
|
|
562
|
+
tiff_files = sorted(glob.glob(os.path.join(input_paths[0],regex)))
|
|
563
|
+
else:
|
|
564
|
+
# For multiple folders:
|
|
565
|
+
tiff_files = []
|
|
566
|
+
for folder in input_paths:
|
|
567
|
+
tiff_files += sorted(glob.glob(os.path.join(folder,regex)))
|
|
568
|
+
tiff_files.sort()
|
|
569
|
+
|
|
570
|
+
else:
|
|
571
|
+
# The input is file list
|
|
572
|
+
tiff_files = input_paths
|
|
573
|
+
|
|
574
|
+
# Count the files
|
|
575
|
+
file_count = len(tiff_files)
|
|
576
|
+
print(f"Found {file_count} files to concatenate.")
|
|
577
|
+
|
|
578
|
+
if output_path is None:
|
|
579
|
+
# Default directory is the input directory
|
|
580
|
+
export_path = os.path.dirname(input_paths[0])
|
|
581
|
+
elif os.path.isfile(output_path) and os.path.exists(output_path) and keep:
|
|
582
|
+
raise FileExistsError('Output file already exists. Aborting.')
|
|
583
|
+
else:
|
|
584
|
+
export_path = output_path
|
|
585
|
+
|
|
586
|
+
print(f"Saving concatenated file to directory: {export_path}")
|
|
587
|
+
|
|
588
|
+
# Start the timer
|
|
589
|
+
start_time = time.time()
|
|
590
|
+
|
|
591
|
+
# Concatenate the tiff files, according to the method
|
|
592
|
+
if method == 'bigtiff':
|
|
593
|
+
concatenate_tiff_to_bigtiff(tiff_files, export_path, compression=compression, scale_range=scale_range, data_path=input_paths[0])
|
|
594
|
+
elif method == 'hdf5':
|
|
595
|
+
concatenate_tiff_to_hdf5(tiff_files, export_path, data_path=input_paths[0])
|
|
596
|
+
|
|
597
|
+
# Extract XML metadata for each run folder and consolidate into the sidecar
|
|
598
|
+
if os.path.isdir(input_paths[0]):
|
|
599
|
+
print("Consolidating XML metadata into sidecar...")
|
|
600
|
+
|
|
601
|
+
# Determine the sidecar path based on export settings
|
|
602
|
+
# Note: simplistic assumption matching concatenate_tiff_to_* behavior
|
|
603
|
+
sidecar_path = None
|
|
604
|
+
if method == 'bigtiff':
|
|
605
|
+
if output_path and os.path.isfile(output_path):
|
|
606
|
+
sidecar_path = Path(output_path).with_name(Path(output_path).name + ".json")
|
|
607
|
+
elif output_path:
|
|
608
|
+
sidecar_path = Path(output_path) / 'cat_tiff_bt.tiff.json'
|
|
609
|
+
else:
|
|
610
|
+
sidecar_path = Path(export_path) / 'cat_tiff_bt.tiff.json'
|
|
611
|
+
elif method == 'hdf5':
|
|
612
|
+
if output_path and os.path.isfile(output_path):
|
|
613
|
+
sidecar_path = Path(output_path).with_name(Path(output_path).name + ".json")
|
|
614
|
+
else:
|
|
615
|
+
sidecar_path = Path(export_path) / 'cat_tiff.h5.json'
|
|
616
|
+
|
|
617
|
+
if sidecar_path and sidecar_path.exists():
|
|
618
|
+
for run_folder in input_paths:
|
|
619
|
+
# Assuming standard Bruker structure where XML has same name as folder
|
|
620
|
+
xml_file = Path(run_folder) / (Path(run_folder).name + ".xml")
|
|
621
|
+
if xml_file.exists():
|
|
622
|
+
update_sidecar_with_xml_metadata(xml_file, sidecar_path, append_to_runs=True)
|
|
623
|
+
else:
|
|
624
|
+
print(f"XML file not found for run {run_folder}, skipping metadata extraction.")
|
|
625
|
+
else:
|
|
626
|
+
print("Sidecar file not found, skipping metadata consolidation.")
|
|
627
|
+
|
|
628
|
+
# Calculate the elapsed time
|
|
629
|
+
elapsed_time = time.time() - start_time
|
|
630
|
+
|
|
631
|
+
print(f"Concatenation completed in {elapsed_time:.2f} seconds.\n")
|
|
632
|
+
|
|
633
|
+
def main():
|
|
634
|
+
parser = argparse.ArgumentParser()
|
|
635
|
+
parser.add_argument('input', nargs='+', help='Input files')
|
|
636
|
+
# Optional arguments
|
|
637
|
+
parser.add_argument('-p', '--pattern', default='*_Ch2_*.ome.tif', help='Regex pattern to match files')
|
|
638
|
+
parser.add_argument('-o', '--output', default=None, help='Output directory or file')
|
|
639
|
+
parser.add_argument('-m', '--method', choices=['bigtiff', 'hdf5'], default='bigtiff', help='Output format')
|
|
640
|
+
parser.add_argument('-c', '--compression', choices=['lzw', 'none'], default='lzw', help='Compression method for BigTIFF')
|
|
641
|
+
parser.add_argument('-s', '--step', choices=['one-step', 'two-step'], default='one-step', help='Conversion steps for BigTIFF')
|
|
642
|
+
parser.add_argument('-r', '--scale_range', action='store_true', default=True, help='Scale range from uint12 to uint16')
|
|
643
|
+
parser.add_argument('-t', '--temp_delete', action='store_true', default=True, help='Delete temporary files')
|
|
644
|
+
parser.add_argument('-k', '--keep', action='store_true', help='Don\'t force overwrite if file exists')
|
|
645
|
+
args = parser.parse_args()
|
|
646
|
+
|
|
647
|
+
concatenate_files(args.input, args.output, args.pattern, args.method, args.keep, args.compression, args.scale_range)
|
|
648
|
+
|
|
649
|
+
if __name__ == '__main__':
|
|
650
|
+
main()
|