"""Maps named faces (from Immich's own face recognition/People feature) onto their position in the final rendered 800x480 frame, for the manage-button overlay's escalated "who's in this photo" menu level. No face detection or recognition happens here or anywhere else in this project -- Immich's GET /api/faces?id={assetId} already returns each detected face's bounding box plus a nullable `person` object (with a `name`, if the user has identified them in Immich); this module only does the coordinate math to place a label next to a *named* one. """ from __future__ import annotations import io from PIL import Image, ImageOps from .image_pipeline import EPD_HEIGHT, EPD_WIDTH, _face_aware_crop_box, _plain_center_crop_box # Small caps, not arbitrary: each label is its own malloc'd overlay # buffer on the device (see firmware/main/manage_qr_overlay.c), and the # four existing fixed corner regions already use a meaningful chunk of # the ESP32-C6's limited RAM. Capping at 4 short names keeps the total # overlay memory budget well clear of the WiFi/HTTP stack's own needs. MAX_LABELED_FACES = 4 NAME_MAX_LEN = 10 def compute_face_labels(preview_bytes: bytes, faces: list[dict], smart_crop_faces: bool) -> list[dict]: """Returns up to MAX_LABELED_FACES [{"name", "x", "y"}], x/y in final 800x480 frame pixel space at each named face's bottom-center point. Faces without an Immich-identified person name are skipped entirely. preview_bytes must be the same preview image render_frame() used for the currently-displayed frame, and smart_crop_faces must match the setting that was active then -- otherwise the crop box computed here won't match what's actually on screen. """ named = [face for face in faces if (face.get("person") or {}).get("name")] if not named: return [] fitted = ImageOps.exif_transpose(Image.open(io.BytesIO(preview_bytes)).convert("RGB")) if smart_crop_faces and faces: left, top, right, bottom = _face_aware_crop_box(fitted.width, fitted.height, EPD_WIDTH, EPD_HEIGHT, faces) crop_w, crop_h = right - left, bottom - top else: left, top, crop_w, crop_h = _plain_center_crop_box(fitted.width, fitted.height, EPD_WIDTH, EPD_HEIGHT) labels = [] for face in named[:MAX_LABELED_FACES]: face_w = face.get("imageWidth") or fitted.width face_h = face.get("imageHeight") or fitted.height scale_x = fitted.width / face_w scale_y = fitted.height / face_h center_x = (face["boundingBoxX1"] + face["boundingBoxX2"]) / 2 * scale_x bottom_y = face["boundingBoxY2"] * scale_y frame_x = (center_x - left) * (EPD_WIDTH / crop_w) frame_y = (bottom_y - top) * (EPD_HEIGHT / crop_h) if not (0 <= frame_x <= EPD_WIDTH and 0 <= frame_y <= EPD_HEIGHT): continue # this face got cropped out of the final frame entirely name = face["person"]["name"] if len(name) > NAME_MAX_LEN: name = name[: NAME_MAX_LEN - 3] + "..." labels.append({"name": name, "x": int(frame_x), "y": int(frame_y)}) return labels