Recipe 9.3: Python Implementation Example

Heads up: This is a deliberately simple, illustrative implementation of the pseudocode walkthrough from Recipe 9.3. It shows one way you could translate wound photography measurement concepts into working Python code. It is not production-ready. There's no error handling to speak of, no retry logic, no input validation beyond the basics. Think of it as the sketchpad version: useful for understanding the shape of the solution, not something you'd deploy to a wound care clinic on Monday morning. Consider it a starting point, not a destination.


Setup

You'll need the AWS SDK for Python and a few image processing libraries:

pip install boto3 numpy pillow scipy

Your environment needs credentials configured (via environment variables, an instance profile, or ~/.aws/credentials). The IAM role or user needs:

  • s3:PutObject, s3:GetObject (wound image storage)
  • sagemaker:InvokeEndpoint (segmentation model inference)
  • dynamodb:PutItem, dynamodb:Query (measurement storage)

For local testing without a trained SageMaker model, we'll include a synthetic segmentation function that simulates what a real U-Net endpoint would return.


Config and Constants

These go at the top of your module. The reference marker specs and measurement thresholds are configuration, not logic. Readers should see these before the functions that use them.

import io
import json
import logging
import datetime
from decimal import Decimal
from datetime import timezone

import boto3
import numpy as np
from PIL import Image, ImageDraw
from botocore.config import Config

# Configure logging. In production, use JSON-formatted output for
# CloudWatch Logs Insights queries. Never log PHI (patient identifiers,
# wound images, or measurement values tied to a patient).
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

# Retry config for AWS API calls. Adaptive mode uses exponential backoff
# with jitter, which handles burst throttling gracefully.
BOTO3_RETRY_CONFIG = Config(retries={"max_attempts": 3, "mode": "adaptive"})

# --- Reference Marker Configuration ---
# We use a circular blue calibration sticker with a known physical diameter.
# The system detects this circle in the image to compute pixels-per-cm.
MARKER_DIAMETER_CM = 2.5  # Physical diameter of the reference marker
MARKER_COLOR_RANGE = {
    # HSV color range for detecting the blue marker.
    # These values work for the standard blue calibration stickers.
    # You'll need to tune these if you use a different colored marker.
    # NOTE: This config shows what a production implementation would use.
    # The simplified detect_reference_marker() below uses RGB thresholds
    # for readability. Wire this config into your detection logic in production.
    "hue_min": 100,
    "hue_max": 130,
    "sat_min": 100,
    "sat_max": 255,
    "val_min": 80,
    "val_max": 255,
}

# --- Measurement Thresholds ---
# Minimum pixels-per-cm for a valid scale factor. Below this, the marker
# is too small in the image (camera too far away) for reliable measurement.
MIN_PIXELS_PER_CM = 10
# Maximum pixels-per-cm. Above this, something is wrong (marker too close,
# or a false detection).
MAX_PIXELS_PER_CM = 100

# Segmentation confidence threshold. Below this, flag for human review.
SEGMENTATION_CONFIDENCE_THRESHOLD = 0.7

# Wound area change threshold for alerting (percentage increase).
WOUND_GROWTH_ALERT_THRESHOLD_PCT = 10.0

# --- AWS Resource Names ---
# Replace these with your actual resource names.
S3_BUCKET = "wound-images-bucket"
SAGEMAKER_ENDPOINT = "wound-segmentation-unet"
DYNAMODB_TABLE = "wound-measurements"

# --- Clients ---
s3_client = boto3.client("s3", config=BOTO3_RETRY_CONFIG)
sagemaker_runtime = boto3.client("sagemaker-runtime", config=BOTO3_RETRY_CONFIG)
dynamodb = boto3.resource("dynamodb", config=BOTO3_RETRY_CONFIG)

Synthetic Test Data

For testing without real wound images (which are PHI and require proper data governance), we generate synthetic images with a known "wound" region and reference marker. This lets you validate the measurement pipeline end-to-end.

