phasor-handler 2.2.3__tar.gz → 2.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {phasor_handler-2.2.3/phasor_handler.egg-info → phasor_handler-2.2.4}/PKG-INFO +1 -1
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/app.py +1 -1
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/scripts/meta_reader.py +158 -10
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/widgets/analysis/components/image_view.py +38 -4
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/widgets/analysis/components/meta_info.py +56 -33
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/widgets/analysis/components/roi_list.py +9 -25
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/widgets/analysis/components/trace_plot.py +178 -36
- {phasor_handler-2.2.3 → phasor_handler-2.2.4/phasor_handler.egg-info}/PKG-INFO +1 -1
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/pyproject.toml +1 -1
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/CONTRIBUTING.md +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/LICENSE.md +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/MANIFEST.in +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/README.md +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/environment.yml +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/__init__.py +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/img/icons/chevron-down.svg +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/img/icons/chevron-up.svg +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/img/logo.ico +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/models/dir_manager.py +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/scripts/contrast.py +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/scripts/convert.py +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/scripts/plot.py +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/scripts/register.py +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/themes/__init__.py +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/themes/dark_theme.py +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/tools/__init__.py +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/tools/check_stylesheet.py +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/tools/misc.py +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/widgets/__init__.py +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/widgets/analysis/components/__init__.py +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/widgets/analysis/components/bnc.py +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/widgets/analysis/components/circle_roi.py +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/widgets/analysis/view.py +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/widgets/conversion/view.py +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/widgets/registration/view.py +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/workers/__init__.py +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/workers/analysis_worker.py +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/workers/conversion_worker.py +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/workers/histogram_worker.py +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/workers/registration_worker.py +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler.egg-info/SOURCES.txt +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler.egg-info/dependency_links.txt +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler.egg-info/entry_points.txt +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler.egg-info/requires.txt +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler.egg-info/top_level.txt +0 -0
- {phasor_handler-2.2.3 → phasor_handler-2.2.4}/setup.cfg +0 -0
|
@@ -244,7 +244,7 @@ class MainWindow(QMainWindow):
|
|
|
244
244
|
def main():
|
|
245
245
|
app = QApplication(sys.argv)
|
|
246
246
|
try:
|
|
247
|
-
qdarktheme.setup_theme(
|
|
247
|
+
qdarktheme.setup_theme() # or your own apply_dark_theme()
|
|
248
248
|
except Exception:
|
|
249
249
|
pass # fall back to default if theme package missing
|
|
250
250
|
window = MainWindow()
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
# TODO read from .txt files of CHA and CHB channels if available
|
|
2
|
-
|
|
3
1
|
#!/usr/bin/env python3
|
|
4
2
|
"""
|
|
5
3
|
meta_reader.py
|
|
@@ -32,6 +30,42 @@ class DataFrameDict(dict):
|
|
|
32
30
|
self[key] = value
|
|
33
31
|
|
|
34
32
|
# ------------- FUNCTIONS -------------
|
|
33
|
+
|
|
34
|
+
# Mini2P text file parser
|
|
35
|
+
_SPLIT = re.compile(r"\s{4,}") # split key/value on 4+ spaces
|
|
36
|
+
|
|
37
|
+
def read_mini2p_meta(path):
|
|
38
|
+
"""
|
|
39
|
+
Read Mini2P metadata from Information-CHA.txt or Information-CHB.txt files.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
path: Path to the text file
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
dict: Nested dictionary with sections as keys
|
|
46
|
+
"""
|
|
47
|
+
from pathlib import Path
|
|
48
|
+
from typing import Dict, Any
|
|
49
|
+
|
|
50
|
+
path = Path(path)
|
|
51
|
+
data: Dict[str, Dict[str, Any]] = {}
|
|
52
|
+
section = None
|
|
53
|
+
with path.open(encoding="utf-8-sig") as f: # utf-8 with BOM safe; handles 'μ'
|
|
54
|
+
for raw in f:
|
|
55
|
+
line = raw.strip()
|
|
56
|
+
if not line or line.startswith(("#", ";")):
|
|
57
|
+
continue
|
|
58
|
+
if line.startswith("[") and line.endswith("]"):
|
|
59
|
+
section = line[1:-1].strip()
|
|
60
|
+
data.setdefault(section, {})
|
|
61
|
+
continue
|
|
62
|
+
if section is None:
|
|
63
|
+
section = "_root_"
|
|
64
|
+
data.setdefault(section, {})
|
|
65
|
+
parts = _SPLIT.split(line, maxsplit=1)
|
|
66
|
+
key, val = (parts[0].strip(), parts[1].strip()) if len(parts) == 2 else (parts[0], "")
|
|
67
|
+
data[section][key] = val
|
|
68
|
+
return data
|
|
35
69
|
def open_overwrite(path, *args, **kwargs):
|
|
36
70
|
path = Path(path)
|
|
37
71
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -358,7 +392,7 @@ def process_i3_folder(folder_path: str):
|
|
|
358
392
|
|
|
359
393
|
# Output a json file
|
|
360
394
|
with open_overwrite(Path(folder_path) / 'experiment_summary.json', 'w', encoding='utf-8') as f:
|
|
361
|
-
json.dump(variables, f, indent=4)
|
|
395
|
+
json.dump(variables, f, indent=4, ensure_ascii=False)
|
|
362
396
|
|
|
363
397
|
print(f"JSON file and Pickle file saved to {folder_path}")
|
|
364
398
|
|
|
@@ -399,6 +433,8 @@ def process_mini2p_folder(folder_path: str):
|
|
|
399
433
|
# Collect all subdirectories and TDMS files
|
|
400
434
|
folders = {}
|
|
401
435
|
path_df = {}
|
|
436
|
+
txt_meta = {} # Store parsed txt metadata
|
|
437
|
+
|
|
402
438
|
for subdir in os.listdir(folder_path):
|
|
403
439
|
subdir_path = os.path.join(folder_path, subdir)
|
|
404
440
|
if os.path.isdir(subdir_path):
|
|
@@ -406,6 +442,16 @@ def process_mini2p_folder(folder_path: str):
|
|
|
406
442
|
tdms_files = [os.path.join(subdir_path, f) for f in os.listdir(subdir_path) if f.endswith('.tdms')]
|
|
407
443
|
txt_files = [os.path.join(subdir_path, f) for f in os.listdir(subdir_path) if f.endswith('.txt')]
|
|
408
444
|
|
|
445
|
+
# Parse txt files for channel information
|
|
446
|
+
for txt_file in txt_files:
|
|
447
|
+
txt_filename = os.path.basename(txt_file)
|
|
448
|
+
if 'Information-CHA.txt' in txt_filename or 'Information-CHB.txt' in txt_filename:
|
|
449
|
+
try:
|
|
450
|
+
txt_meta[txt_filename] = read_mini2p_meta(txt_file)
|
|
451
|
+
print(f"[INFO] Parsed metadata from {txt_filename}")
|
|
452
|
+
except Exception as e:
|
|
453
|
+
print(f"[WARN] Failed to parse {txt_filename}: {e}")
|
|
454
|
+
|
|
409
455
|
if txt_files:
|
|
410
456
|
path_df[subdir] = (tdms_files, txt_files)
|
|
411
457
|
elif tdms_files:
|
|
@@ -449,20 +495,41 @@ def process_mini2p_folder(folder_path: str):
|
|
|
449
495
|
for _, row in info_df.iterrows():
|
|
450
496
|
image_info[row['Item']] = row['Value']
|
|
451
497
|
|
|
452
|
-
#
|
|
498
|
+
# Override/supplement with txt file metadata if available
|
|
499
|
+
txt_cha = next((txt_meta[k] for k in txt_meta if 'CHA' in k), None)
|
|
500
|
+
txt_chb = next((txt_meta[k] for k in txt_meta if 'CHB' in k), None)
|
|
501
|
+
|
|
502
|
+
# Use the first available txt metadata (prefer CHA)
|
|
503
|
+
primary_txt = txt_cha if txt_cha else txt_chb
|
|
504
|
+
|
|
505
|
+
# Parse timestamp from txt file or image info
|
|
453
506
|
import datetime
|
|
454
|
-
|
|
507
|
+
|
|
508
|
+
timestamp_str = "NA"
|
|
509
|
+
if primary_txt and 'Basic Information' in primary_txt:
|
|
510
|
+
timestamp_str = safe_extract(lambda: primary_txt['Basic Information'].get('Time', 'NA'))
|
|
511
|
+
if timestamp_str == "NA":
|
|
512
|
+
timestamp_str = safe_extract(lambda: image_info.get('Image time', 'NA'))
|
|
513
|
+
|
|
455
514
|
if timestamp_str != 'NA':
|
|
456
515
|
try:
|
|
457
|
-
#
|
|
458
|
-
|
|
516
|
+
# Try Mini2P format first: "2025-10-16_23-14-26.286"
|
|
517
|
+
if '_' in timestamp_str and '-' in timestamp_str:
|
|
518
|
+
# Format: YYYY-MM-DD_HH-MM-SS.mmm
|
|
519
|
+
timestamp_str = timestamp_str.split('.')[0] # Remove milliseconds
|
|
520
|
+
dt = datetime.datetime.strptime(timestamp_str, "%Y-%m-%d_%H-%M-%S")
|
|
521
|
+
else:
|
|
522
|
+
# Try format like "10/16/2025 11:19:54 PM"
|
|
523
|
+
dt = datetime.datetime.strptime(timestamp_str, "%m/%d/%Y %I:%M:%S %p")
|
|
524
|
+
|
|
459
525
|
day = dt.day
|
|
460
526
|
month = dt.month
|
|
461
527
|
year = dt.year
|
|
462
528
|
hour = dt.hour
|
|
463
529
|
minute = dt.minute
|
|
464
530
|
second = dt.second
|
|
465
|
-
except:
|
|
531
|
+
except Exception as e:
|
|
532
|
+
print(f"[WARN] Could not parse timestamp '{timestamp_str}': {e}")
|
|
466
533
|
day = month = year = hour = minute = second = "NA"
|
|
467
534
|
else:
|
|
468
535
|
day = month = year = hour = minute = second = "NA"
|
|
@@ -519,7 +586,7 @@ def process_mini2p_folder(folder_path: str):
|
|
|
519
586
|
"pixel_size": safe_extract(lambda: image_info.get('Image size_Pixel', 'NA')),
|
|
520
587
|
"height": safe_extract(lambda: int(image_info.get('Image size_Line', 'NA'))),
|
|
521
588
|
"width": safe_extract(lambda: int(image_info.get('Image size_Pixel', 'NA'))),
|
|
522
|
-
"FOV_size": "NA", #
|
|
589
|
+
"FOV_size": "NA", # Will be updated from txt if available
|
|
523
590
|
"zoom": safe_extract(lambda: image_info.get('Zoom', 'NA')),
|
|
524
591
|
"laser_voltage": safe_extract(lambda: image_info.get('Laser(V)', 'NA')),
|
|
525
592
|
"pmt_voltage": safe_extract(lambda: image_info.get('PMT(V)', 'NA')),
|
|
@@ -553,12 +620,93 @@ def process_mini2p_folder(folder_path: str):
|
|
|
553
620
|
"data_folders": list(tdms_data.keys())
|
|
554
621
|
}
|
|
555
622
|
|
|
623
|
+
# Enhance with txt file metadata if available
|
|
624
|
+
if primary_txt:
|
|
625
|
+
# Basic Information
|
|
626
|
+
if 'Basic Information' in primary_txt:
|
|
627
|
+
basic = primary_txt['Basic Information']
|
|
628
|
+
variables["system_config"] = safe_extract(lambda: basic.get('SystemConfig', 'NA'))
|
|
629
|
+
variables["probe"] = safe_extract(lambda: basic.get('Probe', 'NA'))
|
|
630
|
+
variables["imaging_mode"] = safe_extract(lambda: basic.get('ImagingMode', 'NA'))
|
|
631
|
+
variables["supergin_version"] = safe_extract(lambda: basic.get('SUPERGIN_Version', 'NA'))
|
|
632
|
+
variables["probe_type"] = safe_extract(lambda: basic.get('Probe_Type', 'NA'))
|
|
633
|
+
variables["pmt_gain"] = safe_extract(lambda: basic.get('PMT_Gain', 'NA'))
|
|
634
|
+
|
|
635
|
+
# Power Regulation
|
|
636
|
+
if 'PowerRegulation' in primary_txt:
|
|
637
|
+
power = primary_txt['PowerRegulation']
|
|
638
|
+
variables["power_regulation_mode"] = safe_extract(lambda: power.get('PowerRegulationMode', 'NA'))
|
|
639
|
+
variables["power_voltage"] = safe_extract(lambda: power.get('Power', 'NA'))
|
|
640
|
+
variables["power_percentage"] = safe_extract(lambda: power.get('PowerPercentage', 'NA'))
|
|
641
|
+
|
|
642
|
+
# Scan Information
|
|
643
|
+
if 'Scan' in primary_txt:
|
|
644
|
+
scan = primary_txt['Scan']
|
|
645
|
+
variables["scan_direction"] = safe_extract(lambda: scan.get('Scan_Direction', 'NA'))
|
|
646
|
+
variables["pixel_dwell"] = safe_extract(lambda: scan.get('Pixel_Dwell', 'NA'))
|
|
647
|
+
variables["frame_rate"] = safe_extract(lambda: scan.get('Frame_Rate', 'NA'))
|
|
648
|
+
variables["scan_frequency"] = safe_extract(lambda: scan.get('Frequency', 'NA'))
|
|
649
|
+
variables["fps_division"] = safe_extract(lambda: scan.get('FPS_Division', 'NA'))
|
|
650
|
+
|
|
651
|
+
# Update dimensions from txt if available
|
|
652
|
+
pixel_x = safe_extract(lambda: int(scan.get('Pixel_X', 'NA')))
|
|
653
|
+
pixel_y = safe_extract(lambda: int(scan.get('Pixel_Y', 'NA')))
|
|
654
|
+
if pixel_x != "NA":
|
|
655
|
+
variables["width"] = pixel_x
|
|
656
|
+
if pixel_y != "NA":
|
|
657
|
+
variables["height"] = pixel_y
|
|
658
|
+
|
|
659
|
+
# Zoom Information
|
|
660
|
+
if 'Zoom' in primary_txt:
|
|
661
|
+
zoom_info = primary_txt['Zoom']
|
|
662
|
+
variables["zoom"] = safe_extract(lambda: zoom_info.get('Zoom', 'NA'))
|
|
663
|
+
variables["amplitude_x"] = safe_extract(lambda: zoom_info.get('Amplitude_X', 'NA'))
|
|
664
|
+
variables["amplitude_y"] = safe_extract(lambda: zoom_info.get('Amplitude_Y', 'NA'))
|
|
665
|
+
variables["pixel_size"] = safe_extract(lambda: zoom_info.get('Pixel_Size', 'NA'))
|
|
666
|
+
variables["fov_x"] = safe_extract(lambda: zoom_info.get('Fov_X', 'NA'))
|
|
667
|
+
variables["fov_y"] = safe_extract(lambda: zoom_info.get('Fov_Y', 'NA'))
|
|
668
|
+
variables["FOV_size"] = safe_extract(lambda: f"{zoom_info.get('Fov_X', 'NA')} x {zoom_info.get('Fov_Y', 'NA')}")
|
|
669
|
+
variables["save_frames"] = safe_extract(lambda: zoom_info.get('Save Frames', 'NA'))
|
|
670
|
+
|
|
671
|
+
# Stage Position
|
|
672
|
+
if 'Stage' in primary_txt:
|
|
673
|
+
stage = primary_txt['Stage']
|
|
674
|
+
variables["X_start_position"] = safe_extract(lambda: stage.get('Displacement_X', 'NA'))
|
|
675
|
+
variables["Y_start_position"] = safe_extract(lambda: stage.get('Displacement_Y', 'NA'))
|
|
676
|
+
variables["Z_start_position"] = safe_extract(lambda: stage.get('Displacement_Z', 'NA'))
|
|
677
|
+
|
|
678
|
+
# ETL Information
|
|
679
|
+
if 'ETL' in primary_txt:
|
|
680
|
+
etl = primary_txt['ETL']
|
|
681
|
+
variables["etl_voltage"] = safe_extract(lambda: etl.get('Voltage', 'NA'))
|
|
682
|
+
variables["etl_distance"] = safe_extract(lambda: etl.get('Distance', 'NA'))
|
|
683
|
+
|
|
684
|
+
# Behavioral Setting
|
|
685
|
+
if 'Behavioral Setting' in primary_txt:
|
|
686
|
+
behavioral = primary_txt['Behavioral Setting']
|
|
687
|
+
variables["camera_framerate"] = safe_extract(lambda: behavioral.get('Camera FrameRate', 'NA'))
|
|
688
|
+
|
|
689
|
+
# Time Division Mode
|
|
690
|
+
if 'TimeDivisionMode' in primary_txt:
|
|
691
|
+
tdm = primary_txt['TimeDivisionMode']
|
|
692
|
+
variables["time_division_power"] = safe_extract(lambda: tdm.get('TimeDivisionModePower', 'NA'))
|
|
693
|
+
variables["channel_framerate"] = safe_extract(lambda: tdm.get('Channel FrameRate', 'NA'))
|
|
694
|
+
|
|
695
|
+
# Add channel-specific metadata if both channels are available
|
|
696
|
+
if txt_cha and txt_chb:
|
|
697
|
+
variables["cha_metadata"] = txt_cha
|
|
698
|
+
variables["chb_metadata"] = txt_chb
|
|
699
|
+
elif txt_cha:
|
|
700
|
+
variables["cha_metadata"] = txt_cha
|
|
701
|
+
elif txt_chb:
|
|
702
|
+
variables["chb_metadata"] = txt_chb
|
|
703
|
+
|
|
556
704
|
# Save metadata
|
|
557
705
|
with open(Path(folder_path) / 'experiment_summary.pkl', 'wb') as f:
|
|
558
706
|
pickle.dump(variables, f)
|
|
559
707
|
|
|
560
708
|
with open_overwrite(Path(folder_path) / 'experiment_summary.json', 'w', encoding='utf-8') as f:
|
|
561
|
-
json.dump(variables, f, indent=4)
|
|
709
|
+
json.dump(variables, f, indent=4, ensure_ascii=False)
|
|
562
710
|
|
|
563
711
|
print(f"[OK] Mini2P metadata saved to {folder_path}")
|
|
564
712
|
print("\n[OK] Files saved successfully.")
|
|
@@ -817,9 +817,10 @@ class ImageViewWidget(QWidget):
|
|
|
817
817
|
if isinstance(metadata, dict):
|
|
818
818
|
# Check direct key
|
|
819
819
|
if 'pixel_size' in metadata:
|
|
820
|
-
|
|
820
|
+
pixel_size_str = str(metadata['pixel_size'])
|
|
821
|
+
return self._parse_pixel_size_string(pixel_size_str)
|
|
821
822
|
|
|
822
|
-
# Check nested structure from ImageRecord.yaml
|
|
823
|
+
# Check nested structure from ImageRecord.yaml (3i format)
|
|
823
824
|
if ('ImageRecord.yaml' in metadata and
|
|
824
825
|
'CLensDef70' in metadata['ImageRecord.yaml'] and
|
|
825
826
|
'mMicronPerPixel' in metadata['ImageRecord.yaml']['CLensDef70']):
|
|
@@ -827,11 +828,44 @@ class ImageViewWidget(QWidget):
|
|
|
827
828
|
|
|
828
829
|
# Try as object with attributes
|
|
829
830
|
elif hasattr(metadata, 'pixel_size'):
|
|
830
|
-
|
|
831
|
+
pixel_size_str = str(metadata.pixel_size)
|
|
832
|
+
return self._parse_pixel_size_string(pixel_size_str)
|
|
831
833
|
elif hasattr(metadata, 'mMicronPerPixel'):
|
|
832
834
|
return float(metadata.mMicronPerPixel)
|
|
833
835
|
|
|
834
836
|
except (KeyError, TypeError, ValueError, AttributeError) as e:
|
|
835
837
|
print(f"DEBUG: Could not extract pixel size from metadata: {e}")
|
|
836
838
|
|
|
837
|
-
return None
|
|
839
|
+
return None
|
|
840
|
+
|
|
841
|
+
def _parse_pixel_size_string(self, pixel_size_str):
|
|
842
|
+
"""
|
|
843
|
+
Parse pixel size string to extract numeric value in microns.
|
|
844
|
+
Handles formats like:
|
|
845
|
+
- '0.201μm/pixel'
|
|
846
|
+
- '0.201'
|
|
847
|
+
- 0.201
|
|
848
|
+
|
|
849
|
+
Args:
|
|
850
|
+
pixel_size_str: String or number containing pixel size
|
|
851
|
+
|
|
852
|
+
Returns:
|
|
853
|
+
float: Pixel size in microns per pixel
|
|
854
|
+
"""
|
|
855
|
+
import re
|
|
856
|
+
|
|
857
|
+
# If already a number, return it
|
|
858
|
+
if isinstance(pixel_size_str, (int, float)):
|
|
859
|
+
return float(pixel_size_str)
|
|
860
|
+
|
|
861
|
+
# Convert to string and extract numeric part
|
|
862
|
+
pixel_size_str = str(pixel_size_str)
|
|
863
|
+
|
|
864
|
+
# Try to extract the first number from the string
|
|
865
|
+
# Handles formats like "0.201μm/pixel" or "0.598μm/pixel"
|
|
866
|
+
match = re.search(r'(\d+\.?\d*)', pixel_size_str)
|
|
867
|
+
if match:
|
|
868
|
+
return float(match.group(1))
|
|
869
|
+
|
|
870
|
+
# If no match, try direct float conversion as last resort
|
|
871
|
+
return float(pixel_size_str)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# TODO Implement behavioural and sync information to metadata.
|
|
2
|
+
|
|
1
3
|
"""
|
|
2
4
|
Metadata Information Viewer Component
|
|
3
5
|
|
|
@@ -13,6 +15,7 @@ from PyQt6.QtWidgets import (
|
|
|
13
15
|
from PyQt6.QtCore import Qt, QSize
|
|
14
16
|
from PyQt6.QtGui import QFont
|
|
15
17
|
import json
|
|
18
|
+
import re
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
class MetadataViewer(QDialog):
|
|
@@ -104,7 +107,7 @@ class MetadataViewer(QDialog):
|
|
|
104
107
|
self.timing_group.setLayout(self.timing_layout)
|
|
105
108
|
layout.addWidget(self.timing_group)
|
|
106
109
|
|
|
107
|
-
# Stimulation Information Group
|
|
110
|
+
# Stimulation/Behavioral Information Group (will be renamed based on device)
|
|
108
111
|
self.stim_group = QGroupBox("Stimulation Information")
|
|
109
112
|
self.stim_layout = QGridLayout()
|
|
110
113
|
self.stim_group.setLayout(self.stim_layout)
|
|
@@ -216,6 +219,16 @@ class MetadataViewer(QDialog):
|
|
|
216
219
|
|
|
217
220
|
def update_overview_from_dict(self):
|
|
218
221
|
"""Update overview from dictionary metadata."""
|
|
222
|
+
# Determine device type
|
|
223
|
+
device_name = self.metadata.get('device_name', '').lower() if isinstance(self.metadata.get('device_name'), str) else ''
|
|
224
|
+
is_mini2p = 'mini' in device_name
|
|
225
|
+
|
|
226
|
+
# Update group title based on device
|
|
227
|
+
if is_mini2p:
|
|
228
|
+
self.stim_group.setTitle("Behavioral Information")
|
|
229
|
+
else:
|
|
230
|
+
self.stim_group.setTitle("Stimulation Information")
|
|
231
|
+
|
|
219
232
|
# Experiment Summary
|
|
220
233
|
row = 0
|
|
221
234
|
for key in ['device_name', 'n_frames']:
|
|
@@ -270,37 +283,43 @@ class MetadataViewer(QDialog):
|
|
|
270
283
|
display_value) if key != "Date (DDMMYYYY)" else self.add_info_row(self.timing_layout, row, key, display_value)
|
|
271
284
|
row += 1
|
|
272
285
|
|
|
273
|
-
# Stimulation Information
|
|
286
|
+
# Stimulation/Behavioral Information (device-specific)
|
|
274
287
|
row = 0
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
key
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
key
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
display_value = ', '.join(
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
288
|
+
if is_mini2p:
|
|
289
|
+
# Mini2P: Show only camera frame rate for behavioral information
|
|
290
|
+
camera_framerate = self.metadata.get('camera_framerate', 'NA')
|
|
291
|
+
self.add_info_row(self.stim_layout, row, "Camera Frame Rate (Hz)", str(camera_framerate))
|
|
292
|
+
else:
|
|
293
|
+
# 3i: Show stimulation information
|
|
294
|
+
stim_keys = ['stimulation_timeframes', 'stimulation_ms', 'duty_cycle', 'stimulated_roi_location', 'stimulated_rois']
|
|
295
|
+
for key in stim_keys:
|
|
296
|
+
if key in self.metadata:
|
|
297
|
+
value = self.metadata[key]
|
|
298
|
+
if isinstance(value, list):
|
|
299
|
+
if key == 'stimulation_timeframes':
|
|
300
|
+
display_value = ', '.join(map(str, value))
|
|
301
|
+
key = "Stimulation Timeframes"
|
|
302
|
+
elif key == 'stimulation_ms':
|
|
303
|
+
display_value = ', '.join(map(str, [int(v/1000) for v in value]))
|
|
304
|
+
key = "Stimulation Time (s)"
|
|
305
|
+
elif key == 'duty_cycle':
|
|
306
|
+
duty_cycle_counts = {val: value.count(val) for val in set(value)}
|
|
307
|
+
display_value = ' | '.join([f'{val}: {count}X' for val, count in duty_cycle_counts.items()])
|
|
308
|
+
key = "Duty Cycle"
|
|
309
|
+
elif key == 'stimulated_roi_location':
|
|
310
|
+
display_value = ', '.join(map(str, [len(x) for x in value]))
|
|
311
|
+
key = "Number of stimulated ROIs"
|
|
312
|
+
elif key == 'stimulated_rois':
|
|
313
|
+
if not value or all(not sublist for sublist in value):
|
|
314
|
+
display_value = "NA"
|
|
315
|
+
else:
|
|
316
|
+
display_value = ', '.join([', '.join(map(str, x)) for x in value])
|
|
317
|
+
key = "Stimulated ROIs"
|
|
318
|
+
else:
|
|
319
|
+
display_value = str(value)
|
|
320
|
+
self.add_info_row(self.stim_layout, row, key.replace('_', ' '),
|
|
321
|
+
display_value)
|
|
322
|
+
row += 1
|
|
304
323
|
|
|
305
324
|
# Image Information
|
|
306
325
|
row = 0
|
|
@@ -308,10 +327,14 @@ class MetadataViewer(QDialog):
|
|
|
308
327
|
for key in image_keys:
|
|
309
328
|
if key in self.metadata:
|
|
310
329
|
value = self.metadata[key]
|
|
311
|
-
if key == "pixel_size":
|
|
330
|
+
if key == "pixel_size":
|
|
331
|
+
key = "Pixel Size (µm)"
|
|
332
|
+
value = re.sub(r'[^0-9.]+', '', str(value))
|
|
312
333
|
elif key == "FOV_size":
|
|
313
334
|
key = "FOV Size (µm)"
|
|
314
|
-
|
|
335
|
+
# Parse FOV_size to format as "206 x 176" (without μm on individual values)
|
|
336
|
+
# Example input: "206μm x 176μm"
|
|
337
|
+
value = str(value).replace('μm', '').replace('um', '').replace('microns', '').strip()
|
|
315
338
|
|
|
316
339
|
self.add_info_row(self.image_layout, row, key.replace('_', ' '),
|
|
317
340
|
str(value))
|
{phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/widgets/analysis/components/roi_list.py
RENAMED
|
@@ -74,20 +74,15 @@ class RoiListWidget(QWidget):
|
|
|
74
74
|
roi_grid_layout.addWidget(self.save_roi_btn, 1, 0)
|
|
75
75
|
roi_grid_layout.addWidget(self.load_roi_btn, 1, 1)
|
|
76
76
|
roi_grid_layout.addWidget(self.export_trace_btn, 2, 0, 1, 2)
|
|
77
|
+
|
|
78
|
+
from PyQt6.QtWidgets import QCheckBox
|
|
79
|
+
self.hide_rois_checkbox = QCheckBox("Hide ROIs")
|
|
80
|
+
self.hide_rois_checkbox.stateChanged.connect(self._on_hide_rois_toggled)
|
|
81
|
+
roi_grid_layout.addWidget(self.hide_rois_checkbox, 3, 0)
|
|
77
82
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
self.hide_rois_checkbox = QCheckBox("Hide ROIs")
|
|
82
|
-
self.hide_rois_checkbox.stateChanged.connect(self._on_hide_rois_toggled)
|
|
83
|
-
roi_grid_layout.addWidget(self.hide_rois_checkbox, 3, 0, 1, 2)
|
|
84
|
-
|
|
85
|
-
self.display_labels_checkbox = QCheckBox("Hide Labels")
|
|
86
|
-
self.display_labels_checkbox.stateChanged.connect(self._on_hide_labels_toggled)
|
|
87
|
-
roi_grid_layout.addWidget(self.display_labels_checkbox, 3, 1, 1, 2)
|
|
88
|
-
except Exception:
|
|
89
|
-
self.hide_rois_checkbox = None
|
|
90
|
-
self.display_labels_checkbox = None
|
|
83
|
+
self.display_labels_checkbox = QCheckBox("Hide Labels")
|
|
84
|
+
self.display_labels_checkbox.stateChanged.connect(self._on_hide_labels_toggled)
|
|
85
|
+
roi_grid_layout.addWidget(self.display_labels_checkbox, 3, 1)
|
|
91
86
|
|
|
92
87
|
roi_vbox.addLayout(roi_grid_layout)
|
|
93
88
|
roi_group.setLayout(roi_vbox)
|
|
@@ -202,18 +197,7 @@ class RoiListWidget(QWidget):
|
|
|
202
197
|
except Exception:
|
|
203
198
|
pass
|
|
204
199
|
else:
|
|
205
|
-
|
|
206
|
-
existing_numbers = []
|
|
207
|
-
for roi in self.main_window._saved_rois:
|
|
208
|
-
roi_name = roi.get('name', '')
|
|
209
|
-
if roi_name.startswith('ROI '):
|
|
210
|
-
try:
|
|
211
|
-
number = int(roi_name.split('ROI ')[1])
|
|
212
|
-
existing_numbers.append(number)
|
|
213
|
-
except (IndexError, ValueError):
|
|
214
|
-
pass
|
|
215
|
-
|
|
216
|
-
next_num = max(existing_numbers) + 1 if existing_numbers else 1
|
|
200
|
+
next_num = len(self.main_window._saved_rois) + 1
|
|
217
201
|
name = f"ROI {next_num}"
|
|
218
202
|
|
|
219
203
|
color = (
|
|
@@ -303,25 +303,58 @@ class TraceplotWidget(QWidget):
|
|
|
303
303
|
self.trace_ax.cla()
|
|
304
304
|
|
|
305
305
|
# Compute metric depending on available channels and selected formula
|
|
306
|
-
# If red channel missing,
|
|
306
|
+
# If red channel missing, show only single-channel formulas
|
|
307
307
|
if sig2 is None:
|
|
308
|
-
# Single-channel data:
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
self.formula_dropdown.
|
|
316
|
-
|
|
317
|
-
|
|
308
|
+
# Single-channel data: limit dropdown to (Fg - Fo)/Fo and raw Fg only
|
|
309
|
+
# Only modify dropdown if it currently has more than 2 items (switching from dual to single channel)
|
|
310
|
+
if self.formula_dropdown.count() > 2:
|
|
311
|
+
self.formula_dropdown.blockSignals(True)
|
|
312
|
+
current_index = self.formula_dropdown.currentIndex()
|
|
313
|
+
|
|
314
|
+
# Clear and repopulate with single-channel options only
|
|
315
|
+
self.formula_dropdown.clear()
|
|
316
|
+
self.formula_dropdown.addItem("(Fg - Fog) / Fog") # Index 0 for single channel
|
|
317
|
+
self.formula_dropdown.addItem("Fg only") # Index 1 for single channel
|
|
318
|
+
|
|
319
|
+
# Set appropriate default selection based on previous selection
|
|
320
|
+
if current_index == 2: # Was "Fg only"
|
|
321
|
+
self.formula_dropdown.setCurrentIndex(1)
|
|
322
|
+
else: # Default to (Fg - Fog) / Fog
|
|
323
|
+
self.formula_dropdown.setCurrentIndex(0)
|
|
324
|
+
|
|
325
|
+
self.formula_dropdown.blockSignals(False)
|
|
326
|
+
print("DEBUG: Switched to single-channel formula dropdown")
|
|
318
327
|
|
|
319
|
-
#
|
|
320
|
-
|
|
321
|
-
|
|
328
|
+
# Calculate metric based on selected formula
|
|
329
|
+
formula_index = self.formula_dropdown.currentIndex() if index is None else index
|
|
330
|
+
if formula_index == 1: # Fg only (raw)
|
|
331
|
+
metric = sig1
|
|
332
|
+
else: # (Fg - Fog) / Fog (default, index 0)
|
|
333
|
+
denom_val = Fog if (Fog is not None and Fog != 0) else 1e-6
|
|
334
|
+
metric = (sig1 - Fog) / denom_val
|
|
322
335
|
else:
|
|
323
|
-
# Two-channel data:
|
|
324
|
-
|
|
336
|
+
# Two-channel data: restore all formula options
|
|
337
|
+
# Only modify dropdown if it currently has 2 items (switching from single to dual channel)
|
|
338
|
+
if self.formula_dropdown.count() == 2:
|
|
339
|
+
self.formula_dropdown.blockSignals(True)
|
|
340
|
+
current_index = self.formula_dropdown.currentIndex()
|
|
341
|
+
|
|
342
|
+
# Clear and repopulate with all formula options
|
|
343
|
+
self.formula_dropdown.clear()
|
|
344
|
+
self.formula_dropdown.addItem("Fg - Fog / Fr") # Index 0
|
|
345
|
+
self.formula_dropdown.addItem("Fg - Fog / Fog") # Index 1
|
|
346
|
+
self.formula_dropdown.addItem("Fg only") # Index 2
|
|
347
|
+
self.formula_dropdown.addItem("Fr only") # Index 3
|
|
348
|
+
|
|
349
|
+
# Restore appropriate selection based on previous selection
|
|
350
|
+
if current_index == 1: # Was "Fg only" in single-channel mode
|
|
351
|
+
self.formula_dropdown.setCurrentIndex(2) # Set to "Fg only" in two-channel mode
|
|
352
|
+
else: # Was "(Fg - Fog) / Fog"
|
|
353
|
+
self.formula_dropdown.setCurrentIndex(1) # Keep as "(Fg - Fog) / Fog"
|
|
354
|
+
|
|
355
|
+
self.formula_dropdown.blockSignals(False)
|
|
356
|
+
print("DEBUG: Switched to dual-channel formula dropdown")
|
|
357
|
+
|
|
325
358
|
formula_index = self.formula_dropdown.currentIndex() if index is None else index
|
|
326
359
|
if formula_index == 0:
|
|
327
360
|
denom = sig2.copy().astype(np.float32)
|
|
@@ -377,16 +410,61 @@ class TraceplotWidget(QWidget):
|
|
|
377
410
|
print(f"DEBUG: First few time stamps: {time_stamps[:min(5, len(time_stamps))]}")
|
|
378
411
|
break
|
|
379
412
|
|
|
380
|
-
if
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
413
|
+
# Check if we have valid time stamps (not empty and has enough data)
|
|
414
|
+
has_valid_timestamps = (time_stamps is not None and
|
|
415
|
+
hasattr(time_stamps, '__len__') and
|
|
416
|
+
len(time_stamps) > 0 and
|
|
417
|
+
len(time_stamps) >= len(metric))
|
|
418
|
+
|
|
419
|
+
if has_valid_timestamps:
|
|
420
|
+
# Parse time stamps - they could be numbers (ms or seconds) or datetime strings
|
|
421
|
+
time_array = np.array(time_stamps[:len(metric)])
|
|
422
|
+
|
|
423
|
+
# Check if timestamps are strings (datetime format) or numbers
|
|
424
|
+
if isinstance(time_stamps[0], str):
|
|
425
|
+
# Parse datetime strings and convert to relative seconds
|
|
426
|
+
from datetime import datetime
|
|
427
|
+
try:
|
|
428
|
+
# Parse the datetime strings
|
|
429
|
+
dt_objects = []
|
|
430
|
+
for ts in time_stamps[:len(metric)]:
|
|
431
|
+
# Handle format like '2025-10-26 19:08:38.626'
|
|
432
|
+
dt = datetime.strptime(ts, '%Y-%m-%d %H:%M:%S.%f')
|
|
433
|
+
dt_objects.append(dt)
|
|
434
|
+
|
|
435
|
+
# Convert to seconds relative to first timestamp
|
|
436
|
+
first_dt = dt_objects[0]
|
|
437
|
+
x_values = np.array([(dt - first_dt).total_seconds() for dt in dt_objects])
|
|
438
|
+
print(f"DEBUG: Parsed datetime timestamps, duration: {x_values[-1]:.2f}s")
|
|
439
|
+
except Exception as e:
|
|
440
|
+
print(f"DEBUG: Error parsing datetime strings: {e}")
|
|
441
|
+
# Fall back to frame numbers
|
|
442
|
+
show_time = False
|
|
443
|
+
x_values = None
|
|
387
444
|
else:
|
|
388
|
-
|
|
389
|
-
|
|
445
|
+
# Numeric timestamps - detect if milliseconds or seconds
|
|
446
|
+
max_time = np.max(time_array) if len(time_array) > 0 else 0
|
|
447
|
+
|
|
448
|
+
# Heuristic: if max time > 10000, assume milliseconds; otherwise seconds
|
|
449
|
+
if max_time > 10000:
|
|
450
|
+
# Data is in milliseconds, convert to seconds
|
|
451
|
+
x_values = time_array / 1000.0
|
|
452
|
+
print(f"DEBUG: Time stamps appear to be in milliseconds (max={max_time:.2f}), converting to seconds")
|
|
453
|
+
else:
|
|
454
|
+
# Data is already in seconds (or very short recording in ms)
|
|
455
|
+
x_values = time_array
|
|
456
|
+
print(f"DEBUG: Time stamps appear to be in seconds (max={max_time:.2f})")
|
|
457
|
+
|
|
458
|
+
if x_values is not None:
|
|
459
|
+
x_label = "Time (s)"
|
|
460
|
+
|
|
461
|
+
# Convert current frame position to time
|
|
462
|
+
if current_frame < len(x_values):
|
|
463
|
+
current_x_pos = x_values[current_frame]
|
|
464
|
+
else:
|
|
465
|
+
current_x_pos = x_values[-1] if len(x_values) > 0 else current_frame
|
|
466
|
+
|
|
467
|
+
print(f"DEBUG: Using time stamps for x-axis, current position: {current_x_pos}s")
|
|
390
468
|
else:
|
|
391
469
|
# Fallback: estimate time based on frame rate (if available)
|
|
392
470
|
frame_rate = getattr(ed, 'frame_rate', None) if ed else None
|
|
@@ -432,14 +510,38 @@ class TraceplotWidget(QWidget):
|
|
|
432
510
|
print(f"DEBUG: Found {len(stims)} stimulation timeframes: {stims}")
|
|
433
511
|
|
|
434
512
|
# Convert stimulation timeframes to appropriate x-axis units
|
|
435
|
-
if show_time and x_values is not None:
|
|
436
|
-
#
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
513
|
+
if show_time and x_values is not None and time_stamps is not None:
|
|
514
|
+
# Handle both datetime strings and numeric timestamps
|
|
515
|
+
if isinstance(time_stamps[0], str):
|
|
516
|
+
# Parse datetime strings for stimulation times
|
|
517
|
+
from datetime import datetime
|
|
518
|
+
try:
|
|
519
|
+
first_dt = datetime.strptime(time_stamps[0], '%Y-%m-%d %H:%M:%S.%f')
|
|
520
|
+
for stim in stims:
|
|
521
|
+
stim_frame = int(stim)
|
|
522
|
+
if stim_frame < len(time_stamps):
|
|
523
|
+
stim_dt = datetime.strptime(time_stamps[stim_frame], '%Y-%m-%d %H:%M:%S.%f')
|
|
524
|
+
stim_x_pos = (stim_dt - first_dt).total_seconds()
|
|
525
|
+
print(f"DEBUG: Adding stimulation vline at time {stim_x_pos:.2f}s (frame {stim_frame})")
|
|
526
|
+
self.trace_ax.axvline(stim_x_pos, color='red', linestyle='--', zorder=15, linewidth=2)
|
|
527
|
+
except Exception as e:
|
|
528
|
+
print(f"DEBUG: Error parsing datetime for stimulation: {e}")
|
|
529
|
+
else:
|
|
530
|
+
# Numeric timestamps - detect format
|
|
531
|
+
time_array = np.array(time_stamps)
|
|
532
|
+
max_time = np.max(time_array) if len(time_array) > 0 else 0
|
|
533
|
+
is_milliseconds = max_time > 10000
|
|
534
|
+
|
|
535
|
+
# Convert stim frames to time positions
|
|
536
|
+
for stim in stims:
|
|
537
|
+
stim_frame = int(stim)
|
|
538
|
+
if stim_frame < len(time_stamps):
|
|
539
|
+
if is_milliseconds:
|
|
540
|
+
stim_x_pos = time_stamps[stim_frame] / 1000.0
|
|
541
|
+
else:
|
|
542
|
+
stim_x_pos = time_stamps[stim_frame]
|
|
543
|
+
print(f"DEBUG: Adding stimulation vline at time {stim_x_pos:.2f}s (frame {stim_frame})")
|
|
544
|
+
self.trace_ax.axvline(stim_x_pos, color='red', linestyle='--', zorder=15, linewidth=2)
|
|
443
545
|
else:
|
|
444
546
|
# Use frame numbers
|
|
445
547
|
for stim in stims:
|
|
@@ -538,8 +640,27 @@ class TraceplotWidget(QWidget):
|
|
|
538
640
|
time_stamps = getattr(ed, attr_name)
|
|
539
641
|
break
|
|
540
642
|
|
|
541
|
-
if time_stamps is not None and current_frame < len(time_stamps):
|
|
542
|
-
|
|
643
|
+
if time_stamps is not None and len(time_stamps) > 0 and current_frame < len(time_stamps):
|
|
644
|
+
# Handle both datetime strings and numeric timestamps
|
|
645
|
+
if isinstance(time_stamps[0], str):
|
|
646
|
+
# Parse datetime strings
|
|
647
|
+
from datetime import datetime
|
|
648
|
+
try:
|
|
649
|
+
first_dt = datetime.strptime(time_stamps[0], '%Y-%m-%d %H:%M:%S.%f')
|
|
650
|
+
current_dt = datetime.strptime(time_stamps[current_frame], '%Y-%m-%d %H:%M:%S.%f')
|
|
651
|
+
current_x_pos = (current_dt - first_dt).total_seconds()
|
|
652
|
+
except Exception:
|
|
653
|
+
# Fall back to frame rate if parsing fails
|
|
654
|
+
pass
|
|
655
|
+
else:
|
|
656
|
+
# Numeric timestamps - detect if milliseconds or seconds
|
|
657
|
+
max_time = np.max(time_stamps) if len(time_stamps) > 0 else 0
|
|
658
|
+
if max_time > 10000:
|
|
659
|
+
# Data is in milliseconds, convert to seconds
|
|
660
|
+
current_x_pos = time_stamps[current_frame] / 1000.0
|
|
661
|
+
else:
|
|
662
|
+
# Data is already in seconds
|
|
663
|
+
current_x_pos = time_stamps[current_frame]
|
|
543
664
|
elif ed is not None:
|
|
544
665
|
# Fallback: estimate time based on frame rate
|
|
545
666
|
if isinstance(ed, dict):
|
|
@@ -575,7 +696,28 @@ class TraceplotWidget(QWidget):
|
|
|
575
696
|
break
|
|
576
697
|
|
|
577
698
|
if time_stamps is not None and len(time_stamps) > 0:
|
|
578
|
-
|
|
699
|
+
# Handle both datetime strings and numeric timestamps
|
|
700
|
+
if isinstance(time_stamps[0], str):
|
|
701
|
+
# Parse datetime strings
|
|
702
|
+
from datetime import datetime
|
|
703
|
+
try:
|
|
704
|
+
first_dt = datetime.strptime(time_stamps[0], '%Y-%m-%d %H:%M:%S.%f')
|
|
705
|
+
last_idx = min(nframes, len(time_stamps)) - 1
|
|
706
|
+
last_dt = datetime.strptime(time_stamps[last_idx], '%Y-%m-%d %H:%M:%S.%f')
|
|
707
|
+
xmax = (last_dt - first_dt).total_seconds()
|
|
708
|
+
except Exception:
|
|
709
|
+
xmax = max(1, nframes - 1)
|
|
710
|
+
else:
|
|
711
|
+
# Numeric timestamps - detect if milliseconds or seconds
|
|
712
|
+
time_array = np.array(time_stamps[:min(nframes, len(time_stamps))])
|
|
713
|
+
max_time = np.max(time_array) if len(time_array) > 0 else 0
|
|
714
|
+
|
|
715
|
+
if max_time > 10000:
|
|
716
|
+
# Data is in milliseconds, convert to seconds
|
|
717
|
+
xmax = max(time_array / 1000.0)
|
|
718
|
+
else:
|
|
719
|
+
# Data is already in seconds
|
|
720
|
+
xmax = max(time_array)
|
|
579
721
|
elif ed is not None:
|
|
580
722
|
frame_rate = getattr(ed, 'frame_rate', None)
|
|
581
723
|
if frame_rate and frame_rate > 0:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "phasor-handler"
|
|
7
|
-
version = "2.2.
|
|
7
|
+
version = "2.2.4"
|
|
8
8
|
authors = [{ name = "Josia Shemuel", email = "joshemuel@users.noreply.github.com" }]
|
|
9
9
|
description = "A PyQt6 GUI toolbox for processing two-photon phasor imaging data"
|
|
10
10
|
readme = "README.md"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/widgets/analysis/components/__init__.py
RENAMED
|
File without changes
|
{phasor_handler-2.2.3 → phasor_handler-2.2.4}/phasor_handler/widgets/analysis/components/bnc.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|