Spaces:
Running
Running
Signs on the film, draw tool, My search off by default, example films and Radiopaedia links (part 2)
2e6a666 verified Download frontend/src/reference/VolumeExample.tsx from yashwrites/blindspot: direct link, hf CLI and curl.
- Browser
- Download file 5.34 kB
-
https://huggingface.co/spaces/yashwrites/blindspot/resolve/main/frontend/src/reference/VolumeExample.tsx
- Command line
-
hf download hf://spaces/yashwrites/blindspot/frontend/src/reference/VolumeExample.tsx
-
curl -L -o VolumeExample.tsx https://huggingface.co/spaces/yashwrites/blindspot/resolve/main/frontend/src/reference/VolumeExample.tsx
5.34 kB
| // One CT / MR example in the finding library: an axial slice drawn from the gzipped voxels (decoded in the browser), | |
| // the reference outline from the label mask in cyan, and a slice scrubber that starts on the finding's measured slice. | |
| // Decoding reuses the reading room's helpers (viewer/volume: gunzip, int16/uint8 decode, window LUT, marching | |
| // squares); nothing here duplicates them. Nothing about the learner's own cases is shown. | |
| import { useEffect, useMemo, useRef, useState } from 'react'; | |
| import { assetUrl } from '../api/client'; | |
| import { ProvenanceBadge } from '../app/ProvenanceBadge'; | |
| import { marchingSquares } from '../viewer/volume/marching'; | |
| import { planeGeom } from '../viewer/volume/planes'; | |
| import { cachedGunzip, decodeMask, decodeVolume, extractSlice, type MaskVolume, type Volume } from '../viewer/volume/volume'; | |
| import { paintSlice, windowLut } from '../viewer/volume/window'; | |
| import type { ReferenceExample } from './guard'; | |
| import s from './Reference.module.css'; | |
| type Loaded = { vol: Volume; mask: MaskVolume }; | |
| /** Fetch and decode both arrays once per URL pair (the viewer's cache keeps the gunzipped bytes). */ | |
| async function loadVolumeExample(v: NonNullable<ReferenceExample['volume']>): Promise<Loaded> { | |
| const meta = { shape: v.shape, spacing: v.spacing }; | |
| const [vb, mb] = await Promise.all([cachedGunzip(assetUrl(v.volume_url)), cachedGunzip(assetUrl(v.mask_url))]); | |
| return { vol: decodeVolume(vb, meta), mask: decodeMask(mb, meta) }; | |
| } | |
| /** Which mask values are the finding: the example's own, else every non-zero value. */ | |
| const insideFn = (values: number[]) => (values.length ? (x: number) => values.includes(x) : (x: number) => x > 0); | |
| /** `thumb` (round 5): the measure slice only, no scrubber or caption — the debrief row's 120 px thumbnail. */ | |
| export function VolumeExample({ ex, outline, alt, small = false, thumb = false }: { ex: ReferenceExample; outline: boolean; alt: string; small?: boolean; thumb?: boolean }) { | |
| const v = ex.volume!; | |
| const [z, setZ] = useState(v.slice); | |
| const [data, setData] = useState<Loaded | null>(null); | |
| const [failed, setFailed] = useState<string | null>(null); | |
| const canvas = useRef<HTMLCanvasElement>(null); | |
| const g = useMemo(() => planeGeom({ shape: v.shape, spacing: v.spacing }, 'axial'), [v.shape, v.spacing]); | |
| const lut = useMemo(() => windowLut(v.window.wc, v.window.ww), [v.window.wc, v.window.ww]); | |
| useEffect(() => { | |
| let live = true; | |
| loadVolumeExample(v).then((d) => { if (live) setData(d); }, (e: unknown) => { if (live) setFailed(e instanceof Error ? e.message : 'decode failed'); }); | |
| return () => { live = false; }; | |
| }, [v]); | |
| // Paint the current slice (w×h voxels; CSS scales it to the display aspect). | |
| useEffect(() => { | |
| const c = canvas.current; | |
| if (!c || !data) return; | |
| const ctx = c.getContext('2d'); | |
| if (!ctx) return; | |
| const img = ctx.createImageData(g.w, g.h); | |
| paintSlice(extractSlice(data.vol.data, data.vol.shape, 'axial', z), lut, img.data); | |
| ctx.putImageData(img, 0, 0); | |
| }, [data, z, g.w, g.h, lut]); | |
| const rings = useMemo(() => { | |
| if (!data || !outline) return []; | |
| return marchingSquares(extractSlice(data.mask.data, data.mask.shape, 'axial', z), g.w, g.h, insideFn(v.label_values)); | |
| }, [data, outline, z, g.w, g.h, v.label_values]); | |
| const range = ex.finding?.slice_range; | |
| const onFinding = range ? z >= range[0] && z <= range[1] : rings.length > 0; | |
| return ( | |
| <figure className={`${s.figure} ${small || thumb ? s.figureSmall : ''}`} data-testid="reference-example" data-modality={ex.modality} data-slice={z}> | |
| <div className={s.film} style={{ aspectRatio: `${g.W} / ${g.H}` }}> | |
| {failed ? <p className={s.filmNote}>This example scan could not be decoded. {failed}</p> : ( | |
| <> | |
| <canvas ref={canvas} width={g.w} height={g.h} className={s.sliceCanvas} role="img" aria-label={`${alt}, axial slice ${z + 1} of ${g.n}`} data-testid="reference-slice" /> | |
| {!data && <p className={s.filmNote}>Decoding the scan…</p>} | |
| {outline && rings.length > 0 && ( | |
| <svg viewBox={`0 0 ${g.w} ${g.h}`} preserveAspectRatio="none" aria-hidden="true" data-testid="reference-outline"> | |
| {rings.map((r, i) => <polygon key={i} points={r.map(([x, y]) => `${x},${y}`).join(' ')} className={s.outline} vectorEffect="non-scaling-stroke" />)} | |
| </svg> | |
| )} | |
| </> | |
| )} | |
| </div> | |
| {!thumb && <div className={s.scrub}> | |
| <label> | |
| <span className="sr-only">Axial slice</span> | |
| <input type="range" min={0} max={Math.max(0, g.n - 1)} value={z} onChange={(e) => setZ(Number(e.target.value))} disabled={!data} data-testid="reference-slice-scrub" /> | |
| </label> | |
| <span className={s.scrubText} data-testid="reference-slice-text"> | |
| Slice {z + 1} of {g.n}{range ? (onFinding ? ' · on the finding' : ` · finding on ${range[0] + 1}–${range[1] + 1}`) : ''} | |
| </span> | |
| </div>} | |
| {!thumb && <figcaption className={s.caption}> | |
| {ex.finding?.relative_location ? `${ex.finding.relative_location.charAt(0).toUpperCase()}${ex.finding.relative_location.slice(1)}. ` : ''} | |
| <ProvenanceBadge provenance={ex.provenance} modality={ex.modality} short /> | |
| </figcaption>} | |
| </figure> | |
| ); | |
| } | |