The Problem
Two weeks ago, I got a knee MRI after a Brazilian Jiu-Jitsu injury (a "knee reaping" - massive valgus stress + external rotation). My doctor sent me a link to view my images on an easyRadiology portal.
I clicked the link. Login screen. Entered my code. The viewer loaded. My knee MRI appeared on screen.
But here's the thing - I wanted to download my own images and analyze them. The portal had a download button, but the files were completely encrypted. I couldn't open them in any DICOM viewer.
So I did what any engineer would do: I opened DevTools.
Reverse-Engineering the Encryption
The portal uses a multi-layer encryption scheme:
-
Access code (
K6P-8ZT-M9X-9JE) is split into aViewCodeNameand a password - A scrypt key verification proves you know the password without sending it
- The server returns an encrypted access key (AES-CBC with scrypt-derived key)
- This access key decrypts the patient data JSON (which contains yet another password)
- That final password decrypts the actual DICOM image data (AES-256, WinZip format)
Access Code -> scrypt -> KeyVerification -> API -> Encrypted AccessKey
|
scrypt + AES-CBC
|
AccessKey (plain)
|
Decrypt PatientData JSON
|
PasswordForDicomZip
|
AES-256 decrypt DICOM entries
I traced every network request in Playwright, read the minified JavaScript, and implemented the full decryption chain in Python:
# Derive key using scrypt (matching the portal's JS implementation)
key = hashlib.scrypt(password.encode('utf-8'), salt=salt, n=16384, r=8, p=1, dklen=32)
# Decrypt with AES-CBC
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(ciphertext)
# Result: {"PasswordForDicomZip": "rqNRwRnB-aU6H4fM3-..."}
140 DICOM files decrypted. My knee images, finally accessible.
From Decryption to AI Analysis
With the images in hand, I thought: what if I could analyze them automatically?
Step 1: Local ML Analysis
I used a pretrained ResNet18 (ImageNet weights) as a feature extractor. For each MRI slice:
- Extract a 512-dimensional feature vector
- Compute anomaly scores (distance from series mean)
- Analyze signal intensity patterns (high signal on PD FS = fluid/edema)
- Identify the top suspicious slices
# Feature extraction per slice
features = resnet18_features(volume) # (28, 512)
anomaly_scores = normalized_distance_from_mean(features) # (28,)
top_slices = argsort(anomaly_scores)[-5:] # Most suspicious
Step 2: Vision-LLM Analysis
Then I fed the key slices to Claude Opus 4.7 with a structured medical prompt including my clinical context:
"Knee Reaping in BJJ 10 days ago. Valgus stress + external rotation. Audible pop. Point tenderness at tibial MCL insertion."
Claude analyzed each structure systematically - ACL, MCL, meniscus, bone bruise, cartilage, effusion - and returned structured JSON findings.
Result: MCL Grade I-II at the tibial insertion (matching my point tenderness exactly), intact ACL, no bone bruise, mild effusion. The clinical correlation noted that the "pop" was unusual for an isolated MCL injury.
Step 3: Professional PDF Report
I generated a 6-page PDF with:
- Annotated MRI images (arrows, circles, color-coded findings)
- Per-structure findings tables
- Traffic-light summary (normal/borderline/pathological)
- Clinical correlation and recommendations
Making It Open-Source: MedCheck
I realized this could help others. So I packaged everything into MedCheck - an open-source toolkit that anyone can use.
Architecture
Ingest -> Preprocess -> ML Analysis -> Vision AI -> Report
| | | | |
v v v v v
DICOM Normalize ResNet18 Claude/GPT PDF/HTML
Portal Detect Anomaly GPT-5.5 Annotated
Local anatomy scores Gemini images
Key Features
- Multiple data sources: Local DICOM files, ZIP archives, or fetch directly from radiology portals
- Local ML: ResNet18 anomaly detection - no API key needed
- Vision-LLMs: Claude Opus 4.7, GPT-5.5, Gemini 3.5 Flash with automatic fallback
- Clinical context: Input your symptoms and trauma history for targeted analysis
- Professional reports: PDF with annotated images, HTML for interactive viewing, JSON for APIs
-
Docker-ready:
docker runand open your browser - YAML workflows: Define custom analysis pipelines
Quick Start
# Install
pip install medcheck
# Analyze local DICOM files
medcheck analyze ./my-dicom-folder \
--symptoms "Medial knee pain" \
--trauma "Valgus stress injury" \
--model claude \
--report pdf
# Or use Docker
docker run -p 8080:8080 \
-e ANTHROPIC_API_KEY=sk-... \
ghcr.io/liohtml/medcheck:lite
Supported Anatomy
| Region | Structures Analyzed |
|---|---|
| Knee | ACL, PCL, MCL, LCL, menisci, cartilage, bone bruise, effusion |
| Shoulder | Rotator cuff, labrum, biceps tendon, AC joint |
| Spine | Discs, stenosis, foramina, vertebral bodies, ligaments |
| More coming | Hip (#10), Ankle (#11), Wrist (#12) |
Want to Contribute?
MedCheck has 9 open issues labeled good first issue - perfect for first-time contributors:
- Add anatomy templates (hip, ankle, wrist) - if you know anatomy, you can help
- New data providers (Orthanc, DICOMweb) - if you work with DICOM servers
- Translations (French, Spanish) - if you speak these languages
- Test coverage - always welcome
- Local LLM (LLaVA-Med) - for fully offline analysis
# Get started in 30 seconds
git clone https://github.com/Liohtml/MedCheck.git
cd MedCheck
uv sync --all-extras
uv run pytest # 65 tests, all passing
Important Disclaimer
MedCheck is NOT a medical device. It's a research and educational tool. All analysis results must be verified by a qualified radiologist. Don't make medical decisions based solely on MedCheck output.
Links
- GitHub: github.com/Liohtml/MedCheck
- PyPI: pypi.org/project/medcheck
- Discussion: GitHub Discussions
Have you worked with medical imaging or DICOM data? I'd love to hear about your experiences in the comments.
Top comments (0)