Python Quick Start

The affvisionpy package wraps the native SDK in a Pythonic API: the same detectors, listeners, and metrics as C++, with snake_case names and Python dictionaries for metric maps. The native libraries are bundled inside the wheel β€” pip install is the whole setup.

1 Β· Installation

Install the platform wheel you received with your SDK delivery:

pip3 install ./affvisionpy-*.whl

A wheel filename encodes the interpreter and platform it was built for, so it is longer than the package name alone β€” for example affvisionpy-<version>-cp312-cp312-linux_x86_64.whl. The cp312 tags mean CPython 3.12: pip refuses a wheel whose tags do not match your interpreter, so match the wheel to the Python you intend to run:

PlatformPython versionWheel tags
Ubuntu 24.04 (x86_64)CPython 3.12cp312-cp312-linux_x86_64
Windows 10 / 11 (x64)CPython 3.13cp313-cp313-win_amd64

The wheel is a platform wheel: it contains the compiled extension module plus the native affectiva-vision library and its dependencies. No separate SDK install, model files, or environment variables are needed. Verify the installation:

python3 -c "import affvisionpy; print(affvisionpy.__version__)"

The examples below also use OpenCV for video capture: pip3 install opencv-python (any recent version; it is used only to read frames, not by the SDK itself).

2 Β· First program

Analyze a video file (or webcam) and print metrics for every processed frame. The flow is the same five steps as every platform: construct β†’ enable β†’ listen β†’ start β†’ process.

analyze.py

import cv2
import affvisionpy as af


class Listener(af.FaceListener):
    """Receives results for every processed frame."""

    def face_found(self, timestamp, face_id):
        print(f"face {face_id} found at {timestamp} ms")

    def face_lost(self, timestamp, face_id):
        print(f"face {face_id} lost at {timestamp} ms")

    def results_updated(self, faces, frame):
        for face_id, face in faces.items():
            expressions = face.get_expressions()   # dict {af.Expression: float}
            emotions = face.get_emotions()         # dict {af.Emotion: float}
            print(f"{frame.timestamp()} ms  face {face_id}  "
                  f"smile={expressions[af.Expression.smile]:.1f}  "
                  f"joy={emotions[af.Emotion.joy]:.1f}  "
                  f"valence={emotions[af.Emotion.valence]:.1f}")


# 1. Construct: processes every frame, callbacks on the calling thread.
detector = af.SyncFrameDetector()

# 2. Enable the metric families you need.
detector.enable_features({af.Feature.expressions, af.Feature.emotions})

# 3. Register your listener (keep a reference for the detector's lifetime).
listener = Listener()
detector.set_face_listener(listener)

# 4. Start. Pass your key, or "" to use the AFFECTIVA_LICENSE_KEY env var.
detector.start("YOUR-LICENSE-KEY")

# 5. Process frames with strictly increasing timestamps.
capture = cv2.VideoCapture("input.mp4")     # or 0 for the default webcam
while capture.isOpened():
    ok, image = capture.read()
    if not ok:
        break
    height, width = image.shape[:2]
    timestamp_ms = int(capture.get(cv2.CAP_PROP_POS_MSEC))
    frame = af.Frame(timestamp_ms, width, height, image,
                     af.Frame.ColorFormat.bgr, af.Frame.Rotation.upright)
    detector.process(frame)

capture.release()
detector.stop()

Run it:

AFFECTIVA_LICENSE_KEY="YOUR-LICENSE-KEY" python3 analyze.py

Webcam note. When reading from a live camera, CAP_PROP_POS_MSEC may return 0 for every frame. Use a monotonic clock instead β€” for example timestamp_ms = int((time.monotonic() - t0) * 1000) β€” and remember that timestamps must strictly increase or process() raises an error.

The example, explained

Imports

import cv2
import affvisionpy as af

affvisionpy is the SDK β€” one module, everything included (the native libraries load automatically on import). OpenCV is used purely as the capture layer, to decode video frames into NumPy arrays; the SDK itself has no OpenCV requirement on your side.

The listener β€” where output arrives

class Listener(af.FaceListener):

    def face_found(self, timestamp, face_id): ...
    def face_lost(self, timestamp, face_id): ...

The SDK never returns metrics from process(); all output is pushed to a listener you subclass from af.FaceListener. All three methods must be overridden. face_found/face_lost fire once when a face enters/leaves tracking. Input: the frame timestamp (int, ms) where the event was observed and the face’s integer tracking id β€” use them to bracket per-person sessions.

Per-frame results

def results_updated(self, faces, frame):
    for face_id, face in faces.items():
        expressions = face.get_expressions()   # dict {af.Expression: float}
        emotions = face.get_emotions()         # dict {af.Emotion: float}

Called once for every processed frame. Input: faces is a dict mapping tracking id β†’ af.Face (empty if no face is visible β€” the callback still fires), and frame is the processed af.Frame, whose frame.timestamp() tells you which moment the results describe. Output you read: each getter returns a plain dict with one entry per metric of the families you enabled β€” values 0–100 (see the Metrics guide), keyed by enum, e.g. expressions[af.Expression.smile].

