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:
| Platform | Python version | Wheel tags |
|---|---|---|
| Ubuntu 24.04 (x86_64) | CPython 3.12 | cp312-cp312-linux_x86_64 |
| Windows 10 / 11 (x64) | CPython 3.13 | cp313-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, andaf.__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()andstop()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.Queueor 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.