Spaces:
Sleeping
Sleeping
Commit ·
6f38849
0
Parent(s):
Initial BioMedVisionLab research workbench
Browse files- .gitignore +18 -0
- .streamlit/config.toml +5 -0
- README.md +399 -0
- app.py +0 -0
- build_index.py +291 -0
- requirements.txt +10 -0
.gitignore
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
data/
|
| 2 |
+
venv/
|
| 3 |
+
__pycache__/
|
| 4 |
+
.streamlit/secrets.toml
|
| 5 |
+
|
| 6 |
+
data/
|
| 7 |
+
embeddings/
|
| 8 |
+
*.pyc
|
| 9 |
+
.DS_Store
|
| 10 |
+
|
| 11 |
+
*.pt
|
| 12 |
+
*.pth
|
| 13 |
+
*.onnx
|
| 14 |
+
*.h5
|
| 15 |
+
*.ckpt
|
| 16 |
+
*.npy
|
| 17 |
+
*.npz
|
| 18 |
+
*.csv
|
.streamlit/config.toml
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[server]
|
| 2 |
+
maxUploadSize = 500
|
| 3 |
+
|
| 4 |
+
[browser]
|
| 5 |
+
gatherUsageStats = false
|
README.md
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# BioMedVisionLab
|
| 2 |
+
|
| 3 |
+
**Biomedical Imaging Visualization Toolkit for Retrieval and Super-Resolution**
|
| 4 |
+
|
| 5 |
+
BioMedVisionLab is a polished, professor-facing Streamlit workbench for evaluating biomedical image-like data. It supports visual inspection, embedding comparison, retrieval benchmarking, super-resolution baseline evaluation, future model-output comparison, contact-map visualization, and CSV experiment export.
|
| 6 |
+
|
| 7 |
+
1. Chest X-ray similarity retrieval visualization
|
| 8 |
+
2. Biomedical image super-resolution demo
|
| 9 |
+
3. Grant Alignment Lab for chemical imaging and Hi-C contact-map research directions
|
| 10 |
+
|
| 11 |
+
BioMedVisionLab is upload-first for deployed demonstrations. The app can use a built-in/local CXR embedding index when one is available, but it also works without a local Kaggle dataset by letting users upload biomedical images or contact-map matrices directly in the browser.
|
| 12 |
+
|
| 13 |
+
The project is intended for education, research prototyping, professor-facing demonstrations, and portfolio presentation. It is not intended for medical diagnosis, triage, treatment, or clinical decision-making.
|
| 14 |
+
|
| 15 |
+
## Overview
|
| 16 |
+
|
| 17 |
+
BioMedVisionLab combines representation learning, medical image retrieval, CXR-specific encoder support, batch retrieval quality measurement, super-resolution baseline evaluation, model-output comparison, Hi-C-like matrix visualization, and report export in one interactive dashboard.
|
| 18 |
+
|
| 19 |
+
- `build_index.py` scans a local chest X-ray dataset, extracts embeddings, and saves searchable encoder-specific indexes.
|
| 20 |
+
- `app.py` launches a Streamlit toolkit with tabs for retrieval, super-resolution visualization, and grant-alignment demos.
|
| 21 |
+
|
| 22 |
+
## Motivation
|
| 23 |
+
|
| 24 |
+
Biomedical imaging research often needs visual, explainable tools for understanding what models learn and how image quality affects downstream interpretation. This project focuses on showing model behavior and image-processing concepts without making diagnostic claims.
|
| 25 |
+
|
| 26 |
+
## Module 1: CXR Retrieval
|
| 27 |
+
|
| 28 |
+
The CXR retrieval module visualizes similarity search over a local chest X-ray dataset.
|
| 29 |
+
|
| 30 |
+
Features:
|
| 31 |
+
|
| 32 |
+
- Loads saved embeddings from `embeddings/`
|
| 33 |
+
- Shows a clear **Local CXR index available** or **Upload-only mode** status
|
| 34 |
+
- Supports dataset image queries and uploaded image queries
|
| 35 |
+
- If no saved embeddings are available, supports upload-first retrieval:
|
| 36 |
+
- upload a query image
|
| 37 |
+
- upload a small reference gallery of JPG/PNG/JPEG files
|
| 38 |
+
- build temporary in-session embeddings for the uploaded gallery
|
| 39 |
+
- retrieve visually similar uploaded reference images
|
| 40 |
+
- Uploaded gallery retrieval is temporary and is not saved
|
| 41 |
+
- Uses pretrained torchvision ResNet18 as a lightweight generic ImageNet feature-extraction baseline
|
| 42 |
+
- Adds pretrained torchvision ResNet50 as a deeper generic ImageNet feature-extraction baseline
|
| 43 |
+
- Optionally supports TorchXRayVision DenseNet121 as a CXR-specific embedding encoder when `torchxrayvision` is installed
|
| 44 |
+
- Uses 512-dimensional L2-normalized embeddings for ResNet18, 2048-dimensional L2-normalized embeddings for ResNet50, and DenseNet feature embeddings for TorchXRayVision
|
| 45 |
+
- Stores encoder-specific indexes in `embeddings/resnet18_imagenet/`, `embeddings/resnet50_imagenet/`, and `embeddings/torchxrayvision_densenet121/`
|
| 46 |
+
- Stores metadata for encoder name, embedding dimension, dataset size, build time, and device
|
| 47 |
+
- Lets the app sidebar switch between the generic ImageNet baseline and the CXR-specific encoder when their indexes are available
|
| 48 |
+
- Retrieves top-k visually similar images with cosine similarity
|
| 49 |
+
- Avoids returning the same image as the top result for dataset queries
|
| 50 |
+
- Shows query image, query filename, query label, image resolution, embedding dimension, and query source
|
| 51 |
+
- Uses colored clinical-style labels for `NORMAL`, `PNEUMONIA`, agreement state, and retrieval confidence
|
| 52 |
+
- Includes a compact model card with encoder, embedding dimension, similarity metric, dataset, and clinical status
|
| 53 |
+
- Adds a clinical-style retrieval triage panel for query source, majority retrieved pattern, agreement, similarity range, and review status
|
| 54 |
+
- Reports retrieval quality metrics for dataset-selected queries:
|
| 55 |
+
- Top-k label agreement count
|
| 56 |
+
- Precision@K
|
| 57 |
+
- Mean cosine similarity
|
| 58 |
+
- Shows Top-k sensitivity for Top-3, Top-5, and Top-10 label agreement when the query label is known
|
| 59 |
+
- Shows a retrieval confidence badge using a simple heuristic based on top similarity and, when available, Precision@K
|
| 60 |
+
- Makes clear that retrieval confidence is not clinical or diagnostic confidence
|
| 61 |
+
- Renames retrieved examples as a **Similar Case Board**
|
| 62 |
+
- Shows rank, filename, label badge, cosine similarity, query-label agreement, and a progress bar on each result card
|
| 63 |
+
- Includes a compact result table with rank, filename, label, cosine similarity, and label agreement
|
| 64 |
+
- Keeps compact charts for rank-based similarity scores and retrieved-label distribution
|
| 65 |
+
- Adds a query-vs-retrieved comparison view with query image, selected retrieved image, and a simple absolute difference map
|
| 66 |
+
- Adds an **Intensity Heatmap Preview** for the query image
|
| 67 |
+
- Exports the current retrieval experiment as CSV
|
| 68 |
+
- Adds batch retrieval evaluation for 20, 50, or 100 sampled queries
|
| 69 |
+
- Reports Mean Precision@K, label-specific Precision@K, Top-1 match rate, same-label counts, and retrieved-label distributions
|
| 70 |
+
- Exports a batch retrieval benchmark CSV with one row per retrieved result for comparing encoder experiments
|
| 71 |
+
- Adds an Encoder Comparison Benchmark when both indexes exist, using the same sampled queries to compare Mean Precision@K, class Precision@K, and Top-1 match rate
|
| 72 |
+
- Includes the Research Extension Roadmap in the CXR Retrieval tab only, so retrieval users can explore future encoder, explainability, text, microscopy, diffusion, and genomics directions without repeating the roadmap across every tab
|
| 73 |
+
|
| 74 |
+
The intensity heatmap is a simple grayscale intensity visualization. It is not Grad-CAM and does not provide model-based lesion localization.
|
| 75 |
+
The difference map is a visual comparison aid, not a clinical finding map.
|
| 76 |
+
|
| 77 |
+
## Module 2: Super-Resolution Demo
|
| 78 |
+
|
| 79 |
+
The super-resolution module demonstrates the basic image degradation and reconstruction problem.
|
| 80 |
+
|
| 81 |
+
Features:
|
| 82 |
+
|
| 83 |
+
- Select an image from the indexed CXR dataset or upload a biomedical image
|
| 84 |
+
- Works in upload-only deployments when no local CXR index exists
|
| 85 |
+
- Simulate low resolution by downsampling by `2x` or `4x`
|
| 86 |
+
- Reconstruct the image back to original size with bicubic interpolation
|
| 87 |
+
- Display original, simulated low-resolution, and bicubic enhanced images side by side
|
| 88 |
+
- Calculate MSE, PSNR, and SSIM when `scikit-image` is available
|
| 89 |
+
- Compare zoomed center crops of the original, low-resolution, and bicubic enhanced images
|
| 90 |
+
- Use a crop-size slider for quick local inspection
|
| 91 |
+
- Includes a baseline model card for degradation, reconstruction, metrics, and clinical status
|
| 92 |
+
- Adds an artifact risk card explaining that bicubic interpolation can smooth fine structures and cannot recover true missing anatomical detail
|
| 93 |
+
- Provides a large preview selector for original, simulated low-resolution, and bicubic enhanced images
|
| 94 |
+
- Supports uploading a model-enhanced image for comparison against the bicubic baseline
|
| 95 |
+
- Shows bicubic vs original and uploaded-output vs original metrics in a comparison table
|
| 96 |
+
- Adds batch super-resolution evaluation for 20, 50, or 100 indexed images
|
| 97 |
+
- Exports batch super-resolution benchmark CSV files for method comparison
|
| 98 |
+
- Exports the current super-resolution experiment as CSV
|
| 99 |
+
- Explains the relevance of throughput/resolution tradeoffs, microscopy/pathology, and future diffusion-based enhancement
|
| 100 |
+
- Adds a **Super-Resolution Exploration Studio** with prompts for uploaded learned outputs, crop-level inspection, metric deltas, and artifact checks
|
| 101 |
+
|
| 102 |
+
This module is a lightweight educational prototype. Bicubic super-resolution is an educational baseline, not a trained AI model or a clinical enhancement method.
|
| 103 |
+
|
| 104 |
+
## Module 3: Grant Alignment Lab
|
| 105 |
+
|
| 106 |
+
The Grant Alignment Lab frames BioMedVisionLab as a reusable visual evaluation workbench for biomedical imaging and multimodal biology research.
|
| 107 |
+
|
| 108 |
+
Features:
|
| 109 |
+
|
| 110 |
+
- Chemical Imaging Super-Resolution alignment card:
|
| 111 |
+
- low-resolution biomedical/chemical imaging challenge
|
| 112 |
+
- current support for degradation simulation, bicubic baseline, PSNR, SSIM, MSE, crop inspection, and artifact warnings
|
| 113 |
+
- future diffusion-based super-resolution, microscopy/pathology datasets, and SRS/Raman-style image support
|
| 114 |
+
- Hi-C Contact Map Visualization alignment card:
|
| 115 |
+
- sparse, noisy, high-dimensional image-like contact matrices
|
| 116 |
+
- current support for matrix visualization and resolution-enhancement concepts
|
| 117 |
+
- future real Hi-C/Micro-C upload, contact-map retrieval, and RNA-seq/ATAC-seq metadata integration
|
| 118 |
+
- Synthetic Hi-C contact map mini-demo:
|
| 119 |
+
- generates synthetic contact matrices with diagonal structure and loop-like bright spots
|
| 120 |
+
- simulates low-resolution contact maps and bicubic upscaling
|
| 121 |
+
- displays original, low-resolution, upscaled, and absolute difference maps
|
| 122 |
+
- reports Matrix MSE, Matrix PSNR, and optional Matrix SSIM
|
| 123 |
+
- exports the synthetic Hi-C demo report as CSV
|
| 124 |
+
- Upload support for `.npy` or `.csv` square contact matrices:
|
| 125 |
+
- validates square matrices
|
| 126 |
+
- safely normalizes values
|
| 127 |
+
- displays uploaded contact map, simulated low-resolution map, bicubic-upscaled map, and difference map
|
| 128 |
+
- exports Hi-C matrix evaluation metrics as CSV
|
| 129 |
+
- Adds a **Multimodal Exploration Hub** for matrix uploads, low/high-resolution matrix comparison, diagonal profiles, and future metadata links
|
| 130 |
+
|
| 131 |
+
The deployed app can therefore demonstrate radiology image retrieval, biomedical super-resolution baselines, and contact-map matrix visualization without requiring users to download the repository or install the original local dataset.
|
| 132 |
+
|
| 133 |
+
Synthetic contact maps are used only to demonstrate the visualization interface. They are not biological results.
|
| 134 |
+
Uploaded contact maps are visualized as matrix images. This prototype does not yet perform biological feature calling.
|
| 135 |
+
|
| 136 |
+
## Biomedical Imaging Relevance
|
| 137 |
+
|
| 138 |
+
BioMedVisionLab connects to common biomedical imaging research workflows:
|
| 139 |
+
|
| 140 |
+
- Chest X-ray dataset exploration
|
| 141 |
+
- Visual search and content-based medical image retrieval
|
| 142 |
+
- Representation learning with pretrained vision models
|
| 143 |
+
- Retrieval confidence and uncertainty-style summaries for visual search behavior
|
| 144 |
+
- Clinical-style retrieval triage and Top-k sensitivity checks for research review
|
| 145 |
+
- Comparing generic ImageNet embeddings with CXR-specific embeddings
|
| 146 |
+
- Batch measurement of retrieval quality across repeated query samples
|
| 147 |
+
- Query-vs-retrieved visual comparison with a non-clinical difference map
|
| 148 |
+
- Explainable AI previews and future Grad-CAM visual grounding
|
| 149 |
+
- Image degradation and reconstruction experiments
|
| 150 |
+
- Super-resolution metrics for biomedical imaging baselines
|
| 151 |
+
- Artifact-risk framing for educational super-resolution baselines
|
| 152 |
+
- Comparing future learned or diffusion-based super-resolution outputs against bicubic interpolation
|
| 153 |
+
- Chemical imaging super-resolution grant prototypes
|
| 154 |
+
- Chromatin contact-map visualization and retrieval concepts
|
| 155 |
+
- Multimodal biology workbench patterns for image-like matrices and metadata
|
| 156 |
+
- Educational demonstrations for biomedical AI portfolios
|
| 157 |
+
- Conceptual links to microscopy and pathology super-resolution
|
| 158 |
+
|
| 159 |
+
## Connection to Core Concepts
|
| 160 |
+
|
| 161 |
+
**Representation learning:** ResNet18, ResNet50, or TorchXRayVision converts images into numerical embeddings that summarize visual patterns.
|
| 162 |
+
|
| 163 |
+
**Image retrieval:** Cosine similarity compares embeddings and ranks images by visual resemblance.
|
| 164 |
+
|
| 165 |
+
**Explainable AI:** The dashboard includes a simple intensity heatmap preview as a non-diagnostic visualization placeholder. Future work can add Grad-CAM or other model-based visual grounding.
|
| 166 |
+
|
| 167 |
+
**Super-resolution:** The demo degrades an image and reconstructs it, illustrating why high-resolution structure matters in biomedical imaging.
|
| 168 |
+
|
| 169 |
+
## Dataset Instructions
|
| 170 |
+
|
| 171 |
+
Place a small public chest X-ray dataset inside:
|
| 172 |
+
|
| 173 |
+
```text
|
| 174 |
+
data/chest_xray/
|
| 175 |
+
train/
|
| 176 |
+
NORMAL/
|
| 177 |
+
PNEUMONIA/
|
| 178 |
+
test/
|
| 179 |
+
NORMAL/
|
| 180 |
+
PNEUMONIA/
|
| 181 |
+
val/
|
| 182 |
+
NORMAL/
|
| 183 |
+
PNEUMONIA/
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
The app also works if only `train/` and `test/` exist.
|
| 187 |
+
|
| 188 |
+
Supported image formats:
|
| 189 |
+
|
| 190 |
+
- `.jpg`
|
| 191 |
+
- `.jpeg`
|
| 192 |
+
- `.png`
|
| 193 |
+
|
| 194 |
+
## Setup Commands
|
| 195 |
+
|
| 196 |
+
Create and activate a virtual environment:
|
| 197 |
+
|
| 198 |
+
```bash
|
| 199 |
+
python -m venv venv
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
+
On Windows:
|
| 203 |
+
|
| 204 |
+
```bash
|
| 205 |
+
venv\Scripts\activate
|
| 206 |
+
```
|
| 207 |
+
|
| 208 |
+
On macOS or Linux:
|
| 209 |
+
|
| 210 |
+
```bash
|
| 211 |
+
source venv/bin/activate
|
| 212 |
+
```
|
| 213 |
+
|
| 214 |
+
Install dependencies:
|
| 215 |
+
|
| 216 |
+
```bash
|
| 217 |
+
pip install -r requirements.txt
|
| 218 |
+
```
|
| 219 |
+
|
| 220 |
+
Build the CXR retrieval index with the default generic ResNet18 ImageNet encoder:
|
| 221 |
+
|
| 222 |
+
```bash
|
| 223 |
+
python build_index.py --encoder resnet18_imagenet
|
| 224 |
+
```
|
| 225 |
+
|
| 226 |
+
Build an optional TorchXRayVision DenseNet121 CXR-specific index:
|
| 227 |
+
|
| 228 |
+
```bash
|
| 229 |
+
python build_index.py --encoder torchxrayvision_densenet121
|
| 230 |
+
```
|
| 231 |
+
|
| 232 |
+
Indexes are stored separately:
|
| 233 |
+
|
| 234 |
+
```text
|
| 235 |
+
embeddings/
|
| 236 |
+
resnet18_imagenet/
|
| 237 |
+
resnet50_imagenet/
|
| 238 |
+
torchxrayvision_densenet121/
|
| 239 |
+
```
|
| 240 |
+
|
| 241 |
+
Build the deeper generic ResNet50 ImageNet encoder:
|
| 242 |
+
|
| 243 |
+
```bash
|
| 244 |
+
python build_index.py --encoder resnet50_imagenet
|
| 245 |
+
```
|
| 246 |
+
|
| 247 |
+
ResNet18 is a lightweight generic natural-image baseline pretrained on ImageNet. ResNet50 is a deeper generic ImageNet baseline with a 2048-dimensional penultimate feature vector. TorchXRayVision DenseNet121 is pretrained for chest X-ray tasks and is included to support research comparisons between generic and CXR-specific visual representations.
|
| 248 |
+
|
| 249 |
+
Run the dashboard:
|
| 250 |
+
|
| 251 |
+
```bash
|
| 252 |
+
python -m streamlit run app.py
|
| 253 |
+
```
|
| 254 |
+
|
| 255 |
+
If `streamlit` is available on your PATH, this also works:
|
| 256 |
+
|
| 257 |
+
```bash
|
| 258 |
+
streamlit run app.py
|
| 259 |
+
```
|
| 260 |
+
|
| 261 |
+
## Deployment on Hugging Face Spaces
|
| 262 |
+
|
| 263 |
+
BioMedVisionLab is prepared for Hugging Face Spaces as a Streamlit app. The main entry file is:
|
| 264 |
+
|
| 265 |
+
```text
|
| 266 |
+
app.py
|
| 267 |
+
```
|
| 268 |
+
|
| 269 |
+
The Streamlit configuration is stored in:
|
| 270 |
+
|
| 271 |
+
```text
|
| 272 |
+
.streamlit/config.toml
|
| 273 |
+
```
|
| 274 |
+
|
| 275 |
+
with a `500 MB` upload limit and browser usage-stat collection disabled.
|
| 276 |
+
|
| 277 |
+
### Run Locally
|
| 278 |
+
|
| 279 |
+
Install dependencies and start the app:
|
| 280 |
+
|
| 281 |
+
```bash
|
| 282 |
+
pip install -r requirements.txt
|
| 283 |
+
python -m streamlit run app.py
|
| 284 |
+
```
|
| 285 |
+
|
| 286 |
+
Local CXR indexes are optional. If no embeddings are present, the app opens in upload-only mode for query/gallery retrieval, image super-resolution, and contact-map visualization.
|
| 287 |
+
|
| 288 |
+
### Deploy to Hugging Face Spaces
|
| 289 |
+
|
| 290 |
+
1. Create a new Hugging Face Space.
|
| 291 |
+
2. Select **Streamlit** as the Space SDK.
|
| 292 |
+
3. Upload or push the repository files, including:
|
| 293 |
+
- `app.py`
|
| 294 |
+
- `requirements.txt`
|
| 295 |
+
- `.streamlit/config.toml`
|
| 296 |
+
- `README.md`
|
| 297 |
+
4. Spaces will install dependencies from `requirements.txt` and run the Streamlit app from `app.py`.
|
| 298 |
+
5. Optional local embedding folders can be added under `embeddings/`, but deployed demos can also run without them through upload-first workflows.
|
| 299 |
+
|
| 300 |
+
### Deployment Limitations
|
| 301 |
+
|
| 302 |
+
- Uploaded files are temporary and are not saved as a persistent dataset.
|
| 303 |
+
- Large datasets should be uploaded as smaller representative samples.
|
| 304 |
+
- The app is not for diagnosis, triage, treatment, or clinical decision-making.
|
| 305 |
+
- Heavy diffusion-model training or large-scale model inference is not supported in this demo.
|
| 306 |
+
- If TorchXRayVision is unavailable, uploaded-image embedding falls back to ResNet18 and the app shows a warning.
|
| 307 |
+
- If `cooler` is unavailable, `.cool` and `.mcool` upload is disabled with a helpful message; `.csv` and `.npy` contact matrices still work.
|
| 308 |
+
|
| 309 |
+
## Method Details
|
| 310 |
+
|
| 311 |
+
For CXR retrieval, each image is:
|
| 312 |
+
|
| 313 |
+
1. Converted to RGB
|
| 314 |
+
2. Resized to `224x224`
|
| 315 |
+
3. Normalized with ImageNet mean and standard deviation
|
| 316 |
+
4. Passed through pretrained ResNet18 with the classifier removed
|
| 317 |
+
5. Converted into a 512-dimensional embedding
|
| 318 |
+
6. L2-normalized for cosine similarity search
|
| 319 |
+
7. Compared with indexed embeddings using cosine similarity
|
| 320 |
+
8. Summarized with rank-based similarity, label agreement, Precision@K, and mean similarity
|
| 321 |
+
9. Reviewed with model-card metadata, retrieval triage, Top-k sensitivity, and optional visual comparison
|
| 322 |
+
10. Exported as a CSV experiment report
|
| 323 |
+
|
| 324 |
+
Batch retrieval evaluation samples labeled dataset queries, retrieves Top-k neighbors while excluding the query image, and exports one row per retrieved result. This makes CSV exports useful for comparing generic and CXR-specific encoders.
|
| 325 |
+
|
| 326 |
+
Retrieval confidence is a heuristic:
|
| 327 |
+
|
| 328 |
+
- High confidence for dataset queries if top similarity is at least `0.90` and Precision@K is at least `0.80`
|
| 329 |
+
- Medium confidence if top similarity is at least `0.80`
|
| 330 |
+
- Low confidence otherwise
|
| 331 |
+
- Uploaded images use top similarity only because query-label agreement is unavailable
|
| 332 |
+
|
| 333 |
+
For super-resolution, each image is:
|
| 334 |
+
|
| 335 |
+
1. Converted safely to RGB
|
| 336 |
+
2. Downsampled by `2x` or `4x`
|
| 337 |
+
3. Reconstructed to original size using bicubic interpolation
|
| 338 |
+
4. Compared against the original image using MSE, PSNR, and optional SSIM
|
| 339 |
+
5. Displayed with center-crop comparisons for local detail inspection
|
| 340 |
+
6. Reviewed with artifact-risk and baseline-method cards
|
| 341 |
+
7. Exported as a CSV experiment report
|
| 342 |
+
|
| 343 |
+
ChemDiffuse-style comparison allows an externally generated enhanced image to be uploaded and compared against the original image and bicubic baseline using PSNR, SSIM, and MSE.
|
| 344 |
+
|
| 345 |
+
For the synthetic Hi-C mini-demo:
|
| 346 |
+
|
| 347 |
+
1. A synthetic square contact matrix is generated with diagonal structure and loop-like bright spots.
|
| 348 |
+
2. Noise is added using the dashboard slider.
|
| 349 |
+
3. The matrix is downsampled by `2x` or `4x`.
|
| 350 |
+
4. The low-resolution matrix is bicubic-upscaled to the original matrix size.
|
| 351 |
+
5. Matrix MSE, Matrix PSNR, and optional Matrix SSIM are reported.
|
| 352 |
+
6. The demo can be exported as a CSV report.
|
| 353 |
+
|
| 354 |
+
## Limitations
|
| 355 |
+
|
| 356 |
+
- ResNet18 and ResNet50 are pretrained on natural images, not chest X-rays.
|
| 357 |
+
- TorchXRayVision support requires optional dependency installation and a separately built index.
|
| 358 |
+
- Retrieval results show visual similarity, not clinical equivalence.
|
| 359 |
+
- Retrieval confidence is not clinical confidence.
|
| 360 |
+
- Retrieval triage is a research dashboard summary, not clinical triage.
|
| 361 |
+
- Query-vs-retrieved difference maps are visual comparison aids, not finding maps.
|
| 362 |
+
- Folder labels are used only for visualization.
|
| 363 |
+
- The intensity heatmap is not Grad-CAM and does not identify pathology.
|
| 364 |
+
- Bicubic interpolation is a simple baseline, not a learned super-resolution model.
|
| 365 |
+
- Bicubic interpolation may smooth fine detail and cannot recover true missing anatomical structure.
|
| 366 |
+
- Synthetic Hi-C contact maps are interface demonstrations only and are not biological results.
|
| 367 |
+
- Uploaded Hi-C/contact matrices are visualized as matrix images and are not biologically interpreted.
|
| 368 |
+
- PSNR is a basic image-quality metric and does not measure clinical usefulness.
|
| 369 |
+
- SSIM is an image-similarity metric and does not measure clinical usefulness.
|
| 370 |
+
- The project should not be used for diagnosis, triage, treatment, or clinical decision-making.
|
| 371 |
+
|
| 372 |
+
## CXR Retrieval Research Extension Roadmap
|
| 373 |
+
|
| 374 |
+
The Research Extension Roadmap is shown only in the CXR Retrieval tab. The Super-Resolution and Grant Alignment tabs have their own exploration sections instead of repeating the same roadmap.
|
| 375 |
+
|
| 376 |
+
- CXR-specific encoders such as TorchXRayVision or medical foundation models
|
| 377 |
+
- Grad-CAM visual grounding
|
| 378 |
+
- Report-aware retrieval using radiology text
|
| 379 |
+
- Microscopy/pathology super-resolution datasets
|
| 380 |
+
- Diffusion-based biomedical image enhancement
|
| 381 |
+
- Learned super-resolution models such as SRCNN, EDSR, or SwinIR variants
|
| 382 |
+
- Metadata-aware retrieval filters
|
| 383 |
+
- Hi-C contact map visualization as a future biomedical data visualization extension
|
| 384 |
+
- Real Hi-C/Micro-C upload, contact-map retrieval, and omics metadata integration
|
| 385 |
+
- Chemical imaging super-resolution with SRS/Raman-style data
|
| 386 |
+
- Grant-ready experiment report templates
|
| 387 |
+
|
| 388 |
+
## Disclaimer
|
| 389 |
+
|
| 390 |
+
BioMedVisionLab is a research visualization prototype and is not intended for medical diagnosis or clinical decision-making.
|
| 391 |
+
|
| 392 |
+
- Retrieval confidence is not diagnostic or clinical confidence.
|
| 393 |
+
- The intensity heatmap preview is not Grad-CAM.
|
| 394 |
+
- Bicubic super-resolution is an educational baseline, not a trained AI model.
|
| 395 |
+
- Synthetic Hi-C contact maps are not biological results.
|
| 396 |
+
|
| 397 |
+
## Suggested LinkedIn Project Description
|
| 398 |
+
|
| 399 |
+
Built **BioMedVisionLab**, a reusable biomedical visual evaluation workbench in Python and Streamlit. The app includes chest X-ray similarity retrieval using pretrained ResNet18, ResNet50, and optional TorchXRayVision embeddings with cosine similarity, retrieval triage, Top-k sensitivity, query-vs-result comparison, experiment report export, an educational super-resolution demo with MSE, PSNR, SSIM, artifact-risk framing, and a Grant Alignment Lab with a synthetic Hi-C contact-map mini-demo. The project highlights medical image retrieval, chemical imaging super-resolution, explainable AI, and chromatin contact-map visualization concepts for research and portfolio demonstration, not clinical diagnosis or biological interpretation.
|
app.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
build_index.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import json
|
| 3 |
+
from datetime import datetime, timezone
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch
|
| 8 |
+
import torch.nn as nn
|
| 9 |
+
from PIL import Image, UnidentifiedImageError
|
| 10 |
+
from torchvision import models, transforms
|
| 11 |
+
|
| 12 |
+
try:
|
| 13 |
+
import torchxrayvision as xrv
|
| 14 |
+
except ImportError:
|
| 15 |
+
xrv = None
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
DATA_DIR = Path("data/chest_xray")
|
| 19 |
+
EMBEDDINGS_ROOT = Path("embeddings")
|
| 20 |
+
ENCODER_TYPE = "resnet18_imagenet"
|
| 21 |
+
VALID_EXTENSIONS = {".jpg", ".jpeg", ".png"}
|
| 22 |
+
VALID_LABELS = {"NORMAL", "PNEUMONIA"}
|
| 23 |
+
|
| 24 |
+
IMAGENET_MEAN = [0.485, 0.456, 0.406]
|
| 25 |
+
IMAGENET_STD = [0.229, 0.224, 0.225]
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class EncoderBundle:
|
| 29 |
+
def __init__(self, key: str, name: str, model: nn.Module, transform: transforms.Compose, output_dim: int):
|
| 30 |
+
self.key = key
|
| 31 |
+
self.name = name
|
| 32 |
+
self.model = model
|
| 33 |
+
self.transform = transform
|
| 34 |
+
self.output_dim = output_dim
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
ENCODER_CONFIGS = {
|
| 38 |
+
"resnet18_imagenet": {
|
| 39 |
+
"name": "ResNet18 ImageNet baseline",
|
| 40 |
+
"output_dim": 512,
|
| 41 |
+
},
|
| 42 |
+
"resnet50_imagenet": {
|
| 43 |
+
"name": "ResNet50 ImageNet baseline",
|
| 44 |
+
"output_dim": 2048,
|
| 45 |
+
},
|
| 46 |
+
"torchxrayvision_densenet121": {
|
| 47 |
+
"name": "TorchXRayVision DenseNet121 CXR encoder",
|
| 48 |
+
"output_dim": 1024,
|
| 49 |
+
},
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def canonical_encoder_name(encoder_name: str) -> str:
|
| 54 |
+
aliases = {
|
| 55 |
+
"resnet18": "resnet18_imagenet",
|
| 56 |
+
"resnet50": "resnet50_imagenet",
|
| 57 |
+
"torchxrayvision": "torchxrayvision_densenet121",
|
| 58 |
+
}
|
| 59 |
+
return aliases.get(encoder_name, encoder_name)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def find_image_files(data_dir: Path) -> list[Path]:
|
| 63 |
+
"""Find supported image files that live under NORMAL or PNEUMONIA folders."""
|
| 64 |
+
image_files = []
|
| 65 |
+
|
| 66 |
+
for path in data_dir.rglob("*"):
|
| 67 |
+
if not path.is_file() or path.suffix.lower() not in VALID_EXTENSIONS:
|
| 68 |
+
continue
|
| 69 |
+
|
| 70 |
+
if infer_label(path) in VALID_LABELS:
|
| 71 |
+
image_files.append(path)
|
| 72 |
+
|
| 73 |
+
return sorted(image_files)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def infer_label(image_path: Path) -> str:
|
| 77 |
+
"""Infer a chest X-ray label from any parent folder named NORMAL or PNEUMONIA."""
|
| 78 |
+
for parent in image_path.parents:
|
| 79 |
+
label = parent.name.upper()
|
| 80 |
+
if label in VALID_LABELS:
|
| 81 |
+
return label
|
| 82 |
+
return "UNKNOWN"
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def get_resnet_transform() -> transforms.Compose:
|
| 86 |
+
return transforms.Compose(
|
| 87 |
+
[
|
| 88 |
+
transforms.Resize((224, 224)),
|
| 89 |
+
transforms.Grayscale(num_output_channels=3),
|
| 90 |
+
transforms.ToTensor(),
|
| 91 |
+
transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
|
| 92 |
+
]
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def get_xrv_transform() -> transforms.Compose:
|
| 97 |
+
return transforms.Compose(
|
| 98 |
+
[
|
| 99 |
+
transforms.Resize((224, 224)),
|
| 100 |
+
transforms.Grayscale(num_output_channels=1),
|
| 101 |
+
transforms.ToTensor(),
|
| 102 |
+
transforms.Lambda(lambda tensor: (tensor * 2048.0) - 1024.0),
|
| 103 |
+
]
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def load_encoder(encoder_name: str, device: torch.device | None = None) -> EncoderBundle:
|
| 108 |
+
encoder_name = canonical_encoder_name(encoder_name)
|
| 109 |
+
device = device or torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 110 |
+
if encoder_name == "torchxrayvision_densenet121":
|
| 111 |
+
if xrv is None:
|
| 112 |
+
raise ImportError(
|
| 113 |
+
"torchxrayvision is not installed. Install requirements or use --encoder resnet18_imagenet."
|
| 114 |
+
)
|
| 115 |
+
model = xrv.models.DenseNet(weights="densenet121-res224-all")
|
| 116 |
+
model.op_threshs = None
|
| 117 |
+
model.to(device)
|
| 118 |
+
model.eval()
|
| 119 |
+
return EncoderBundle(
|
| 120 |
+
key="torchxrayvision_densenet121",
|
| 121 |
+
name="TorchXRayVision DenseNet121 CXR encoder",
|
| 122 |
+
model=model,
|
| 123 |
+
transform=get_xrv_transform(),
|
| 124 |
+
output_dim=1024,
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
if encoder_name == "resnet50_imagenet":
|
| 128 |
+
weights = models.ResNet50_Weights.DEFAULT
|
| 129 |
+
model = models.resnet50(weights=weights)
|
| 130 |
+
output_dim = 2048
|
| 131 |
+
encoder_label = "ResNet50 ImageNet baseline"
|
| 132 |
+
else:
|
| 133 |
+
weights = models.ResNet18_Weights.DEFAULT
|
| 134 |
+
model = models.resnet18(weights=weights)
|
| 135 |
+
output_dim = 512
|
| 136 |
+
encoder_label = "ResNet18 ImageNet baseline"
|
| 137 |
+
model.fc = nn.Identity()
|
| 138 |
+
model.to(device)
|
| 139 |
+
model.eval()
|
| 140 |
+
return EncoderBundle(
|
| 141 |
+
key=encoder_name,
|
| 142 |
+
name=encoder_label,
|
| 143 |
+
model=model,
|
| 144 |
+
transform=get_resnet_transform(),
|
| 145 |
+
output_dim=output_dim,
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def preprocess_image_for_encoder(image: Image.Image, encoder_name: str) -> torch.Tensor:
|
| 150 |
+
encoder_name = canonical_encoder_name(encoder_name)
|
| 151 |
+
transform = get_xrv_transform() if encoder_name == "torchxrayvision_densenet121" else get_resnet_transform()
|
| 152 |
+
return transform(image).unsqueeze(0)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def load_rgb_image(image_path: Path) -> Image.Image:
|
| 156 |
+
"""Open a chest X-ray and convert grayscale images to 3-channel RGB."""
|
| 157 |
+
return Image.open(image_path).convert("RGB")
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def extract_embedding(
|
| 161 |
+
image: Image.Image,
|
| 162 |
+
encoder_name: str,
|
| 163 |
+
encoder: EncoderBundle | None = None,
|
| 164 |
+
device: torch.device | None = None,
|
| 165 |
+
) -> np.ndarray:
|
| 166 |
+
"""Extract one L2-normalized embedding."""
|
| 167 |
+
encoder_name = canonical_encoder_name(encoder_name)
|
| 168 |
+
device = device or torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 169 |
+
encoder = encoder or load_encoder(encoder_name, device)
|
| 170 |
+
image_tensor = preprocess_image_for_encoder(image, encoder_name).to(device)
|
| 171 |
+
|
| 172 |
+
with torch.no_grad():
|
| 173 |
+
if encoder_name == "torchxrayvision_densenet121":
|
| 174 |
+
features = encoder.model.features(image_tensor)
|
| 175 |
+
features = torch.nn.functional.relu(features, inplace=False)
|
| 176 |
+
features = torch.nn.functional.adaptive_avg_pool2d(features, (1, 1))
|
| 177 |
+
embedding = torch.flatten(features, 1).cpu().numpy()[0]
|
| 178 |
+
else:
|
| 179 |
+
embedding = encoder.model(image_tensor).cpu().numpy()[0]
|
| 180 |
+
|
| 181 |
+
norm = np.linalg.norm(embedding)
|
| 182 |
+
if norm > 0:
|
| 183 |
+
embedding = embedding / norm
|
| 184 |
+
|
| 185 |
+
return embedding.astype(np.float32)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def extract_embedding_from_path(
|
| 189 |
+
image_path: Path,
|
| 190 |
+
encoder: EncoderBundle,
|
| 191 |
+
device: torch.device,
|
| 192 |
+
) -> np.ndarray | None:
|
| 193 |
+
try:
|
| 194 |
+
image = load_rgb_image(image_path)
|
| 195 |
+
except (OSError, UnidentifiedImageError) as error:
|
| 196 |
+
print(f"Skipping unreadable image: {image_path} ({error})")
|
| 197 |
+
return None
|
| 198 |
+
return extract_embedding(image, encoder.key, encoder, device)
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def encoder_feature_note(encoder_key: str) -> str:
|
| 202 |
+
if encoder_key == "torchxrayvision_densenet121":
|
| 203 |
+
return "TorchXRayVision DenseNet121 intermediate convolutional features with global average pooling."
|
| 204 |
+
if encoder_key == "resnet50_imagenet":
|
| 205 |
+
return "ResNet50 penultimate 2048-D feature vector."
|
| 206 |
+
return "ResNet18 penultimate 512-D feature vector."
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def build_index(encoder_type: str) -> None:
|
| 210 |
+
encoder_type = canonical_encoder_name(encoder_type)
|
| 211 |
+
if not DATA_DIR.exists():
|
| 212 |
+
raise FileNotFoundError(
|
| 213 |
+
"Dataset folder was not found. Please place images under data/chest_xray/ "
|
| 214 |
+
"with folders such as train/NORMAL and train/PNEUMONIA."
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
image_files = find_image_files(DATA_DIR)
|
| 218 |
+
if not image_files:
|
| 219 |
+
raise ValueError(
|
| 220 |
+
"No JPG, JPEG, or PNG images were found under data/chest_xray inside "
|
| 221 |
+
"NORMAL or PNEUMONIA folders."
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 225 |
+
print(f"Using device: {device}")
|
| 226 |
+
|
| 227 |
+
encoder = load_encoder(encoder_type, device)
|
| 228 |
+
print(f"Using encoder: {encoder.name}")
|
| 229 |
+
|
| 230 |
+
indexed_paths = []
|
| 231 |
+
labels = []
|
| 232 |
+
embeddings = []
|
| 233 |
+
|
| 234 |
+
for image_path in image_files:
|
| 235 |
+
embedding = extract_embedding_from_path(image_path, encoder, device)
|
| 236 |
+
if embedding is None:
|
| 237 |
+
continue
|
| 238 |
+
|
| 239 |
+
indexed_paths.append(str(image_path))
|
| 240 |
+
labels.append(infer_label(image_path))
|
| 241 |
+
embeddings.append(embedding)
|
| 242 |
+
|
| 243 |
+
if not embeddings:
|
| 244 |
+
raise ValueError("Images were found, but none could be read successfully.")
|
| 245 |
+
|
| 246 |
+
output_dir = EMBEDDINGS_ROOT / encoder.key
|
| 247 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 248 |
+
np.save(output_dir / "image_paths.npy", np.array(indexed_paths, dtype=object))
|
| 249 |
+
np.save(output_dir / "labels.npy", np.array(labels, dtype=object))
|
| 250 |
+
np.save(output_dir / "embeddings.npy", np.vstack(embeddings).astype(np.float32))
|
| 251 |
+
|
| 252 |
+
metadata = {
|
| 253 |
+
"encoder_key": encoder.key,
|
| 254 |
+
"encoder_name": encoder.name,
|
| 255 |
+
"embedding_dimension": int(np.vstack(embeddings).shape[1]),
|
| 256 |
+
"dataset_size": len(indexed_paths),
|
| 257 |
+
"build_time": datetime.now(timezone.utc).isoformat(),
|
| 258 |
+
"device": str(device),
|
| 259 |
+
"feature_note": encoder_feature_note(encoder.key),
|
| 260 |
+
}
|
| 261 |
+
(output_dir / "metadata.json").write_text(json.dumps(metadata, indent=2), encoding="utf-8")
|
| 262 |
+
|
| 263 |
+
print(f"Indexed {len(indexed_paths)} images.")
|
| 264 |
+
print(f"Saved embeddings to: {output_dir.resolve()}")
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def parse_args() -> argparse.Namespace:
|
| 268 |
+
parser = argparse.ArgumentParser(description="Build BioMedVisionLab CXR embedding index.")
|
| 269 |
+
parser.add_argument(
|
| 270 |
+
"--encoder",
|
| 271 |
+
choices=[
|
| 272 |
+
"resnet18_imagenet",
|
| 273 |
+
"resnet50_imagenet",
|
| 274 |
+
"torchxrayvision_densenet121",
|
| 275 |
+
"resnet18",
|
| 276 |
+
"resnet50",
|
| 277 |
+
"torchxrayvision",
|
| 278 |
+
],
|
| 279 |
+
default=ENCODER_TYPE,
|
| 280 |
+
help="Encoder used to build embeddings.",
|
| 281 |
+
)
|
| 282 |
+
return parser.parse_args()
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
if __name__ == "__main__":
|
| 286 |
+
args = parse_args()
|
| 287 |
+
try:
|
| 288 |
+
build_index(args.encoder)
|
| 289 |
+
except (FileNotFoundError, ValueError, ImportError) as error:
|
| 290 |
+
print(f"Error: {error}")
|
| 291 |
+
raise SystemExit(1)
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit
|
| 2 |
+
torch
|
| 3 |
+
torchvision
|
| 4 |
+
Pillow
|
| 5 |
+
numpy
|
| 6 |
+
pandas
|
| 7 |
+
scikit-image
|
| 8 |
+
altair
|
| 9 |
+
torchxrayvision
|
| 10 |
+
cooler
|