Skip to content

Classification model

Every piece of annotation work in TRAPPER — AI predictions, human edits, post-approval corrections — lands in one unified Classification table with four types. Understanding the four types and how they relate is the key to understanding almost everything else in the AI pipeline and the classify UI.

The four types

graph TD
    FINAL["FINAL<br/>(container, no dynamic_attrs of its own)"]
    AI["AI<br/>(AI Provider's prediction)"]
    USER["USER<br/>(human-edited / forked)"]
    FEEDBACK["FEEDBACK<br/>(post-approval correction)"]

    AI -->|"final_classification"| FINAL
    USER -->|"final_classification"| FINAL
    FEEDBACK -->|"final_classification"| FINAL
    USER -.->|"source_classification<br/>(forked from)"| AI
    FEEDBACK -.->|"source_classification"| USER
    FINAL ==>|"source_classification<br/>(once approved)"| AI
    FINAL ==>|"source_classification<br/>(once approved)"| USER
Type What it is Has dynamic_attrs?
FINAL A container for one resource's approved-or-pending classification state. There's exactly one FINAL per (resource, project) pair. No — reads observation details via source_classification.dynamic_attrs
AI An AI Provider's prediction: detections, species, distance, etc. Yes
USER A human-authored or human-edited classification, often forked from an AI classification. Yes
FEEDBACK A correction reported against an already-approved classification. Yes

dynamic_attrs is where the actual observation data lives — species, sex, age, behaviour, count, bounding boxes, per-frame distance — one row per detected object/group, attached to an AI/USER/FEEDBACK classification. FINAL never has its own; it always defers to whichever classification it points at. See Data model reference for the full field list.

Approval flow

  1. The AI pipeline creates an AI Classification with its dynamic_attrs (species, bboxes, confidence, …), linked to the resource's FINAL via final_classification.
  2. A human annotator reviews it. If they need to make changes, they fork it into a USER Classification (source_classification → the AI row they forked from).
  3. When a USER Classification is approved:
  4. FINAL.is_approved = True
  5. FINAL.source_classification → that USER Classification
  6. The USER Classification becomes locked for further editing.
  7. Alternatively, the AI Classification can be approved directly without a human edit — same effect, but FINAL.source_classification points at the AI row instead.
  8. After approval, anyone can still log a FEEDBACK Classification against the approved row — source_classification → the approved USER/AI row, final_classification → FINAL. This does not flip FINAL.is_approved back to false; it's a permanent quality-control trail, not a re-opening of the case.

This is why "approve" in the classify UI doesn't destroy the AI prediction — the AI Classification row stays in the database (and in child_classifications) even after a USER fork is approved over it. Re-running the AI pipeline overwrites the AI Classification's dynamic_attrs but never touches an approved USER Classification.

Object vs. group observations

Within dynamic_attrs, each row has an observation_level:

  • object (the default) — one row per individually detected/tracked animal, with a bounding box and first_frame_index.
  • group — an aggregate row an expert adds by hand to describe a group size that wasn't (or can't be) resolved into per-individual tracks. Group rows carry no bbox and no individual_idcount is the only meaningful number, and it's enforced at the database level (dynattrs_group_rows_have_no_bboxes / ..._null_individual_id / ..._positive_count constraints).

Why a unified table instead of separate models

Putting AI/USER/FEEDBACK/FINAL in one Classification table (discriminated by classification_type) rather than four separate Django models means:

  • Forking (AI → USER) and approval (USER/AI → FINAL) are just FK rewrites, not cross-table copies.
  • The full history of a resource's classification — every AI run, every human edit, every later correction — stays queryable from one place via child_classifications/derived_classifications.
  • Permission and visibility logic (who can see what) is written once against Classification, not duplicated four times.

See also