def generate_synthetic_wound_image(
    image_size=(1024, 768),
    wound_center=(500, 400),
    wound_axes=(80, 55),
    marker_center=(200, 600),
    marker_radius_px=53,
):
    """
    Generate a synthetic wound image for testing.

    Creates an image with:
    - A skin-toned background (simulating periwound tissue)
    - An elliptical red region (simulating a wound bed)
    - A blue circle (simulating the calibration marker)

    The known dimensions let us verify that our measurement pipeline
    produces correct results. If the marker is 53px radius and represents
    a 2.5cm diameter circle, then pixels_per_cm = (53*2) / 2.5 = 42.4.

    Returns:
        tuple: (PIL Image, dict of ground truth values)
    """
    img = Image.new("RGB", image_size, color=(210, 180, 160))  # skin tone
    draw = ImageDraw.Draw(img)

    # Draw the "wound" as a red ellipse
    wound_bbox = [
        wound_center[0] - wound_axes[0],
        wound_center[1] - wound_axes[1],
        wound_center[0] + wound_axes[0],
        wound_center[1] + wound_axes[1],
    ]
    draw.ellipse(wound_bbox, fill=(180, 40, 40))  # dark red, granulation tissue

    # Draw the reference marker as a blue circle
    marker_bbox = [
        marker_center[0] - marker_radius_px,
        marker_center[1] - marker_radius_px,
        marker_center[0] + marker_radius_px,
        marker_center[1] + marker_radius_px,
    ]
    draw.ellipse(marker_bbox, fill=(30, 60, 200))  # blue calibration marker

    # Compute ground truth measurements
    pixels_per_cm = (marker_radius_px * 2) / MARKER_DIAMETER_CM
    # Ellipse area = pi * a * b (in pixels), then convert to cm²
    area_px = np.pi * wound_axes[0] * wound_axes[1]
    area_cm2 = area_px / (pixels_per_cm ** 2)
    length_cm = (wound_axes[0] * 2) / pixels_per_cm
    width_cm = (wound_axes[1] * 2) / pixels_per_cm

    ground_truth = {
        "pixels_per_cm": round(pixels_per_cm, 2),
        "area_cm2": round(area_cm2, 2),
        "length_cm": round(length_cm, 2),
        "width_cm": round(width_cm, 2),
    }

    return img, ground_truth

Step 1: Validate and Upload the Wound Image

Maps to pseudocode Step 1 in the main recipe. We check basic image properties and upload to S3 with metadata for the audit trail.

def validate_and_upload(
    image: Image.Image,
    patient_id: str,
    wound_id: str,
    wound_location: str,
    clinician_id: str,
) -> str:
    """
    Validate image quality and upload to S3.

    Args:
        image: PIL Image of the wound photograph
        patient_id: Patient identifier
        wound_id: Unique wound identifier (patient can have multiple wounds)
        wound_location: Anatomical location (e.g., "sacrum", "left heel")
        clinician_id: Who took the photo

    Returns:
        The S3 key where the image was stored.

    Raises:
        ValueError: If image fails validation checks.
    """
    # Check minimum resolution. Below 640x640, wound boundary detection
    # becomes unreliable because there aren't enough pixels to distinguish
    # wound edge from periwound tissue.
    width, height = image.size
    if width < 640 or height < 640:
        raise ValueError(
            f"Image resolution {width}x{height} too low. "
            f"Minimum 640x640 required for reliable measurement."
        )

    # Generate the S3 key with a path structure that supports efficient
    # querying by patient and wound.
    timestamp = datetime.datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S")
    s3_key = f"wound-images/{patient_id}/{wound_id}/{timestamp}.jpg"

    # Convert to bytes for upload
    buffer = io.BytesIO()
    image.save(buffer, format="JPEG", quality=95)
    buffer.seek(0)

    # Upload with server-side encryption and metadata.
    # The metadata fields are NOT PHI themselves (they're identifiers),
    # but they link to PHI, so the bucket must be under BAA.
    s3_client.put_object(
        Bucket=S3_BUCKET,
        Key=s3_key,
        Body=buffer.getvalue(),
        ContentType="image/jpeg",
        ServerSideEncryption="aws:kms",
        Metadata={
            "patient_id": patient_id,
            "wound_id": wound_id,
            "wound_location": wound_location,
            "clinician_id": clinician_id,
            "capture_timestamp": timestamp,
        },
    )

    logger.info(f"Uploaded wound image to s3://{S3_BUCKET}/{s3_key}")
    return s3_key

Step 2: Detect Reference Marker and Compute Scale

Maps to pseudocode Step 2. We find the blue calibration circle in the image and compute how many pixels equal one centimeter.

