# Spec 36: React Frontend + FastAPI Backend for HuggingFace Spaces **Status**: APPROVED PLAN **Date**: 2025-12-11 **Goal**: Replace Gradio with React frontend for NiiVue, FastAPI backend for DeepISLES --- ## Security Note: CVE-2025-55182 Does NOT Affect This App **CVE-2025-55182 (React2Shell)** is a critical RCE vulnerability disclosed December 3, 2025. | What | Status | |------|--------| | **React 19.x with RSC** | VULNERABLE if using Server Components | | **React 19.x client-only** | SAFE - no Server Components = no vulnerability | | **React 18.x** | NOT AFFECTED - no Server Components | **We use React 19.2.0** which is **safe for our use case** because: - CVE-2025-55182 only affects React Server Components (RSC) - Our app is **client-only** (Static Space = no server-side rendering) - We do not use React Server Components - The vulnerability requires SSR/RSC to be exploitable Sources: - [React Security Advisory](https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components) - [Wiz Analysis](https://www.wiz.io/blog/critical-vulnerability-in-react-cve-2025-55182) --- ## The Stack | Component | Technology | Version | Purpose | |-----------|------------|---------|---------| | **Frontend Framework** | React | 19.2.0 | UI components (client-only, see security note) | | **Type Safety** | TypeScript | 5.9.3 | Type checking | | **Build Tool** | Vite | 7.2.4 | Fast builds, HMR | | **CSS Framework** | Tailwind CSS | 4.1.17 | Utility-first styling | | **3D Viewer** | @niivue/niivue | 0.65.0 | WebGL2 NIfTI viewer | | **Testing** | Vitest + Playwright | 4.0.15 / 1.57.0 | Unit, integration, E2E tests | | **Backend Framework** | FastAPI | 0.124.2 | Python REST API | | **ML Pipeline** | DeepISLES | existing | Stroke segmentation | --- ## Architecture: Two HuggingFace Spaces You **need both** because: - **Static Space** = JavaScript only (React, NiiVue) - cannot run Python - **Docker Space** = Python runtime (FastAPI, DeepISLES, PyTorch) ``` ┌─────────────────────────────────────┐ │ HuggingFace Static Space │ │ stroke-viewer-frontend │ │ │ │ React 19 + TypeScript + Tailwind │ │ @niivue/niivue for 3D viewing │ │ │ │ Serves: index.html, JS, CSS │ │ Always on, never sleeps │ └──────────────┬──────────────────────┘ │ HTTPS API calls ▼ ┌─────────────────────────────────────┐ │ HuggingFace Docker Space │ │ stroke-viewer-api │ │ │ │ FastAPI + DeepISLES + PyTorch │ │ │ │ Endpoints: │ │ - GET /api/cases │ │ - POST /api/segment │ │ - GET /api/files/{path} │ │ │ │ Sleeps after 48h inactivity │ └─────────────────────────────────────┘ ``` --- ## Project Structure ``` stroke-viewer/ ├── frontend/ # Static Space │ ├── src/ │ │ ├── components/ │ │ │ ├── NiiVueViewer.tsx │ │ │ ├── CaseSelector.tsx │ │ │ ├── MetricsPanel.tsx │ │ │ └── Layout.tsx │ │ ├── hooks/ │ │ │ └── useSegmentation.ts │ │ ├── api/ │ │ │ └── client.ts │ │ ├── types/ │ │ │ └── index.ts │ │ ├── App.tsx │ │ ├── main.tsx │ │ └── index.css │ ├── public/ │ ├── index.html │ ├── vite.config.ts │ ├── tsconfig.json │ ├── package.json │ └── README.md # HF Spaces YAML config │ ├── backend/ # Docker Space │ ├── api/ │ │ ├── __init__.py │ │ ├── main.py # FastAPI app │ │ ├── routes.py # API endpoints │ │ └── schemas.py # Pydantic models │ ├── Dockerfile │ ├── requirements.txt │ └── README.md # HF Spaces YAML config │ └── README.md # Project overview ``` --- ## Frontend Implementation ### package.json ```json { "name": "frontend", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc -b && vite build", "preview": "vite preview", "lint": "eslint .", "test": "vitest", "test:coverage": "vitest run --coverage", "test:e2e": "playwright test" }, "dependencies": { "@niivue/niivue": "^0.65.0", "react": "^19.2.0", "react-dom": "^19.2.0" }, "devDependencies": { "@playwright/test": "^1.57.0", "@tailwindcss/vite": "^4.1.17", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@vitejs/plugin-react": "^5.1.1", "@vitest/coverage-v8": "^4.0.15", "eslint": "^9.39.1", "tailwindcss": "^4.1.17", "typescript": "~5.9.3", "vite": "^7.2.4", "vitest": "^4.0.15" } } ``` **Why these versions:** - `react` / `react-dom` **19.2.0**: Latest React 19 - client-only so CVE-2025-55182 doesn't apply - `@niivue/niivue` **0.65.0**: Latest stable (Dec 2025) - `vite` **7.2.4**: Latest stable v7 - `vitest` **4.0.15**: Fast unit testing with React Testing Library - `@playwright/test` **1.57.0**: E2E browser testing - `tailwindcss` **4.1.17**: Latest stable v4 - `typescript` **5.9.3**: Latest stable - ESLint included for code quality in CI ### vite.config.ts ```typescript import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' export default defineConfig({ plugins: [react(), tailwindcss()], build: { outDir: 'dist', }, }) ``` ### tsconfig.json ```json { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["src"] } ``` ### src/index.css ```css @import "tailwindcss"; ``` ### src/main.tsx ```tsx import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import App from './App' import './index.css' createRoot(document.getElementById('root')!).render( , ) ``` ### src/App.tsx ```tsx import { useState } from 'react' import { Layout } from './components/Layout' import { CaseSelector } from './components/CaseSelector' import { NiiVueViewer } from './components/NiiVueViewer' import { MetricsPanel } from './components/MetricsPanel' import { useSegmentation } from './hooks/useSegmentation' export default function App() { const [selectedCase, setSelectedCase] = useState(null) const { result, isLoading, error, runSegmentation } = useSegmentation() const handleRunSegmentation = async () => { if (selectedCase) { await runSegmentation(selectedCase) } } return ( {/* Left Panel: Controls */} {isLoading ? 'Processing...' : 'Run Segmentation'} {error && ( {error} )} {result && } {/* Right Panel: Viewer */} {result ? ( ) : ( Select a case and run segmentation to view results )} ) } ``` ### src/components/Layout.tsx ```tsx import { ReactNode } from 'react' interface LayoutProps { children: ReactNode } export function Layout({ children }: LayoutProps) { return ( Stroke Lesion Segmentation DeepISLES segmentation on ISLES24 dataset {children} ) } ``` ### src/components/NiiVueViewer.tsx ```tsx import { useRef, useEffect } from 'react' import { Niivue } from '@niivue/niivue' interface NiiVueViewerProps { backgroundUrl: string overlayUrl?: string } export function NiiVueViewer({ backgroundUrl, overlayUrl }: NiiVueViewerProps) { const canvasRef = useRef(null) const nvRef = useRef(null) useEffect(() => { if (!canvasRef.current) return // Only instantiate NiiVue once; reuse for volume reloads let nv = nvRef.current if (!nv) { nv = new Niivue({ backColor: [0.05, 0.05, 0.05, 1], show3Dcrosshair: true, crosshairColor: [1, 0, 0, 0.5], }) nv.attachToCanvas(canvasRef.current) nvRef.current = nv } // Build volumes array - always reload when URLs change const volumes: Array<{ url: string; colormap: string; opacity: number }> = [ { url: backgroundUrl, colormap: 'gray', opacity: 1 }, ] if (overlayUrl) { volumes.push({ url: overlayUrl, colormap: 'red', opacity: 0.5, }) } // Load volumes (async but we don't await - just fire off) void nv.loadVolumes(volumes) // Cleanup on unmount - CRITICAL: Release WebGL context // Browsers limit WebGL contexts (~16 in Chrome). Without cleanup, // navigating between results will exhaust contexts and break the viewer. return () => { if (nvRef.current) { // Capture gl BEFORE cleanup (cleanup may null internal state) const gl = nvRef.current.gl try { // NiiVue's cleanup() releases event listeners and observers // See: https://niivue.github.io/niivue/devdocs/classes/Niivue.html#cleanup nvRef.current.cleanup() // Force WebGL context loss to free GPU memory immediately if (gl) { const ext = gl.getExtension('WEBGL_lose_context') ext?.loseContext() } } catch { // Ignore cleanup errors } nvRef.current = null } } }, [backgroundUrl, overlayUrl]) return ( Scroll: Navigate slices Drag: Adjust contrast Right-click: Pan ) } ``` ### src/components/CaseSelector.tsx ```tsx import { useEffect, useState } from 'react' import { apiClient } from '../api/client' interface CaseSelectorProps { selectedCase: string | null onSelectCase: (caseId: string) => void } export function CaseSelector({ selectedCase, onSelectCase }: CaseSelectorProps) { const [cases, setCases] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { const fetchCases = async () => { try { const data = await apiClient.getCases() setCases(data.cases) } catch (err) { setError('Failed to load cases') console.error(err) } finally { setLoading(false) } } fetchCases() }, []) if (loading) { return ( Loading cases... ) } if (error) { return ( {error} ) } return ( Select Case onSelectCase(e.target.value)} className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent" > Choose a case... {cases.map((caseId) => ( {caseId} ))} ) } ``` ### src/components/MetricsPanel.tsx ```tsx interface Metrics { caseId: string diceScore: number | null volumeMl: number | null elapsedSeconds: number } interface MetricsPanelProps { metrics: Metrics } export function MetricsPanel({ metrics }: MetricsPanelProps) { return ( Results Case: {metrics.caseId} {metrics.diceScore !== null && ( Dice Score: {metrics.diceScore.toFixed(3)} )} {metrics.volumeMl !== null && ( Volume: {metrics.volumeMl.toFixed(2)} mL )} Time: {metrics.elapsedSeconds.toFixed(1)}s ) } ``` ### src/api/client.ts ```typescript // API base URL - configure via environment variable const API_BASE = import.meta.env.VITE_API_URL || 'https://your-backend.hf.space' interface CasesResponse { cases: string[] } interface SegmentResponse { caseId: string diceScore: number | null volumeMl: number | null elapsedSeconds: number dwiUrl: string predictionUrl: string } class ApiClient { private baseUrl: string constructor(baseUrl: string) { this.baseUrl = baseUrl } async getCases(): Promise { const response = await fetch(`${this.baseUrl}/api/cases`) if (!response.ok) { throw new Error(`Failed to fetch cases: ${response.statusText}`) } return response.json() } async runSegmentation(caseId: string, fastMode = true): Promise { const response = await fetch(`${this.baseUrl}/api/segment`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ case_id: caseId, fast_mode: fastMode, }), }) if (!response.ok) { throw new Error(`Segmentation failed: ${response.statusText}`) } return response.json() } } export const apiClient = new ApiClient(API_BASE) ``` ### src/hooks/useSegmentation.ts ```typescript import { useState, useCallback } from 'react' import { apiClient } from '../api/client' interface SegmentationResult { dwiUrl: string predictionUrl: string metrics: { caseId: string diceScore: number | null volumeMl: number | null elapsedSeconds: number } } export function useSegmentation() { const [result, setResult] = useState(null) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) const runSegmentation = useCallback(async (caseId: string) => { setIsLoading(true) setError(null) try { const data = await apiClient.runSegmentation(caseId) setResult({ dwiUrl: data.dwiUrl, predictionUrl: data.predictionUrl, metrics: { caseId: data.caseId, diceScore: data.diceScore, volumeMl: data.volumeMl, elapsedSeconds: data.elapsedSeconds, }, }) } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error') setResult(null) } finally { setIsLoading(false) } }, []) return { result, isLoading, error, runSegmentation } } ``` ### src/types/index.ts ```typescript export interface Case { id: string name: string } export interface Metrics { caseId: string diceScore: number | null volumeMl: number | null elapsedSeconds: number } export interface SegmentationResult { dwiUrl: string predictionUrl: string metrics: Metrics } ``` ### index.html ```html Stroke Lesion Segmentation ``` ### frontend/README.md (HuggingFace Spaces Config) ```markdown --- title: Stroke Lesion Viewer emoji: 🧠 colorFrom: blue colorTo: purple sdk: static app_file: dist/index.html app_build_command: npm run build # CRITICAL: Vite 6 requires Node.js >= 20. HF Spaces defaults to Node 18. # Without this, the build will fail or produce warnings. nodejs_version: "20" pinned: false --- # Stroke Lesion Segmentation Viewer Interactive 3D viewer for stroke lesion segmentation results using NiiVue. Built with React, TypeScript, Tailwind CSS, and Vite. ``` --- ## Backend Implementation ### requirements.txt ``` fastapi==0.124.2 uvicorn[standard]==0.34.0 pydantic==2.10.4 python-multipart>=0.0.18 # Existing project dependencies stroke-deepisles-demo @ file:. ``` **Why these exact versions (Dec 2025):** - `fastapi` **0.124.2**: Latest stable (Dec 10, 2025) - `uvicorn[standard]` **0.34.0**: Latest stable - `pydantic` **2.10.4**: Latest stable - `python-multipart` **>=0.0.18**: Required by FastAPI 0.124.x ### backend/api/main.py ```python """FastAPI backend for stroke segmentation.""" import os import re from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from api.routes import router app = FastAPI( title="Stroke Segmentation API", description="DeepISLES stroke lesion segmentation", version="1.0.0", ) # CORS for frontend - HF Spaces use dashed hostnames: {org}--{space}.hf.space # Also supports PR previews: pr-{n}--{org}--{space}.hf.space FRONTEND_ORIGIN = os.environ.get("FRONTEND_ORIGIN", "") CORS_ORIGINS = [ "http://localhost:5173", # Local Vite dev server "http://localhost:3000", # Alternative local port ] if FRONTEND_ORIGIN: CORS_ORIGINS.append(FRONTEND_ORIGIN) app.add_middleware( CORSMiddleware, allow_origins=CORS_ORIGINS, # Regex matches HuggingFace Spaces origins: # - Production: https://{org}--stroke-viewer-frontend.hf.space # - PR preview: https://{org}--stroke-viewer-frontend--pr-{N}.hf.space # - Branch: https://{org}--stroke-viewer-frontend--{branch}.hf.space # Pattern: anything--stroke-viewer-frontend, optionally followed by --anything allow_origin_regex=r"https://.*--stroke-viewer-frontend(--.*)?\.hf\.space", allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # API routes app.include_router(router, prefix="/api") # Serve NIfTI files from results directory # Files are stored as /tmp/stroke-results/{run_id}/{case_id}/{filename} app.mount("/files", StaticFiles(directory="/tmp/stroke-results"), name="files") @app.get("/") async def root(): return {"status": "healthy", "service": "stroke-segmentation-api"} ``` ### backend/api/routes.py ```python """API route handlers.""" import os import uuid from pathlib import Path from fastapi import APIRouter, HTTPException, Request from api.schemas import SegmentRequest, SegmentResponse, CasesResponse from stroke_deepisles_demo.data import list_case_ids from stroke_deepisles_demo.pipeline import run_pipeline_on_case from stroke_deepisles_demo.metrics import compute_volume_ml router = APIRouter() # Base directory for results (must match StaticFiles mount in main.py) RESULTS_BASE = Path("/tmp/stroke-results") def get_backend_base_url(request: Request) -> str: """Get the backend's public URL for building absolute file URLs. Priority: 1. BACKEND_PUBLIC_URL env var (for production HF Spaces) 2. Request's base URL (for local development) """ env_url = os.environ.get("BACKEND_PUBLIC_URL", "").rstrip("/") if env_url: return env_url # Fall back to request origin (works for local dev) return str(request.base_url).rstrip("/") @router.get("/cases", response_model=CasesResponse) async def get_cases(): """List available cases from dataset.""" try: cases = list_case_ids() return CasesResponse(cases=cases) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/segment", response_model=SegmentResponse) async def run_segmentation(request: Request, body: SegmentRequest): """Run DeepISLES segmentation on a case.""" try: # Generate unique run ID to avoid conflicts between concurrent requests run_id = str(uuid.uuid4())[:8] output_dir = RESULTS_BASE / run_id result = run_pipeline_on_case( body.case_id, output_dir=output_dir, fast=body.fast_mode, compute_dice=True, cleanup_staging=True, ) # Compute volume volume_ml = None try: volume_ml = round(compute_volume_ml(result.prediction_mask, threshold=0.5), 2) except Exception: pass # Build ABSOLUTE file URLs for cross-origin NiiVue loading # Files are at: /tmp/stroke-results/{run_id}/{case_id}/{filename} # Served at: /files/{run_id}/{case_id}/{filename} backend_url = get_backend_base_url(request) dwi_filename = result.input_files["dwi"].name pred_filename = result.prediction_mask.name # URL path: /files/{run_id}/{case_id}/{filename} file_path_prefix = f"/files/{run_id}/{result.case_id}" return SegmentResponse( caseId=result.case_id, diceScore=result.dice_score, volumeMl=volume_ml, elapsedSeconds=round(result.elapsed_seconds, 2), dwiUrl=f"{backend_url}{file_path_prefix}/{dwi_filename}", predictionUrl=f"{backend_url}{file_path_prefix}/{pred_filename}", ) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) ``` ### backend/api/schemas.py ```python """Pydantic schemas for API.""" from pydantic import BaseModel class CasesResponse(BaseModel): cases: list[str] class SegmentRequest(BaseModel): case_id: str fast_mode: bool = True class SegmentResponse(BaseModel): caseId: str diceScore: float | None volumeMl: float | None elapsedSeconds: float dwiUrl: str predictionUrl: str ``` ### backend/Dockerfile ```dockerfile # CRITICAL: Must use isleschallenge/deepisles base image # This image contains: # - PyTorch with CUDA support # - Pre-installed DeepISLES model weights (~18GB) # - All medical imaging dependencies (nibabel, nnunet, etc.) # # Using python:3.11-slim would require manually downloading weights # and reinstalling all CUDA/PyTorch dependencies - not feasible. FROM isleschallenge/deepisles:latest WORKDIR /app # Copy the ENTIRE project (stroke-deepisles-demo package) # This is required because requirements.txt references "stroke-deepisles-demo @ file:." COPY pyproject.toml . COPY src/ src/ COPY README.md . # Copy API code COPY backend/api/ api/ COPY backend/requirements.txt . # Install API dependencies (FastAPI, uvicorn) + local package # Note: Base image already has torch, nibabel, etc. RUN pip install --no-cache-dir -r requirements.txt # Create results directory (used by StaticFiles mount) RUN mkdir -p /tmp/stroke-results # Environment variables for HuggingFace Spaces ENV HF_SPACES=1 ENV DEEPISLES_DIRECT_INVOCATION=1 # Expose port (HF Spaces expects 7860) EXPOSE 7860 # Run FastAPI CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "7860"] ``` **CRITICAL: GPU Required** DeepISLES requires GPU acceleration. HuggingFace Spaces FREE tier (`cpu-basic`) will NOT work. | Tier | GPU | Will Work? | |------|-----|------------| | `cpu-basic` (free) | None | ❌ No | | `t4-small` | NVIDIA T4 (16GB) | ✅ Yes | | `t4-medium` | NVIDIA T4 (16GB) | ✅ Yes | | `a10g-small` | NVIDIA A10G (24GB) | ✅ Yes | When creating the HF Space, select **T4-small** or higher. **Note:** The Dockerfile copies the full project because `requirements.txt` has: ``` stroke-deepisles-demo @ file:. ``` This PEP 508 local path reference requires the package source to be present. ### backend/README.md (HuggingFace Spaces Config) ```markdown --- title: Stroke Segmentation API emoji: 🧠 colorFrom: blue colorTo: purple sdk: docker app_port: 7860 pinned: false --- # Stroke Segmentation API FastAPI backend running DeepISLES stroke lesion segmentation. ## Endpoints - `GET /api/cases` - List available cases - `POST /api/segment` - Run segmentation - `GET /files/{filename}` - Download result files ``` --- ## Setup Commands ### Frontend (Local Development) ```bash # Create project npm create vite@latest stroke-viewer-frontend -- --template react-ts cd stroke-viewer-frontend # Install dependencies npm install @niivue/niivue npm install -D tailwindcss @tailwindcss/vite # Copy the files from this spec into src/ # Run dev server npm run dev # Opens http://localhost:5173 ``` ### Backend (Local Development) ```bash cd backend # Create virtual environment python -m venv venv source venv/bin/activate # Install dependencies pip install -r requirements.txt # Run server uvicorn api.main:app --reload --port 7860 # Opens http://localhost:7860 ``` ### Deploy to HuggingFace ```bash # Frontend (Static Space) cd frontend huggingface-cli repo create stroke-viewer-frontend --type space --space-sdk static huggingface-cli upload stroke-viewer-frontend ./dist . --repo-type space # Backend (Docker Space) cd backend huggingface-cli repo create stroke-viewer-api --type space --space-sdk docker huggingface-cli upload stroke-viewer-api . . --repo-type space ``` --- ## Environment Variables ### Frontend (.env) ```env VITE_API_URL=https://your-username-stroke-viewer-api.hf.space ``` ### Backend No additional env vars needed - uses existing stroke-deepisles-demo configuration. --- ## Key Differences from Gradio | What | Gradio (broken) | This Stack | |------|-----------------|------------| | NiiVue JavaScript | Blocked by innerHTML | Full execution ✓ | | WebGL2 context | Frozen during hydration | Works normally ✓ | | Bundle size | ~2MB Gradio overhead | ~200KB total | | Cold start | Python + Gradio init | Instant (static) | | Customization | Limited to Gradio components | Full React control | --- ## Next Steps 1. Create `frontend/` directory with files from this spec 2. Create `backend/` directory with files from this spec 3. Test locally: `npm run dev` + `uvicorn api.main:app` 4. Create HuggingFace Spaces (one Static, one Docker) 5. Deploy and test --- ## Dependencies Summary (Verified Dec 11, 2025) **Frontend (npm) - PINNED VERSIONS:** | Package | Version | Notes | |---------|---------|-------| | react | 18.3.1 | NOT React 19 (CVE-2025-55182) | | react-dom | 18.3.1 | Must match react version | | @niivue/niivue | 0.65.0 | Latest stable | | typescript | 5.6.3 | Latest 5.6.x | | vite | 6.0.5 | Stable v6 (not v7/v8 beta) | | tailwindcss | 4.1.7 | Latest v4 | | @tailwindcss/vite | 4.1.7 | Must match tailwindcss | | @vitejs/plugin-react | 4.3.4 | Latest stable | **Backend (pip) - PINNED VERSIONS:** | Package | Version | Notes | |---------|---------|-------| | fastapi | 0.124.2 | Latest (Dec 10, 2025) | | uvicorn[standard] | 0.34.0 | Latest stable | | pydantic | 2.10.4 | Latest stable | | python-multipart | >=0.0.18 | Required by FastAPI | **Node.js:** >= 20.0.0 (required for Vite 6) **Python:** >= 3.11 (recommended for FastAPI)
Select a case and run segmentation to view results
DeepISLES segmentation on ISLES24 dataset
Loading cases...
{error}