Spaces:
Running on Zero
Running on Zero
Virtual OpenCell: switch between 2D and 3D virtual staining; 3D shown one confocal slice at a time
Browse files- app.py +58 -31
- cell_fm/apps/virtual_opencell/viewer.py +62 -11
app.py
CHANGED
|
@@ -342,29 +342,38 @@ def build_substitution_scan_section():
|
|
| 342 |
|
| 343 |
def build_virtual_opencell_section():
|
| 344 |
"""Pick an OpenCell protein and browse CELL-FM's virtual staining of it, laid out like an
|
| 345 |
-
OpenCell target page: the real cell line's annotations on the left, the images on the right.
|
|
|
|
| 346 |
|
| 347 |
def load(gene):
|
| 348 |
row = PROTEINS.loc[gene]
|
| 349 |
stack, paths = viewer.load_samples(gene, int(row["n_samples"]))
|
| 350 |
return row, stack, paths
|
| 351 |
|
| 352 |
-
def show(gene, index, channel, n_min, n_max, n_gamma, t_min, t_max, t_gamma):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
_, stack, _ = load(gene)
|
| 354 |
-
return viewer.render(stack[index], channel,
|
| 355 |
|
| 356 |
-
def sample_file(gene, index):
|
| 357 |
"""The raw sample under a readable name, copied where Gradio is allowed to serve it."""
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
|
|
|
|
|
|
|
|
|
| 361 |
return path
|
| 362 |
|
| 363 |
-
def protein_view(gene, *settings):
|
| 364 |
"""Panel, thumbnails, first sample and its file for one protein."""
|
| 365 |
-
row
|
| 366 |
-
|
| 367 |
-
|
|
|
|
| 368 |
|
| 369 |
def umap_plot(gene):
|
| 370 |
try:
|
|
@@ -373,21 +382,32 @@ def build_virtual_opencell_section():
|
|
| 373 |
print(f"[virtual opencell] map unavailable: {exc}")
|
| 374 |
return None
|
| 375 |
|
| 376 |
-
def select_protein(gene,
|
| 377 |
try:
|
| 378 |
-
info, thumbs, image, path = protein_view(gene,
|
| 379 |
except Exception as exc:
|
| 380 |
-
raise gr.Error(f"Could not load the images for {gene}: {viewer.load_error(exc)}") from exc
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
defaults = ("Both", 0, 100, 1.0, 0, 100, 1.0)
|
|
|
|
| 389 |
try:
|
| 390 |
-
info0, thumbs0, image0, file0 = protein_view(viewer.DEFAULT_GENE, *defaults)
|
| 391 |
except Exception as exc: # e.g. no access to the dataset; the error is shown in the panel
|
| 392 |
print(f"[startup] virtual OpenCell preload failed: {exc}")
|
| 393 |
info0, thumbs0, image0, file0 = viewer.error_html(viewer.DEFAULT_GENE, exc), None, None, None
|
|
@@ -409,11 +429,13 @@ def build_virtual_opencell_section():
|
|
| 409 |
with gr.Row():
|
| 410 |
with gr.Column(scale=4):
|
| 411 |
info = gr.HTML(info0)
|
| 412 |
-
gr.HTML(viewer.UMAP_HTML, padding=False)
|
| 413 |
umap = gr.Plot(value=umap0, show_label=False, elem_classes="vo-umap")
|
| 414 |
|
| 415 |
with gr.Column(scale=6):
|
| 416 |
with gr.Row():
|
|
|
|
|
|
|
| 417 |
channel = gr.Radio(viewer.CHANNELS, value=defaults[0], label="Channel",
|
| 418 |
elem_classes="vo-buttons")
|
| 419 |
download = gr.DownloadButton("Download TIFF", value=file0, size="sm", variant="secondary",
|
|
@@ -422,6 +444,9 @@ def build_virtual_opencell_section():
|
|
| 422 |
value=image0, show_label=False, interactive=False, height=600,
|
| 423 |
show_download_button=True, elem_classes="vo-viewer",
|
| 424 |
)
|
|
|
|
|
|
|
|
|
|
| 425 |
gr.HTML('<div class="vo-settings-label">Samples</div>', padding=False)
|
| 426 |
samples = gr.Gallery(
|
| 427 |
value=thumbs0, selected_index=0 if thumbs0 else None, show_label=False,
|
|
@@ -442,24 +467,26 @@ def build_virtual_opencell_section():
|
|
| 442 |
|
| 443 |
# the full map: every image and every protein named. One fixed figure (~2 MB) sent with the
|
| 444 |
# page; the panel map above follows the selection, so this one never has to be re-sent
|
| 445 |
-
gr.HTML('<div class="vo-target" style="text-align: center; padding-right: 0"><div class="vo-section-header">Full virtual staining map</div>'
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
gr.Plot(value=full_map0, show_label=False, elem_classes="vo-umap-full")
|
| 449 |
|
| 450 |
gr.HTML(viewer.FOOTER_HTML)
|
| 451 |
|
| 452 |
index = gr.State(0)
|
| 453 |
settings = [channel, n_min, n_max, n_gamma, t_min, t_max, t_gamma]
|
| 454 |
|
| 455 |
-
protein.change(select_protein, inputs=[protein] + settings,
|
| 456 |
outputs=[info, samples, image, index, download, umap])
|
|
|
|
|
|
|
| 457 |
|
| 458 |
# Redraws take milliseconds: no progress overlay, and a dragged slider only renders its latest value
|
| 459 |
-
samples.select(select_sample, inputs=[protein] + settings, outputs=[image, index, download],
|
| 460 |
show_progress="hidden")
|
| 461 |
-
for control in settings:
|
| 462 |
-
control.change(show, inputs=[protein, index] + settings, outputs=image,
|
| 463 |
show_progress="hidden", trigger_mode="always_last")
|
| 464 |
|
| 465 |
|
|
|
|
| 342 |
|
| 343 |
def build_virtual_opencell_section():
|
| 344 |
"""Pick an OpenCell protein and browse CELL-FM's virtual staining of it, laid out like an
|
| 345 |
+
OpenCell target page: the real cell line's annotations on the left, the images on the right.
|
| 346 |
+
"2D" is the 2D projection set, with its map; "3D" the CELL-FM 3D stacks, one z slice at a time."""
|
| 347 |
|
| 348 |
def load(gene):
|
| 349 |
row = PROTEINS.loc[gene]
|
| 350 |
stack, paths = viewer.load_samples(gene, int(row["n_samples"]))
|
| 351 |
return row, stack, paths
|
| 352 |
|
| 353 |
+
def show(gene, index, mode, z, channel, n_min, n_max, n_gamma, t_min, t_max, t_gamma):
|
| 354 |
+
nucleus, target = (n_min, n_max, n_gamma), (t_min, t_max, t_gamma)
|
| 355 |
+
if mode == "3D":
|
| 356 |
+
stack, limits = viewer.load_sample_3d(gene, index)
|
| 357 |
+
return viewer.render(stack[int(z) - 1], channel, nucleus, target, limits)
|
| 358 |
_, stack, _ = load(gene)
|
| 359 |
+
return viewer.render(stack[index], channel, nucleus, target)
|
| 360 |
|
| 361 |
+
def sample_file(gene, index, mode):
|
| 362 |
"""The raw sample under a readable name, copied where Gradio is allowed to serve it."""
|
| 363 |
+
if mode == "3D":
|
| 364 |
+
source, name = viewer.sample_file_3d(gene, index), f"virtual_opencell_3d_{gene}_{index + 1:04d}.tif"
|
| 365 |
+
else:
|
| 366 |
+
source, name = load(gene)[2][index], f"virtual_opencell_{gene}_{index + 1:04d}.tif"
|
| 367 |
+
path = os.path.join(tempfile.gettempdir(), name)
|
| 368 |
+
shutil.copyfile(source, path)
|
| 369 |
return path
|
| 370 |
|
| 371 |
+
def protein_view(gene, mode, z, *settings):
|
| 372 |
"""Panel, thumbnails, first sample and its file for one protein."""
|
| 373 |
+
row = PROTEINS.loc[gene]
|
| 374 |
+
thumbs = viewer.thumbnails_3d(gene) if mode == "3D" else viewer.thumbnails(load(gene)[1])
|
| 375 |
+
image = show(gene, 0, mode, z, *settings)
|
| 376 |
+
return viewer.target_html(gene, row, mode == "3D"), thumbs, image, sample_file(gene, 0, mode)
|
| 377 |
|
| 378 |
def umap_plot(gene):
|
| 379 |
try:
|
|
|
|
| 382 |
print(f"[virtual opencell] map unavailable: {exc}")
|
| 383 |
return None
|
| 384 |
|
| 385 |
+
def select_protein(gene, mode, z, *settings):
|
| 386 |
try:
|
| 387 |
+
info, thumbs, image, path = protein_view(gene, mode, z, *settings)
|
| 388 |
except Exception as exc:
|
| 389 |
+
raise gr.Error(f"Could not load the {mode} images for {gene}: {viewer.load_error(exc)}") from exc
|
| 390 |
+
# there is no 3D map yet: leave the hidden one alone rather than redraw it
|
| 391 |
+
umap_update = gr.update() if mode == "3D" else umap_plot(gene)
|
| 392 |
+
return info, gr.Gallery(value=thumbs, selected_index=0), image, 0, path, umap_update
|
| 393 |
+
|
| 394 |
+
def select_mode(gene, mode, z, *settings):
|
| 395 |
+
"""2D <-> 3D: the protein's other image set, the slice slider for 3D, the maps for 2D only."""
|
| 396 |
+
info, samples, image, index, path, _ = select_protein(gene, mode, z, *settings)
|
| 397 |
+
two_d = mode == "2D"
|
| 398 |
+
umap_update = gr.update(value=umap_plot(gene), visible=True) if two_d else gr.update(visible=False)
|
| 399 |
+
return (info, samples, image, index, path, umap_update, gr.update(visible=not two_d),
|
| 400 |
+
*[gr.update(visible=two_d)] * 3)
|
| 401 |
+
|
| 402 |
+
def select_sample(gene, mode, z, channel, n_min, n_max, n_gamma, t_min, t_max, t_gamma, evt: gr.SelectData):
|
| 403 |
+
image = show(gene, evt.index, mode, z, channel, n_min, n_max, n_gamma, t_min, t_max, t_gamma)
|
| 404 |
+
return image, evt.index, sample_file(gene, evt.index, mode)
|
| 405 |
+
|
| 406 |
+
# Shown on first load: the default protein in 2D, unless its images cannot be fetched
|
| 407 |
defaults = ("Both", 0, 100, 1.0, 0, 100, 1.0)
|
| 408 |
+
slice0 = viewer.DEFAULT_SLICE_3D + 1
|
| 409 |
try:
|
| 410 |
+
info0, thumbs0, image0, file0 = protein_view(viewer.DEFAULT_GENE, "2D", slice0, *defaults)
|
| 411 |
except Exception as exc: # e.g. no access to the dataset; the error is shown in the panel
|
| 412 |
print(f"[startup] virtual OpenCell preload failed: {exc}")
|
| 413 |
info0, thumbs0, image0, file0 = viewer.error_html(viewer.DEFAULT_GENE, exc), None, None, None
|
|
|
|
| 429 |
with gr.Row():
|
| 430 |
with gr.Column(scale=4):
|
| 431 |
info = gr.HTML(info0)
|
| 432 |
+
umap_header = gr.HTML(viewer.UMAP_HTML, padding=False)
|
| 433 |
umap = gr.Plot(value=umap0, show_label=False, elem_classes="vo-umap")
|
| 434 |
|
| 435 |
with gr.Column(scale=6):
|
| 436 |
with gr.Row():
|
| 437 |
+
mode = gr.Radio(["2D", "3D"], value="2D", label="Virtual staining", scale=0,
|
| 438 |
+
min_width=170, elem_classes="vo-buttons")
|
| 439 |
channel = gr.Radio(viewer.CHANNELS, value=defaults[0], label="Channel",
|
| 440 |
elem_classes="vo-buttons")
|
| 441 |
download = gr.DownloadButton("Download TIFF", value=file0, size="sm", variant="secondary",
|
|
|
|
| 444 |
value=image0, show_label=False, interactive=False, height=600,
|
| 445 |
show_download_button=True, elem_classes="vo-viewer",
|
| 446 |
)
|
| 447 |
+
# OpenCell's "2D slice" view: one confocal slice of the stack, picked under the image
|
| 448 |
+
z = gr.Slider(1, viewer.SLICES_3D, value=slice0, step=1, visible=False,
|
| 449 |
+
label=f"Confocal slice (of {viewer.SLICES_3D}, centred on the nucleus)")
|
| 450 |
gr.HTML('<div class="vo-settings-label">Samples</div>', padding=False)
|
| 451 |
samples = gr.Gallery(
|
| 452 |
value=thumbs0, selected_index=0 if thumbs0 else None, show_label=False,
|
|
|
|
| 467 |
|
| 468 |
# the full map: every image and every protein named. One fixed figure (~2 MB) sent with the
|
| 469 |
# page; the panel map above follows the selection, so this one never has to be re-sent
|
| 470 |
+
full_header = gr.HTML('<div class="vo-target" style="text-align: center; padding-right: 0"><div class="vo-section-header">Full virtual staining map</div>'
|
| 471 |
+
'<div class="vo-section-caption">All 93,510 generated images, each protein named at the '
|
| 472 |
+
'centre of its images; zoom in to separate the names</div></div>', padding=False)
|
| 473 |
+
full_map = gr.Plot(value=full_map0, show_label=False, elem_classes="vo-umap-full")
|
| 474 |
|
| 475 |
gr.HTML(viewer.FOOTER_HTML)
|
| 476 |
|
| 477 |
index = gr.State(0)
|
| 478 |
settings = [channel, n_min, n_max, n_gamma, t_min, t_max, t_gamma]
|
| 479 |
|
| 480 |
+
protein.change(select_protein, inputs=[protein, mode, z] + settings,
|
| 481 |
outputs=[info, samples, image, index, download, umap])
|
| 482 |
+
mode.change(select_mode, inputs=[protein, mode, z] + settings,
|
| 483 |
+
outputs=[info, samples, image, index, download, umap, z, umap_header, full_header, full_map])
|
| 484 |
|
| 485 |
# Redraws take milliseconds: no progress overlay, and a dragged slider only renders its latest value
|
| 486 |
+
samples.select(select_sample, inputs=[protein, mode, z] + settings, outputs=[image, index, download],
|
| 487 |
show_progress="hidden")
|
| 488 |
+
for control in [z] + settings:
|
| 489 |
+
control.change(show, inputs=[protein, index, mode, z] + settings, outputs=image,
|
| 490 |
show_progress="hidden", trigger_mode="always_last")
|
| 491 |
|
| 492 |
|
cell_fm/apps/virtual_opencell/viewer.py
CHANGED
|
@@ -21,6 +21,15 @@ read them from disk instead.
|
|
| 21 |
|
| 22 |
The map of all samples is opencell/vs_umap_embedding.npz in the weights repo: one UMAP point
|
| 23 |
per sample (93,510, grouped by protein), embedded with opencell/vit.bin.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
"""
|
| 25 |
|
| 26 |
import html
|
|
@@ -32,10 +41,19 @@ import numpy as np
|
|
| 32 |
import pandas as pd
|
| 33 |
import plotly.graph_objects as go
|
| 34 |
import tifffile
|
|
|
|
| 35 |
|
| 36 |
DATASET_REPO = os.environ.get("VIRTUAL_OPENCELL_REPO", "BoHuangLab/CELL-FM")
|
| 37 |
LOCAL_DIR = os.environ.get("VIRTUAL_OPENCELL_LOCAL_DIR", "")
|
| 38 |
IMAGE_DIR = "OpenCell/virtual_staining"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
MODEL_REPO = os.environ.get("CELLFM_MODEL_REPO", "BoHuangLab/CELL-FM")
|
| 41 |
UMAP_FILE = "opencell/vs_umap_embedding.npz"
|
|
@@ -106,28 +124,61 @@ def load_samples(gene: str, n_samples: int):
|
|
| 106 |
return np.stack([tifffile.imread(p) for p in paths]), tuple(paths)
|
| 107 |
|
| 108 |
|
| 109 |
-
def
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
x = (channel.astype(np.float32) - lo) / max(hi - lo, 1.0)
|
| 113 |
low, high = low / 100.0, max(high, low + 1.0) / 100.0
|
| 114 |
return np.clip((x - low) / (high - low), 0.0, 1.0) ** gamma
|
| 115 |
|
| 116 |
|
| 117 |
def render(sample: np.ndarray, channel: str = "Both",
|
| 118 |
-
nucleus=(0, 100, 1.0), target=(0, 100, 1.0)) -> np.ndarray:
|
| 119 |
-
"""A (2, H, W) sample as an (H, W, 3) uint8 image; nucleus/target are (min %, max %, gamma)
|
|
|
|
| 120 |
rgb = np.zeros(sample.shape[1:] + (3,), dtype=np.float32)
|
| 121 |
if channel in ("Nucleus", "Both"):
|
| 122 |
-
rgb += _scale(sample[0], *nucleus)[..., None] * NUCLEUS_RGB
|
| 123 |
if channel in ("Target", "Both"):
|
| 124 |
-
rgb += _scale(sample[1], *target)[..., None] * TARGET_RGB
|
| 125 |
return (np.clip(rgb, 0.0, 1.0) * 255).astype(np.uint8)
|
| 126 |
|
| 127 |
|
| 128 |
-
def thumbnails(stack: np.ndarray) -> list:
|
| 129 |
"""Both channels at default settings, downsized for the sample strip."""
|
| 130 |
-
step = max(stack.shape[-1] //
|
| 131 |
return [render(sample)[::step, ::step] for sample in stack]
|
| 132 |
|
| 133 |
|
|
@@ -227,7 +278,7 @@ def _metadata_item(label: str, value: str) -> str:
|
|
| 227 |
f'<div class="vo-metadata-value">{value}</div></div>')
|
| 228 |
|
| 229 |
|
| 230 |
-
def target_html(gene: str, row: pd.Series) -> str:
|
| 231 |
"""The left-hand panel of an OpenCell target page: name, identifiers, localization."""
|
| 232 |
esc = html.escape
|
| 233 |
uniprot, ensg, cid = row["uniprot_id"], row["ensg_id"], row["opencell_cid"]
|
|
@@ -258,7 +309,7 @@ def target_html(gene: str, row: pd.Series) -> str:
|
|
| 258 |
<div class="vo-target">
|
| 259 |
<div class="vo-target-name">{esc(gene)}</div>
|
| 260 |
<div class="vo-protein-description">{esc(name)}</div>
|
| 261 |
-
<div class="vo-status">CELL-FM virtual staining · {int(row['n_samples'])} samples</div>
|
| 262 |
<div class="vo-metadata">{''.join(items)}</div>
|
| 263 |
<div class="vo-section-header">Protein localization</div>
|
| 264 |
<div class="vo-section-caption">OpenCell's annotation of the real cell line; bar height is the grade</div>
|
|
|
|
| 21 |
|
| 22 |
The map of all samples is opencell/vs_umap_embedding.npz in the weights repo: one UMAP point
|
| 23 |
per sample (93,510, grouped by protein), embedded with opencell/vit.bin.
|
| 24 |
+
|
| 25 |
+
The 3D virtual staining (CELL-FM 3D) is in the same repo, 64 samples for every protein:
|
| 26 |
+
|
| 27 |
+
OpenCell/virtual_staining_3d/<GENE>/<NNNN>.tif (48, 2, 192, 192) uint16, z first
|
| 28 |
+
OpenCell/virtual_staining_3d_thumbnails/<GENE>.jpg
|
| 29 |
+
|
| 30 |
+
A stack is about 7 MB, so a protein's 64 are fetched one at a time, when opened, and the sample
|
| 31 |
+
strip comes from the thumbnail sheet (notebooks/tools/build_virtual_opencell_3d_thumbnails.py).
|
| 32 |
+
Stacks are shown one z slice at a time, like OpenCell's "2D slice" view. There is no 3D map yet.
|
| 33 |
"""
|
| 34 |
|
| 35 |
import html
|
|
|
|
| 41 |
import pandas as pd
|
| 42 |
import plotly.graph_objects as go
|
| 43 |
import tifffile
|
| 44 |
+
from PIL import Image
|
| 45 |
|
| 46 |
DATASET_REPO = os.environ.get("VIRTUAL_OPENCELL_REPO", "BoHuangLab/CELL-FM")
|
| 47 |
LOCAL_DIR = os.environ.get("VIRTUAL_OPENCELL_LOCAL_DIR", "")
|
| 48 |
IMAGE_DIR = "OpenCell/virtual_staining"
|
| 49 |
+
IMAGE_DIR_3D = "OpenCell/virtual_staining_3d"
|
| 50 |
+
THUMBNAIL_DIR_3D = "OpenCell/virtual_staining_3d_thumbnails"
|
| 51 |
+
SAMPLES_3D = 64
|
| 52 |
+
SLICES_3D = 48
|
| 53 |
+
# Crops are centred on the nucleus' 3D centroid, so its centre slice is the default view
|
| 54 |
+
DEFAULT_SLICE_3D = SLICES_3D // 2
|
| 55 |
+
THUMBNAIL_SIZE_3D = 96
|
| 56 |
+
THUMBNAIL_COLUMNS_3D = 8
|
| 57 |
|
| 58 |
MODEL_REPO = os.environ.get("CELLFM_MODEL_REPO", "BoHuangLab/CELL-FM")
|
| 59 |
UMAP_FILE = "opencell/vs_umap_embedding.npz"
|
|
|
|
| 124 |
return np.stack([tifffile.imread(p) for p in paths]), tuple(paths)
|
| 125 |
|
| 126 |
|
| 127 |
+
def _download(name: str) -> str:
|
| 128 |
+
if LOCAL_DIR:
|
| 129 |
+
return os.path.join(LOCAL_DIR, name)
|
| 130 |
+
from huggingface_hub import hf_hub_download
|
| 131 |
+
|
| 132 |
+
return hf_hub_download(repo_id=DATASET_REPO, filename=name, repo_type="dataset")
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def sample_file_3d(gene: str, index: int) -> str:
|
| 136 |
+
"""Local path of one 3D sample (0-based index), downloading it first unless reading from disk."""
|
| 137 |
+
return _download(f"{IMAGE_DIR_3D}/{gene}/{index + 1:04d}.tif")
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
@lru_cache(maxsize=32)
|
| 141 |
+
def load_sample_3d(gene: str, index: int):
|
| 142 |
+
"""(stack, limits): stack is (Z, 2, H, W) uint16; limits are each channel's auto-contrast
|
| 143 |
+
window over the whole stack, so a slice keeps the same contrast as the others."""
|
| 144 |
+
stack = tifffile.imread(sample_file_3d(gene, index))
|
| 145 |
+
limits = tuple(tuple(np.percentile(stack[:, c], AUTO_PERCENTILES)) for c in range(stack.shape[1]))
|
| 146 |
+
return stack, limits
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
@lru_cache(maxsize=64)
|
| 150 |
+
def thumbnails_3d(gene: str) -> list:
|
| 151 |
+
"""The sample strip of a protein's 3D stacks: its centre slices, cut from the thumbnail sheet."""
|
| 152 |
+
sheet = np.asarray(Image.open(_download(f"{THUMBNAIL_DIR_3D}/{gene}.jpg")).convert("RGB"))
|
| 153 |
+
size = THUMBNAIL_SIZE_3D
|
| 154 |
+
return [sheet[r:r + size, c:c + size] for r in range(0, sheet.shape[0], size)
|
| 155 |
+
for c in range(0, sheet.shape[1], size)]
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def _scale(channel: np.ndarray, low: float, high: float, gamma: float, limits=None) -> np.ndarray:
|
| 159 |
+
"""One channel to [0, 1]: auto-contrast (over `limits` if given, else this image's own
|
| 160 |
+
percentiles), then OpenCell's intensity range (%) and gamma."""
|
| 161 |
+
lo, hi = limits if limits is not None else np.percentile(channel, AUTO_PERCENTILES)
|
| 162 |
x = (channel.astype(np.float32) - lo) / max(hi - lo, 1.0)
|
| 163 |
low, high = low / 100.0, max(high, low + 1.0) / 100.0
|
| 164 |
return np.clip((x - low) / (high - low), 0.0, 1.0) ** gamma
|
| 165 |
|
| 166 |
|
| 167 |
def render(sample: np.ndarray, channel: str = "Both",
|
| 168 |
+
nucleus=(0, 100, 1.0), target=(0, 100, 1.0), limits=(None, None)) -> np.ndarray:
|
| 169 |
+
"""A (2, H, W) sample as an (H, W, 3) uint8 image; nucleus/target are (min %, max %, gamma),
|
| 170 |
+
and limits each channel's auto-contrast window (None: from this image)."""
|
| 171 |
rgb = np.zeros(sample.shape[1:] + (3,), dtype=np.float32)
|
| 172 |
if channel in ("Nucleus", "Both"):
|
| 173 |
+
rgb += _scale(sample[0], *nucleus, limits=limits[0])[..., None] * NUCLEUS_RGB
|
| 174 |
if channel in ("Target", "Both"):
|
| 175 |
+
rgb += _scale(sample[1], *target, limits=limits[1])[..., None] * TARGET_RGB
|
| 176 |
return (np.clip(rgb, 0.0, 1.0) * 255).astype(np.uint8)
|
| 177 |
|
| 178 |
|
| 179 |
+
def thumbnails(stack: np.ndarray, size: int = THUMBNAIL_SIZE) -> list:
|
| 180 |
"""Both channels at default settings, downsized for the sample strip."""
|
| 181 |
+
step = max(stack.shape[-1] // size, 1)
|
| 182 |
return [render(sample)[::step, ::step] for sample in stack]
|
| 183 |
|
| 184 |
|
|
|
|
| 278 |
f'<div class="vo-metadata-value">{value}</div></div>')
|
| 279 |
|
| 280 |
|
| 281 |
+
def target_html(gene: str, row: pd.Series, three_d: bool = False) -> str:
|
| 282 |
"""The left-hand panel of an OpenCell target page: name, identifiers, localization."""
|
| 283 |
esc = html.escape
|
| 284 |
uniprot, ensg, cid = row["uniprot_id"], row["ensg_id"], row["opencell_cid"]
|
|
|
|
| 309 |
<div class="vo-target">
|
| 310 |
<div class="vo-target-name">{esc(gene)}</div>
|
| 311 |
<div class="vo-protein-description">{esc(name)}</div>
|
| 312 |
+
<div class="vo-status">CELL-FM {"3D " if three_d else ""}virtual staining · {SAMPLES_3D if three_d else int(row['n_samples'])} samples</div>
|
| 313 |
<div class="vo-metadata">{''.join(items)}</div>
|
| 314 |
<div class="vo-section-header">Protein localization</div>
|
| 315 |
<div class="vo-section-caption">OpenCell's annotation of the real cell line; bar height is the grade</div>
|