def detect_reference_marker(image: Image.Image) -> dict:
    """
    Detect the circular reference marker and compute pixels-per-cm.

    This uses a simple color-based detection approach: find the largest
    cluster of blue pixels, fit a circle to it, and use the known physical
    diameter to compute scale.

    In production, you'd use a more robust approach (Hough circle transform,
    or a small trained detector). This simplified version works for the
    synthetic test images and demonstrates the concept.

    Args:
        image: PIL Image containing the wound and reference marker.

    Returns:
        dict with keys:
        - "detected": bool, whether a valid marker was found
        - "pixels_per_cm": float, the scale factor (if detected)
        - "marker_center": tuple (x, y) pixel coordinates of marker center
        - "marker_radius_px": float, detected radius in pixels
    """
    img_array = np.array(image)

    # Simple color thresholding for blue marker detection.
    # We work in RGB here for simplicity. A production system would
    # convert to HSV for more robust color detection under varying lighting.
    r, g, b = img_array[:, :, 0], img_array[:, :, 1], img_array[:, :, 2]

    # Blue marker: high blue channel, low red and green
    blue_mask = (b > 150) & (r < 100) & (g < 120)

    # Count blue pixels
    blue_pixel_count = np.sum(blue_mask)

    if blue_pixel_count < 100:
        # Not enough blue pixels to be a marker
        logger.warning("No reference marker detected in image.")
        return {"detected": False, "pixels_per_cm": None}

    # Find the centroid of blue pixels (marker center)
    blue_coords = np.argwhere(blue_mask)  # returns (row, col) pairs
    center_row = np.mean(blue_coords[:, 0])
    center_col = np.mean(blue_coords[:, 1])

    # Estimate radius from the area of blue pixels.
    # Area of circle = pi * r^2, so r = sqrt(area / pi)
    estimated_radius_px = np.sqrt(blue_pixel_count / np.pi)

    # Compute pixels per centimeter from the known marker diameter
    marker_diameter_px = estimated_radius_px * 2
    pixels_per_cm = marker_diameter_px / MARKER_DIAMETER_CM

    # Sanity check: is the scale factor in a reasonable range?
    if pixels_per_cm < MIN_PIXELS_PER_CM or pixels_per_cm > MAX_PIXELS_PER_CM:
        logger.warning(
            f"Scale factor {pixels_per_cm:.1f} px/cm outside expected range "
            f"[{MIN_PIXELS_PER_CM}, {MAX_PIXELS_PER_CM}]. "
            f"Check marker positioning and camera distance."
        )
        return {"detected": False, "pixels_per_cm": None}

    return {
        "detected": True,
        "pixels_per_cm": round(pixels_per_cm, 2),
        "marker_center": (int(center_col), int(center_row)),
        "marker_radius_px": round(estimated_radius_px, 1),
    }

Step 3: Segment the Wound

Maps to pseudocode Step 3. In production, this calls a SageMaker endpoint hosting a trained U-Net model. For testing, we include a synthetic segmentation function that uses color thresholding to simulate what the model would return.

def segment_wound_synthetic(image: Image.Image) -> tuple:
    """
    Synthetic wound segmentation for testing.

    In production, you'd call a SageMaker endpoint with a trained U-Net.
    This function simulates that by detecting the red wound region using
    color thresholding. It returns the same data structure a real model would.

    Args:
        image: PIL Image of the wound.

    Returns:
        tuple: (binary_mask as numpy array, confidence score)
    """
    img_array = np.array(image)
    r, g, b = img_array[:, :, 0], img_array[:, :, 1], img_array[:, :, 2]

    # Detect red wound region (high red, low green and blue)
    wound_mask = (r > 120) & (g < 100) & (b < 100)

    # Simulate a confidence score (in reality, this comes from the model's
    # output probabilities averaged over wound pixels)
    confidence = 0.91 if np.sum(wound_mask) > 500 else 0.45

    return wound_mask.astype(np.uint8), confidence

