Skip to content

Clinical Image Analysis Pathway — Design Spec

Issue: curaway-ai/curaway-backend#347 Date: 2026-04-23 Scope: Distinguish clinical photos (wounds, X-rays) from document scans in the OCR pipeline. Add observational analysis for clinical images.


Problem Statement

When patients upload clinical photos (wound images, X-rays, ultrasound screenshots), the OCR pipeline treats them as documents to read text from. A wound photo has no text — OCR finds nothing — observation_count: 0. The agent then claims to have "reviewed reports" with no data.

Real case: CRW-2026-00310 — patient uploaded 12 WhatsApp images including wound photos of venous leg ulcers. All yielded 0 observations. The agent fabricated laterality ("right iliac vein" when patient had left).

What Clinical Image Observations Look Like

For a wound photo like the one from CRW-2026-00310, the system should capture:

Clinical Photo Observations:
- Body region: Lower leg / ankle
- Laterality: Left (based on patient context)
- Image type: Clinical photo (wound)
- Observations:
  • Severe skin breakdown with multiple open wounds
  • Discoloration consistent with venous insufficiency (dark/purplish skin changes)
  • Active wound discharge visible
  • Tissue maceration and peeling
  • Clinical setting visible (hospital bed, IV equipment)
- Image quality: Clear

These observations are descriptive, not diagnostic. They describe what is visually present. The triage agent can reference them: "I can see the wound photos showing skin breakdown on your left leg." A provider reviewing the case gets visual context alongside the medical reports.


Design Decisions

1. Detection: How to distinguish clinical photos from document scans

