Skip to content

Working with results

The results

analyze() returns a dictionary that maps each output's name to its value:

results = face.analyze()
print(results["fai"])  # in millimeters
print(results["oce.l"])

The measures page lists every output and its units. analyze() returns None if Dynaface found no face in the image, which face.is_no_face() also tells you. Within the results, a value that couldn't be determined is NaN, and the outputs of a measure that couldn't run at all are None.

Choosing measures

load_face_image() and AnalyzeFace take the list of measures to run. measures.all_measures() includes Landmarks, which adds 196 landmark coordinates to the results, so leave it out if you don't need them:

from dynaface import facial, measures
from dynaface.measures_frontal import AnalyzeLandmarks

chosen = [m for m in measures.all_measures() if not isinstance(m, AnalyzeLandmarks)]
face = facial.load_face_image("subject.jpg", measures=chosen)

Or list only the ones you want:

face = facial.load_face_image(
    "subject.jpg",
    measures=[measures.AnalyzeFAI(), measures.AnalyzeOralCommissureExcursion()],
)

Choosing what to draw and export

Each output has an item that controls whether it's drawn. Switching an item off doesn't remove its value from the results, but get_all_items() lists only the outputs whose items are on, which is how the apps choose their chart and CSV columns:

eyes = measures.AnalyzeEyeArea()
eyes.set_item_enabled("eye.diff", False)  # don't draw or list the difference

face = facial.load_face_image("subject.jpg", measures=[eyes])
results = face.analyze()  # still includes eye.diff
print(face.get_all_items())  # ['eye.left', 'eye.right', 'eye.ratio']

Frontal photos and profiles

When it loads an image, Dynaface decides whether it shows the face from the front or in profile, and sets face.lateral to True for a profile. analyze() runs every measure you gave it either way, and frontal measures don't mean anything on a profile. To keep only the outputs that fit the view, switch the others off before analyzing:

face = facial.load_face_image("photo.jpg")
for measure in face.measures:
    measure.update_for_type(face.lateral)

results = face.analyze()
row = {name: results.get(name) for name in face.get_all_items()}

If Dynaface gets the view wrong, give it the view yourself when you load the image:

import cv2
from dynaface.const import Pose

image = cv2.cvtColor(cv2.imread("profile.jpg"), cv2.COLOR_BGR2RGB)  # OpenCV loads BGR
face = facial.AnalyzeFace(measures=measures.all_measures())
face.load_image(image, crop=True, force_pose=Pose.LATERAL)

The image

face.render_img is the standard image with the measures drawn on it, as an RGB NumPy array, and face.save("measured.jpg") writes it to a file. face.original_img is the same image without the drawings.

Measuring a folder of photos

This measures every photo in a folder and writes one row per photo to a CSV file:

import csv
from pathlib import Path

from dynaface import facial, measures, models
from dynaface.measures_frontal import AnalyzeLandmarks

models.init_models(models.download_models(), models.detect_device())
chosen = [m for m in measures.all_measures() if not isinstance(m, AnalyzeLandmarks)]

rows = []
for path in sorted(Path("photos").glob("*.jpg")):
    face = facial.load_face_image(str(path), measures=chosen)
    if face.is_no_face():
        print(f"No face found in {path.name}")
        continue
    for measure in face.measures:
        measure.update_for_type(face.lateral)
    results = face.analyze()
    rows.append({"file": path.name, **{n: results.get(n) for n in face.get_all_items()}})

# Frontal photos and profiles have different outputs, so gather every column.
columns = list(dict.fromkeys(name for row in rows for name in row))
with open("measures.csv", "w", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=columns)
    writer.writeheader()
    writer.writerows(rows)