def segment_wound_sagemaker(image: Image.Image) -> tuple:
    """
    Call SageMaker endpoint for wound segmentation.

    This is what you'd use in production. The endpoint hosts a trained
    U-Net model that takes an image and returns a probability mask.

    Args:
        image: PIL Image of the wound.

    Returns:
        tuple: (binary_mask as numpy array, confidence score)
    """
    # Resize to model's expected input dimensions.
    # Most U-Net variants expect square inputs. 512x512 is common.
    original_size = image.size  # (width, height)
    resized = image.resize((512, 512))

    # Convert to bytes for the SageMaker endpoint
    buffer = io.BytesIO()
    resized.save(buffer, format="JPEG")
    payload = buffer.getvalue()

    # Invoke the SageMaker endpoint
    response = sagemaker_runtime.invoke_endpoint(
        EndpointName=SAGEMAKER_ENDPOINT,
        ContentType="image/jpeg",
        Accept="application/json",
        Body=payload,
    )

    # Parse the response. The model returns a probability mask as a
    # flattened array of floats (0.0 to 1.0 per pixel).
    result = json.loads(response["Body"].read().decode("utf-8"))
    probability_mask = np.array(result["mask"]).reshape((512, 512))

    # Threshold to binary
    binary_mask = (probability_mask > 0.5).astype(np.uint8)

    # Compute confidence: mean probability of pixels classified as wound
    wound_probs = probability_mask[binary_mask == 1]
    confidence = float(np.mean(wound_probs)) if len(wound_probs) > 0 else 0.0

    # Resize mask back to original image dimensions
    mask_image = Image.fromarray(binary_mask * 255)
    mask_resized = mask_image.resize(original_size, resample=Image.NEAREST)
    final_mask = (np.array(mask_resized) > 127).astype(np.uint8)

    return final_mask, confidence

Step 4: Compute Wound Measurements

Maps to pseudocode Step 4. Given the segmentation mask and scale factor, we compute area, length, width, perimeter, and circularity in real-world units.

def compute_measurements(segmentation_mask: np.ndarray, pixels_per_cm: float) -> dict:
    """
    Compute wound measurements from the segmentation mask.

    This is where pixels become centimeters. The scale factor from the
    reference marker converts pixel counts into physical dimensions that
    clinicians can use for treatment decisions.

    Args:
        segmentation_mask: Binary numpy array (1 = wound, 0 = background)
        pixels_per_cm: Scale factor from marker detection

    Returns:
        dict with area_cm2, length_cm, width_cm, perimeter_cm, circularity
    """
    # Count wound pixels for area
    wound_pixel_count = int(np.sum(segmentation_mask))

    if wound_pixel_count == 0:
        return {
            "area_cm2": 0.0,
            "length_cm": 0.0,
            "width_cm": 0.0,
            "perimeter_cm": 0.0,
            "circularity": 0.0,
        }

    # Convert pixel area to cm²
    # Each pixel represents (1/pixels_per_cm)² square centimeters
    area_cm2 = wound_pixel_count / (pixels_per_cm ** 2)

    # Find wound boundary pixels for perimeter calculation.
    # A boundary pixel is a wound pixel that has at least one non-wound neighbor.
    # We use a simple erosion approach: erode the mask by 1 pixel, then
    # subtract from original to get the boundary.
    from scipy import ndimage  # only needed for morphological operations

    eroded = ndimage.binary_erosion(segmentation_mask)
    boundary = segmentation_mask.astype(int) - eroded.astype(int)
    perimeter_px = int(np.sum(boundary))
    perimeter_cm = perimeter_px / pixels_per_cm

    # Find bounding box for length and width.
    # We use the minimum bounding rectangle of the wound region.
    wound_coords = np.argwhere(segmentation_mask == 1)  # (row, col) pairs
    # Simple approach: axis-aligned bounding box
    min_row, min_col = wound_coords.min(axis=0)
    max_row, max_col = wound_coords.max(axis=0)
    bbox_height_px = max_row - min_row
    bbox_width_px = max_col - min_col

    # Length = longer dimension, Width = shorter dimension
    length_px = max(bbox_height_px, bbox_width_px)
    width_px = min(bbox_height_px, bbox_width_px)
    length_cm = length_px / pixels_per_cm
    width_cm = width_px / pixels_per_cm

    # Circularity: how round is the wound? 1.0 = perfect circle.
    # Formula: 4 * pi * area / perimeter^2
    # Useful for tracking wound shape changes over time.
    if perimeter_px > 0:
        circularity = (4 * np.pi * wound_pixel_count) / (perimeter_px ** 2)
    else:
        circularity = 0.0

    return {
        "area_cm2": round(float(area_cm2), 2),
        "length_cm": round(float(length_cm), 2),
        "width_cm": round(float(width_cm), 2),
        "perimeter_cm": round(float(perimeter_cm), 2),
        "circularity": round(float(circularity), 3),
    }

