Spaces:
Running
Running
optimised, removed dead codes
Browse files- app.py +7 -22
- static/s2f_styles.css +65 -103
- ui/measure_tool.py +10 -27
- ui/result_display.py +17 -11
- 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 |
-
|
| 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
|
| 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="
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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
|
| 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:
|
| 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:
|
| 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 |
-
|
| 544 |
-
|
| 545 |
-
|
| 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:
|
| 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:
|
| 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(
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 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 |
-
|
| 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(
|
| 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
|
| 89 |
-
st.markdown(
|
| 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(
|
| 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(
|
| 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
|
| 215 |
|
| 216 |
tit1, tit2 = st.columns(2)
|
| 217 |
with tit1:
|
| 218 |
-
st.markdown(
|
| 219 |
with tit2:
|
| 220 |
-
st.markdown(
|
| 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
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
"""
|
| 33 |
Apply display scaling. Display only—does not change underlying values.
|
| 34 |
|
|
|
|
|
|
|
|
|
|
| 35 |
Parameters
|
| 36 |
----------
|
| 37 |
clamp_only : bool
|
| 38 |
-
|
| 39 |
|
| 40 |
-
- **False** (default
|
| 41 |
-
to 0; values inside are linearly mapped to ``[0, 1]``
|
| 42 |
-
|
| 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 =
|
| 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)
|