Install the optional shiny package, then run one of these commands in an interactive R session. The first starts empty; loading the demonstration is an explicit action. The second opens deterministic synthetic measurements and keeps the demonstration label visible.
Stop the app with R’s stop button or Escape/Ctrl+C in the console. The app displays recorded measurements and linked slide images. It does not edit annotations, run inference, train a model, or save changes to the supplied result object.
The viewer accepts any organ labels present in the results. The built-in viewer example includes liver, spleen, lung and kidney, and generates only synthetic images and measurements:
demo <- bs_viewer_example()
unique(demo$results$slides$organ)
#> [1] "Liver" "Spleen" "Lung" "Kidney"
demo$images[, c("image_id", "slide_id", "label")]
#> image_id slide_id label
#> 1 image-1 DEMO-001 Liver preview
#> 2 image-2 DEMO-002 Liver preview
#> 3 image-3 DEMO-003 Spleen preview
#> 4 image-4 DEMO-004 Spleen preview
#> 5 image-5 DEMO-005 Lung preview
#> 6 image-6 DEMO-006 Lung preview
#> 7 image-7 DEMO-007 Kidney preview
#> 8 image-8 DEMO-008 Kidney previewUse Previous/Next or the Slide selector to move through the filtered slides. Choose an image/layer when a slide has several regions or a raw/overlay pair. Drag to pan, scroll or use the zoom buttons, and choose Fit image to return to the whole image. Click a vector annotation to see its saved properties and measurements; the annotation selector provides a keyboard alternative. A baked PNG overlay is viewable but its marks are not individually selectable without corresponding vector annotations.
The images manifest has one row per image, with these
fields:
| Field | Meaning |
|---|---|
image_id |
Unique character image identity |
slide_id |
Matching slide_id or physical_slide_id in
the results |
path |
Existing PNG/JPEG preview or native whole-slide file |
label |
Optional display name for the region or layer |
annotations |
Optional saved QuPath GeoJSON or point CSV (x,
y, optional annotation_id, plus recorded
measurements) |
scale_x, scale_y |
Positive annotation-to-image coordinate scales, default 1 |
offset_x, offset_y |
Annotation-to-image offsets in displayed image pixels, default 0 |
A CSV manifest resolves file paths relative to its own directory. A data frame uses paths relative to the R working directory. Only these explicitly supplied files are served to the session; paths in uploaded result tables do not grant access to server files.
Coordinates are in the displayed image’s pixel frame, with x
rightward and y downward. QuPath exports use full-resolution image
pixels. For a preview at one-eighth resolution, set
scale_x = scale_y = 1/8. For crops, supply the
corresponding offsets. The transform is
display = annotation * scale + offset. No registration is
inferred. Points, lines and polygons (including polygon holes and
multipart geometries) are supported. Limit vector overlays to 10,000
objects and 20 MiB per image; use review regions or rendered overlays
for dense fields.
Native tiled viewing uses OpenSlide and the bundled OpenSeadragon browser viewer. PNG/JPEG previews do not need Python. For native images, create an optional isolated Python environment and install the reader:
python3 -m venv slide-reader
slide-reader/bin/python -m pip install openslide-python openslide-bin PillowOn Windows, use slide-reader/Scripts/python.exe. Then
point slide_python to that executable (or set
BLOODSPOTTR_PYTHON). No installation or network download is
performed by the app.
images <- data.frame(
image_id = "image-001", slide_id = "001",
path = "/data/slides/slide-001.svs",
annotations = "/data/annotations/slide-001.geojson",
label = "Whole slide"
)
bs_app(results, images = images, slide_python = "/path/to/slide-reader/bin/python")Support depends on the installed OpenSlide reader and the actual
file, not its extension alone. Native Aperio SVS and preview PNG paths
are exercised during local qualification. A QuPath
.qpproj/.qpdata or TissueGnostics
.aqproj is a project container, not an OpenSlide image:
export the image and QuPath GeoJSON first. This viewer does not write
back to the native project.
title, group_by and
group_label adapt the interface to other tissue panels or a
different metadata grouping. Labels and organ choices are not fixed:
results$slides$sample_type <- c("Brain", "Skin", "Heart", "Lung",
"Kidney", "Muscle", "Tumor", "Other")
bs_app(results, images = images, title = "My experiment",
group_by = "sample_type", group_label = "Sample type")Pass a saved result file path to enable measurement refresh, and use Refresh saved data or Watch saved files. Watch mode checks every two seconds; saving a new annotation export refreshes its properties without rerunning analysis. A failed result read retains the last successfully loaded measurements and displays an error. The original files remain unchanged. Watch mode follows the supplied result path, not an active QuPath editor’s unsaved state. Selecting a browser upload or demonstration stops following that original result file.
A CSV needs the following columns. physical_slide_id is
accepted in place of slide_id; if both are supplied, they
must agree. Optional organ and qc_stratum (or
stratum) columns populate the filters.
slides <- data.frame(
slide_id = c("001", "002", "003"),
organ = c("Liver", "Liver", "Spleen"),
tissue_area_mm2 = c(1, 9, 2),
red_area_mm2 = c(0.1, 0, NA_real_),
candidate_spots = c(10, 0, NA_real_)
)
slides
#> slide_id organ tissue_area_mm2 red_area_mm2 candidate_spots
#> 1 001 Liver 1 0.1 10
#> 2 002 Liver 9 0.0 0
#> 3 003 Spleen 2 NA NASave this as CSV and select it with Import slide
results. Files must be at most 20 MiB. Counts are checked as
decimal text before conversion, so an integer outside the exact numeric
range is rejected rather than rounded. Blank numeric cells and
NA mean missing. Numeric-looking IDs retain leading zeros;
the literal ID NA remains an identifier. JSON should use
the package’s canonical export, with a slides table and
schema version "1.0".
file <- tempfile(fileext = ".csv")
utils::write.csv(slides, file, row.names = FALSE, na = "")
readLines(file)
#> [1] "\"slide_id\",\"organ\",\"tissue_area_mm2\",\"red_area_mm2\",\"candidate_spots\""
#> [2] "\"001\",\"Liver\",1,0.1,10"
#> [3] "\"002\",\"Liver\",9,0,0"
#> [4] "\"003\",\"Spleen\",2,,"
unlink(file)An invalid upload displays the reason and retains the previous
results. A successful upload resets filters. CSV downloads contain the
filtered rows, including rows with missing measurements; formula-like
text is escaped for spreadsheet viewing. Use
bs_export_results() and its canonical JSON when exact
metadata round-tripping is required.
The cards show selected slide count, summed tissue area, pooled red-positive percentage and pooled candidate density. Rates divide summed numerators by summed support. They do not average percentages across slides.
For the two liver slides above, the red-positive percentage is
100 * (0.1 + 0) / (1 + 9) = 1%, and the candidate density
is (10 + 0) / (1 + 9) = 1/mm². The unweighted mean of their
percentages would be 5%, which answers a different question.
old <- par(mar = c(5, 4, 1, 1))
barplot(c("Pooled by tissue area" = 1, "Unweighted slide mean" = 5),
col = c("#185FA5", "#A3ADB5"), ylab = "Red-positive tissue (%)",
ylim = c(0, 6), las = 1, border = NA)Selecting all three rows makes both pooled rates unavailable because the spleen measurements are unknown. Missing is not zero. A slide with zero tissue support also has undefined rates. The completeness line explains how many selected rows can appear in the plot; the cards still account for every selected row.
A candidate event is not necessarily one erythrocyte. QC labels are supplied review decisions, not automatic diagnoses; low signal does not establish a negative control. Filtering changes descriptive support and must be recorded if used for a report. Biological replication, animal-level inference and validation require a separate study design.
The app is intended for a local R session. If you deploy it to a server, uploads are sent to that server; “local session” is not a confidentiality guarantee for a hosted deployment. Authentication, access control and retention policies belong to the hosting environment. Uploaded CSV/JSON is parsed as data, not executed.