Step 5: Store Measurement and Compute Healing Trajectory

Maps to pseudocode Step 5. We write the measurement to DynamoDB and compare against previous measurements to compute healing rate.

def store_measurement(
    patient_id: str,
    wound_id: str,
    measurements: dict,
    confidence: float,
    image_s3_key: str,
    metadata: dict,
) -> dict:
    """
    Store the wound measurement in DynamoDB and compute healing trajectory.

    The table uses a composite key:
    - Partition key: patient_id (groups all data for one patient)
    - Sort key: wound_id#timestamp (orders measurements chronologically per wound)

    This key design supports two access patterns efficiently:
    1. Get latest measurement for a specific wound (query with sort key prefix)
    2. Get full timeline for a wound (query with sort key begins_with)

    Args:
        patient_id: Patient identifier
        wound_id: Wound identifier
        measurements: Dict from compute_measurements()
        confidence: Segmentation confidence score
        image_s3_key: S3 key of the original image
        metadata: Additional context (clinician, location, device)

    Returns:
        The complete record that was stored, including healing trajectory.
    """
    table = dynamodb.Table(DYNAMODB_TABLE)
    now = datetime.datetime.now(timezone.utc)
    timestamp_str = now.isoformat()

    # Build the measurement record.
    # DynamoDB requires Decimal for numeric values, not float.
    # This is a common gotcha that causes TypeError at runtime.
    record = {
        "patient_id": patient_id,
        "wound_id_timestamp": f"{wound_id}#{timestamp_str}",
        "wound_id": wound_id,
        "measurement_date": timestamp_str,
        "area_cm2": Decimal(str(measurements["area_cm2"])),
        "length_cm": Decimal(str(measurements["length_cm"])),
        "width_cm": Decimal(str(measurements["width_cm"])),
        "perimeter_cm": Decimal(str(measurements["perimeter_cm"])),
        "circularity": Decimal(str(measurements["circularity"])),
        "confidence": Decimal(str(round(confidence, 3))),
        "image_s3_key": image_s3_key,
        # In production, you'd also store the segmentation mask to S3 for audit
        # and include mask_s3_key here. Omitted for simplicity in this example.
        "clinician_id": metadata.get("clinician_id", "unknown"),
        "wound_location": metadata.get("wound_location", "unspecified"),
        "device_info": metadata.get("device_info", "unknown"),
    }

    # Query for the most recent previous measurement of this wound
    # to compute healing trajectory.
    response = table.query(
        KeyConditionExpression="patient_id = :pid AND begins_with(wound_id_timestamp, :wid)",
        ExpressionAttributeValues={
            ":pid": patient_id,
            ":wid": wound_id,
        },
        ScanIndexForward=False,  # Descending order (most recent first)
        Limit=1,
    )

    healing_trajectory = None
    previous_items = response.get("Items", [])

    if previous_items:
        prev = previous_items[0]
        prev_area = float(prev["area_cm2"])
        prev_date = datetime.datetime.fromisoformat(prev["measurement_date"])
        days_elapsed = (now - prev_date).days

        if days_elapsed > 0 and prev_area > 0:
            current_area = measurements["area_cm2"]
            area_change_pct = ((current_area - prev_area) / prev_area) * 100
            healing_rate = (prev_area - current_area) / days_elapsed

            healing_trajectory = {
                "previous_area_cm2": Decimal(str(prev_area)),
                "area_change_pct": Decimal(str(round(area_change_pct, 1))),
                "days_since_last": days_elapsed,
                "healing_rate_cm2_per_day": Decimal(str(round(healing_rate, 4))),
            }

            # Alert if wound is growing significantly
            if area_change_pct > WOUND_GROWTH_ALERT_THRESHOLD_PCT:
                logger.warning(
                    f"ALERT: Wound {wound_id} growth detected: "
                    f"{area_change_pct:.1f}% over {days_elapsed} days. "
                    f"Review required."
                )

            record["healing_trajectory"] = healing_trajectory

    # Write to DynamoDB
    table.put_item(Item=record)
    logger.info(f"Stored measurement for {patient_id}/{wound_id}")

    return record

