Three raccoon visits in a week reads like a fact about raccoons. It is at least as much a fact about the camera: how many of those nights it was running, how far out it could make out movement, and whether the clock stamping each clip was right. Wildlife researchers who leave cameras out unattended share a data format that makes them write those things down, and it gives them a table of their own, ahead of anything the camera saw.
The format is Camtrap DP, maintained by the Camtrap DP Development Team under Biodiversity Information Standards (TDWG). I read its data specification and its FAQ for this piece. I build a camera app for old Android phones and I am not an ecologist, so nothing here is a survey method. What the standard offers a person with a phone pointed at a yard, a feeder or the gap under a fence is a checklist: what to record so that a count can later be read as a rate.
Three tables, and the first one describes the camera
The specification opens its data section plainly: "Data in Camtrap DP are organized as three related resources (CSV files): deployments, media and observations."
Media is the files. Observations are what a person, or a model, decided was in them. Deployments are the camera's own record: where it was, how it was set up, and for how long. Every media file and every observation carries a deploymentID that points back to that first table, so nothing the camera recorded can drift loose from the window it was recorded in.
Of the required fields in the deployments table, two are about time. The start is defined as "Date and time at which the deployment was started. Formatted as an ISO 8601 string with timezone designator", and the end gets the matching definition.
The timezone part deserves a pause. A clip stamped with a bare date and time names a moment only if you know which clock produced it. With a UTC offset attached it names one instant anywhere. A phone that crossed a time zone, or a folder of clips copied to a computer set to a different zone, is how a naive timestamp quietly moves by hours, and nothing in the file announces it.
The fields that describe what the camera could see
Beyond time, the deployments table has room for the physical setup. Among them:
-
cameraHeight, in meters. -
cameraTilt, in degrees, from facing straight down to facing straight up. -
cameraHeading, in degrees clockwise from north. -
detectionDistance, defined as "Maximum distance at which the camera can reliably detect activity. Expressed in meters. Typically measured by having a human move in front of the camera."
That last definition is a procedure, and it is the most useful sentence in the document for anyone mounting a phone. Have someone walk across the frame at increasing distances, then open the recording and find the furthest point at which you can still tell that a body went past. For a phone that records continuously rather than on a trigger, that is my working reading of "detect": the reviewer can see it. Do the walk once in daylight and once after dark, because the answer changes with the light, and write down both.
A second field matters if your setup records on motion: cameraDelay, "Predefined duration after detection when further activity is ignored. Expressed in seconds." During that window a lingering animal is one clip and a second animal arriving behind it may be no clip at all. If your setup records continuously, the field stays empty. If it records on motion, the delay is a setting worth finding and writing down, because it shapes every count that follows.
Then there is timestampIssues, a flag for deployments whose media timestamps are "known to have (unsolvable) issues (e.g. unknown timezone, am/pm switch)". The standard keeps a place for a clock it cannot repair rather than dropping the data or pretending the clock was fine. That is a good habit on its own: if you are not sure the phone's clock was right, say so next to the data.
Gaps belong in the structure, not in a note
The deployments table has a start and an end, and I read the full field list for anything describing an interruption inside a deployment. There is a free-text deploymentComments field and nothing structured for downtime. So under this layout, a camera that stopped for a day and a half reads most cleanly as two deployments, not one deployment with a caveat. That is my reading of the schema rather than a rule the spec states, and it is the habit worth borrowing: when the phone was unplugged, the battery ran flat, or someone lifted it down to look at it, close the row, and open a new one when it is running again.
This matters because a gap is where false nulls live. A night with no clips is evidence that nothing came only if the camera was running that night. Count empty nights against a calendar and every night the camera was dark gets counted as a night nothing visited.
Blanks are observations too
The observations table has a required type with six allowed values: "animal, human, vehicle, blank, unknown, unclassified".
Blank is a category of its own. A clip set off by wind, a headlight sweep or a moth can be recorded as blank rather than deleted. Keeping blanks earns its space twice. They show the camera was alive at that hour, and their share tells you whether a placement is mostly wind and headlights before you spend evenings reviewing it. The standard also has a cameraSetupType field whose two values are setup and calibration, so the clips of you adjusting the mount are labelled as that instead of being counted as a visitor.
Count events, not files
The subtlest definition in the spec is observationLevel. Observations at the media level are tied to a single file and are "especially useful for machine learning and don't need to be mutually exclusive (e.g. multiple classifications are allowed)". Observations at the event level cover a group of files and "should be mutually exclusive, so that their count can be summed."
The practical version: a raccoon that ambles through over several minutes may land in more than one file, especially if a recorder splits long recordings into segments. Tally sightings per file and that single visit is counted as many times as there are files. Group the files into one event first, then count. The spec defines an event as a collection of media files and, in the fields I read, sets no time rule for how to group them, so pick a rule, write it down, and keep it.
A rate needs a denominator
Here is the whole idea in a small script. Two files, shaped like the standard's tables with only the columns needed. The first is the deployment log, kept by hand:
deploymentID,deploymentStart,deploymentEnd,cameraHeight,cameraTilt,cameraHeading,detectionDistance,timestampIssues,deploymentComments
yard-01,2026-10-01T18:00:00-04:00,2026-10-04T07:30:00-04:00,0.5,0,200,6.5,false,phone in fence-post box
yard-02,2026-10-05T19:15:00-04:00,2026-10-09T07:00:00-04:00,0.5,0,200,6.5,false,restarted after charger unplugged
The second holds event-level observations. Notice that o6 sits in the day and a half between the two deployments:
observationID,deploymentID,eventStart,eventEnd,observationLevel,observationType,scientificName,count
o1,yard-01,2026-10-01T18:02:10-04:00,2026-10-01T18:02:40-04:00,event,human,,1
o2,yard-01,2026-10-02T02:14:00-04:00,2026-10-02T02:15:30-04:00,event,animal,Procyon lotor,2
o3,yard-01,2026-10-03T23:40:00-04:00,2026-10-03T23:41:00-04:00,event,blank,,
o4,yard-02,2026-10-06T01:05:00-04:00,2026-10-06T01:06:00-04:00,event,animal,Procyon lotor,1
o5,yard-02,2026-10-07T21:30:00-04:00,2026-10-07T21:31:00-04:00,event,animal,Vulpes vulpes,1
o6,yard-02,2026-10-04T23:00:00-04:00,2026-10-04T23:01:00-04:00,event,animal,Procyon lotor,1
The script divides animal events by the hours the log says the camera was out, and refuses to quietly count anything that falls outside a logged window. Timestamps without an offset are rejected outright:
import csv
from collections import Counter
from datetime import datetime
def ts(s):
t = datetime.fromisoformat(s.replace('Z', '+00:00'))
assert t.tzinfo is not None, f'no UTC offset: {s}'
return t
deps = {}
for r in csv.DictReader(open('deployments.csv')):
start, end = ts(r['deploymentStart']), ts(r['deploymentEnd'])
assert end > start, r['deploymentID']
deps[r['deploymentID']] = (start, end)
days = sum((e - s).total_seconds() for s, e in deps.values()) / 86400
print(f'effort: {days:.2f} camera-days over {len(deps)} deployments')
hits, outside = Counter(), []
for r in csv.DictReader(open('observations.csv')):
if r['observationLevel'] != 'event' or r['observationType'] != 'animal':
continue
s, e = deps[r['deploymentID']]
if not (s <= ts(r['eventStart']) <= e):
outside.append(r['observationID'])
continue
hits[r['scientificName']] += 1
for name, n in hits.most_common():
print(f'{name}: {n} events, {100 * n / days:.1f} per 100 camera-days')
if outside:
print('outside their deployment window:', ', '.join(outside))
Run with Python 3.10, it prints:
effort: 6.05 camera-days over 2 deployments
Procyon lotor: 2 events, 33.0 per 100 camera-days
Vulpes vulpes: 1 events, 16.5 per 100 camera-days
outside their deployment window: o6
Two things in that output are worth more than the rates themselves.
First, the rate uses 6.05 camera-days, not the calendar span. The same records counted from the first start to the last end cover 7.54 days, and dividing the two raccoon events by that gives 26.5 per 100 instead of 33.0, because 1.49 of those days had no camera watching. The calendar version is wrong in the comfortable direction: it makes a place look quieter than it is.
Second, o6 is reported, not counted. The log says the camera was off, and a record says it was on. One of the two is wrong: either the restart went unlogged, or the clip carries a bad timestamp, which is exactly what timestampIssues exists to flag. The script cannot tell which, and it should not guess. Counting it would lift the raccoon rate to 39.8 per 100 calendar days, with nothing in the output to show where the extra visit came from.
The count column holds individuals per event, and the script ignores it on purpose. Two raccoons in one clip is one visit; summing individuals answers a different question, and mixing the two inside one number is how a tally inflates.
Small numbers deserve a plain warning. Three events over six days is an anecdote with a unit attached. The unit is still the point: it is what lets you compare this month with next month, or the fence post with the shed, as long as both denominators are written down. Move the phone, change its height or heading, and that is a new deployment, since a rate from one setup says little about another.
What I did not establish
- Whether rates from a phone camera can be compared with rates from a dedicated trail camera. Lens, trigger and night illumination all differ, and I have no data on how much that moves a count.
- How researchers in a given study define an independent event. The standard leaves room for the rule; it does not supply one in the fields I read.
- How any particular app names, splits or timestamps its files. I am describing a published data standard and a short script, not the internals of my own product or anyone else's.
- Which version of the specification a future reader will see. I read the data page and FAQ as fetched for this piece, and both can change.
Try it: Background Camera RemoteStream on Google Play -- record with the screen off and keep the footage on the phone. Setup notes and other write-ups are at superfunicular.com.
Sources. Camtrap DP data specification and Camtrap DP frequently asked questions, both maintained by the Camtrap DP Development Team under TDWG. Quotations are verbatim from the data page as fetched. A count tells you what came; the deployment log tells you whether anyone was watching, and only the two together say how often.
Top comments (1)
tr.ee/dev-to