Loading Captures#
cg.load(path, Schema) materializes a
captured target as typed Python values. The schema is the one you authored
the procedure with — it drives the walk, so there is no manifest to read.
cg.load#
import capturegraph as cg
class Session(cg.Struct):
photo: cg.Image
notes: cg.String
rating: cg.Number
location: cg.Location
class Survey(cg.Struct):
reference: cg.Image
sessions: cg.Map[cg.Date, Session]
survey = cg.load("/path/to/MySurvey", Survey)
cg.load returns the loaded Survey. Each position is decoded by its type:
JSON scalars become typed values, file scalars hold a path plus their
helpers, and composites become broadcasting containers.
You can also pass a schema dict (the schema block from a
serialized procedure) instead of the class:
On disk#
The schema is the layout, so a target you copy off a phone mirrors it
position for position. The Survey above lays out like this:
MySurvey/ # the target — the Survey struct
├── reference.heic # reference: cg.Image (a file scalar leaf)
└── sessions/ # sessions: cg.Map[cg.Date, Session]
├── 0006380070917400/ # one entry — a 16-hex-microsecond session id
│ ├── photo.heic # photo: cg.Image
│ ├── notes.json # notes: cg.String (JSON scalars are .json)
│ ├── rating.json # rating: cg.Number
│ └── location.json # location: cg.Location
└── 0006382D486C3A00/ # the next entry, sorting after it
├── photo.heic
├── notes.json
├── rating.json
└── location.json
The rules that generate this are the same on every platform:
- A
cg.Structis a directory; each field is a child named for the field. - A field whose name starts with
_is hidden — it stores under a leading.(a_metadatafield becomes.metadata), so a target's bookkeeping stays out of the way. - A
cg.Map[cg.Time, V](and itscg.Datealias) stores each entry under a 16-hex-microsecond folder — the session id, which sorts chronologically. Acg.Map[cg.String, V]orcg.Map[cg.UserID, V]uses the key text itself. - A
cg.Array[T]stores each element under a zero-padded index (00000,00001, …), so position order is name order. - A file scalar is the leaf file, keeping its own extension (
.heic,.dng,.jpegfor an image); a JSON scalar is a.jsonleaf.
You never write this tree by hand — the iOS client and server lay it down from
the embedded schema — but seeing it is the fastest way to recognize what
cg.load is walking.
Accessing data#
Struct fields#
Fields are attributes:
Maps and Arrays#
A cg.Map keeps its keys and broadcasts attribute access over its values:
sessions = survey.sessions
for when, session in sessions.items(): # keys are datetimes
print(when, session.notes.value)
sessions.keys() # every capture time
sessions.rating # broadcast: an Array of Number
sessions.rating.value # chain through: [5, 3, 4, ...]
sessions.location.latitude # nested broadcast: [42.0, ...]
A cg.Array works the same way, indexed by position. A file-sequence — a
burst of images captured as cg.Array[cg.Image] — loads from its zero-padded
index folders (burst/00000.heic, burst/00001.heic, …) as an Array in
order:
class Session(cg.Struct):
burst: cg.Array[cg.Image] # a file sequence: 00000.heic, ...
frames = session.burst # an Array of Image, in order
frames[0] # the first frame
frames.exif() # broadcast a helper over all frames
A nested struct nests the same way — an Array of sub-structs broadcasts a
field across every element, so projection descends through the directories the
schema laid out:
class Reading(cg.Struct):
value: cg.Number
note: cg.String
class Session(cg.Struct):
readings: cg.Array[Reading] # readings/00000/value.json, ...
session.readings[0].value.value # one reading
session.readings.value.value # every reading's value: [1, 2, ...]
File scalars#
A file scalar loads as its path value — a cg.Image that is a Path and
carries its decode helpers as methods, so no pixels are read until you ask:
photo = session.photo # a cg.Image
photo.exif().datetime_original # typed EXIF — a cg.Exif
photo.pil() # a PIL image
photo.tooltip() # a small preview
A loaded cg.Video carries metadata(), frame_at(t), and thumbnail() likewise. See
Types and Nodes.
Missing data#
Field captures are messy: sessions get interrupted, optional steps get
skipped. A failed lookup returns cg.Missing
— a null object that absorbs further access, so a chain like
session.weather.condition is always safe. Test for it with
cg.is_missing:
for session in survey.sessions.values():
if cg.is_missing(session.notes):
print("no notes")
else:
print(session.notes.value)
cg.is_missing(None) is also True — None counts as missing.
Writing back#
To persist a value to a target, cg.commit
stores it through the schema:
loc = cg.Location(latitude=42.0, longitude=-76.0)
cg.commit("/path/to/MySurvey/reference_location", loc, cg.Location)
Into pandas#
Broadcast columns drop straight into a DataFrame:
import pandas as pd
sessions = survey.sessions
df = pd.DataFrame({
"date": list(sessions.keys()),
"rating": sessions.rating.value.to_numpy(),
"notes": list(sessions.notes.value),
})
Because the schema was fixed before capture, this script keeps working as new sessions arrive — write it on day one and rerun it for the life of the study.
See Also#
- Containers —
cg.Array/cg.Mapbroadcasting - Types and Nodes — the schema and on-disk layout
- CaptureGraph Sync — downloading data from a server