spacr 0.4.0__py3-none-any.whl → 0.4.2__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.
- spacr/__init__.py +2 -2
- spacr/core.py +14 -3
- spacr/deep_spacr.py +2 -95
- spacr/gui_core.py +301 -46
- spacr/gui_elements.py +131 -0
- spacr/gui_utils.py +24 -20
- spacr/io.py +312 -8
- spacr/measure.py +11 -12
- spacr/plot.py +2 -2
- spacr/settings.py +157 -49
- spacr/sp_stats.py +221 -0
- spacr/submodules.py +2 -2
- spacr/utils.py +115 -33
- {spacr-0.4.0.dist-info → spacr-0.4.2.dist-info}/METADATA +2 -1
- {spacr-0.4.0.dist-info → spacr-0.4.2.dist-info}/RECORD +19 -18
- {spacr-0.4.0.dist-info → spacr-0.4.2.dist-info}/LICENSE +0 -0
- {spacr-0.4.0.dist-info → spacr-0.4.2.dist-info}/WHEEL +0 -0
- {spacr-0.4.0.dist-info → spacr-0.4.2.dist-info}/entry_points.txt +0 -0
- {spacr-0.4.0.dist-info → spacr-0.4.2.dist-info}/top_level.txt +0 -0
spacr/gui_elements.py
CHANGED
@@ -2285,6 +2285,9 @@ class AnnotateApp:
|
|
2285
2285
|
|
2286
2286
|
self.train_button = Button(self.button_frame,text="orig.",command=self.swich_back_annotation_column,bg=self.bg_color,fg=self.fg_color,highlightbackground=self.fg_color,highlightcolor=self.fg_color,highlightthickness=1)
|
2287
2287
|
self.train_button.pack(side="right", padx=5)
|
2288
|
+
|
2289
|
+
self.settings_button = Button(self.button_frame, text="Settings", command=self.open_settings_window, bg=self.bg_color, fg=self.fg_color, highlightbackground=self.fg_color,highlightcolor=self.fg_color,highlightthickness=1)
|
2290
|
+
self.settings_button.pack(side="right", padx=5)
|
2288
2291
|
|
2289
2292
|
# Calculate grid rows and columns based on the root window size and image size
|
2290
2293
|
self.calculate_grid_dimensions()
|
@@ -2308,6 +2311,134 @@ class AnnotateApp:
|
|
2308
2311
|
for col in range(self.grid_cols):
|
2309
2312
|
self.grid_frame.grid_columnconfigure(col, weight=1)
|
2310
2313
|
|
2314
|
+
def open_settings_window(self):
|
2315
|
+
from .gui_utils import generate_annotate_fields, convert_to_number
|
2316
|
+
|
2317
|
+
# Create settings window
|
2318
|
+
settings_window = tk.Toplevel(self.root)
|
2319
|
+
settings_window.title("Modify Annotation Settings")
|
2320
|
+
|
2321
|
+
style_out = set_dark_style(ttk.Style())
|
2322
|
+
settings_window.configure(bg=style_out['bg_color'])
|
2323
|
+
|
2324
|
+
settings_frame = tk.Frame(settings_window, bg=style_out['bg_color'])
|
2325
|
+
settings_frame.pack(fill=tk.BOTH, expand=True)
|
2326
|
+
|
2327
|
+
# Generate fields with current settings pre-filled
|
2328
|
+
vars_dict = generate_annotate_fields(settings_frame)
|
2329
|
+
|
2330
|
+
# Pre-fill the current settings into vars_dict
|
2331
|
+
current_settings = {
|
2332
|
+
'image_type': self.image_type or '',
|
2333
|
+
'channels': ','.join(self.channels) if self.channels else '',
|
2334
|
+
'img_size': f"{self.image_size[0]},{self.image_size[1]}",
|
2335
|
+
'annotation_column': self.annotation_column or '',
|
2336
|
+
'normalize': str(self.normalize),
|
2337
|
+
'percentiles': ','.join(map(str, self.percentiles)),
|
2338
|
+
'measurement': ','.join(self.measurement) if self.measurement else '',
|
2339
|
+
'threshold': str(self.threshold) if self.threshold is not None else '',
|
2340
|
+
'normalize_channels': ','.join(self.normalize_channels) if self.normalize_channels else ''
|
2341
|
+
}
|
2342
|
+
|
2343
|
+
for key, data in vars_dict.items():
|
2344
|
+
if key in current_settings:
|
2345
|
+
data['entry'].delete(0, tk.END)
|
2346
|
+
data['entry'].insert(0, current_settings[key])
|
2347
|
+
|
2348
|
+
def apply_new_settings():
|
2349
|
+
settings = {key: data['entry'].get() for key, data in vars_dict.items()}
|
2350
|
+
|
2351
|
+
# Process settings exactly as your original initiation function does
|
2352
|
+
settings['channels'] = settings['channels'].split(',') if settings['channels'] else None
|
2353
|
+
settings['img_size'] = list(map(int, settings['img_size'].split(',')))
|
2354
|
+
settings['percentiles'] = list(map(convert_to_number, settings['percentiles'].split(','))) if settings['percentiles'] else [1, 99]
|
2355
|
+
settings['normalize'] = settings['normalize'].lower() == 'true'
|
2356
|
+
settings['normalize_channels'] = settings['normalize_channels'].split(',') if settings['normalize_channels'] else None
|
2357
|
+
|
2358
|
+
try:
|
2359
|
+
settings['measurement'] = settings['measurement'].split(',') if settings['measurement'] else None
|
2360
|
+
settings['threshold'] = None if settings['threshold'].lower() == 'none' else int(settings['threshold'])
|
2361
|
+
except:
|
2362
|
+
settings['measurement'] = None
|
2363
|
+
settings['threshold'] = None
|
2364
|
+
|
2365
|
+
# Convert empty strings to None
|
2366
|
+
for key, value in settings.items():
|
2367
|
+
if isinstance(value, list):
|
2368
|
+
settings[key] = [v if v != '' else None for v in value]
|
2369
|
+
elif value == '':
|
2370
|
+
settings[key] = None
|
2371
|
+
|
2372
|
+
# Apply these settings dynamically using update_settings method
|
2373
|
+
self.update_settings(**{
|
2374
|
+
'image_type': settings.get('image_type'),
|
2375
|
+
'channels': settings.get('channels'),
|
2376
|
+
'image_size': settings.get('img_size'),
|
2377
|
+
'annotation_column': settings.get('annotation_column'),
|
2378
|
+
'normalize': settings.get('normalize'),
|
2379
|
+
'percentiles': settings.get('percentiles'),
|
2380
|
+
'measurement': settings.get('measurement'),
|
2381
|
+
'threshold': settings.get('threshold'),
|
2382
|
+
'normalize_channels': settings.get('normalize_channels')
|
2383
|
+
})
|
2384
|
+
|
2385
|
+
settings_window.destroy()
|
2386
|
+
|
2387
|
+
apply_button = spacrButton(settings_window, text="Apply Settings", command=apply_new_settings,show_text=False)
|
2388
|
+
apply_button.pack(pady=10)
|
2389
|
+
|
2390
|
+
def update_settings(self, **kwargs):
|
2391
|
+
allowed_attributes = {
|
2392
|
+
'image_type', 'channels', 'image_size', 'annotation_column',
|
2393
|
+
'normalize', 'percentiles', 'measurement', 'threshold', 'normalize_channels'
|
2394
|
+
}
|
2395
|
+
|
2396
|
+
updated = False
|
2397
|
+
|
2398
|
+
for attr, value in kwargs.items():
|
2399
|
+
if attr in allowed_attributes and value is not None:
|
2400
|
+
setattr(self, attr, value)
|
2401
|
+
updated = True
|
2402
|
+
|
2403
|
+
if 'image_size' in kwargs:
|
2404
|
+
if isinstance(self.image_size, list):
|
2405
|
+
self.image_size = (int(self.image_size[0]), int(self.image_size[0]))
|
2406
|
+
elif isinstance(self.image_size, int):
|
2407
|
+
self.image_size = (self.image_size, self.image_size)
|
2408
|
+
else:
|
2409
|
+
raise ValueError("Invalid image size")
|
2410
|
+
|
2411
|
+
self.calculate_grid_dimensions()
|
2412
|
+
self.recreate_image_grid()
|
2413
|
+
|
2414
|
+
if updated:
|
2415
|
+
current_index = self.index # Retain current index
|
2416
|
+
self.prefilter_paths_annotations()
|
2417
|
+
|
2418
|
+
# Ensure the retained index is still valid (not out of bounds)
|
2419
|
+
max_index = len(self.filtered_paths_annotations) - 1
|
2420
|
+
self.index = min(current_index, max_index := max(0, max(0, max(len(self.filtered_paths_annotations) - self.grid_rows * self.grid_cols, 0))))
|
2421
|
+
self.load_images()
|
2422
|
+
|
2423
|
+
def recreate_image_grid(self):
|
2424
|
+
# Remove current labels
|
2425
|
+
for label in self.labels:
|
2426
|
+
label.destroy()
|
2427
|
+
self.labels.clear()
|
2428
|
+
|
2429
|
+
# Recreate the labels grid with updated dimensions
|
2430
|
+
for i in range(self.grid_rows * self.grid_cols):
|
2431
|
+
label = Label(self.grid_frame, bg=self.root.cget('bg'))
|
2432
|
+
label.grid(row=i // self.grid_cols, column=i % self.grid_cols, padx=2, pady=2, sticky="nsew")
|
2433
|
+
self.labels.append(label)
|
2434
|
+
|
2435
|
+
# Reconfigure grid weights
|
2436
|
+
for row in range(self.grid_rows):
|
2437
|
+
self.grid_frame.grid_rowconfigure(row, weight=1)
|
2438
|
+
for col in range(self.grid_cols):
|
2439
|
+
self.grid_frame.grid_columnconfigure(col, weight=1)
|
2440
|
+
|
2441
|
+
|
2311
2442
|
def swich_back_annotation_column(self):
|
2312
2443
|
self.annotation_column = self.orig_annotation_columns
|
2313
2444
|
self.prefilter_paths_annotations()
|
spacr/gui_utils.py
CHANGED
@@ -106,7 +106,6 @@ def parse_list(value):
|
|
106
106
|
except (ValueError, SyntaxError) as e:
|
107
107
|
raise ValueError(f"Invalid format for list: {value}. Error: {e}")
|
108
108
|
|
109
|
-
# Usage example in your create_input_field function
|
110
109
|
def create_input_field(frame, label_text, row, var_type='entry', options=None, default_value=None):
|
111
110
|
"""
|
112
111
|
Create an input field in the specified frame.
|
@@ -365,27 +364,30 @@ def convert_settings_dict_for_gui(settings):
|
|
365
364
|
from torchvision import models as torch_models
|
366
365
|
torchvision_models = [name for name, obj in torch_models.__dict__.items() if callable(obj)]
|
367
366
|
chans = ['0', '1', '2', '3', '4', '5', '6', '7', '8', None]
|
367
|
+
chan_list = ['[0,1,2,3,4,5,6,7,8]','[0,1,2,3,4,5,6,7]','[0,1,2,3,4,5,6]','[0,1,2,3,4,5]','[0,1,2,3,4]','[0,1,2,3]', '[0,1,2]', '[0,1]', '[0]', '[0,0]']
|
368
368
|
chans_v2 = [0, 1, 2, 3, None]
|
369
|
+
chans_v3 = list(range(0, 21, 1)) + [None]
|
370
|
+
chans_v4 = [0, 1, 2, 3, None]
|
369
371
|
variables = {}
|
370
372
|
special_cases = {
|
371
|
-
'metadata_type': ('combo', ['cellvoyager', 'cq1', '
|
372
|
-
'channels': ('combo',
|
373
|
+
'metadata_type': ('combo', ['cellvoyager', 'cq1', 'auto', 'custom'], 'cellvoyager'),
|
374
|
+
'channels': ('combo', chan_list, '[0,1,2,3]'),
|
373
375
|
'train_channels': ('combo', ["['r','g','b']", "['r','g']", "['r','b']", "['g','b']", "['r']", "['g']", "['b']"], "['r','g','b']"),
|
374
|
-
'channel_dims': ('combo',
|
376
|
+
'channel_dims': ('combo', chan_list, '[0,1,2,3]'),
|
375
377
|
'dataset_mode': ('combo', ['annotation', 'metadata', 'recruitment'], 'metadata'),
|
376
378
|
'cov_type': ('combo', ['HC0', 'HC1', 'HC2', 'HC3', None], None),
|
377
|
-
'cell_mask_dim': ('combo',
|
378
|
-
'cell_chann_dim': ('combo',
|
379
|
-
'nucleus_mask_dim': ('combo',
|
380
|
-
'nucleus_chann_dim': ('combo',
|
381
|
-
'pathogen_mask_dim': ('combo',
|
382
|
-
'pathogen_chann_dim': ('combo',
|
383
|
-
'crop_mode': ('combo', [['cell'], ['nucleus'], ['pathogen'], ['cell', 'nucleus'], ['cell', 'pathogen'], ['nucleus', 'pathogen'], ['cell', 'nucleus', 'pathogen']], ['cell']),
|
384
|
-
|
385
|
-
'nucleus_channel': ('combo',
|
386
|
-
'cell_channel': ('combo',
|
387
|
-
'channel_of_interest': ('combo',
|
388
|
-
'pathogen_channel': ('combo',
|
379
|
+
#'cell_mask_dim': ('combo', chans_v3, None),
|
380
|
+
#'cell_chann_dim': ('combo', chans_v3, None),
|
381
|
+
#'nucleus_mask_dim': ('combo', chans_v3, None),
|
382
|
+
#'nucleus_chann_dim': ('combo', chans_v3, None),
|
383
|
+
#'pathogen_mask_dim': ('combo', chans_v3, None),
|
384
|
+
#'pathogen_chann_dim': ('combo', chans_v3, None),
|
385
|
+
'crop_mode': ('combo', ["['cell']", "['nucleus']", "['pathogen']", "['cell', 'nucleus']", "['cell', 'pathogen']", "['nucleus', 'pathogen']", "['cell', 'nucleus', 'pathogen']"], "['cell']"),
|
386
|
+
#'magnification': ('combo', [20, 40, 60], 20),
|
387
|
+
#'nucleus_channel': ('combo', chans_v3, None),
|
388
|
+
#'cell_channel': ('combo', chans_v3, None),
|
389
|
+
#'channel_of_interest': ('combo', chans_v3, None),
|
390
|
+
#'pathogen_channel': ('combo', chans_v3, None),
|
389
391
|
'timelapse_mode': ('combo', ['trackpy', 'btrack'], 'trackpy'),
|
390
392
|
'train_mode': ('combo', ['erm', 'irm'], 'erm'),
|
391
393
|
'clustering': ('combo', ['dbscan', 'kmean'], 'dbscan'),
|
@@ -462,10 +464,11 @@ def function_gui_wrapper(function=None, settings={}, q=None, fig_queue=None, imp
|
|
462
464
|
finally:
|
463
465
|
# Restore the original plt.show function
|
464
466
|
plt.show = original_show
|
467
|
+
|
465
468
|
|
469
|
+
|
466
470
|
def run_function_gui(settings_type, settings, q, fig_queue, stop_requested):
|
467
471
|
|
468
|
-
from .gui_utils import process_stdout_stderr
|
469
472
|
from .core import generate_image_umap, preprocess_generate_masks
|
470
473
|
from .cellpose import identify_masks_finetune, check_cellpose_models, compare_cellpose_masks
|
471
474
|
from .submodules import analyze_recruitment
|
@@ -476,9 +479,10 @@ def run_function_gui(settings_type, settings, q, fig_queue, stop_requested):
|
|
476
479
|
from .sim import run_multiple_simulations
|
477
480
|
from .deep_spacr import deep_spacr, apply_model_to_tar
|
478
481
|
from .sequencing import generate_barecode_mapping
|
482
|
+
|
479
483
|
process_stdout_stderr(q)
|
480
|
-
|
481
|
-
print(f'run_function_gui settings_type: {settings_type}')
|
484
|
+
|
485
|
+
print(f'run_function_gui settings_type: {settings_type}')
|
482
486
|
|
483
487
|
if settings_type == 'mask':
|
484
488
|
function = preprocess_generate_masks
|
@@ -523,7 +527,7 @@ def run_function_gui(settings_type, settings, q, fig_queue, stop_requested):
|
|
523
527
|
function = process_non_tif_non_2D_images
|
524
528
|
imports = 1
|
525
529
|
else:
|
526
|
-
raise ValueError(f"Invalid settings type: {settings_type}")
|
530
|
+
raise ValueError(f"Error: Invalid settings type: {settings_type}")
|
527
531
|
try:
|
528
532
|
function_gui_wrapper(function, settings, q, fig_queue, imports)
|
529
533
|
except Exception as e:
|
spacr/io.py
CHANGED
@@ -891,11 +891,16 @@ def _merge_channels(src, plot=False):
|
|
891
891
|
from .utils import print_progress
|
892
892
|
|
893
893
|
stack_dir = os.path.join(src, 'stack')
|
894
|
-
allowed_names = ['01', '02', '03', '04', '00', '1', '2', '3', '4', '0']
|
894
|
+
#allowed_names = ['01', '02', '03', '04', '00', '1', '2', '3', '4', '0']
|
895
|
+
|
896
|
+
string_list = [str(i) for i in range(101)]+[f"{i:02d}" for i in range(10)]
|
897
|
+
allowed_names = sorted(string_list, key=lambda x: int(x))
|
895
898
|
|
896
899
|
# List directories that match the allowed names
|
897
900
|
chan_dirs = [d for d in os.listdir(src) if os.path.isdir(os.path.join(src, d)) and d in allowed_names]
|
898
901
|
chan_dirs.sort()
|
902
|
+
|
903
|
+
num_matching_folders = len(chan_dirs)
|
899
904
|
|
900
905
|
print(f'List of folders in src: {chan_dirs}. Single channel folders.')
|
901
906
|
|
@@ -925,7 +930,7 @@ def _merge_channels(src, plot=False):
|
|
925
930
|
if plot:
|
926
931
|
plot_arrays(os.path.join(src, 'stack'))
|
927
932
|
|
928
|
-
return
|
933
|
+
return num_matching_folders
|
929
934
|
|
930
935
|
def _mip_all(src, include_first_chan=True):
|
931
936
|
|
@@ -1584,10 +1589,6 @@ def preprocess_img_data(settings):
|
|
1584
1589
|
else:
|
1585
1590
|
print(f'Could not find any {valid_ext} files in {src} only found {extension_counts[0]}')
|
1586
1591
|
|
1587
|
-
|
1588
|
-
|
1589
|
-
|
1590
|
-
|
1591
1592
|
if os.path.exists(os.path.join(src,'stack')):
|
1592
1593
|
print('Found existing stack folder.')
|
1593
1594
|
if os.path.exists(os.path.join(src,'channel_stack')):
|
@@ -1644,7 +1645,13 @@ def preprocess_img_data(settings):
|
|
1644
1645
|
print(f"all images: {all_imgs}, full batch: {full_batches}, last batch: {last_batch_size}")
|
1645
1646
|
raise ValueError("Last batch of size 1 detected. Adjust the batch size.")
|
1646
1647
|
|
1647
|
-
_merge_channels(src, plot=False)
|
1648
|
+
nr_channel_folders = _merge_channels(src, plot=False)
|
1649
|
+
|
1650
|
+
if len(settings['channels']) != nr_channel_folders:
|
1651
|
+
print(f"Number of channels does not match number of channel folders. channels: {settings['channels']} channel folders: {nr_channel_folders}")
|
1652
|
+
new_channels = list(range(nr_channel_folders))
|
1653
|
+
print(f"Changing channels from {settings['channels']} to {new_channels}")
|
1654
|
+
settings['channels'] = new_channels
|
1648
1655
|
|
1649
1656
|
if timelapse:
|
1650
1657
|
_create_movies_from_npy_per_channel(stack_path, fps=2)
|
@@ -3095,4 +3102,301 @@ def generate_dataset_from_lists(dst, class_data, classes, test_split=0.1):
|
|
3095
3102
|
test_class_dir = os.path.join(dst, f'test/{cls}')
|
3096
3103
|
print(f'Train class {cls}: {len(os.listdir(train_class_dir))}, Test class {cls}: {len(os.listdir(test_class_dir))}')
|
3097
3104
|
|
3098
|
-
return os.path.join(dst, 'train'), os.path.join(dst, 'test')
|
3105
|
+
return os.path.join(dst, 'train'), os.path.join(dst, 'test')
|
3106
|
+
|
3107
|
+
def convert_to_yokogawa_v1(folder):
|
3108
|
+
"""
|
3109
|
+
Detects file type in the folder and converts them
|
3110
|
+
to Yokogawa-style naming with Maximum Intensity Projection (MIP).
|
3111
|
+
"""
|
3112
|
+
|
3113
|
+
def _get_next_well(used_wells):
|
3114
|
+
"""
|
3115
|
+
Determines the next available well position in a 384-well format.
|
3116
|
+
Iterates wells, and after P24, switches to plate2.
|
3117
|
+
"""
|
3118
|
+
plate = 1
|
3119
|
+
for well in WELLS:
|
3120
|
+
well_name = f"plate{plate}_{well}"
|
3121
|
+
if well_name not in used_wells:
|
3122
|
+
return well_name
|
3123
|
+
if well == "P24":
|
3124
|
+
plate += 1
|
3125
|
+
return f"plate{plate}_A01"
|
3126
|
+
|
3127
|
+
# Define 384-well plate format
|
3128
|
+
ROWS = "ABCDEFGHIJKLMNOP"
|
3129
|
+
COLS = [f"{i:02d}" for i in range(1, 25)]
|
3130
|
+
WELLS = [f"{r}{c}" for r in ROWS for c in COLS]
|
3131
|
+
|
3132
|
+
filenames = []
|
3133
|
+
rename_log = []
|
3134
|
+
csv_path = os.path.join(folder, "rename_log.csv")
|
3135
|
+
used_wells = set(os.listdir(folder))
|
3136
|
+
|
3137
|
+
for file in os.listdir(folder):
|
3138
|
+
path = os.path.join(folder, file)
|
3139
|
+
ext = file.lower().split('.')[-1]
|
3140
|
+
|
3141
|
+
### **Process Nikon ND2 Files**
|
3142
|
+
if ext == 'nd2':
|
3143
|
+
nd2 = ND2Reader(path)
|
3144
|
+
metadata = nd2.metadata
|
3145
|
+
|
3146
|
+
timepoints = metadata.get("frames", [0])
|
3147
|
+
fields = metadata.get("fields_of_view", [0])
|
3148
|
+
z_levels = list(metadata.get("z_levels", range(1)))
|
3149
|
+
channels = metadata.get("channels", [])
|
3150
|
+
|
3151
|
+
for t_idx in timepoints:
|
3152
|
+
for f_idx in fields:
|
3153
|
+
for c_idx, channel in enumerate(channels):
|
3154
|
+
well = _get_next_well(used_wells)
|
3155
|
+
|
3156
|
+
z_stack = [nd2.get_frame_2D(t=t_idx, v=f_idx, z=z_idx, c=c_idx) for z_idx in z_levels]
|
3157
|
+
mip_image = np.max(np.stack(z_stack), axis=0)
|
3158
|
+
dtype = z_stack[0].dtype
|
3159
|
+
|
3160
|
+
filename = f"{well}_T{t_idx+1:04d}F{f_idx+1:03d}L01C{c_idx+1:02d}.tif"
|
3161
|
+
filepath = os.path.join(folder, filename)
|
3162
|
+
tifffile.imwrite(filepath, mip_image.astype(dtype))
|
3163
|
+
used_wells.add(well)
|
3164
|
+
rename_log.append({"Original File": file, "Renamed TIFF": filename})
|
3165
|
+
|
3166
|
+
### **Process Zeiss CZI Files**
|
3167
|
+
elif ext == 'czi':
|
3168
|
+
with czifile.CziFile(path) as czi:
|
3169
|
+
shape = czi.shape
|
3170
|
+
|
3171
|
+
timepoints = range(shape[0])
|
3172
|
+
z_levels = range(shape[1])
|
3173
|
+
channels = range(shape[2])
|
3174
|
+
|
3175
|
+
for t_idx in timepoints:
|
3176
|
+
for c_idx in channels:
|
3177
|
+
well = _get_next_well(used_wells)
|
3178
|
+
|
3179
|
+
z_stack = [czi.asarray()[t_idx, z_idx, c_idx] for z_idx in z_levels]
|
3180
|
+
mip_image = np.max(np.stack(z_stack), axis=0)
|
3181
|
+
dtype = z_stack[0].dtype
|
3182
|
+
|
3183
|
+
filename = f"{well}_T{t_idx+1:04d}F001L01C{c_idx+1:02d}.tif"
|
3184
|
+
filepath = os.path.join(folder, filename)
|
3185
|
+
tifffile.imwrite(filepath, mip_image.astype(dtype))
|
3186
|
+
used_wells.add(well)
|
3187
|
+
rename_log.append({"Original File": file, "Renamed TIFF": filename})
|
3188
|
+
|
3189
|
+
### **Process Leica LIF Files**
|
3190
|
+
elif ext == 'lif':
|
3191
|
+
lif_file = readlif.Reader(path)
|
3192
|
+
|
3193
|
+
for image in lif_file.getIterImage():
|
3194
|
+
timepoints = range(image.dims.t)
|
3195
|
+
z_levels = range(image.dims.z)
|
3196
|
+
channels = range(image.dims.c)
|
3197
|
+
|
3198
|
+
for t_idx in timepoints:
|
3199
|
+
for c_idx in channels:
|
3200
|
+
well = _get_next_well(used_wells)
|
3201
|
+
|
3202
|
+
z_stack = [image.getFrame(z=z_idx, t=t_idx, c=c_idx) for z_idx in z_levels]
|
3203
|
+
mip_image = np.max(np.stack(z_stack), axis=0)
|
3204
|
+
dtype = z_stack[0].dtype
|
3205
|
+
|
3206
|
+
filename = f"{well}_T{t_idx+1:04d}F001L01C{c_idx+1:02d}.tif"
|
3207
|
+
filepath = os.path.join(folder, filename)
|
3208
|
+
tifffile.imwrite(filepath, mip_image.astype(dtype))
|
3209
|
+
used_wells.add(well)
|
3210
|
+
rename_log.append({"Original File": file, "Renamed TIFF": filename})
|
3211
|
+
|
3212
|
+
### **Process Standard Images (.tif, .tiff, .png, .jpg, .bmp, etc.)**
|
3213
|
+
elif ext in ['tif', 'tiff', 'png', 'jpg', 'jpeg', 'bmp'] and not file.startswith("plate"):
|
3214
|
+
with tifffile.TiffFile(path) as tif:
|
3215
|
+
well = _get_next_well(used_wells)
|
3216
|
+
num_pages = len(tif.pages)
|
3217
|
+
|
3218
|
+
if num_pages > 1:
|
3219
|
+
# Assume multi-channel or z-stack
|
3220
|
+
images = tif.asarray()
|
3221
|
+
if images.ndim == 4: # (T, Z, C, Y, X)
|
3222
|
+
timepoints = range(images.shape[0])
|
3223
|
+
z_levels = range(images.shape[1])
|
3224
|
+
channels = range(images.shape[2])
|
3225
|
+
|
3226
|
+
for t_idx in timepoints:
|
3227
|
+
for c_idx in channels:
|
3228
|
+
mip_image = np.max(images[t_idx, :, c_idx], axis=0)
|
3229
|
+
dtype = images.dtype
|
3230
|
+
|
3231
|
+
filename = f"{well}_T{t_idx+1:04d}F001L01C{c_idx+1:02d}.tif"
|
3232
|
+
filepath = os.path.join(folder, filename)
|
3233
|
+
tifffile.imwrite(filepath, mip_image.astype(dtype))
|
3234
|
+
used_wells.add(well)
|
3235
|
+
rename_log.append({"Original File": file, "Renamed TIFF": filename})
|
3236
|
+
|
3237
|
+
elif images.ndim == 3: # (Z, Y, X) or (C, Y, X)
|
3238
|
+
z_stack = images if images.shape[0] > 1 else [images]
|
3239
|
+
mip_image = np.max(np.stack(z_stack), axis=0)
|
3240
|
+
dtype = images.dtype
|
3241
|
+
|
3242
|
+
filename = f"{well}_T0001F001L01C01.tif"
|
3243
|
+
filepath = os.path.join(folder, filename)
|
3244
|
+
tifffile.imwrite(filepath, mip_image.astype(dtype))
|
3245
|
+
used_wells.add(well)
|
3246
|
+
rename_log.append({"Original File": file, "Renamed TIFF": filename})
|
3247
|
+
else:
|
3248
|
+
image = tif.pages[0].asarray()
|
3249
|
+
dtype = image.dtype
|
3250
|
+
|
3251
|
+
filename = f"{well}_T0001F001L01C01.tif"
|
3252
|
+
filepath = os.path.join(folder, filename)
|
3253
|
+
tifffile.imwrite(filepath, image.astype(dtype))
|
3254
|
+
used_wells.add(well)
|
3255
|
+
rename_log.append({"Original File": file, "Renamed TIFF": filename})
|
3256
|
+
|
3257
|
+
# Save rename log as CSV
|
3258
|
+
pd.DataFrame(rename_log).to_csv(csv_path, index=False)
|
3259
|
+
print(f"Processing complete. Files saved in {folder} and rename log saved as {csv_path}.")
|
3260
|
+
|
3261
|
+
def convert_to_yokogawa(folder):
|
3262
|
+
"""
|
3263
|
+
Detects file type in the folder and converts them
|
3264
|
+
to Yokogawa-style naming with Maximum Intensity Projection (MIP).
|
3265
|
+
"""
|
3266
|
+
|
3267
|
+
def _get_next_well(used_wells):
|
3268
|
+
"""
|
3269
|
+
Determines the next available well position in a 384-well format.
|
3270
|
+
Iterates wells, and after P24, switches to plate2.
|
3271
|
+
"""
|
3272
|
+
plate = 1
|
3273
|
+
for well in WELLS:
|
3274
|
+
well_name = f"plate{plate}_{well}"
|
3275
|
+
if well_name not in used_wells:
|
3276
|
+
return well_name
|
3277
|
+
if well == "P24":
|
3278
|
+
plate += 1
|
3279
|
+
return f"plate{plate}_A01"
|
3280
|
+
|
3281
|
+
# Define 384-well plate format
|
3282
|
+
ROWS = "ABCDEFGHIJKLMNOP"
|
3283
|
+
COLS = [f"{i:02d}" for i in range(1, 25)]
|
3284
|
+
WELLS = [f"{r}{c}" for r in ROWS for c in COLS]
|
3285
|
+
|
3286
|
+
filenames = []
|
3287
|
+
rename_log = []
|
3288
|
+
csv_path = os.path.join(folder, "rename_log.csv")
|
3289
|
+
used_wells = set(os.listdir(folder))
|
3290
|
+
|
3291
|
+
# **Dictionary to store well assignments per original file**
|
3292
|
+
file_to_well = {}
|
3293
|
+
|
3294
|
+
for file in os.listdir(folder):
|
3295
|
+
path = os.path.join(folder, file)
|
3296
|
+
ext = file.lower().split('.')[-1]
|
3297
|
+
|
3298
|
+
# **Assign a well only once per original file**
|
3299
|
+
if file not in file_to_well:
|
3300
|
+
file_to_well[file] = _get_next_well(used_wells)
|
3301
|
+
used_wells.add(file_to_well[file]) # Mark it as used
|
3302
|
+
|
3303
|
+
well = file_to_well[file] # Use the same well for all channels/times
|
3304
|
+
|
3305
|
+
### **Process Nikon ND2 Files**
|
3306
|
+
if ext == 'nd2':
|
3307
|
+
nd2 = ND2Reader(path)
|
3308
|
+
metadata = nd2.metadata
|
3309
|
+
|
3310
|
+
timepoints = metadata.get("frames", [0])
|
3311
|
+
fields = metadata.get("fields_of_view", [0])
|
3312
|
+
z_levels = list(metadata.get("z_levels", range(1)))
|
3313
|
+
channels = metadata.get("channels", [])
|
3314
|
+
|
3315
|
+
for t_idx in timepoints:
|
3316
|
+
for f_idx in fields:
|
3317
|
+
for c_idx, channel in enumerate(channels):
|
3318
|
+
z_stack = [nd2.get_frame_2D(t=t_idx, v=f_idx, z=z_idx, c=c_idx) for z_idx in z_levels]
|
3319
|
+
mip_image = np.max(np.stack(z_stack), axis=0)
|
3320
|
+
dtype = z_stack[0].dtype
|
3321
|
+
|
3322
|
+
filename = f"{well}_T{t_idx+1:04d}F{f_idx+1:03d}L01C{c_idx+1:02d}.tif"
|
3323
|
+
filepath = os.path.join(folder, filename)
|
3324
|
+
tifffile.imwrite(filepath, mip_image.astype(dtype))
|
3325
|
+
|
3326
|
+
rename_log.append({"Original File": file, "Renamed TIFF": filename})
|
3327
|
+
|
3328
|
+
### **Process Zeiss CZI Files**
|
3329
|
+
elif ext == 'czi':
|
3330
|
+
with czifile.CziFile(path) as czi:
|
3331
|
+
shape = czi.shape
|
3332
|
+
timepoints = range(shape[0])
|
3333
|
+
z_levels = range(shape[1])
|
3334
|
+
channels = range(shape[2])
|
3335
|
+
|
3336
|
+
for t_idx in timepoints:
|
3337
|
+
for c_idx in channels:
|
3338
|
+
z_stack = [czi.asarray()[t_idx, z_idx, c_idx] for z_idx in z_levels]
|
3339
|
+
mip_image = np.max(np.stack(z_stack), axis=0)
|
3340
|
+
dtype = z_stack[0].dtype
|
3341
|
+
|
3342
|
+
filename = f"{well}_T{t_idx+1:04d}F001L01C{c_idx+1:02d}.tif"
|
3343
|
+
filepath = os.path.join(folder, filename)
|
3344
|
+
tifffile.imwrite(filepath, mip_image.astype(dtype))
|
3345
|
+
|
3346
|
+
rename_log.append({"Original File": file, "Renamed TIFF": filename})
|
3347
|
+
|
3348
|
+
### **Process Leica LIF Files**
|
3349
|
+
elif ext == 'lif':
|
3350
|
+
lif_file = readlif.Reader(path)
|
3351
|
+
|
3352
|
+
for image in lif_file.getIterImage():
|
3353
|
+
timepoints = range(image.dims.t)
|
3354
|
+
z_levels = range(image.dims.z)
|
3355
|
+
channels = range(image.dims.c)
|
3356
|
+
|
3357
|
+
for t_idx in timepoints:
|
3358
|
+
for c_idx in channels:
|
3359
|
+
z_stack = [image.getFrame(z=z_idx, t=t_idx, c=c_idx) for z_idx in z_levels]
|
3360
|
+
mip_image = np.max(np.stack(z_stack), axis=0)
|
3361
|
+
dtype = z_stack[0].dtype
|
3362
|
+
|
3363
|
+
filename = f"{well}_T{t_idx+1:04d}F001L01C{c_idx+1:02d}.tif"
|
3364
|
+
filepath = os.path.join(folder, filename)
|
3365
|
+
tifffile.imwrite(filepath, mip_image.astype(dtype))
|
3366
|
+
|
3367
|
+
rename_log.append({"Original File": file, "Renamed TIFF": filename})
|
3368
|
+
|
3369
|
+
### **Process Standard Image Files**
|
3370
|
+
elif ext in ['tif', 'tiff', 'png', 'jpg', 'jpeg', 'bmp'] and not file.startswith("plate"):
|
3371
|
+
with tifffile.TiffFile(path) as tif:
|
3372
|
+
num_pages = len(tif.pages)
|
3373
|
+
|
3374
|
+
if num_pages > 1:
|
3375
|
+
images = tif.asarray()
|
3376
|
+
if images.ndim == 4:
|
3377
|
+
timepoints = range(images.shape[0])
|
3378
|
+
channels = range(images.shape[2])
|
3379
|
+
|
3380
|
+
for t_idx in timepoints:
|
3381
|
+
for c_idx in channels:
|
3382
|
+
mip_image = np.max(images[t_idx, :, c_idx], axis=0)
|
3383
|
+
dtype = images.dtype
|
3384
|
+
|
3385
|
+
filename = f"{well}_T{t_idx+1:04d}F001L01C{c_idx+1:02d}.tif"
|
3386
|
+
filepath = os.path.join(folder, filename)
|
3387
|
+
tifffile.imwrite(filepath, mip_image.astype(dtype))
|
3388
|
+
|
3389
|
+
rename_log.append({"Original File": file, "Renamed TIFF": filename})
|
3390
|
+
else:
|
3391
|
+
image = tif.pages[0].asarray()
|
3392
|
+
dtype = image.dtype
|
3393
|
+
|
3394
|
+
filename = f"{well}_T0001F001L01C01.tif"
|
3395
|
+
filepath = os.path.join(folder, filename)
|
3396
|
+
tifffile.imwrite(filepath, image.astype(dtype))
|
3397
|
+
|
3398
|
+
rename_log.append({"Original File": file, "Renamed TIFF": filename})
|
3399
|
+
|
3400
|
+
# Save rename log as CSV
|
3401
|
+
pd.DataFrame(rename_log).to_csv(csv_path, index=False)
|
3402
|
+
print(f"Processing complete. Files saved in {folder} and rename log saved as {csv_path}.")
|
spacr/measure.py
CHANGED
@@ -945,20 +945,25 @@ def measure_crop(settings):
|
|
945
945
|
|
946
946
|
from .io import _save_settings_to_db
|
947
947
|
from .timelapse import _timelapse_masks_to_gif
|
948
|
-
from .utils import measure_test_mode, print_progress, delete_intermedeate_files, save_settings
|
948
|
+
from .utils import measure_test_mode, print_progress, delete_intermedeate_files, save_settings, format_path_for_system, normalize_src_path
|
949
949
|
from .settings import get_measure_crop_settings
|
950
950
|
|
951
951
|
if not isinstance(settings['src'], (str, list)):
|
952
952
|
ValueError(f'src must be a string or a list of strings')
|
953
953
|
return
|
954
954
|
|
955
|
+
settings['src'] = normalize_src_path(settings['src'])
|
956
|
+
|
955
957
|
if isinstance(settings['src'], str):
|
956
958
|
settings['src'] = [settings['src']]
|
957
959
|
|
958
960
|
if isinstance(settings['src'], list):
|
959
961
|
source_folders = settings['src']
|
962
|
+
|
960
963
|
for source_folder in source_folders:
|
961
964
|
print(f'Processing folder: {source_folder}')
|
965
|
+
|
966
|
+
source_folder = format_path_for_system(source_folder)
|
962
967
|
settings['src'] = source_folder
|
963
968
|
src = source_folder
|
964
969
|
|
@@ -966,15 +971,12 @@ def measure_crop(settings):
|
|
966
971
|
settings = measure_test_mode(settings)
|
967
972
|
|
968
973
|
src_fldr = settings['src']
|
974
|
+
|
969
975
|
if not os.path.basename(src_fldr).endswith('merged'):
|
970
976
|
print(f"WARNING: Source folder, settings: src: {src_fldr} should end with '/merged'")
|
971
977
|
src_fldr = os.path.join(src_fldr, 'merged')
|
978
|
+
settings['src'] = src_fldr
|
972
979
|
print(f"Changed source folder to: {src_fldr}")
|
973
|
-
|
974
|
-
#if settings['save_measurements']:
|
975
|
-
#source_folder = os.path.dirname(settings['src'])
|
976
|
-
#os.makedirs(source_folder+'/measurements', exist_ok=True)
|
977
|
-
#_create_database(source_folder+'/measurements/measurements.db')
|
978
980
|
|
979
981
|
if settings['cell_mask_dim'] is None:
|
980
982
|
settings['uninfected'] = True
|
@@ -995,12 +997,9 @@ def measure_crop(settings):
|
|
995
997
|
print(f'Warning reserving 6 CPU cores for other processes, setting n_jobs to {spacr_cores}')
|
996
998
|
settings['n_jobs'] = spacr_cores
|
997
999
|
|
998
|
-
|
999
|
-
|
1000
|
-
|
1001
|
-
#os.makedirs(os.path.join(dirname,'settings'), exist_ok=True)
|
1002
|
-
#settings_df.to_csv(settings_csv, index=False)
|
1003
|
-
save_settings(settings, name='measure_crop_settings', show=True)
|
1000
|
+
settings_save = settings.copy()
|
1001
|
+
settings_save['src'] = os.path.dirname(settings['src'])
|
1002
|
+
save_settings(settings_save, name='measure_crop_settings', show=True)
|
1004
1003
|
|
1005
1004
|
if settings['timelapse_objects'] == 'nucleus':
|
1006
1005
|
if not settings['cell_mask_dim'] is None:
|
spacr/plot.py
CHANGED
@@ -2705,7 +2705,7 @@ class spacrGraph:
|
|
2705
2705
|
def perform_posthoc_tests(self, is_normal, unique_groups):
|
2706
2706
|
"""Perform post-hoc tests for multiple groups based on all_to_all flag."""
|
2707
2707
|
|
2708
|
-
from .
|
2708
|
+
from .sp_stats import choose_p_adjust_method
|
2709
2709
|
|
2710
2710
|
posthoc_results = []
|
2711
2711
|
if is_normal and len(unique_groups) > 2 and self.all_to_all:
|
@@ -3815,7 +3815,7 @@ def plot_proportion_stacked_bars(settings, df, group_column, bin_column, prc_col
|
|
3815
3815
|
- pairwise_results (list): Pairwise test results from `chi_pairwise`.
|
3816
3816
|
"""
|
3817
3817
|
|
3818
|
-
from .
|
3818
|
+
from .sp_stats import chi_pairwise
|
3819
3819
|
|
3820
3820
|
# Calculate contingency table for overall chi-squared test
|
3821
3821
|
raw_counts = df.groupby([group_column, bin_column]).size().unstack(fill_value=0)
|