Hidden Signals · Companion site ← All activities DE·EN
Chapter 9 · The Face as a Reader · Activity 9.2

Mood over an Hour

A memoryless model applied many times: you record your emotional course over a longer period and plot it — and see how mood moves across the day.

Duration 90 min + recording Difficulty medium Group alone Fully digital S

In a nutshell

What: You turn the expression mirror from 9.1 into a quiet recorder. It doesn't measure continuously on screen, but every few seconds saves one line: a timestamp and the recognised emotion probabilities. Afterwards you plot a curve of your mood from it.

The core idea: the time is not in the model — that only knows the moment — but in your analysis. This is often the cheapest route to a temporal analysis: a lightweight model, applied many times.

You need: the setup from 9.1 (webcam, hsemotion, OpenCV) plus pandas and matplotlib for the analysis.

What it's about

On this book's map of AI there is a subtle but important distinction. A snapshot model decides for each input on its own, with no memory: one face — one mood. But you can run such a model once a second and look at the course only afterwards. Then the time lives in your analysis, not in the model. That is exactly what you do here: many snapshots become a mood curve.

This is not a gimmick but the basic idea behind virtual mirroring from Chapter 5. When staff at a Hamburg bank received daily feedback about their mood, they became measurably happier and more active — simply because their state was made visible. Whoever sees themselves honestly can change. Your hour-long curve is this mirror in miniature.

A little background

Why average? A memoryless model jumps from image to image. A single frame in which you happen to be yawning or looking away would distort the curve. So we smooth: we collect the predictions over a time window (about ten seconds) and take the average. That turns jittery noise into a readable trend — the same idea as the moving average in plant activity 13.1.

One number for "mood". Instead of tracking seven emotions at once, it is often clearer to form a single wellbeing axis: positive shares (happiness, pleasant surprise) minus negative ones (anger, sadness, fear, disgust). That is a rough simplification — but an honest one, as long as you know you are simplifying.

Your data stays yours

The recorder saves no images, only numbers — timestamps and probabilities. Even so, an hour of your face is very personal. Keep the file on your own computer, don't share it unasked, and if the class discusses results, do it anonymously and voluntarily. That is the golden rule of this book: what is found out about you belongs to you.

Part A — Recording

  1. Add the packages. pip install pandas matplotlib (on top of hsemotion opencv-python from 9.1).
  2. Start the recorder. Run the script below. It opens no large preview window but quietly writes one line every few seconds into mood.csv.
  3. Do something. Work normally for a while — read, solve problems, watch a short funny and a serious video. The more varied, the more interesting the curve. For a lesson 15–20 minutes is enough; "an hour" is the nicer target for home.
  4. Quit. With Ctrl+C in the console. The CSV file stays put.
import cv2, time, csv
from datetime import datetime
from hsemotion.facial_emotions import HSEmotionRecognizer

fer = HSEmotionRecognizer(model_name="enet_b0_8_best_afew")
detector = cv2.CascadeClassifier(
    cv2.data.haarcascades + "haarcascade_frontalface_default.xml")
cam = cv2.VideoCapture(0)

INTERVAL = 3           # one measurement every 3 seconds
labels = ["Anger","Contempt","Disgust","Fear","Happiness","Neutral","Sadness","Surprise"]

with open("mood.csv", "w", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["time"] + labels)          # header row
    print("Recorder running - Ctrl+C to quit.")
    try:
        while True:
            ok, frame = cam.read()
            if ok:
                gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
                faces = detector.detectMultiScale(gray, 1.3, 5)
                if len(faces):
                    x, y, w, h = faces[0]
                    _, scores = fer.predict_emotions(frame[y:y+h, x:x+w], logits=False)
                    row = [datetime.now().isoformat(timespec="seconds")] + list(scores)
                    writer.writerow(row)
                    f.flush()
            time.sleep(INTERVAL)
    except KeyboardInterrupt:
        pass

cam.release()
print("Done - data in mood.csv")

Part B — Analysing and plotting

The second part reads the CSV, smooths it and draws your mood curve. Runs in the browser too (Pyodide); full code on GitHub.

import pandas as pd
import matplotlib.pyplot as plt

df = pd.read_csv("mood.csv", parse_dates=["time"]).set_index("time")

# a single "wellbeing" axis: positive minus negative
positive = df["Happiness"] + df["Surprise"]
negative = df["Anger"] + df["Sadness"] + df["Fear"] + df["Disgust"]
df["wellbeing"] = positive - negative

# smooth: moving average over ~30 seconds (at a 3s interval = 10 measurements)
df["wellbeing_smooth"] = df["wellbeing"].rolling(window=10, center=True, min_periods=3).mean()

plt.figure(figsize=(10, 4))
plt.plot(df.index, df["wellbeing"], color="#cfc6b4", label="raw")
plt.plot(df.index, df["wellbeing_smooth"], color="#2F5D3A", lw=2, label="smoothed")
plt.axhline(0, color="#999", lw=0.8)
plt.ylabel("Wellbeing axis  (positive - negative)")
plt.title("My mood over time")
plt.legend(); plt.tight_layout(); plt.show()

What you should see

A jittery raw line and, above it, a calm green trend curve. It rises in moments when you laughed or saw something nice, and falls with annoyance or boredom. Often you recognise events again: "that's when the funny video played." If your curve stays almost flat, the hour was either calm — or the model mostly read you as "Neutral", which is normal during focused work.

Worksheet

Moments become a course

  1. Mark two places in your curve and describe what you were doing at that moment. Does the deflection match your memory?
  2. What changes if you set the smoothing window from 10 to 3 (or to 30) measurements? What do you gain, what do you lose with heavy smoothing?
  3. Where exactly is the "time" in this experiment — in the model or in your analysis? Justify it.
  4. Give one reason why the "wellbeing axis" (positive minus negative) is a rough simplification. When might it mislead?
  5. This experiment is "virtual mirroring" in miniature. Why can merely making visible your own mood change something — even without any advice?
Show solution

1. Individual. The aim is for learners to link deflections to real events and, in doing so, notice that the curve fits roughly but doesn't capture every inner state.

2. A small window (3) follows the raw line closely — sensitive, but noisy. A large window (30) shows only the rough trend — calm, but short, real deflections disappear. Smoothing is always a trade: calm for resolution.

3. In the analysis. The model is a snapshot model with no memory; it decides each image on its own. Only our collecting many timestamped predictions and stringing them together produces a course.

4. It throws very different states into one pot: surprise can be positive (joy) or negative (fright), but here is always counted as positive. And "neutral" — often the most common state — disappears entirely. The axis condenses, but it loses meaning.

5. Because self-perception is imprecise: you often don't notice how your own mood moves across the day. A visible course gives an honest reference point at which you spot patterns ("it always dips after lunch") — and recognising is the first step to changing.

When it sticks

ProblemLikely cause & fix
CSV stays emptyNo face found (light, position) or camera occupied. Get 9.1 running first, then continue here.
Curve is almost all "Neutral"Normal during focused work. Deliberately build in varied stimuli (funny/serious video), or record for longer.
rolling curve full of gapsmin_periods too high for short recordings. Lower it to min_periods=1 or measure longer.
Time axis squashed and unreadableVery many points. In matplotlib rotate the axis with fig.autofmt_xdate() or downsample to minutes (df.resample("1min").mean()).
Recorder eats a lot of CPUChoose a larger interval (e.g. INTERVAL = 5) — for an hour-long course that is plenty.

Food for thought

Extension

← 9.1 The Expression Mirror 9.3 Personality from Expression →