After OCR returns insufficient text from an image (< 10 words), classify the image: - Document scan (lab report, prescription, doctor's note photographed) → re-attempt with enhanced OCR / different tier - Clinical photo (wound, X-ray, scan, physical exam) → route to clinical observation pathway

Detection method: Claude Vision with a classification prompt. The same vision call that observes the image can also classify it. One LLM call, not two.

2. Observation prompt: What language to use

CRITICAL — Curaway does NOT practice medicine. The observation must be: - Descriptive: "visible wound with discharge" — NOT "infected venous ulcer" - Observational: "skin discoloration" — NOT "cyanosis" or "venous stasis" - Neutral: "multiple open areas" — NOT "Stage 3 pressure injury"

However, the observations must be clinically useful for providers. Purely lay descriptions like "owie on leg" are useless. The sweet spot is informed observation without diagnosis:

Too diagnostic (forbidden) Right level (observational) Too vague (useless)
"Venous leg ulcer" "Open wounds on lower leg with surrounding discoloration" "Hurt leg"
"Grade 3 infection" "Active discharge visible from wound sites" "Something wrong"
"DVT indicated" "Visible swelling of the lower extremity" "Swollen"

3. EXIF stripping: Privacy protection

Patient photos from phones contain EXIF metadata: GPS coordinates, device model, timestamp, sometimes user name. Must strip before storage.

  • Strip on upload, before R2 storage
  • Use Pillow if available, graceful fallback if not
  • Log that stripping occurred (audit trail)

4. Camera capture on mobile

Current state: <input type="file"> with no capture attribute opens file picker (gallery, camera, or files). This is correct as the default.

For wound/clinical photos specifically: Consider adding a separate "Take photo" button alongside "Attach file" in a future iteration. NOT in this spec — this is backend + pipeline work only.

5. How observations flow to the triage agent

Observations stored as clinical_image_observation entities in extracted_data. The existing _get_extracted_findings_summary() (in intake.py) reads extracted_entities from documents — clinical observations flow through the same channel.

The triage agent receives:

[Clinical findings from uploaded documents]
--- wound_photo.jpg (clinical photo) ---
  - Clinical photo: lower leg / ankle (left)
    • Severe skin breakdown with multiple open wounds
    • Discoloration consistent with venous changes
    • Active wound discharge visible
    • Tissue maceration and peeling
    • Clinical setting (hospital bed, IV equipment)

6. Feature flag

clinical_image_analysis — default: enabled. Disable via Flagsmith or env var FF_CLINICAL_IMAGE_ANALYSIS=false.


Architecture

New Components

Component Location Responsibility
strip_exif() app/integrations/image_utils.py (new) Strip EXIF metadata from images
analyze_clinical_image() app/integrations/claude_pdf_extractor.py Claude Vision clinical observation prompt

Modified Components

Component Changes
app/routers/internal.py After OCR fails on image: route to clinical photo analysis. EXIF strip on upload.
app/agents/orchestrator_phases/intake.py Handle clinical_image_observation entity type in findings summary

Processing Flow

Image uploaded
Strip EXIF metadata
Image OCR (Claude Vision — extract text)
Sufficient text? (≥100 chars + ≥10 words)
  ├── YES → Regular post-OCR pipeline (Clinical Context Agent → FHIR)
  └── NO → Is clinical_image_analysis flag enabled?
        ├── NO → Mark as low_quality (existing behavior)
        └── YES → Claude Vision clinical observation prompt
            Returns image_type?
              ├── "other" (not medical) → Mark as low_quality
              └── Clinical type → Store observations as extracted_entities
                  Set analysis_status = "completed"
                  Set ocr_method = "clinical_photo_analysis"
                  Emit SSE progress event
                  Skip Clinical Context Agent (no text to analyze)

Clinical Observation Output Schema

{
  "analysis_type": "clinical_image",
  "extracted_entities": [{
    "type": "clinical_image_observation",
    "body_region": "lower leg / ankle",
    "laterality": "left | right | bilateral | not_determinable",
    "image_type": "clinical_photo | xray | ct_scan | mri | ultrasound | other",
    "observations": [
      "Severe skin breakdown with multiple open wounds",
      "Discoloration consistent with venous changes (dark/purplish)",
      "Active wound discharge visible",
      "Tissue maceration and peeling",
      "Clinical setting visible (hospital bed, IV equipment)"
    ],
    "image_quality": "clear | acceptable | poor | unusable",
    "source": "clinical_photo_analysis"
  }],
  "observations": [],
  "demographics": {}
}

Claude Vision Prompt

You are reviewing a medical image uploaded by a patient seeking cross-border medical care. 
Describe ONLY what is visually observable — use informed observational language, not diagnostic terms.

Return a JSON object:
{
  "body_region": "the body part or anatomical area visible",
  "image_type": "clinical_photo | xray | ct_scan | mri | ultrasound | lab_report_photo | other",
  "laterality": "left | right | bilateral | not_determinable",
  "observations": ["list of descriptive observations — what is visually present"],
  "image_quality": "clear | acceptable | poor | unusable",
  "clinical_setting": true/false
}

RULES:
- DESCRIPTIVE: "visible wound with discharge" — NOT "infected ulcer"
- OBSERVATIONAL: "skin discoloration, dark purplish changes" — NOT "venous stasis dermatitis"
- NEUTRAL: "multiple open areas on skin surface" — NOT "Stage 3 pressure injury"
- Be specific about location, size approximation, color, texture, number of affected areas
- Note if clinical setting is visible (hospital, clinic, home)
- If you cannot determine laterality from the image alone, say "not_determinable"
- If the image is not medical/clinical, return {"image_type": "other"}

Compliance Notes

Concern Handling
EXIF metadata (GPS, device) Stripped before storage
Diagnostic language Prompt strictly observational — enforced by prompt rules
Image storage consent Covered by existing document upload consent flow
Provider sharing Clinical observations included in redacted case forwarding
GDPR erasure Images in R2 already covered by erasure handler
Audit trail ocr_method: "clinical_photo_analysis" logged on document

Edge Cases

  • Non-medical image (selfie, landscape) → image_type = "other" → mark as low_quality, not clinical
  • Blurry/unusable image → image_quality = "unusable" → mark as low_quality with message "Image too unclear for analysis"
  • X-ray / CT scan photo → different observation focus (bone structure, organ contours) — prompt handles via image_type
  • Multiple body regions in one image → observations cover all visible regions, body_region notes "multiple"
  • Image with text overlay (WhatsApp annotations, date stamps) → OCR may extract some text → if sufficient, goes through normal pipeline; if not, clinical analysis
  • Pillow not installed → EXIF stripping skips gracefully, logs warning — image processed with metadata intact
  • Claude Vision API failure → falls back to low_quality status (existing behavior)
  • Very large image (>10MB) → should be resized before sending to Claude Vision to reduce cost
  • Feature flag disabled → existing low_quality behavior unchanged

What This Does NOT Include

  • Camera capture button on mobile (future UX iteration)
  • Wound measurement / size estimation (requires calibration reference)
  • Image comparison over time (before/after)
  • Automated wound scoring (BWAT, PUSH scores) — this would be practicing medicine
  • Provider-side image annotation tools