Full Pipeline: End-to-End Wound Measurement

This assembles all the steps into a single callable function. The print statements show progress so you can trace execution during development.

def measure_wound(
    image: Image.Image,
    patient_id: str,
    wound_id: str,
    wound_location: str,
    clinician_id: str,
    use_sagemaker: bool = False,
) -> dict:
    """
    Full wound measurement pipeline: validate, calibrate, segment, measure, store.

    Args:
        image: PIL Image of the wound with reference marker
        patient_id: Patient identifier
        wound_id: Unique wound identifier
        wound_location: Anatomical location
        clinician_id: Who captured the image
        use_sagemaker: If True, call real SageMaker endpoint. If False, use synthetic.

    Returns:
        Complete measurement record including healing trajectory.
    """
    print(f"\n{'='*60}")
    print(f"WOUND MEASUREMENT PIPELINE")
    print(f"Patient: {patient_id} | Wound: {wound_id} | Location: {wound_location}")
    print(f"{'='*60}\n")

    # Step 1: Validate and upload
    print("[Step 1] Validating image and uploading to S3...")
    s3_key = validate_and_upload(
        image=image,
        patient_id=patient_id,
        wound_id=wound_id,
        wound_location=wound_location,
        clinician_id=clinician_id,
    )
    print(f"  Uploaded to: s3://{S3_BUCKET}/{s3_key}")

    # Step 2: Detect reference marker
    print("\n[Step 2] Detecting reference marker...")
    marker_result = detect_reference_marker(image)

    if not marker_result["detected"]:
        print("  WARNING: No reference marker detected!")
        print("  Image flagged for manual review. Cannot produce calibrated measurement.")
        return {
            "status": "MARKER_NOT_DETECTED",
            "image_s3_key": s3_key,
            "message": "Reference marker not found. Manual review required.",
        }

    pixels_per_cm = marker_result["pixels_per_cm"]
    print(f"  Marker detected at {marker_result['marker_center']}")
    print(f"  Scale factor: {pixels_per_cm} pixels/cm")

    # Step 3: Segment the wound
    print("\n[Step 3] Segmenting wound region...")
    if use_sagemaker:
        mask, confidence = segment_wound_sagemaker(image)
    else:
        mask, confidence = segment_wound_synthetic(image)

    wound_pixel_count = int(np.sum(mask))
    print(f"  Wound pixels detected: {wound_pixel_count}")
    print(f"  Segmentation confidence: {confidence:.3f}")

    if confidence < SEGMENTATION_CONFIDENCE_THRESHOLD:
        print(f"  WARNING: Confidence below threshold ({SEGMENTATION_CONFIDENCE_THRESHOLD})")
        print("  Measurement will be flagged for review.")

    # Step 4: Compute measurements
    print("\n[Step 4] Computing wound measurements...")
    measurements = compute_measurements(mask, pixels_per_cm)
    print(f"  Area:        {measurements['area_cm2']} cm²")
    print(f"  Length:      {measurements['length_cm']} cm")
    print(f"  Width:       {measurements['width_cm']} cm")
    print(f"  Perimeter:   {measurements['perimeter_cm']} cm")
    print(f"  Circularity: {measurements['circularity']}")

    # Step 5: Store and compute trajectory
    print("\n[Step 5] Storing measurement and computing healing trajectory...")
    metadata = {
        "clinician_id": clinician_id,
        "wound_location": wound_location,
        "device_info": "synthetic_test",
    }

    record = store_measurement(
        patient_id=patient_id,
        wound_id=wound_id,
        measurements=measurements,
        confidence=confidence,
        image_s3_key=s3_key,
        metadata=metadata,
    )

    if "healing_trajectory" in record:
        traj = record["healing_trajectory"]
        print(f"  Previous area: {traj['previous_area_cm2']} cm²")
        print(f"  Area change:   {traj['area_change_pct']}%")
        print(f"  Days elapsed:  {traj['days_since_last']}")
        print(f"  Healing rate:  {traj['healing_rate_cm2_per_day']} cm²/day")
    else:
        print("  No previous measurement found (first measurement for this wound)")

    print(f"\n{'='*60}")
    print("MEASUREMENT COMPLETE")
    print(f"{'='*60}\n")

    return record

