flimkit 0.13.5__tar.gz → 0.13.7__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.5 → flimkit-0.13.7}/PKG-INFO +5 -29
  2. {flimkit-0.13.5 → flimkit-0.13.7}/README.md +4 -28
  3. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/FLIM/batch.py +18 -2
  4. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/UI/fov_preview.py +98 -10
  5. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/UI/gui.py +340 -26
  6. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/UI/irf_widget.py +15 -0
  7. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/UI/modes/batch_mode.py +1 -34
  8. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/UI/modes/fov_mode.py +2 -3
  9. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/UI/modes/stitch_mode.py +2 -30
  10. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/UI/phasor_panel.py +135 -148
  11. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/UI/project_panel.py +7 -0
  12. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/UI/roi_tools.py +35 -34
  13. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/_version.py +4 -2
  14. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/configs.py +21 -1
  15. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/formats/PTU/reader.py +11 -10
  16. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/interactive.py +19 -3
  17. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/phasor_launcher.py +45 -9
  18. flimkit-0.13.7/flimkit/utils/apply_settings.py +89 -0
  19. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/utils/display.py +27 -3
  20. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit.egg-info/PKG-INFO +5 -29
  21. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit.egg-info/SOURCES.txt +1 -0
  22. {flimkit-0.13.5 → flimkit-0.13.7}/LICENSE.md +0 -0
  23. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/FLIM/__init__.py +0 -0
  24. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/FLIM/assemble.py +0 -0
  25. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/FLIM/bg_tools.py +0 -0
  26. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/FLIM/fit_tools.py +0 -0
  27. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/FLIM/fitters.py +0 -0
  28. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/FLIM/irf_tools.py +0 -0
  29. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/FLIM/models.py +0 -0
  30. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/GPU/__init__.py +0 -0
  31. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/GPU/_base.py +0 -0
  32. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/GPU/cuda.py +0 -0
  33. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/GPU/mlx_backend.py +0 -0
  34. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/GPU/mps.py +0 -0
  35. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/GPU/rocm.py +0 -0
  36. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/GPU/torch_backend.py +0 -0
  37. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/UI/app_state.py +0 -0
  38. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/UI/controller.py +0 -0
  39. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/UI/expert_settings.py +0 -0
  40. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/UI/fit_help.py +0 -0
  41. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/UI/icon.icns +0 -0
  42. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/UI/icon.ico +0 -0
  43. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/UI/icon.png +0 -0
  44. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/UI/mode_controller.py +0 -0
  45. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/UI/modes/__init__.py +0 -0
  46. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/UI/modes/base.py +0 -0
  47. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/UI/modes/irf_mode.py +0 -0
  48. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/UI/modes/phasor_mode.py +0 -0
  49. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/UI/progress_window.py +0 -0
  50. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/UI/results_panel.py +0 -0
  51. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/UI/utils.py +0 -0
  52. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/__init__.py +0 -0
  53. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/cli.py +0 -0
  54. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/dialogs.py +0 -0
  55. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/formats/BH/__init__.py +0 -0
  56. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/formats/BH/reader.py +0 -0
  57. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/formats/BH/writer.py +0 -0
  58. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/formats/ISS/__init__.py +0 -0
  59. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/formats/ISS/fdflim.py +0 -0
  60. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/formats/ISS/image.py +0 -0
  61. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/formats/ISS/reader.py +0 -0
  62. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/formats/PS/__init__.py +0 -0
  63. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/formats/PS/reader.py +0 -0
  64. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/formats/PTU/__init__.py +0 -0
  65. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/formats/PTU/decode.py +0 -0
  66. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/formats/PTU/phu.py +0 -0
  67. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/formats/PTU/series.py +0 -0
  68. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/formats/PTU/stitch.py +0 -0
  69. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/formats/PTU/tools.py +0 -0
  70. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/formats/__init__.py +0 -0
  71. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/formats/flim_file.py +0 -0
  72. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/formats/phasor.py +0 -0
  73. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/formats/signal.py +0 -0
  74. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/image/tools.py +0 -0
  75. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/mpl_backend.py +0 -0
  76. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/phasor/__init__.py +0 -0
  77. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/phasor/filters.py +0 -0
  78. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/phasor/fret.py +0 -0
  79. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/phasor/interactive.py +0 -0
  80. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/phasor/peaks.py +0 -0
  81. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/phasor/signal.py +0 -0
  82. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/plugins/__init__.py +0 -0
  83. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/plugins/bindings.py +0 -0
  84. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/plugins/builtin/__init__.py +0 -0
  85. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/plugins/builtin/core_tools.py +0 -0
  86. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/plugins/loader.py +0 -0
  87. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/plugins/registry.py +0 -0
  88. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/project.py +0 -0
  89. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/synth.py +0 -0
  90. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/utils/__init__.py +0 -0
  91. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/utils/batch_fit.py +0 -0
  92. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/utils/config_manager.py +0 -0
  93. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/utils/config_snapshot.py +0 -0
  94. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/utils/crash_handler.py +0 -0
  95. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/utils/enhanced_outputs.py +0 -0
  96. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/utils/fancy.py +0 -0
  97. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/utils/lifetime_image.py +0 -0
  98. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/utils/misc.py +0 -0
  99. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/utils/ome_zarr.py +0 -0
  100. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/utils/plotting.py +0 -0
  101. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/utils/roi.py +0 -0
  102. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/utils/session.py +0 -0
  103. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/utils/update_check.py +0 -0
  104. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/utils/xlsx_tools.py +0 -0
  105. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit/utils/xml_utils.py +0 -0
  106. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit.egg-info/dependency_links.txt +0 -0
  107. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit.egg-info/entry_points.txt +0 -0
  108. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit.egg-info/requires.txt +0 -0
  109. {flimkit-0.13.5 → flimkit-0.13.7}/flimkit.egg-info/top_level.txt +0 -0
  110. {flimkit-0.13.5 → flimkit-0.13.7}/pyproject.toml +0 -0
  111. {flimkit-0.13.5 → flimkit-0.13.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flimkit
3
- Version: 0.13.5
3
+ Version: 0.13.7
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,45 +133,21 @@ 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 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
-
138
- **Pull and run:**
136
+ Pre-built images are on Docker Hub as `alex1075/flimkit`. They run 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.
139
137
 
140
138
  ```bash
141
139
  docker run -d \
142
140
  -p 14500:14500 \
143
141
  -e FLIMKIT_PASSWORD=choose-a-password \
144
142
  -v /path/to/your/data:/data \
145
- -v /path/to/flimkit-config:/root/.flimkit \
143
+ -v /path/to/flimkit-config:/config \
146
144
  --name flimkit \
147
145
  alex1075/flimkit:latest
148
146
  ```
149
147
 
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`.
160
-
161
- **TrueNAS SCALE (Custom App):**
162
-
163
- 1. Apps → Discover Apps → Custom App
164
- 2. Paste the contents of `docker/docker-compose.yaml` from this repo
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
166
- 4. Deploy - TrueNAS will pull the image automatically
167
-
168
- **Build from source** (required if you want to push your own changes):
169
-
170
- ```bash
171
- python docker/build_docker.py # builds linux/amd64, pushes to Docker Hub
172
- ```
148
+ Then open http://localhost:14500 and log in as `flimkit` with that password.
173
149
 
174
- Requires Docker Desktop with buildx. On Apple Silicon, buildx cross-compiles for `linux/amd64` automatically.
150
+ The images are built in [flimkit-docker](https://github.com/FLIMKit/flimkit-docker), which lists the tags (Cellpose, CUDA, ROCm, the noVNC desktop and the headless bridge images), the settings, the compose file for a TrueNAS SCALE custom app, and how to build one yourself.
175
151
 
176
152
  ## Usage
177
153
 
@@ -71,45 +71,21 @@ 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 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
-
76
- **Pull and run:**
74
+ Pre-built images are on Docker Hub as `alex1075/flimkit`. They run 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.
77
75
 
78
76
  ```bash
79
77
  docker run -d \
80
78
  -p 14500:14500 \
81
79
  -e FLIMKIT_PASSWORD=choose-a-password \
82
80
  -v /path/to/your/data:/data \
83
- -v /path/to/flimkit-config:/root/.flimkit \
81
+ -v /path/to/flimkit-config:/config \
84
82
  --name flimkit \
85
83
  alex1075/flimkit:latest
86
84
  ```
87
85
 
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`.
98
-
99
- **TrueNAS SCALE (Custom App):**
100
-
101
- 1. Apps → Discover Apps → Custom App
102
- 2. Paste the contents of `docker/docker-compose.yaml` from this repo
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
104
- 4. Deploy - TrueNAS will pull the image automatically
105
-
106
- **Build from source** (required if you want to push your own changes):
107
-
108
- ```bash
109
- python docker/build_docker.py # builds linux/amd64, pushes to Docker Hub
110
- ```
86
+ Then open http://localhost:14500 and log in as `flimkit` with that password.
111
87
 
112
- Requires Docker Desktop with buildx. On Apple Silicon, buildx cross-compiles for `linux/amd64` automatically.
88
+ The images are built in [flimkit-docker](https://github.com/FLIMKit/flimkit-docker), which lists the tags (Cellpose, CUDA, ROCm, the noVNC desktop and the headless bridge images), the settings, the compose file for a TrueNAS SCALE custom app, and how to build one yourself.
113
89
 
114
90
  ## Usage
115
91
 
@@ -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():