flimkit 0.13.4__tar.gz → 0.13.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. {flimkit-0.13.4 → flimkit-0.13.6}/PKG-INFO +15 -4
  2. {flimkit-0.13.4 → flimkit-0.13.6}/README.md +14 -3
  3. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/FLIM/batch.py +18 -2
  4. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/UI/fov_preview.py +98 -10
  5. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/UI/gui.py +340 -26
  6. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/UI/irf_widget.py +15 -0
  7. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/UI/modes/batch_mode.py +1 -34
  8. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/UI/modes/fov_mode.py +2 -3
  9. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/UI/modes/stitch_mode.py +2 -30
  10. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/UI/phasor_panel.py +135 -148
  11. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/UI/project_panel.py +7 -0
  12. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/UI/roi_tools.py +34 -34
  13. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/_version.py +4 -2
  14. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/configs.py +21 -1
  15. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/formats/PTU/reader.py +11 -10
  16. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/interactive.py +19 -3
  17. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/phasor_launcher.py +45 -9
  18. flimkit-0.13.6/flimkit/utils/apply_settings.py +89 -0
  19. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/utils/display.py +27 -3
  20. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit.egg-info/PKG-INFO +15 -4
  21. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit.egg-info/SOURCES.txt +1 -0
  22. {flimkit-0.13.4 → flimkit-0.13.6}/LICENSE.md +0 -0
  23. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/FLIM/__init__.py +0 -0
  24. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/FLIM/assemble.py +0 -0
  25. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/FLIM/bg_tools.py +0 -0
  26. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/FLIM/fit_tools.py +0 -0
  27. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/FLIM/fitters.py +0 -0
  28. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/FLIM/irf_tools.py +0 -0
  29. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/FLIM/models.py +0 -0
  30. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/GPU/__init__.py +0 -0
  31. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/GPU/_base.py +0 -0
  32. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/GPU/cuda.py +0 -0
  33. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/GPU/mlx_backend.py +0 -0
  34. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/GPU/mps.py +0 -0
  35. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/GPU/rocm.py +0 -0
  36. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/GPU/torch_backend.py +0 -0
  37. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/UI/app_state.py +0 -0
  38. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/UI/controller.py +0 -0
  39. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/UI/expert_settings.py +0 -0
  40. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/UI/fit_help.py +0 -0
  41. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/UI/icon.icns +0 -0
  42. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/UI/icon.ico +0 -0
  43. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/UI/icon.png +0 -0
  44. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/UI/mode_controller.py +0 -0
  45. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/UI/modes/__init__.py +0 -0
  46. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/UI/modes/base.py +0 -0
  47. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/UI/modes/irf_mode.py +0 -0
  48. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/UI/modes/phasor_mode.py +0 -0
  49. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/UI/progress_window.py +0 -0
  50. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/UI/results_panel.py +0 -0
  51. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/UI/utils.py +0 -0
  52. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/__init__.py +0 -0
  53. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/cli.py +0 -0
  54. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/dialogs.py +0 -0
  55. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/formats/BH/__init__.py +0 -0
  56. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/formats/BH/reader.py +0 -0
  57. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/formats/BH/writer.py +0 -0
  58. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/formats/ISS/__init__.py +0 -0
  59. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/formats/ISS/fdflim.py +0 -0
  60. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/formats/ISS/image.py +0 -0
  61. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/formats/ISS/reader.py +0 -0
  62. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/formats/PS/__init__.py +0 -0
  63. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/formats/PS/reader.py +0 -0
  64. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/formats/PTU/__init__.py +0 -0
  65. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/formats/PTU/decode.py +0 -0
  66. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/formats/PTU/phu.py +0 -0
  67. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/formats/PTU/series.py +0 -0
  68. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/formats/PTU/stitch.py +0 -0
  69. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/formats/PTU/tools.py +0 -0
  70. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/formats/__init__.py +0 -0
  71. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/formats/flim_file.py +0 -0
  72. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/formats/phasor.py +0 -0
  73. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/formats/signal.py +0 -0
  74. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/image/tools.py +0 -0
  75. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/mpl_backend.py +0 -0
  76. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/phasor/__init__.py +0 -0
  77. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/phasor/filters.py +0 -0
  78. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/phasor/fret.py +0 -0
  79. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/phasor/interactive.py +0 -0
  80. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/phasor/peaks.py +0 -0
  81. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/phasor/signal.py +0 -0
  82. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/plugins/__init__.py +0 -0
  83. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/plugins/bindings.py +0 -0
  84. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/plugins/builtin/__init__.py +0 -0
  85. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/plugins/builtin/core_tools.py +0 -0
  86. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/plugins/loader.py +0 -0
  87. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/plugins/registry.py +0 -0
  88. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/project.py +0 -0
  89. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/synth.py +0 -0
  90. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/utils/__init__.py +0 -0
  91. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/utils/batch_fit.py +0 -0
  92. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/utils/config_manager.py +0 -0
  93. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/utils/config_snapshot.py +0 -0
  94. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/utils/crash_handler.py +0 -0
  95. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/utils/enhanced_outputs.py +0 -0
  96. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/utils/fancy.py +0 -0
  97. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/utils/lifetime_image.py +0 -0
  98. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/utils/misc.py +0 -0
  99. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/utils/ome_zarr.py +0 -0
  100. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/utils/plotting.py +0 -0
  101. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/utils/roi.py +0 -0
  102. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/utils/session.py +0 -0
  103. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/utils/update_check.py +0 -0
  104. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/utils/xlsx_tools.py +0 -0
  105. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit/utils/xml_utils.py +0 -0
  106. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit.egg-info/dependency_links.txt +0 -0
  107. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit.egg-info/entry_points.txt +0 -0
  108. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit.egg-info/requires.txt +0 -0
  109. {flimkit-0.13.4 → flimkit-0.13.6}/flimkit.egg-info/top_level.txt +0 -0
  110. {flimkit-0.13.4 → flimkit-0.13.6}/pyproject.toml +0 -0
  111. {flimkit-0.13.4 → flimkit-0.13.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flimkit
3
- Version: 0.13.4
3
+ Version: 0.13.6
4
4
  Summary: Fluorescence lifetime imaging analysis: reading, fitting, phasors and stitching
5
5
  Author-email: Alex Hunt <alexander.hunt@ed.ac.uk>
6
6
  License-Expression: MIT
@@ -133,25 +133,36 @@ python install.py --dev
133
133
 
134
134
  ## Docker / TrueNAS SCALE
135
135
 
136
- A pre-built image is available on Docker Hub. It runs the full desktop GUI in a browser via xpra (no installation needed on the client).
136
+ A pre-built image is available on Docker Hub. It runs FLIMKit with the [web UI](https://github.com/FLIMKit/flimkit-web-ui), so the whole app is used from a browser with nothing installed on the client.
137
137
 
138
138
  **Pull and run:**
139
139
 
140
140
  ```bash
141
141
  docker run -d \
142
142
  -p 14500:14500 \
143
+ -e FLIMKIT_PASSWORD=choose-a-password \
143
144
  -v /path/to/your/data:/data \
145
+ -v /path/to/flimkit-config:/root/.flimkit \
144
146
  --name flimkit \
145
147
  alex1075/flimkit:latest
146
148
  ```
147
149
 
148
- Then open **http://localhost:14500** in your browser. PTU files and data should be placed in the folder you mount to `/data` - use the file dialog inside the app to navigate there.
150
+ Then open **http://localhost:14500** and log in as `flimkit` with that password. Put PTU files and data in the folder you mount to `/data` and use the Browse buttons in the page to navigate there. The config mount keeps expert settings, preferences and recent files when the container is recreated.
151
+
152
+ | Variable | Default | Effect |
153
+ |---|---|---|
154
+ | `FLIMKIT_PASSWORD` | not set | Password for the web UI (user `flimkit`) and for the desktop view. Without it, anyone who can reach the port can use FLIMKit and browse `/data`. |
155
+ | `FLIMKIT_DESKTOP` | `0` | Set to `1` to also serve the full desktop through noVNC on port 14501, for the few windows the web UI does not cover. Publish that port too. |
156
+ | `FLIMKIT_GEOMETRY` | `1440x900` | Size of the virtual screen FLIMKit draws on |
157
+ | `TZ` | `Etc/UTC` | Time zone for log timestamps |
158
+
159
+ The container reports healthy once the web UI answers on `/healthz`.
149
160
 
150
161
  **TrueNAS SCALE (Custom App):**
151
162
 
152
163
  1. Apps → Discover Apps → Custom App
153
164
  2. Paste the contents of `docker/docker-compose.yaml` from this repo
154
- 3. Edit the volume paths to match your pool (e.g. `/mnt/tank/microscopy:/data`)
165
+ 3. Edit the volume paths to match your pool (e.g. `/mnt/tank/microscopy:/data`) and uncomment `FLIMKIT_PASSWORD` with a password of your own
155
166
  4. Deploy - TrueNAS will pull the image automatically
156
167
 
157
168
  **Build from source** (required if you want to push your own changes):
@@ -71,25 +71,36 @@ python install.py --dev
71
71
 
72
72
  ## Docker / TrueNAS SCALE
73
73
 
74
- A pre-built image is available on Docker Hub. It runs the full desktop GUI in a browser via xpra (no installation needed on the client).
74
+ A pre-built image is available on Docker Hub. It runs FLIMKit with the [web UI](https://github.com/FLIMKit/flimkit-web-ui), so the whole app is used from a browser with nothing installed on the client.
75
75
 
76
76
  **Pull and run:**
77
77
 
78
78
  ```bash
79
79
  docker run -d \
80
80
  -p 14500:14500 \
81
+ -e FLIMKIT_PASSWORD=choose-a-password \
81
82
  -v /path/to/your/data:/data \
83
+ -v /path/to/flimkit-config:/root/.flimkit \
82
84
  --name flimkit \
83
85
  alex1075/flimkit:latest
84
86
  ```
85
87
 
86
- Then open **http://localhost:14500** in your browser. PTU files and data should be placed in the folder you mount to `/data` - use the file dialog inside the app to navigate there.
88
+ Then open **http://localhost:14500** and log in as `flimkit` with that password. Put PTU files and data in the folder you mount to `/data` and use the Browse buttons in the page to navigate there. The config mount keeps expert settings, preferences and recent files when the container is recreated.
89
+
90
+ | Variable | Default | Effect |
91
+ |---|---|---|
92
+ | `FLIMKIT_PASSWORD` | not set | Password for the web UI (user `flimkit`) and for the desktop view. Without it, anyone who can reach the port can use FLIMKit and browse `/data`. |
93
+ | `FLIMKIT_DESKTOP` | `0` | Set to `1` to also serve the full desktop through noVNC on port 14501, for the few windows the web UI does not cover. Publish that port too. |
94
+ | `FLIMKIT_GEOMETRY` | `1440x900` | Size of the virtual screen FLIMKit draws on |
95
+ | `TZ` | `Etc/UTC` | Time zone for log timestamps |
96
+
97
+ The container reports healthy once the web UI answers on `/healthz`.
87
98
 
88
99
  **TrueNAS SCALE (Custom App):**
89
100
 
90
101
  1. Apps → Discover Apps → Custom App
91
102
  2. Paste the contents of `docker/docker-compose.yaml` from this repo
92
- 3. Edit the volume paths to match your pool (e.g. `/mnt/tank/microscopy:/data`)
103
+ 3. Edit the volume paths to match your pool (e.g. `/mnt/tank/microscopy:/data`) and uncomment `FLIMKIT_PASSWORD` with a password of your own
93
104
  4. Deploy - TrueNAS will pull the image automatically
94
105
 
95
106
  **Build from source** (required if you want to push your own changes):
@@ -20,6 +20,18 @@ from ..utils.batch_fit import (
20
20
  save_tile_lifetime_txt, save_map_stacks, plot_metric_summary,
21
21
  )
22
22
 
23
+ def _window_kwargs(args):
24
+ from ..interactive import parse_exclude_ns
25
+ return dict(
26
+ irf_shift_bins=getattr(args, 'irf_shift_bins', 2),
27
+ fit_start_ns=getattr(args, 'fit_start_ns', None),
28
+ fit_end_ns=getattr(args, 'fit_end_ns', None),
29
+ exclude_ns=parse_exclude_ns(getattr(args, 'exclude_ns', None)),
30
+ )
31
+
32
+ def _binning(args):
33
+ return max(1, int(getattr(args, 'binning', 1) or 1))
34
+
23
35
  def _save_4d_stacks(frame_positions, group_dir, group_label):
24
36
  sorted_t = sorted(frame_positions.keys())
25
37
  all_s = sorted({s for pos in frame_positions.values() for s in pos})
@@ -122,6 +134,7 @@ def fit_timelapse(ptu_dir, output_dir, args,
122
134
  polish=not getattr(args, 'no_polish', False),
123
135
  cost_function=getattr(args, 'cost_function', 'poisson'),
124
136
  sigma_max=sigma_max,
137
+ **_window_kwargs(args),
125
138
  )
126
139
  print(f' Reference fit: {time.time() - t0:.1f} s')
127
140
  taus_ns = global_summary.get('taus_ns', global_popt[:args.nexp] * 1e9)
@@ -163,7 +176,7 @@ def fit_timelapse(ptu_dir, output_dir, args,
163
176
  print(f'\n t={t} s={s}: {ptu_path.name}')
164
177
  t_start = time.time()
165
178
  ptu = FLIMFile(str(ptu_path), verbose=False)
166
- pixel_stack = ptu.raw_pixel_stack(channel=channel)
179
+ pixel_stack = ptu.raw_pixel_stack(channel=channel, binning=_binning(args))
167
180
  if pixel_stack.shape[2] != n_bins:
168
181
  nb = pixel_stack.shape[2]
169
182
  if nb > n_bins:
@@ -229,6 +242,7 @@ def fit_timelapse(ptu_dir, output_dir, args,
229
242
  polish=not getattr(args, 'no_polish', False),
230
243
  cost_function=getattr(args, 'cost_function', 'poisson'),
231
244
  sigma_max=sigma_max,
245
+ **_window_kwargs(args),
232
246
  )
233
247
  plot_summed(
234
248
  tile_decay, tile_summary, ptu, None,
@@ -430,6 +444,7 @@ def fit_zstack(ptu_dir, output_dir, args,
430
444
  polish=not getattr(args, 'no_polish', False),
431
445
  cost_function=getattr(args, 'cost_function', 'poisson'),
432
446
  sigma_max=sigma_max,
447
+ **_window_kwargs(args),
433
448
  )
434
449
  print(f' Reference fit: {time.time() - t0:.1f} s')
435
450
  taus_ns = global_summary.get('taus_ns', global_popt[:args.nexp] * 1e9)
@@ -489,7 +504,7 @@ def fit_zstack(ptu_dir, output_dir, args,
489
504
  slice_dir = group_dir / f'z{z:04d}'
490
505
  slice_dir.mkdir(exist_ok=True)
491
506
  ptu = FLIMFile(str(ptu_path), verbose=False)
492
- pixel_stack = ptu.raw_pixel_stack(channel=channel)
507
+ pixel_stack = ptu.raw_pixel_stack(channel=channel, binning=_binning(args))
493
508
  if pixel_stack.shape[2] != n_bins:
494
509
  nb = pixel_stack.shape[2]
495
510
  if nb > n_bins:
@@ -555,6 +570,7 @@ def fit_zstack(ptu_dir, output_dir, args,
555
570
  polish=not getattr(args, 'no_polish', False),
556
571
  cost_function=getattr(args, 'cost_function', 'poisson'),
557
572
  sigma_max=sigma_max,
573
+ **_window_kwargs(args),
558
574
  )
559
575
  plot_summed(
560
576
  tile_decay, tile_summary, ptu, None,
@@ -86,7 +86,7 @@ class FOVPreviewPanel:
86
86
  state='readonly', width=10)
87
87
  self._cmap_combo.grid(row=1, column=4, sticky='w', padx=2)
88
88
  self._cmap_combo['values'] = list(display.COLORMAPS.keys())
89
- ttk.Button(ctrl_frame, text='Update', width=8, command=self._update_flim_display).grid(row=1, column=5, sticky='w', padx=2)
89
+ ttk.Button(ctrl_frame, text='Update', width=8, command=self._on_update_display).grid(row=1, column=5, sticky='w', padx=2)
90
90
  self._bv_show_decay = tk.BooleanVar(value=True)
91
91
  ttk.Checkbutton(ctrl_frame, text='Show Decay Plot',
92
92
  variable=self._bv_show_decay,
@@ -108,6 +108,22 @@ class FOVPreviewPanel:
108
108
  value='amplitude', command=self._on_weighting_changed).pack(side='left')
109
109
  ttk.Radiobutton(wt_frame, text='Intensity', variable=self._sv_tau_weighting,
110
110
  value='intensity', command=self._on_weighting_changed).pack(side='left')
111
+ int_frame = ttk.Frame(ctrl_frame)
112
+ int_frame.grid(row=4, column=0, columnspan=6, sticky='w', pady=(4, 0))
113
+ ttk.Label(int_frame, text='Intensity:').pack(side='left', padx=(0, 4))
114
+ ttk.Label(int_frame, text='Min').pack(side='left')
115
+ self._sv_int_min = tk.StringVar()
116
+ ttk.Entry(int_frame, textvariable=self._sv_int_min, width=7).pack(side='left', padx=2)
117
+ ttk.Label(int_frame, text='Max').pack(side='left', padx=(6, 0))
118
+ self._sv_int_max = tk.StringVar()
119
+ ttk.Entry(int_frame, textvariable=self._sv_int_max, width=7).pack(side='left', padx=2)
120
+ ttk.Button(int_frame, text='Auto', width=6,
121
+ command=self._auto_intensity_scale).pack(side='left', padx=2)
122
+ self._sv_int_cmap = tk.StringVar(value='inferno')
123
+ int_combo = ttk.Combobox(int_frame, textvariable=self._sv_int_cmap, state='readonly',
124
+ width=9, values=display.INTENSITY_COLORMAPS)
125
+ int_combo.pack(side='left', padx=(6, 2))
126
+ int_combo.bind('<<ComboboxSelected>>', lambda _e: self._on_update_display())
111
127
  self._ptu_path = None
112
128
  self._lifetime_map = None
113
129
  self._pixel_maps = None
@@ -119,6 +135,7 @@ class FOVPreviewPanel:
119
135
  'gamma': 1.0,
120
136
  'cmap': 'viridis',
121
137
  }
138
+ self._int_display = {'vmin': None, 'vmax': None, 'cmap': 'inferno'}
122
139
  self._n_exp = 1
123
140
  self._irf_prompt = None
124
141
  self._roi_manager = RoiManager()
@@ -140,6 +157,7 @@ class FOVPreviewPanel:
140
157
  if not _keep_zstack:
141
158
  self._hide_zstack()
142
159
  self._lifetime_map = None
160
+ self._intensity_map = None
143
161
  self._pixel_maps = None
144
162
  self._cached_resid_data = None
145
163
  if not ptu_path or not Path(ptu_path).exists():
@@ -158,8 +176,8 @@ class FOVPreviewPanel:
158
176
  if is_image:
159
177
  stack = ptu.pixel_stack(channel=None, binning=1)
160
178
  intensity = stack.sum(axis=2)
161
- intensity_clipped = np.clip(intensity, 0, np.percentile(intensity, 99))
162
- self._ax_img.imshow(intensity_clipped, cmap='inferno', origin='upper')
179
+ self._intensity_map = intensity
180
+ self._draw_intensity(self._ax_img, intensity)
163
181
  else:
164
182
  intensity = None
165
183
  self._ax_img.text(0.5, 0.5, 'Point measurement\nno image to reconstruct',
@@ -187,6 +205,9 @@ class FOVPreviewPanel:
187
205
  self._ax_resid.grid(True, alpha=0.3)
188
206
  self._cached_resid_data = None
189
207
  self._redraw_region_overlays()
208
+ if is_image:
209
+ self._sync_intensity_controls()
210
+ self._ctrl_frame.grid()
190
211
  self._canvas_mpl.draw_idle()
191
212
  n_photons = int(decay.sum())
192
213
  if is_image:
@@ -275,8 +296,7 @@ class FOVPreviewPanel:
275
296
  taus_fit = global_summary.get('taus_ns', [])
276
297
  model = global_summary.get('model')
277
298
  self._ax_img.clear()
278
- intensity_clipped = np.clip(intensity, 0, np.percentile(intensity, 99))
279
- self._ax_img.imshow(intensity_clipped, cmap='inferno', origin='upper')
299
+ self._draw_intensity(self._ax_img, intensity)
280
300
  self._ax_img.set_title('Intensity', fontsize=9, fontweight='bold', color='white')
281
301
  self._strip_image_axes(self._ax_img)
282
302
  self._ax_flim.clear()
@@ -492,8 +512,7 @@ class FOVPreviewPanel:
492
512
  return
493
513
  intensity = tifffile.imread(str(intensity_files[0]))
494
514
  self._ax_img.clear()
495
- intensity_clipped = np.clip(intensity, 0, np.percentile(intensity, 99))
496
- self._ax_img.imshow(intensity_clipped, cmap='inferno', origin='upper')
515
+ self._draw_intensity(self._ax_img, intensity)
497
516
  self._ax_img.set_title('Stitched ROI', fontsize=9, fontweight='bold', color='white')
498
517
  self._strip_image_axes(self._ax_img)
499
518
  lifetime_data = None
@@ -595,6 +614,75 @@ class FOVPreviewPanel:
595
614
  pass
596
615
  self._lifetime_map = lifetime_map
597
616
  self._update_flim_display()
617
+ def _intensity_limits(self, intensity):
618
+ import numpy as np
619
+ lo = self._int_display.get('vmin')
620
+ hi = self._int_display.get('vmax')
621
+ if lo is None:
622
+ lo = 0.0
623
+ if hi is None:
624
+ finite = np.asarray(intensity)[np.isfinite(intensity)]
625
+ hi = float(np.percentile(finite, 99)) if finite.size > 0 else 1.0
626
+ if hi <= lo:
627
+ hi = lo + 1.0
628
+ return lo, hi
629
+
630
+ def _draw_intensity(self, ax, intensity):
631
+ lo, hi = self._intensity_limits(intensity)
632
+ cmap = display.get_colormap(self._int_display.get('cmap', 'inferno'))
633
+ return ax.imshow(intensity, cmap=cmap, origin='upper', vmin=lo, vmax=hi)
634
+
635
+ def _read_intensity_controls(self):
636
+ def num(var):
637
+ text = var.get().strip()
638
+ if text == '':
639
+ return None
640
+ try:
641
+ return float(text)
642
+ except ValueError:
643
+ return None
644
+ self._int_display = {
645
+ 'vmin': num(self._sv_int_min),
646
+ 'vmax': num(self._sv_int_max),
647
+ 'cmap': self._sv_int_cmap.get() or 'inferno',
648
+ }
649
+
650
+ def _sync_intensity_controls(self):
651
+ cs = self._int_display
652
+ self._sv_int_min.set('' if cs.get('vmin') is None else f"{cs['vmin']:g}")
653
+ self._sv_int_max.set('' if cs.get('vmax') is None else f"{cs['vmax']:g}")
654
+ self._sv_int_cmap.set(cs.get('cmap', 'inferno'))
655
+
656
+ def _sync_flim_controls(self):
657
+ cs = self._flim_color_scale
658
+ self._sv_tau_min.set('' if cs.get('vmin') is None else f"{cs['vmin']:.2f}")
659
+ self._sv_tau_max.set('' if cs.get('vmax') is None else f"{cs['vmax']:.2f}")
660
+ self._sv_gamma.set(str(cs.get('gamma', 1.0)))
661
+ self._sv_cmap.set(cs.get('cmap', 'viridis'))
662
+
663
+ def _update_intensity_display(self):
664
+ if self._intensity_map is None or not self._ax_img.get_visible():
665
+ return
666
+ title = self._ax_img.get_title() or 'Intensity'
667
+ self._ax_img.clear()
668
+ self._draw_intensity(self._ax_img, self._intensity_map)
669
+ self._ax_img.set_title(title, fontsize=9, fontweight='bold', color='white')
670
+ self._strip_image_axes(self._ax_img)
671
+ self._redraw_region_overlays()
672
+ self._canvas_mpl.draw_idle()
673
+
674
+ def _auto_intensity_scale(self):
675
+ self._sv_int_min.set('')
676
+ self._sv_int_max.set('')
677
+ self._on_update_display()
678
+
679
+ def _on_update_display(self):
680
+ self._read_intensity_controls()
681
+ self._update_intensity_display()
682
+ self._update_flim_display()
683
+ if self._lifetime_map is None:
684
+ self._save_color_scale_update()
685
+
598
686
  def _auto_detect_scale(self):
599
687
  import numpy as np
600
688
  if self._lifetime_map is None:
@@ -677,6 +765,7 @@ class FOVPreviewPanel:
677
765
  session_data = {key: existing_data[key].item() if existing_data[key].ndim == 0 else existing_data[key]
678
766
  for key in existing_data.files}
679
767
  session_data['fov_color_scale'] = json.dumps(self._flim_color_scale)
768
+ session_data['fov_intensity_scale'] = json.dumps(self._int_display)
680
769
  np.savez_compressed(session_file, **session_data)
681
770
  print(f"[Color Scale] ✓ Saved to {session_file.name}")
682
771
  except Exception as e:
@@ -706,6 +795,7 @@ class FOVPreviewPanel:
706
795
  if self._intensity_map is not None:
707
796
  session_data['fov_intensity_map'] = self._intensity_map
708
797
  session_data['fov_color_scale'] = json.dumps(self._flim_color_scale)
798
+ session_data['fov_intensity_scale'] = json.dumps(self._int_display)
709
799
  session_data['fov_n_exp'] = self._n_exp
710
800
  if self._ptu_path:
711
801
  session_data['fov_ptu_path'] = self._ptu_path
@@ -927,9 +1017,7 @@ class FOVPreviewPanel:
927
1017
  _ax.set_facecolor('black')
928
1018
  if self._ax_img.get_visible() and self._intensity_map is not None:
929
1019
  import numpy as np
930
- intensity_clipped = np.clip(self._intensity_map, 0,
931
- np.percentile(self._intensity_map, 99))
932
- self._ax_img.imshow(intensity_clipped, cmap='inferno', origin='upper')
1020
+ self._draw_intensity(self._ax_img, self._intensity_map)
933
1021
  self._ax_img.set_title(img_title, fontsize=9, fontweight='bold', color='white')
934
1022
  self._strip_image_axes(self._ax_img)
935
1023
  elif self._ax_img.get_visible():