kaveh commited on
Commit
fd5af45
·
1 Parent(s): 893dcb6

optimised, removed dead codes

Browse files
Files changed (5) hide show
  1. app.py +7 -22
  2. static/s2f_styles.css +65 -103
  3. ui/measure_tool.py +10 -27
  4. ui/result_display.py +17 -11
  5. utils/display.py +30 -49
app.py CHANGED
@@ -68,13 +68,9 @@ if HAS_DRAWABLE_CANVAS and ST_DIALOG:
68
  st.warning("No prediction available to measure.")
69
  return
70
  display_mode = st.session_state.get("measure_display_mode", "Default")
71
- _m_clamp = st.session_state.get(
72
- "measure_clamp_only", st.session_state.get("measure_clip_bounds", False)
73
- )
74
  display_heatmap = apply_display_scale(
75
  raw_heatmap, display_mode,
76
- min_percentile=st.session_state.get("measure_min_percentile", 0),
77
- max_percentile=st.session_state.get("measure_max_percentile", 100),
78
  clip_min=st.session_state.get("measure_clip_min", 0),
79
  clip_max=st.session_state.get("measure_clip_max", 1),
80
  clamp_only=_m_clamp,
@@ -102,15 +98,12 @@ def _get_measure_dialog_fn():
102
 
103
  def _populate_measure_session_state(heatmap, img, pixel_sum, force, key_img, colormap_name,
104
  display_mode, auto_cell_boundary, cell_mask=None,
105
- min_percentile=0, max_percentile=100, clip_min=0, clip_max=1,
106
- clamp_only=False):
107
  """Populate session state for the measure tool. If cell_mask is None and auto_cell_boundary, computes it."""
108
  if cell_mask is None and auto_cell_boundary:
109
  cell_mask = estimate_cell_mask(heatmap)
110
  st.session_state["measure_raw_heatmap"] = heatmap.copy()
111
  st.session_state["measure_display_mode"] = display_mode
112
- st.session_state["measure_min_percentile"] = min_percentile
113
- st.session_state["measure_max_percentile"] = max_percentile
114
  st.session_state["measure_clip_min"] = clip_min
115
  st.session_state["measure_clip_max"] = clip_max
116
  st.session_state["measure_clamp_only"] = clamp_only
@@ -252,7 +245,7 @@ with st.sidebar:
252
  if model_type == "single_cell":
253
  try:
254
  with st.container(border=False, key="s2f_grp_conditions"):
255
- st.markdown('<p style="font-size: 0.95rem; font-weight: 500; margin-bottom: 0.5rem;">Conditions</p>', unsafe_allow_html=True)
256
  conditions_source = st.radio(
257
  "Conditions",
258
  ["From config", "Manually"],
@@ -333,11 +326,10 @@ with st.sidebar:
333
  clip_min, clip_max = 0.0, 1.0
334
  display_mode = "Range"
335
  clamp_only = False
336
- min_percentile, max_percentile = 0, 100
337
 
338
  cm_col_lbl, cm_col_sb = st.columns([1, 2])
339
  with cm_col_lbl:
340
- st.markdown('<p class="selectbox-label">Colormap</p>', unsafe_allow_html=True)
341
  with cm_col_sb:
342
  colormap_name = st.selectbox(
343
  "Colormap",
@@ -438,15 +430,13 @@ def _load_predictor(model_type, checkpoint, ckp_folder):
438
 
439
 
440
  def _prepare_and_render_cached_result(r, key_img, colormap_name, display_mode, auto_cell_boundary,
441
- min_percentile, max_percentile, clip_min, clip_max, clamp_only,
442
  download_key_suffix="", check_measure_dialog=False,
443
  show_success=False):
444
  """Prepare display from cached result and render. Used by both just_ran and has_cached paths."""
445
  img, heatmap, force, pixel_sum = r["img"], r["heatmap"], r["force"], r["pixel_sum"]
446
  display_heatmap = apply_display_scale(
447
  heatmap, display_mode,
448
- min_percentile=min_percentile,
449
- max_percentile=max_percentile,
450
  clip_min=clip_min,
451
  clip_max=clip_max,
452
  clamp_only=clamp_only,
@@ -455,7 +445,6 @@ def _prepare_and_render_cached_result(r, key_img, colormap_name, display_mode, a
455
  _populate_measure_session_state(
456
  heatmap, img, pixel_sum, force, key_img, colormap_name,
457
  display_mode, auto_cell_boundary, cell_mask=cell_mask,
458
- min_percentile=min_percentile, max_percentile=max_percentile,
459
  clip_min=clip_min, clip_max=clip_max, clamp_only=clamp_only,
460
  )
461
  if check_measure_dialog and st.session_state.pop("open_measure_dialog", False):
@@ -513,8 +502,6 @@ if just_ran_batch:
513
  batch_results,
514
  colormap_name=colormap_name,
515
  display_mode=display_mode,
516
- min_percentile=min_percentile,
517
- max_percentile=max_percentile,
518
  clip_min=clip_min,
519
  clip_max=clip_max,
520
  auto_cell_boundary=auto_cell_boundary,
@@ -531,8 +518,6 @@ elif batch_mode and st.session_state.get("batch_results"):
531
  st.session_state["batch_results"],
532
  colormap_name=colormap_name,
533
  display_mode=display_mode,
534
- min_percentile=min_percentile,
535
- max_percentile=max_percentile,
536
  clip_min=clip_min,
537
  clip_max=clip_max,
538
  auto_cell_boundary=auto_cell_boundary,
@@ -561,7 +546,7 @@ elif just_ran:
561
  st.session_state["prediction_result"] = r
562
  _prepare_and_render_cached_result(
563
  r, key_img, colormap_name, display_mode, auto_cell_boundary,
564
- min_percentile, max_percentile, clip_min, clip_max, clamp_only,
565
  download_key_suffix="", check_measure_dialog=False,
566
  show_success=True,
567
  )
@@ -573,7 +558,7 @@ elif has_cached:
573
  r = st.session_state["prediction_result"]
574
  _prepare_and_render_cached_result(
575
  r, key_img, colormap_name, display_mode, auto_cell_boundary,
576
- min_percentile, max_percentile, clip_min, clip_max, clamp_only,
577
  download_key_suffix="_cached", check_measure_dialog=True,
578
  show_success=False,
579
  )
 
68
  st.warning("No prediction available to measure.")
69
  return
70
  display_mode = st.session_state.get("measure_display_mode", "Default")
71
+ _m_clamp = st.session_state.get("measure_clamp_only", False)
 
 
72
  display_heatmap = apply_display_scale(
73
  raw_heatmap, display_mode,
 
 
74
  clip_min=st.session_state.get("measure_clip_min", 0),
75
  clip_max=st.session_state.get("measure_clip_max", 1),
76
  clamp_only=_m_clamp,
 
98
 
99
  def _populate_measure_session_state(heatmap, img, pixel_sum, force, key_img, colormap_name,
100
  display_mode, auto_cell_boundary, cell_mask=None,
101
+ clip_min=0, clip_max=1, clamp_only=False):
 
102
  """Populate session state for the measure tool. If cell_mask is None and auto_cell_boundary, computes it."""
103
  if cell_mask is None and auto_cell_boundary:
104
  cell_mask = estimate_cell_mask(heatmap)
105
  st.session_state["measure_raw_heatmap"] = heatmap.copy()
106
  st.session_state["measure_display_mode"] = display_mode
 
 
107
  st.session_state["measure_clip_min"] = clip_min
108
  st.session_state["measure_clip_max"] = clip_max
109
  st.session_state["measure_clamp_only"] = clamp_only
 
245
  if model_type == "single_cell":
246
  try:
247
  with st.container(border=False, key="s2f_grp_conditions"):
248
+ st.markdown('<p class="s2f-form-label s2f-form-label--section">Conditions</p>', unsafe_allow_html=True)
249
  conditions_source = st.radio(
250
  "Conditions",
251
  ["From config", "Manually"],
 
326
  clip_min, clip_max = 0.0, 1.0
327
  display_mode = "Range"
328
  clamp_only = False
 
329
 
330
  cm_col_lbl, cm_col_sb = st.columns([1, 2])
331
  with cm_col_lbl:
332
+ st.markdown('<p class="s2f-form-label s2f-form-label--colormap">Colormap</p>', unsafe_allow_html=True)
333
  with cm_col_sb:
334
  colormap_name = st.selectbox(
335
  "Colormap",
 
430
 
431
 
432
  def _prepare_and_render_cached_result(r, key_img, colormap_name, display_mode, auto_cell_boundary,
433
+ clip_min, clip_max, clamp_only,
434
  download_key_suffix="", check_measure_dialog=False,
435
  show_success=False):
436
  """Prepare display from cached result and render. Used by both just_ran and has_cached paths."""
437
  img, heatmap, force, pixel_sum = r["img"], r["heatmap"], r["force"], r["pixel_sum"]
438
  display_heatmap = apply_display_scale(
439
  heatmap, display_mode,
 
 
440
  clip_min=clip_min,
441
  clip_max=clip_max,
442
  clamp_only=clamp_only,
 
445
  _populate_measure_session_state(
446
  heatmap, img, pixel_sum, force, key_img, colormap_name,
447
  display_mode, auto_cell_boundary, cell_mask=cell_mask,
 
448
  clip_min=clip_min, clip_max=clip_max, clamp_only=clamp_only,
449
  )
450
  if check_measure_dialog and st.session_state.pop("open_measure_dialog", False):
 
502
  batch_results,
503
  colormap_name=colormap_name,
504
  display_mode=display_mode,
 
 
505
  clip_min=clip_min,
506
  clip_max=clip_max,
507
  auto_cell_boundary=auto_cell_boundary,
 
518
  st.session_state["batch_results"],
519
  colormap_name=colormap_name,
520
  display_mode=display_mode,
 
 
521
  clip_min=clip_min,
522
  clip_max=clip_max,
523
  auto_cell_boundary=auto_cell_boundary,
 
546
  st.session_state["prediction_result"] = r
547
  _prepare_and_render_cached_result(
548
  r, key_img, colormap_name, display_mode, auto_cell_boundary,
549
+ clip_min, clip_max, clamp_only,
550
  download_key_suffix="", check_measure_dialog=False,
551
  show_success=True,
552
  )
 
558
  r = st.session_state["prediction_result"]
559
  _prepare_and_render_cached_result(
560
  r, key_img, colormap_name, display_mode, auto_cell_boundary,
561
+ clip_min, clip_max, clamp_only,
562
  download_key_suffix="_cached", check_measure_dialog=True,
563
  show_success=False,
564
  )
static/s2f_styles.css CHANGED
@@ -11,6 +11,7 @@
11
  --s2f-sidebar-panel-border: rgba(203, 213, 225, 0.34);
12
  /* Match Streamlit’s sidebar horizontal padding so panels can full-bleed the track */
13
  --s2f-sidebar-inline-inset: 1rem;
 
14
  }
15
  /* Streamlit renders a fixed header; without this offset, the sidebar and first main block sit underneath it. */
16
  .stApp:has(header[data-testid="stHeader"]) {
@@ -34,30 +35,26 @@ html, body {
34
  overflow: hidden !important;
35
  margin: 0 !important;
36
  }
 
37
  .stApp {
38
  height: 100vh !important;
39
  max-height: 100dvh !important;
40
  overflow: hidden !important;
41
  display: flex !important;
42
  flex-direction: column !important;
 
 
 
 
 
43
  }
44
- /* Flex row: sidebar + main (first flex child of .stApp that wraps the app shell; header may be a sibling above this) */
45
  .stApp > div {
46
  display: flex !important;
47
  flex: 1 !important;
48
  min-height: 0 !important;
49
  overflow: hidden !important;
50
  }
51
-
52
- /* === Modern background patterns === */
53
- .stApp {
54
- background-color: #fafbfc !important;
55
- background-image:
56
- /* Subtle dot grid */
57
- radial-gradient(circle at 1px 1px, rgba(148, 163, 184, 0.12) 1px, transparent 0);
58
- background-size: 28px 28px !important;
59
- background-position: 0 0, 0 0 !important;
60
- }
61
  /* Soft gradient overlay for depth */
62
  .stApp::before {
63
  content: '';
@@ -139,7 +136,7 @@ section[data-testid="stSidebar"] {
139
  position: fixed !important;
140
  top: var(--s2f-streamlit-header-offset, 0px) !important;
141
  left: 0 !important;
142
- width: 360px !important;
143
  height: calc(100dvh - var(--s2f-streamlit-header-offset, 0px)) !important;
144
  max-height: calc(100dvh - var(--s2f-streamlit-header-offset, 0px)) !important;
145
  overflow-x: hidden !important;
@@ -170,22 +167,6 @@ section[data-testid="stSidebar"] [data-testid="stWidgetLabel"] p {
170
  font-weight: 500 !important;
171
  color: #334155 !important;
172
  }
173
- .sidebar-section {
174
- display: flex;
175
- align-items: center;
176
- gap: 8px;
177
- padding: 0.5rem 0 0.25rem;
178
- margin-top: 0.6rem;
179
- border-bottom: 2px solid #94a3b8;
180
- margin-bottom: 0.75rem;
181
- }
182
- .sidebar-section .section-title {
183
- font-size: 0.78rem;
184
- font-weight: 700;
185
- color: var(--s2f-primary-dark);
186
- text-transform: uppercase;
187
- letter-spacing: 0.06em;
188
- }
189
  .sidebar-brand {
190
  display: flex;
191
  align-items: center;
@@ -193,10 +174,6 @@ section[data-testid="stSidebar"] [data-testid="stWidgetLabel"] p {
193
  padding-bottom: 0.65rem;
194
  margin-bottom: 0.35rem;
195
  }
196
- .sidebar-brand .brand-icon {
197
- font-size: 1.6rem;
198
- line-height: 1;
199
- }
200
  .sidebar-brand .brand-text {
201
  font-size: 1.1rem;
202
  font-weight: 700;
@@ -387,67 +364,6 @@ div[data-testid="stHorizontalBlock"]:has([data-testid="stDownloadButton"]):has([
387
  color: var(--s2f-primary-dark);
388
  }
389
 
390
- /* === Scale visualization === */
391
- .scale-viz {
392
- margin: 0.3rem 0 0.5rem;
393
- font-size: 0.78rem;
394
- color: #64748b;
395
- }
396
- .sv-track {
397
- display: flex;
398
- align-items: center;
399
- gap: 6px;
400
- }
401
- .sv-end {
402
- font-weight: 600;
403
- font-size: 0.72rem;
404
- color: #94a3b8;
405
- min-width: 14px;
406
- text-align: center;
407
- }
408
- .sv-bar {
409
- flex: 1;
410
- height: 10px;
411
- background: #e2e8f0;
412
- border-radius: 5px;
413
- position: relative;
414
- overflow: visible;
415
- }
416
- .sv-active {
417
- position: absolute;
418
- top: 0;
419
- height: 100%;
420
- background: linear-gradient(90deg, var(--s2f-primary), var(--s2f-primary-dark));
421
- border-radius: 5px;
422
- box-shadow: 0 1px 4px rgba(var(--s2f-primary-rgb), 0.3);
423
- }
424
- .sv-lbl {
425
- position: absolute;
426
- top: 14px;
427
- font-size: 0.7rem;
428
- font-weight: 700;
429
- color: var(--s2f-primary-dark);
430
- white-space: nowrap;
431
- }
432
- .sv-lbl-l { left: 0; }
433
- .sv-lbl-r { right: 0; }
434
- .sv-note {
435
- display: flex;
436
- align-items: center;
437
- gap: 4px;
438
- margin-top: 10px;
439
- font-size: 0.75rem;
440
- color: #64748b;
441
- }
442
- .sv-pill {
443
- background: rgba(var(--s2f-primary-rgb), 0.15);
444
- color: var(--s2f-primary-dark);
445
- font-weight: 700;
446
- padding: 1px 6px;
447
- border-radius: 4px;
448
- font-size: 0.72rem;
449
- }
450
-
451
  /* === Run prediction info bar === */
452
  .run-info {
453
  display: flex;
@@ -506,10 +422,7 @@ section[data-testid="stSidebar"] [data-testid="stMultiSelect"] > div > div:focus
506
  border-color: var(--s2f-primary) !important;
507
  box-shadow: 0 0 0 1px rgba(var(--s2f-primary-rgb), 0.28) !important;
508
  }
509
- /* Radio: selected circle uses accent color */
510
- [data-testid="stRadio"] input[type="radio"] {
511
- accent-color: var(--s2f-primary);
512
- }
513
  [data-testid="stRadio"] input {
514
  accent-color: var(--s2f-primary);
515
  }
@@ -540,13 +453,19 @@ ul[role="listbox"] li {
540
  color: #1e293b !important;
541
  background-color: #ffffff !important;
542
  }
543
- .selectbox-label {
544
- margin: 0;
545
- padding-top: 0.4rem;
546
- font-size: 0.9rem;
547
  font-weight: 500;
548
  color: #334155;
549
  line-height: 1.2;
 
 
 
 
 
 
 
550
  }
551
 
552
  /* === Dataframe === */
@@ -598,7 +517,7 @@ hr { border-color: #cbd5e1 !important; opacity: 0.7; }
598
  .footer-citation {
599
  position: fixed;
600
  bottom: 0;
601
- left: 360px;
602
  right: 0;
603
  z-index: 999;
604
  padding: 0.5rem 1rem 0.55rem;
@@ -615,7 +534,7 @@ hr { border-color: #cbd5e1 !important; opacity: 0.7; }
615
  */
616
  [data-testid="stAppViewContainer"],
617
  .appview-container .main {
618
- margin-left: 360px !important;
619
  flex: 1 !important;
620
  min-height: 0 !important;
621
  overflow-y: auto !important;
@@ -639,6 +558,49 @@ section[data-testid="stSidebar"] > div:first-child {
639
  padding-bottom: 0.5rem !important;
640
  }
641
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
642
  /* === Responsive === */
643
  @media (max-width: 768px) {
644
  .s2f-header {
 
11
  --s2f-sidebar-panel-border: rgba(203, 213, 225, 0.34);
12
  /* Match Streamlit’s sidebar horizontal padding so panels can full-bleed the track */
13
  --s2f-sidebar-inline-inset: 1rem;
14
+ --s2f-sidebar-width: 360px;
15
  }
16
  /* Streamlit renders a fixed header; without this offset, the sidebar and first main block sit underneath it. */
17
  .stApp:has(header[data-testid="stHeader"]) {
 
35
  overflow: hidden !important;
36
  margin: 0 !important;
37
  }
38
+ /* App shell: flex column, dot grid background */
39
  .stApp {
40
  height: 100vh !important;
41
  max-height: 100dvh !important;
42
  overflow: hidden !important;
43
  display: flex !important;
44
  flex-direction: column !important;
45
+ background-color: #fafbfc !important;
46
+ background-image:
47
+ radial-gradient(circle at 1px 1px, rgba(148, 163, 184, 0.12) 1px, transparent 0);
48
+ background-size: 28px 28px !important;
49
+ background-position: 0 0, 0 0 !important;
50
  }
51
+ /* Flex row: sidebar + main */
52
  .stApp > div {
53
  display: flex !important;
54
  flex: 1 !important;
55
  min-height: 0 !important;
56
  overflow: hidden !important;
57
  }
 
 
 
 
 
 
 
 
 
 
58
  /* Soft gradient overlay for depth */
59
  .stApp::before {
60
  content: '';
 
136
  position: fixed !important;
137
  top: var(--s2f-streamlit-header-offset, 0px) !important;
138
  left: 0 !important;
139
+ width: var(--s2f-sidebar-width) !important;
140
  height: calc(100dvh - var(--s2f-streamlit-header-offset, 0px)) !important;
141
  max-height: calc(100dvh - var(--s2f-streamlit-header-offset, 0px)) !important;
142
  overflow-x: hidden !important;
 
167
  font-weight: 500 !important;
168
  color: #334155 !important;
169
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  .sidebar-brand {
171
  display: flex;
172
  align-items: center;
 
174
  padding-bottom: 0.65rem;
175
  margin-bottom: 0.35rem;
176
  }
 
 
 
 
177
  .sidebar-brand .brand-text {
178
  font-size: 1.1rem;
179
  font-weight: 700;
 
364
  color: var(--s2f-primary-dark);
365
  }
366
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  /* === Run prediction info bar === */
368
  .run-info {
369
  display: flex;
 
422
  border-color: var(--s2f-primary) !important;
423
  box-shadow: 0 0 0 1px rgba(var(--s2f-primary-rgb), 0.28) !important;
424
  }
425
+ /* Radio: accent color */
 
 
 
426
  [data-testid="stRadio"] input {
427
  accent-color: var(--s2f-primary);
428
  }
 
453
  color: #1e293b !important;
454
  background-color: #ffffff !important;
455
  }
456
+ /* Sidebar / form labels (colormap row, section titles) */
457
+ .s2f-form-label {
458
+ font-size: 0.95rem;
 
459
  font-weight: 500;
460
  color: #334155;
461
  line-height: 1.2;
462
+ margin: 0;
463
+ }
464
+ .s2f-form-label--colormap {
465
+ padding-top: 0.4rem;
466
+ }
467
+ .s2f-form-label--section {
468
+ margin-bottom: 0.5rem;
469
  }
470
 
471
  /* === Dataframe === */
 
517
  .footer-citation {
518
  position: fixed;
519
  bottom: 0;
520
+ left: var(--s2f-sidebar-width);
521
  right: 0;
522
  z-index: 999;
523
  padding: 0.5rem 1rem 0.55rem;
 
534
  */
535
  [data-testid="stAppViewContainer"],
536
  .appview-container .main {
537
+ margin-left: var(--s2f-sidebar-width) !important;
538
  flex: 1 !important;
539
  min-height: 0 !important;
540
  overflow-y: auto !important;
 
558
  padding-bottom: 0.5rem !important;
559
  }
560
 
561
+ /* Measure tool: value panel (replaces inline styles in measure_tool.py) */
562
+ .s2f-measure-vals-heading {
563
+ font-weight: 400;
564
+ color: #334155;
565
+ font-size: 0.95rem;
566
+ margin: 0 20px 4px 4px;
567
+ }
568
+ .s2f-measure-vals-panel {
569
+ width: 100%;
570
+ box-sizing: border-box;
571
+ border: 1px solid #e2e8f0;
572
+ border-radius: 10px;
573
+ padding: 10px 12px;
574
+ margin: 0 10px 20px 10px;
575
+ background: linear-gradient(145deg, #f8fafc 0%, #f1f5f9 100%);
576
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
577
+ }
578
+ .s2f-measure-vals-grid {
579
+ display: flex;
580
+ flex-wrap: wrap;
581
+ gap: 5px;
582
+ font-size: 0.9rem;
583
+ }
584
+
585
+ /* Dialog / expander: measure canvas layout */
586
+ [data-testid="stDialog"] [data-testid="stSelectbox"],
587
+ [data-testid="stExpander"] [data-testid="stSelectbox"],
588
+ [data-testid="stDialog"] [data-testid="stSelectbox"] > div,
589
+ [data-testid="stExpander"] [data-testid="stSelectbox"] > div {
590
+ width: 100% !important;
591
+ max-width: 100% !important;
592
+ }
593
+ [data-testid="stDialog"] [data-testid="stMetric"] label,
594
+ [data-testid="stDialog"] [data-testid="stMetric"] [data-testid="stMetricValue"],
595
+ [data-testid="stExpander"] [data-testid="stMetric"] label,
596
+ [data-testid="stExpander"] [data-testid="stMetric"] [data-testid="stMetricValue"] {
597
+ font-size: 0.95rem !important;
598
+ }
599
+ [data-testid="stDialog"] img,
600
+ [data-testid="stExpander"] img {
601
+ border-radius: 0 !important;
602
+ }
603
+
604
  /* === Responsive === */
605
  @media (max-width: 768px) {
606
  .s2f-header {
ui/measure_tool.py CHANGED
@@ -257,20 +257,6 @@ def render_region_canvas(display_heatmap, raw_heatmap=None, bf_img=None, origina
257
  heatmap_rgb = heatmap_to_rgb_with_contour(display_heatmap, colormap_name, cell_mask)
258
  pil_bg = Image.fromarray(heatmap_rgb).resize((CANVAS_SIZE, CANVAS_SIZE), Image.Resampling.LANCZOS)
259
 
260
- st.markdown("""
261
- <style>
262
- [data-testid="stDialog"] [data-testid="stSelectbox"], [data-testid="stExpander"] [data-testid="stSelectbox"],
263
- [data-testid="stDialog"] [data-testid="stSelectbox"] > div, [data-testid="stExpander"] [data-testid="stSelectbox"] > div {
264
- width: 100% !important; max-width: 100% !important;
265
- }
266
- [data-testid="stDialog"] [data-testid="stMetric"] label, [data-testid="stDialog"] [data-testid="stMetric"] [data-testid="stMetricValue"],
267
- [data-testid="stExpander"] [data-testid="stMetric"] label, [data-testid="stExpander"] [data-testid="stMetric"] [data-testid="stMetricValue"] {
268
- font-size: 0.95rem !important;
269
- }
270
- [data-testid="stDialog"] img, [data-testid="stExpander"] img { border-radius: 0 !important; }
271
- </style>
272
- """, unsafe_allow_html=True)
273
-
274
  if bf_img is not None:
275
  bf_resized = cv2.resize(bf_img, (CANVAS_SIZE, CANVAS_SIZE))
276
  bf_rgb = cv2.cvtColor(bf_resized, cv2.COLOR_GRAY2RGB) if bf_img.ndim == 2 else cv2.cvtColor(bf_resized, cv2.COLOR_BGR2RGB)
@@ -288,19 +274,16 @@ def render_region_canvas(display_heatmap, raw_heatmap=None, bf_img=None, origina
288
  vals = cell_vals if cell_vals else original_vals
289
  if vals:
290
  label = "Cell area" if cell_vals else "Full map"
291
- st.markdown(f'<p style="font-weight: 400; color: #334155; font-size: 0.95rem; margin: 0 20px 4px 4px;">{label}</p>', unsafe_allow_html=True)
292
- st.markdown(f"""
293
- <div style="width: 100%; box-sizing: border-box; border: 1px solid #e2e8f0; border-radius: 10px;
294
- padding: 10px 12px; margin: 0 10px 20px 10px; background: linear-gradient(145deg, #f8fafc 0%, #f1f5f9 100%);
295
- box-shadow: 0 1px 3px rgba(0,0,0,0.06);">
296
- <div style="display: flex; flex-wrap: wrap; gap: 5px; font-size: 0.9rem;">
297
- <span><strong>Sum:</strong> {vals['pixel_sum']:.1f}</span>
298
- <span><strong>Force:</strong> {vals['force']:.1f}</span>
299
- <span><strong>Max:</strong> {vals['max']:.3f}</span>
300
- <span><strong>Mean:</strong> {vals['mean']:.3f}</span>
301
- </div>
302
- </div>
303
- """, unsafe_allow_html=True)
304
  st.caption("Bright-field")
305
  bf_display = bf_rgb.copy()
306
  if cell_mask is not None and np.any(cell_mask > 0):
 
257
  heatmap_rgb = heatmap_to_rgb_with_contour(display_heatmap, colormap_name, cell_mask)
258
  pil_bg = Image.fromarray(heatmap_rgb).resize((CANVAS_SIZE, CANVAS_SIZE), Image.Resampling.LANCZOS)
259
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  if bf_img is not None:
261
  bf_resized = cv2.resize(bf_img, (CANVAS_SIZE, CANVAS_SIZE))
262
  bf_rgb = cv2.cvtColor(bf_resized, cv2.COLOR_GRAY2RGB) if bf_img.ndim == 2 else cv2.cvtColor(bf_resized, cv2.COLOR_BGR2RGB)
 
274
  vals = cell_vals if cell_vals else original_vals
275
  if vals:
276
  label = "Cell area" if cell_vals else "Full map"
277
+ st.markdown(
278
+ f'<p class="s2f-measure-vals-heading">{html.escape(label)}</p>'
279
+ f'<div class="s2f-measure-vals-panel"><div class="s2f-measure-vals-grid">'
280
+ f"<span><strong>Sum:</strong> {vals['pixel_sum']:.1f}</span>"
281
+ f"<span><strong>Force:</strong> {vals['force']:.1f}</span>"
282
+ f"<span><strong>Max:</strong> {vals['max']:.3f}</span>"
283
+ f"<span><strong>Mean:</strong> {vals['mean']:.3f}</span>"
284
+ f"</div></div>",
285
+ unsafe_allow_html=True,
286
+ )
 
 
 
287
  st.caption("Bright-field")
288
  bf_display = bf_rgb.copy()
289
  if cell_mask is not None and np.any(cell_mask > 0):
ui/result_display.py CHANGED
@@ -10,7 +10,7 @@ import streamlit as st
10
  import plotly.graph_objects as go
11
  from plotly.subplots import make_subplots
12
 
13
- from utils.display import apply_display_scale, cv_colormap_to_plotly_colorscale
14
  from utils.report import heatmap_to_rgb_with_contour, heatmap_to_png_bytes, create_pdf_report
15
  from utils.segmentation import estimate_cell_mask
16
  from ui.heatmaps import render_horizontal_colorbar, add_cell_contour_to_fig
@@ -29,8 +29,15 @@ _HISTOGRAM_HEIGHT = 180
29
  _BATCH_PREVIEW_LIMIT = 3
30
 
31
 
 
 
 
 
 
 
 
32
  def render_batch_results(batch_results, colormap_name="Jet", display_mode="Default",
33
- min_percentile=0, max_percentile=100, clip_min=0, clip_max=1,
34
  auto_cell_boundary=False, clamp_only=False):
35
  """
36
  Render batch prediction results: summary table, bright-field row, heatmap row, and bulk download.
@@ -48,7 +55,6 @@ def render_batch_results(batch_results, colormap_name="Jet", display_mode="Defau
48
  r["_cell_mask"] = r.get("cell_mask") if auto_cell_boundary else None
49
  r["_display_heatmap"] = apply_display_scale(
50
  r["heatmap"], display_mode,
51
- min_percentile=min_percentile, max_percentile=max_percentile,
52
  clip_min=clip_min, clip_max=clip_max, clamp_only=clamp_only,
53
  )
54
  # Build table rows - consistent column names for both modes
@@ -71,7 +77,7 @@ def render_batch_results(batch_results, colormap_name="Jet", display_mode="Defau
71
  f"{np.max(heatmap):.4f}", f"{np.mean(heatmap):.4f}"]
72
  rows.append(row)
73
  csv_rows.append([os.path.splitext(key)[0]] + row[1:])
74
- st.markdown('<div class="result-label"><span class="result-badge input">INPUT</span> Bright-field images</div>', unsafe_allow_html=True)
75
  n_cols = min(5, len(batch_results))
76
  preview_count = min(_BATCH_PREVIEW_LIMIT, len(batch_results))
77
  preview_results = batch_results[:preview_count]
@@ -85,8 +91,8 @@ def render_batch_results(batch_results, colormap_name="Jet", display_mode="Defau
85
  img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
86
  with bf_cols[i % min(n_cols, preview_count)]:
87
  st.image(img_rgb, caption=r["key_img"], use_container_width=True)
88
- is_rescale_b = display_mode == "Range" and clip_max > clip_min and not (clip_min == 0 and clip_max == 1)
89
- st.markdown('<div class="result-label"><span class="result-badge output">OUTPUT</span> Predicted force maps</div>', unsafe_allow_html=True)
90
  hm_cols = st.columns(min(n_cols, preview_count))
91
  for i, r in enumerate(preview_results):
92
  hm_rgb = heatmap_to_rgb_with_contour(
@@ -97,7 +103,7 @@ def render_batch_results(batch_results, colormap_name="Jet", display_mode="Defau
97
  st.image(hm_rgb, caption=r["key_img"], use_container_width=True)
98
  if remaining_results:
99
  with st.expander(f"Show remaining batch previews ({len(remaining_results)})", expanded=False):
100
- st.markdown('<div class="result-label"><span class="result-badge input">INPUT</span> Remaining bright-field images</div>', unsafe_allow_html=True)
101
  rem_bf_cols = st.columns(min(5, len(remaining_results)))
102
  for i, r in enumerate(remaining_results):
103
  img = r["img"]
@@ -107,7 +113,7 @@ def render_batch_results(batch_results, colormap_name="Jet", display_mode="Defau
107
  img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
108
  with rem_bf_cols[i % min(5, len(remaining_results))]:
109
  st.image(img_rgb, caption=r["key_img"], use_container_width=True)
110
- st.markdown('<div class="result-label"><span class="result-badge output">OUTPUT</span> Remaining predicted force maps</div>', unsafe_allow_html=True)
111
  rem_hm_cols = st.columns(min(5, len(remaining_results)))
112
  for i, r in enumerate(remaining_results):
113
  hm_rgb = heatmap_to_rgb_with_contour(
@@ -211,13 +217,13 @@ def render_result_display(img, raw_heatmap, display_heatmap, pixel_sum, force, k
211
 
212
  buf_hm = heatmap_to_png_bytes(display_heatmap, colormap_name, cell_mask=cell_mask)
213
 
214
- is_rescale = display_mode == "Range" and clip_max > clip_min and not (clip_min == 0.0 and clip_max == 1.0)
215
 
216
  tit1, tit2 = st.columns(2)
217
  with tit1:
218
- st.markdown('<div class="result-label"><span class="result-badge input">INPUT</span> Bright-field image</div>', unsafe_allow_html=True)
219
  with tit2:
220
- st.markdown('<div class="result-label"><span class="result-badge output">OUTPUT</span> Predicted force map</div>', unsafe_allow_html=True)
221
  fig_pl = make_subplots(rows=1, cols=2)
222
  fig_pl.add_trace(go.Heatmap(z=img, colorscale="gray", showscale=False), row=1, col=1)
223
  plotly_colorscale = cv_colormap_to_plotly_colorscale(colormap_name)
 
10
  import plotly.graph_objects as go
11
  from plotly.subplots import make_subplots
12
 
13
+ from utils.display import apply_display_scale, cv_colormap_to_plotly_colorscale, is_display_range_remapped
14
  from utils.report import heatmap_to_rgb_with_contour, heatmap_to_png_bytes, create_pdf_report
15
  from utils.segmentation import estimate_cell_mask
16
  from ui.heatmaps import render_horizontal_colorbar, add_cell_contour_to_fig
 
29
  _BATCH_PREVIEW_LIMIT = 3
30
 
31
 
32
+ def _result_banner(badge: str, badge_class: str, title: str) -> str:
33
+ """HTML row for INPUT/OUTPUT section titles (batch + single views). badge_class: input | output."""
34
+ return (
35
+ f'<div class="result-label"><span class="result-badge {badge_class}">{badge}</span> {title}</div>'
36
+ )
37
+
38
+
39
  def render_batch_results(batch_results, colormap_name="Jet", display_mode="Default",
40
+ clip_min=0, clip_max=1,
41
  auto_cell_boundary=False, clamp_only=False):
42
  """
43
  Render batch prediction results: summary table, bright-field row, heatmap row, and bulk download.
 
55
  r["_cell_mask"] = r.get("cell_mask") if auto_cell_boundary else None
56
  r["_display_heatmap"] = apply_display_scale(
57
  r["heatmap"], display_mode,
 
58
  clip_min=clip_min, clip_max=clip_max, clamp_only=clamp_only,
59
  )
60
  # Build table rows - consistent column names for both modes
 
77
  f"{np.max(heatmap):.4f}", f"{np.mean(heatmap):.4f}"]
78
  rows.append(row)
79
  csv_rows.append([os.path.splitext(key)[0]] + row[1:])
80
+ st.markdown(_result_banner("INPUT", "input", "Bright-field images"), unsafe_allow_html=True)
81
  n_cols = min(5, len(batch_results))
82
  preview_count = min(_BATCH_PREVIEW_LIMIT, len(batch_results))
83
  preview_results = batch_results[:preview_count]
 
91
  img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
92
  with bf_cols[i % min(n_cols, preview_count)]:
93
  st.image(img_rgb, caption=r["key_img"], use_container_width=True)
94
+ is_rescale_b = is_display_range_remapped(display_mode, clip_min, clip_max)
95
+ st.markdown(_result_banner("OUTPUT", "output", "Predicted force maps"), unsafe_allow_html=True)
96
  hm_cols = st.columns(min(n_cols, preview_count))
97
  for i, r in enumerate(preview_results):
98
  hm_rgb = heatmap_to_rgb_with_contour(
 
103
  st.image(hm_rgb, caption=r["key_img"], use_container_width=True)
104
  if remaining_results:
105
  with st.expander(f"Show remaining batch previews ({len(remaining_results)})", expanded=False):
106
+ st.markdown(_result_banner("INPUT", "input", "Remaining bright-field images"), unsafe_allow_html=True)
107
  rem_bf_cols = st.columns(min(5, len(remaining_results)))
108
  for i, r in enumerate(remaining_results):
109
  img = r["img"]
 
113
  img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
114
  with rem_bf_cols[i % min(5, len(remaining_results))]:
115
  st.image(img_rgb, caption=r["key_img"], use_container_width=True)
116
+ st.markdown(_result_banner("OUTPUT", "output", "Remaining predicted force maps"), unsafe_allow_html=True)
117
  rem_hm_cols = st.columns(min(5, len(remaining_results)))
118
  for i, r in enumerate(remaining_results):
119
  hm_rgb = heatmap_to_rgb_with_contour(
 
217
 
218
  buf_hm = heatmap_to_png_bytes(display_heatmap, colormap_name, cell_mask=cell_mask)
219
 
220
+ is_rescale = is_display_range_remapped(display_mode, clip_min, clip_max)
221
 
222
  tit1, tit2 = st.columns(2)
223
  with tit1:
224
+ st.markdown(_result_banner("INPUT", "input", "Bright-field image"), unsafe_allow_html=True)
225
  with tit2:
226
+ st.markdown(_result_banner("OUTPUT", "output", "Predicted force map"), unsafe_allow_html=True)
227
  fig_pl = make_subplots(rows=1, cols=2)
228
  fig_pl.add_trace(go.Heatmap(z=img, colorscale="gray", showscale=False), row=1, col=1)
229
  plotly_colorscale = cv_colormap_to_plotly_colorscale(colormap_name)
utils/display.py CHANGED
@@ -4,14 +4,6 @@ import cv2
4
 
5
  from config.constants import COLORMAPS, COLORMAP_N_SAMPLES
6
 
7
- # Legacy aliases (single mapping at entry — app uses Default / Range / Percentile / Rescale only)
8
- _DISPLAY_MODE_ALIASES = {
9
- "Auto": "Default",
10
- "Full": "Default",
11
- "Filter": "Range",
12
- "Threshold": "Range",
13
- }
14
-
15
 
16
  def cv_colormap_to_plotly_colorscale(colormap_name, n_samples=None):
17
  """Build a Plotly colorscale from OpenCV colormap so UI matches download/PDF exactly."""
@@ -27,54 +19,43 @@ def cv_colormap_to_plotly_colorscale(colormap_name, n_samples=None):
27
  return scale
28
 
29
 
30
- def apply_display_scale(heatmap, mode, min_percentile=0, max_percentile=100,
31
- clip_min=0, clip_max=1, clamp_only=False):
 
 
 
 
 
 
 
 
 
 
32
  """
33
  Apply display scaling. Display only—does not change underlying values.
34
 
 
 
 
35
  Parameters
36
  ----------
37
  clamp_only : bool
38
- Only used for ``mode == "Range"`` or ``mode == "Rescale"`` when ``clip_max > clip_min``:
39
 
40
- - **False** (default for Range in the app): pixels outside ``[clip_min, clip_max]`` are set
41
- to 0; values inside are linearly mapped to ``[0, 1]`` so the colormap uses the full
42
- dynamic range (blue→red) within that interval.
43
- - **True**: values are clamped to ``[clip_min, clip_max]`` but **not** rescaled to
44
- ``[0, 1]`` (rarely used for the main heatmap view; can compress colormap contrast).
45
  """
46
- mode = _DISPLAY_MODE_ALIASES.get(mode, mode)
47
-
48
- if mode == "Default":
49
- return np.clip(heatmap, 0, 1).astype(np.float32)
50
- if mode == "Percentile":
51
- pmin = float(np.percentile(heatmap, min_percentile))
52
- pmax = float(np.percentile(heatmap, max_percentile))
53
- if pmax > pmin:
54
- out = (heatmap.astype(np.float32) - pmin) / (pmax - pmin)
55
- return np.clip(out, 0, 1).astype(np.float32)
56
- return np.clip(heatmap, 0, 1).astype(np.float32)
57
- if mode == "Range":
58
- vmin, vmax = float(clip_min), float(clip_max)
59
- if vmax > vmin:
60
- h = heatmap.astype(np.float32)
61
- if clamp_only:
62
- return np.clip(h, vmin, vmax).astype(np.float32)
63
- mask = (h >= vmin) & (h <= vmax)
64
- out = np.zeros_like(h)
65
- out[mask] = (h[mask] - vmin) / (vmax - vmin)
66
- return np.clip(out, 0, 1).astype(np.float32)
67
- return np.clip(heatmap, 0, 1).astype(np.float32)
68
- if mode == "Rescale":
69
- vmin, vmax = float(clip_min), float(clip_max)
70
- if vmax > vmin:
71
- h = heatmap.astype(np.float32)
72
- if clamp_only:
73
- clamped = np.clip(h, vmin, vmax)
74
- return ((clamped - vmin) / (vmax - vmin)).astype(np.float32)
75
- mask = (h >= vmin) & (h <= vmax)
76
- out = np.zeros_like(h)
77
- out[mask] = (h[mask] - vmin) / (vmax - vmin)
78
- return np.clip(out, 0, 1).astype(np.float32)
79
  return np.clip(heatmap, 0, 1).astype(np.float32)
 
 
 
 
 
 
 
 
 
 
80
  return np.clip(heatmap, 0, 1).astype(np.float32)
 
4
 
5
  from config.constants import COLORMAPS, COLORMAP_N_SAMPLES
6
 
 
 
 
 
 
 
 
 
7
 
8
  def cv_colormap_to_plotly_colorscale(colormap_name, n_samples=None):
9
  """Build a Plotly colorscale from OpenCV colormap so UI matches download/PDF exactly."""
 
19
  return scale
20
 
21
 
22
+ def is_display_range_remapped(display_mode, clip_min, clip_max):
23
+ """
24
+ True when the UI uses Range mode with a clip window other than the full [0, 1]
25
+ (colorbar tick labels map normalized 0–1 to that interval).
26
+ """
27
+ if display_mode != "Range":
28
+ return False
29
+ lo, hi = float(clip_min), float(clip_max)
30
+ return hi > lo and not (lo == 0.0 and hi == 1.0)
31
+
32
+
33
+ def apply_display_scale(heatmap, mode, clip_min=0, clip_max=1, clamp_only=False):
34
  """
35
  Apply display scaling. Display only—does not change underlying values.
36
 
37
+ Supports modes used by the app: ``Default`` (clip to [0, 1]) and ``Range``
38
+ (window to [clip_min, clip_max]). Unknown ``mode`` is treated like ``Default``.
39
+
40
  Parameters
41
  ----------
42
  clamp_only : bool
43
+ For ``mode == "Range"`` when ``clip_max > clip_min``:
44
 
45
+ - **False** (default in the app for Range): pixels outside ``[clip_min, clip_max]`` are set
46
+ to 0; values inside are linearly mapped to ``[0, 1]`` for the colormap.
47
+ - **True**: values are clamped to ``[clip_min, clip_max]`` without rescaling to ``[0, 1]``.
 
 
48
  """
49
+ if mode != "Range":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  return np.clip(heatmap, 0, 1).astype(np.float32)
51
+ # Range
52
+ vmin, vmax = float(clip_min), float(clip_max)
53
+ if vmax > vmin:
54
+ h = heatmap.astype(np.float32)
55
+ if clamp_only:
56
+ return np.clip(h, vmin, vmax).astype(np.float32)
57
+ mask = (h >= vmin) & (h <= vmax)
58
+ out = np.zeros_like(h)
59
+ out[mask] = (h[mask] - vmin) / (vmax - vmin)
60
+ return np.clip(out, 0, 1).astype(np.float32)
61
  return np.clip(heatmap, 0, 1).astype(np.float32)