Custom Camera Interaction
Camera Triggers
Every time a frame is captured from the box camera, if CAM_BOX_TRACKING is enabled
and a task is running,
the (x, y) position of the animal inside the operant box is computed and the trigger
method of CameraTriggerBase is called.
The default implementation checks each of the four configurable areas (the rectangles defined in the GUI). For any area set to TRIGGER status, if the animal is detected inside it, the text “Area X triggered” is overlaid on the camera feed.
Note
Active areas can be set to ALLOWED, NOT_ALLOWED, or TRIGGER.
You can override this behavior by creating a custom class in your project. Create a
file named camera_trigger inside your project’s code directory and define a class
named CameraTrigger that inherits from CameraTriggerBase.
If the system detects a class inheriting from CameraTriggerBase in your project, it will use your custom class instead of the default base class.
In the following example, a subclass is created that writes a text when the subject is detected in Area1, executes direct function number 2 when detected in Area2, and triggers function number 6 when detected within a circular region at the center of the image.
from village.custom_classes.camera_trigger_base import CameraTriggerBase
class CameraTrigger(CameraTriggerBase):
"""Implement specific actions when an animal enters
a detection area or reaches a specific position in the camera feed.
You have access to self.task, so any variable or function defined in the
task can be used to specify the actions to perform.
"""
def __init__(self) -> None:
super().__init__()
def trigger(self, cam: Camera) -> None:
"""Evaluates camera triggers and performs corresponding actions.
Called on every frame to check whether the subject has entered any
defined area or reached a specific position in the camera feed.
Available area triggers (return True if the area is marked as a TRIGGER area
and a subject is detected within it):
- cam.area1_is_triggered
- cam.area2_is_triggered
- cam.area3_is_triggered
- cam.area4_is_triggered
Available position properties:
- cam.x_position, cam.y_position (subject position in pixels)
- cam.width, cam.height (camera feed dimensions in pixels)
Other:
- cam.write_text("text") to overlay text on the camera feed
Args:
cam (Camera): The camera instance providing trigger status
and position data.
"""
# Check if the animal is in some of the defined areas
if cam.area1_is_triggered:
cam.write_text("Area 1 triggered") # write an annotation text to the cam
if cam.area2_is_triggered:
self.task.execute_function(2) # execute the direct function number 2
# Check if the animal is within a 50-pixel radius of the center
distance = ((cam.x_position - cam.width / 2) ** 2 +
(cam.y_position - cam.height / 2) ** 2) ** 0.5
if distance < 50:
self.task.execute_function(6) # execute the direct function number 6
Custom Camera Drawing
Annotating the frame from a task
The simplest way to overlay text on the camera feed is to call write_text() from
the task. The text is rendered on every subsequent frame at a fixed position in the
status bar and stays visible until it is changed or cleared.
from village.devices.camera import cam_box
cam_box.write_text("reward delivered") # persists across frames until changed
cam_box.write_text("") # clear
The annotation is also saved frame-by-frame in the session CSV alongside the position data, so it is useful for marking task events for post-hoc analysis.
For anything beyond plain text — shapes, coloured overlays, position-dependent
graphics — use the CameraDrawBase subclass described below.
Drawing with CameraDrawBase
CameraDrawBase exposes two methods that are called automatically on every frame:
Method |
Renderer |
Destination |
|---|---|---|
|
cv2 (modifies |
disk and screen |
|
QPainter (widget overlay) |
screen only |
The default implementation of draw writes the status bar texts and pixel counts
for both cameras, and for the CORRIDOR camera also draws the thresholded detection
mask and area rectangles (if VIEW_DETECTION is active).
The default draw_preview draws those same overlays for the BOX camera via QPainter
so they appear on screen but are not encoded into the video file.
Create a file named camera_draw inside your project’s code directory and define
a class named CameraDraw that inherits from CameraDrawBase. The system detects
it automatically and uses it instead of the base class.
Overriding draw — runs on every frame, changes are saved to disk:
from village.custom_classes.camera_draw_base import CameraDrawBase
import cv2
class CameraDraw(CameraDrawBase):
def __init__(self) -> None:
super().__init__()
def draw(self, cam) -> None:
super().draw(cam) # keep default status bar and detection overlays
# Add a red circle at the animal's position (BOX only, task running)
if cam.name == "BOX" and cam.task_is_running and cam.x_position != -1:
cv2.circle(
cam.frame,
(cam.x_position, cam.y_position),
radius=20,
color=(0, 0, 255), # BGR
thickness=2,
)
Overriding draw_preview — runs at preview framerate, screen only:
from PyQt5.QtCore import QRect
from PyQt5.QtGui import QBrush, QColor, QPainter
def draw_preview(self, cam, painter) -> None:
super().draw_preview(cam, painter) # keep default detection overlays
# Highlight the reward zone on screen (never saved to video)
zone = cam.items_to_draw.get("reward_zone")
if zone and cam.name == "BOX":
device = painter.device()
sx = device.width() / cam.width
sy = device.height() / cam.height
x, y, w, h = zone
painter.setPen(QColor(0, 255, 0))
painter.setBrush(QBrush())
painter.drawRect(QRect(int(x*sx), int(y*sy), int(w*sx), int(h*sy)))
Both methods receive these attributes on cam (updated every frame):
Identity & state
cam.name—'BOX'or'CORRIDOR'.cam.task_is_running—Trueif a task is active,Falseif not.cam.view_detection— whether detection overlays are enabled in the GUI.cam.tracking— whether animal position tracking is active (BOX only).cam.color—Color.BLACKorColor.WHITE: which pixels to detect.
Frame data
cam.frame— current frame as a BGR numpy array (read/write, cv2 only).cam.width,cam.height— frame dimensions in pixels.cam.frame_number— frames captured since recording started.cam.timing— milliseconds elapsed since recording started.
Session info
cam.filename— base name of the current video file (empty if not recording).cam.trial— current trial number (0 if no task running or CORRIDOR camera).cam.annotation— currentwrite_text()string; persists until changed.cam.items_to_draw— plaindictfor passing arbitrary data from the task (see below).
Detection areas
cam.number_of_areas— number of configurable areas (always 4).cam.areas— list of[x1, y1, x2, y2]pixel coordinates per area.cam.areas_active— bool list, whether each area is enabled.cam.areas_allowed— bool list, whether animals are allowed in each area.
Detection results
cam.masks— grayscale numpy arrays with the thresholded mask per area.cam.counts— detected pixel count per area.cam.x_position,cam.y_position— tracked animal position in pixels (-1if not detected).
Task access
self.task— the current task instance.
Passing data from the task via items_to_draw
cam.items_to_draw is a plain dictionary. The task can populate it at any point
to pass coordinates, labels, or any other data to the drawing methods without
coupling the task directly to this class:
# Inside the task, at any point:
from village.devices.camera import cam_box
cam_box.items_to_draw["reward_zone"] = (200, 150, 80, 80) # x, y, w, h
cam_box.items_to_draw["reward_zone"] = None # remove