# --- Run the pipeline with synthetic data ---
if __name__ == "__main__":
    # Generate a synthetic wound image for testing
    print("Generating synthetic wound image...")
    image, ground_truth = generate_synthetic_wound_image()
    print(f"Ground truth: {ground_truth}")

    # Run the measurement pipeline
    # Note: This will fail on S3/DynamoDB calls unless you have
    # the actual AWS resources set up. For local testing, you'd
    # mock the AWS clients or use localstack.
    result = measure_wound(
        image=image,
        patient_id="PAT-TEST-001",
        wound_id="WND-001-sacral",
        wound_location="sacrum",
        clinician_id="RN-SMITH",
        use_sagemaker=False,
    )

    print("\nFinal result:")
    print(json.dumps(result, indent=2, default=str))

Gap to Production

This example demonstrates the measurement pipeline's shape. Here's what you'd need to add before deploying to a wound care clinic:

Error handling and retries. Every AWS API call can fail. S3 uploads can timeout. SageMaker endpoints can return 5xx errors under load. DynamoDB can throttle writes. Wrap each call in try/except with exponential backoff. The BOTO3_RETRY_CONFIG handles transient errors, but you need application-level retry logic for cases where you want to retry the entire step (e.g., re-upload after a network timeout).

Input validation. Check image format (JPEG, PNG, HEIC), color space (RGB vs. CMYK), orientation (EXIF rotation), and file integrity before processing. Reject corrupted files early rather than letting them cause cryptic failures downstream.

Robust marker detection. The color-thresholding approach in this example is fragile. Production systems use Hough circle transforms, template matching, or small trained object detectors that handle varying lighting, partial occlusion, and marker rotation. Consider supporting multiple marker types (circular stickers, ruler strips, color calibration cards).

Model versioning and A/B testing. When you retrain your segmentation model, you need to deploy it alongside the existing model and compare performance before cutting over. SageMaker supports production variants for this. Track which model version produced each measurement so you can audit accuracy over time.

Structured logging. Replace print statements with structured JSON logging. Include correlation IDs that link the image upload, segmentation call, and measurement storage into a single traceable request. Never log PHI values (patient names, wound images, measurement data tied to identifiable patients).

IAM least privilege. The example uses broad permissions. In production, each Lambda function should have its own role with only the specific actions it needs. The upload function needs s3:PutObject on the specific bucket prefix. The segmentation function needs sagemaker:InvokeEndpoint on the specific endpoint ARN. The storage function needs dynamodb:PutItem and dynamodb:Query on the specific table.

VPC and VPC endpoints. SageMaker endpoints should run in a VPC. Use VPC endpoints for S3 and DynamoDB to keep PHI traffic off the public internet. This adds latency (usually negligible) but is required for HIPAA compliance in most interpretations.

KMS customer-managed keys. The example uses aws:kms (AWS-managed key). Production should use customer-managed KMS keys so you control key rotation, access policies, and can revoke access if needed. Each service (S3, DynamoDB, SageMaker) should use its own key.

DynamoDB Decimal handling. Already addressed in the code (wrapping floats in Decimal(str(...))), but worth emphasizing: DynamoDB's put_item raises TypeError if you pass Python floats. This is the single most common boto3 gotcha for numeric data. The str() wrapper avoids floating-point representation artifacts that Decimal(0.1) would introduce.

Image retention and lifecycle policies. Wound images are part of the medical record. Configure S3 lifecycle policies that match your organization's retention requirements (typically 7-10 years for medical records, varies by state). Use S3 Intelligent-Tiering or Glacier transitions for older images that are rarely accessed but must be retained.

Concurrent measurement handling. If two clinicians photograph the same wound within minutes (shift change, second opinion), you need idempotency or conflict resolution. The timestamp-based sort key handles this naturally (both measurements are stored), but your healing trajectory logic should handle the case where "previous measurement" was 5 minutes ago, not 7 days ago.

Testing. Unit tests for each function with mocked AWS clients. Integration tests with localstack or a dedicated test AWS account. Validation tests comparing pipeline output against clinician-traced ground truth on a held-out dataset. Regression tests that run automatically when the model is retrained.


← Recipe 9.3: Wound Photography Measurement | Chapter 9 Index | Recipe 9.4: Dermatology Lesion Triage →