portrait-analyser
Extract quantitative facial and dental measurements from iOS Portrait Mode photos (.heic/.heif) captured with Apple's TrueDepth camera.
Why?
iPhone Portrait Mode photos (TrueDepth front camera) carry more than a picture: the HEIC container also stores a depth map and Apple's own semantic segmentation mattes (teeth, skin) used for the bokeh effect. That data is normally locked away — consumer apps only ever show you the blurred photo. portrait-analyser unpacks the container and turns those extra layers into physical, reproducible measurements (incisor distance, mouth opening, neck circumference, jaw/chin position) instead of just pixels, which is useful for orthodontic/clinical tracking, research, or any workflow that needs quantitative facial metrics from a phone photo instead of specialized 3D-scanning hardware.
Features
- HEIC/HEIF parsing — decode the primary photo, depth map, and Apple semantic segmentation mattes (teeth, skin) out of the TrueDepth container, with EXIF-based validation that the source is actually a TrueDepth capture
- Face & eye detection — OpenCV Haar-cascade based, with coordinate translation between image regions
- Teeth & incisor analysis — teeth bounding box, incisor centroid detection, and 3D incisor distance computed from depth data (not just pixel distance)
- Mouth opening measurement — MediaPipe FaceMesh-based fallback for patients without visible upper teeth
- Neck & chin detection — three independent strategies depending on what's available: MediaPipe Pose (shoulder/nose interpolation), MediaPipe Selfie Segmentation (silhouette width profile), or a dual-mask approach combining the skin matte, depth map, and hair mask
- 3D neck circumference — dense arc integration over the depth map to estimate physical neck circumference, not just a 2D collar-line width
- CLI diagnostic tool (
analyse-portrait) — inspect a HEIC file's raw container, EXIF, depth metadata, and segmentation mattes from the command line
Requirements
- Python 3.12 (see Supported versions)
- iOS Portrait Mode photos in HEIC/HEIF format, taken with a TrueDepth camera (e.g. iPhone 12, 14)
Installation
Using uv (recommended)
uv add portrait-analyser
Using pip
pip install portrait-analyser
Platform-specific HEIF support
- macOS -- works out of the box (uses
pyheif-iplweb) - Linux -- requires system packages before installing:
sudo apt install libheif-dev libde265-dev
Supported versions
Python
| 3.12 |
|---|
| ✓ |
Quick start
from portrait_analyser import load_image, get_face_parameters, find_neck_measurement_point
# Load an iOS Portrait Mode photo
portrait = load_image("photo.heic")
# portrait.photo -- PIL Image of the photo
# portrait.depthmap -- PIL Image of the depth map
# portrait.teethmap -- PIL Image of the teeth segmentation mask (or None)
# portrait.skinmap -- PIL Image of the skin segmentation mask (or None)
# Detect face and eyes
face = get_face_parameters(portrait.photo)
print(f"Face at ({face.x}, {face.y}), size {face.width}x{face.height}")
print(f"Eyes detected: {len(face.eyes)}")
# Measure neck width using the skin map
if portrait.skinmap is not None:
neck = find_neck_measurement_point(portrait.skinmap, face)
# Returns (x1, y1, x2, y2) of the narrowest horizontal line below the face
API reference
load_image(fileName, use_exif=True) -> IOSPortrait
Parses a HEIC/HEIF file and returns an IOSPortrait containing the photo, depth map, and Apple semantic segmentation masks. Validates TrueDepth EXIF data by default.
IOSPortrait
Attributes:
photo-- primary PIL Imagedepthmap-- depth map as PIL Imageteethmap-- teeth segmentation mask (PIL Image orNone)skinmap-- skin segmentation mask (PIL Image orNone)teeth_bbox-- bounding box(x, y, width, height)of detected teeth, orNoneincisor_distance-- incisor measurement as(x, y1, x, y2), orNonefloatValueMin,floatValueMax-- depth map float range from Apple metadata
Methods:
teeth_bbox_translated(max_wi, max_he)-- scale teeth bounding box to a target resolution
get_face_parameters(image, raise_opencv_exceptions=False) -> Face
Detects a single face in a PIL Image using OpenCV Haar cascades. Raises NoFacesDetected or MultipleFacesDetected if not exactly one face is found.
Face and Eye
Both extend Rectangle (attributes: x, y, width, height, center_x, center_y).
Face:
image-- reference to the source PIL Imageeyes-- list ofEyeinstances (detected automatically)translate_coordinates(new_max_width, new_max_height)-- scale face coordinates to a target resolutioncalculate_percentage_of_image()-- returns(percent_width, percent_height)
Eye:
face-- reference to the parentFacetranslate_coordinates(max_wi, max_he)-- absolute coordinates in a target resolution
Teeth & incisor utility functions
find_neck_measurement_point(skinmap, face_location, threshold=200)-- finds the narrowest horizontal line below the face in the skin map. Returns(x1, y1, x2, y2).find_bounding_box_teeth(teethmap, margin_x=100, margin_y=100, min_value=200)-- finds the bounding box of teeth in the teeth map. Returns(x, y, width, height)orNone.find_incisor_distance_teeth(teethmap, bounding_box_teeth, threshold=200, margin_x=0.5)-- measures the vertical pixel distance between upper and lower incisors. Returns(x, y1, x, y2)orNone.find_incisor_centroids(teethmap, bounding_box_teeth, threshold=200, margin_x=0.5, min_pixels=50, centroid_margin_x=0.5)-- finds the centroids of the upper and lower incisor surfaces. Returns((upper_cx, upper_cy), (lower_cx, lower_cy))in teethmap coordinates, orNone.sample_depth_at_point(depthmap, point_x, point_y, photo_width, photo_height, kernel_size=3) -> int | None-- samples the depth map at a photo-space coordinate using median filtering over akernel_size x kernel_sizeregion.
3D depth conversion (incisor module)
depth_raw_to_distance_cm(value, float_min, float_max) -> float | None-- converts a raw depth pixel value (0-255) to physical distance in centimeters, using Apple's disparity-based depth encoding.pixel_to_mm(pixel_coord, distance_cm, image_dimension) -> float | None-- converts a pixel coordinate (original, full-resolution image space) to physical millimeters at a given camera distance, via a calibration polynomial fitted to TrueDepth camera data.image_dimensionis the full image width (for an x coordinate) or height (for a y coordinate), used to centre the conversion on the principal point.vector_length_3d(x1, y1, z1, x2, y2, z2) -> float-- Euclidean distance between two 3D points.compute_incisor_distance_3d(upper_centroid, lower_centroid, upper_depth_raw, lower_depth_raw, float_min, float_max, image_width, image_height) -> tuple[float, float, float] | None-- converts two incisor centroids + their raw depth values into physical mm/cm and returns(distance_3d_mm, upper_distance_cm, lower_distance_cm).
Mouth opening (mouth module)
compute_mouth_measurement_from_facemesh(landmarks, depthmap, photo_w, photo_h, float_min, float_max) -> MouthMeasurement | None-- fallback mouth-opening measurement using MediaPipe FaceMesh outer lip landmarks (indices 0 and 17) and the depth map, for cases where teethmap-based incisor detection fails (e.g. no visible upper teeth).MouthMeasurement-- dataclass withupper_point,lower_point(photo-space pixels),upper_depth_raw,lower_depth_raw,upper_distance_cm,lower_distance_cm,distance_3d_mm.
Neck & chin detection (pose module — MediaPipe Pose)
detect_neck_midpoint(image, interpolation_ratio=0.35, min_detection_confidence=0.5, min_visibility=0.5) -> tuple[NeckMidpoint | None, MediaPipeDebug | None, FaceMeshDebug | None]-- locates shoulders and nose via MediaPipe PoseLandmarker, then interpolates between the shoulder midpoint (neck base, ~C7/T1) and the nose to approximate the mid-cervical level (~C3-C4). FaceMesh detection runs independently, soFaceMeshDebugmay be populated even when pose detection fails.NeckMidpoint-- dataclass withnose,mouth_left,mouth_right,chin,neck_extended(True when the neck appears maximally extended, detected via face-flattening ratio),face_flatness_ratio,pose,mouth_open_ratio, plus shoulder-dependent fields (x,y,left_shoulder,right_shoulder, visibilities,interpolation_ratio) that areNonewhen only FaceMesh (not Pose) detected the face.PortraitPose,MediaPipeDebug,FaceMeshDebug-- raw MediaPipe landmark containers, useful for debug visualization.
Neck & chin detection (extended_neck module — segmentation-based)
detect_neck_midpoint_from_segmentation(image, threshold=0.5, jaw_flare_fraction=0.15, smoothing_window=15) -> tuple[NeckMidpoint | None, SegmentationDebug | None]-- uses MediaPipe Selfie Segmentation to build a person silhouette, then analyzes the width profile to find the narrowest point (neck) and where the jaw flares out above it (chin).detect_neck_midpoint_from_dual_mask(image, skinmap, depthmap, hairmap=None, threshold=0.5, skin_threshold=30, float_min=None, float_max=None) -> tuple[NeckMidpoint | None, SegmentationDebug | None]-- combines the iOS skin matte, depth map, and (optional) hair mask: the chin is found as the closest-to-camera skin pixel, neck/shoulders from the depth width profile with hair removed.compute_neck_width_3d(depthmap, neck_y, neck_left_x, neck_right_x, photo_width, photo_height, float_min, float_max, n_samples=25) -> tuple[float | None, float | None]-- samples N evenly-spaced points across the neck row and converts them to 3D coordinates, returning front-arc length and straight-line width.SegmentationDebug-- dataclass exposing the binary mask, width profile, and detected neck/chin/shoulder/ear rows for debug visualization.
Neck circumference (neck module — 3D arc integration)
compute_neck_circumference(skinmap, depthmap, photo_width, photo_height, float_min, float_max, face_location=None, n_samples=25, skin_threshold=1, circumference_multiplier=2.7, arc_sag=None, face=None, eyes=None, image_width=None, scan_start_y=None, scan_end_y=None, neck_midpoint_y=None) -> NeckMeasurement | None-- computes neck circumference by densely sampling the front arc of the neck (using the skin matte and depth map together) and extrapolating to a full circumference.estimate_face_from_skinmap(skinmap, threshold=1) -> tuple[int, int, int, int] | None-- estimates a synthetic face bounding box from the skin segmentation map alone, for when no OpenCV face detection is available.NeckMeasurement-- dataclass withneck_y,left_x,right_x(photo-space),arc_points_3d(physical mm coordinates),arc_points_photo(pixel coordinates, for overlay painting).
Exceptions
UnknownExtension-- file is not .heic or .heifExifValidationFailed-- EXIF data does not indicate a TrueDepth cameraNoDepthMapFound-- HEIF container has no depth dataNoFacesDetected-- no face found in imageMultipleFacesDetected-- more than one face found
Development
# Clone and set up
git clone https://github.com/fidmaa/portrait-analyser.git
cd portrait-analyser
uv sync
# Run tests
uv run pytest
# Build package
uv build
License
MIT
Release files for portrait-analyser 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| portrait_analyser-0.4.0.tar.gz | 3.3 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| portrait_analyser-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 3.4 MB
Release files / portrait_analyser-0.4.0.tar.gz
| Download URL | portrait_analyser-0.4.0.tar.gz |
|---|---|
| Size | 3.3 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d6ae2aa4e8ebd0b085b4274f2838cebaba37d115ad3e109b367c7643d3cf3797
|
|
BLAKE2b-256 checksum How to use checksums |
8a0b475f0c4d3d82909ebefcc04d2631b72b94a6edccb21a19d48d1a8ef52c04
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.2
|
Release files / portrait_analyser-0.4.0-py3-none-any.whl
| Download URL | portrait_analyser-0.4.0-py3-none-any.whl |
|---|---|
| Size | 42.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cd7b5264518651b24dcabab1eb011803f4cabdd2f5e7b5e826924ae399baf69a
|
|
BLAKE2b-256 checksum How to use checksums |
4df5b06d107a34eb5dee77472778772fdc5100a2a55b62ce0b99ccacd471d72d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.2
|