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 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()