Constructing the detector

detector = af.SyncFrameDetector()

The synchronous detector is the easiest starting point: it processes every frame and runs your callbacks inside process() on the calling thread β€” no concurrency to manage, and errors surface directly as exceptions. Defaults: max_num_faces=1 (raise to track more faces, at extra CPU cost) and max_thread_pool_size=0 (auto: CPU cores βˆ’ 1).

Enabling features

detector.enable_features({af.Feature.expressions, af.Feature.emotions})

Input: a set of af.Feature values naming the metric families to compute. Only enabled families cost CPU and appear in the result dicts; face tracking metrics (bounding box, measurements, confidence) are always delivered. Call before start().

Registering the listener and starting

listener = Listener()
detector.set_face_listener(listener)

detector.start("YOUR-LICENSE-KEY")

Keep the listener in a variable β€” the native side holds a reference to it, and it must stay alive for as long as it is registered. start() validates the license (input: your key, or "" to fall back to the AFFECTIVA_LICENSE_KEY environment variable), loads the embedded models, and allocates the analysis threads. It raises RuntimeError if the license is missing or invalid β€” the call most worth wrapping in try/except.

The capture loop and Frame construction

height, width = image.shape[:2]
timestamp_ms = int(capture.get(cv2.CAP_PROP_POS_MSEC))
frame = af.Frame(timestamp_ms, width, height, image,
                 af.Frame.ColorFormat.bgr, af.Frame.Rotation.upright)

Input: the raw pixels plus their geometry and timing. image is the NumPy array exactly as OpenCV returned it β€” OpenCV decodes to BGR byte order, which is why we declare ColorFormat.bgr; note NumPy’s shape is (rows, cols) = (height, width), so the two are swapped when passed. CAP_PROP_POS_MSEC gives the video’s presentation time in ms β€” each frame must carry a strictly larger timestamp than the last. Rotation.upright says the image needs no rotation correction. The constructor copies the pixel data, so reusing image on the next loop iteration is safe.

Processing and shutting down

detector.process(frame)
...
capture.release()
detector.stop()

process() analyzes the frame and β€” with the synchronous detector β€” calls results_updated on this thread before returning; its only output is those callbacks (and a possible RuntimeError). stop() frees the models and threads; to analyze another video with the same detector, call detector.reset() so timestamps may restart from 0.

3 Β· Naming conventions and API shape

  • Classes keep their C++ names: af.Frame, af.Face, af.SyncFrameDetector, af.FrameDetector, af.FaceListener, af.Point, af.BoundingBox.
  • Methods are snake_case: get_expressions(), set_face_listener(), enable_features().
  • Enum values are lowercase: af.Expression.brow_raise, af.Emotion.joy, af.Frame.ColorFormat.bgr, af.Feature.emotions.
  • Metric maps are Python dicts keyed by enum: face.get_expressions()[af.Expression.smile].
  • Module constants: af.ALL_EXPRESSIONS, af.ALL_EMOTIONS, af.ALL_MEASUREMENTS, af.ALL_FACE_POINTS, af.ALL_QUALITIES, af.ALL_FEATURES, af.DEFAULT_PROCESSING_FRAME_RATE, af.DEFAULT_MAX_NUM_FACES, af.DEFAULT_MAX_THREAD_POOL_SIZE, and af.__version__.

One asymmetry to know about: checking whether a feature is enabled is spelled is_enabled(...) on SyncFrameDetector but is_feature_enabled(...) / are_features_enabled(...) on FrameDetector. Both classes share the rest of the detector API (enable_feature, enable_features, disable_feature, disable_features, get_enabled_features, start, stop, reset, is_running, process).

The pain metric is available as af.Expression.pain β€” a continuous 0–100 estimate, with 50 as the model’s decision threshold. See the Expression reference for the full metric list.

4 Β· Asynchronous mode (FrameDetector)

af.FrameDetector processes frames on a native background thread and drops frames if input outpaces its target rate β€” the right choice for live cameras:

detector = af.FrameDetector(process_frame_rate=30.0,
                            max_num_faces=2,
                            max_thread_pool_size=0)   # 0 = cores - 1

Two things Python developers should know:

  • process() and stop() release the GIL while the native code runs, so your capture loop keeps running while frames are analyzed.
  • Your listener callbacks are invoked from the SDK’s background thread. Don’t touch UI or non-thread-safe state directly from them; hand results off via a queue.Queue or similar.

5 Β· Error handling

Native SDK exceptions are translated to Python RuntimeError, with nested causes flattened into the message:

try:
    detector.start()          # no key passed and AFFECTIVA_LICENSE_KEY unset
except RuntimeError as err:
    print(f"failed to start: {err}")

Next steps

  • Full class and enum documentation in Python naming: Python API reference.
  • Head pose and landmark data: face.get_measurements(), face.get_face_points(), face.get_bounding_box() β€” see af.Face.