How

from a street photo to a life on record

Work in progress. This is the shape of the system as currently designed, not a finished spec — expect it to change as we run the first pilots.

In one sentence: Namma Indies is open-set individual re-identification of free-ranging dogs, from opportunistic, crowdsourced, uncontrolled imagery — using a strong geographic and temporal prior plus human confirmation to produce two outputs: a bias-corrected population estimate over time, and a persistent longitudinal record for each individual dog.

Two consumers, one pipeline, two thresholds

The same stream of photos feeds two different products, and they tolerate error very differently.

Every sighting begins anonymous and points at an individual slot that may stay empty for months, until the model earns confidence or someone who knows the dog fills it in. That empty-slot-waiting-to-be-named is the emotional and architectural centre of the system: a sighting can always be linked to an individual later, even if it starts life as a nameless street photo.

Why it's hard

This is an open-set matching problem, not closed-set classification. The gallery of known dogs is unbounded, most individuals appear only once or twice, and for each new photo the system must decide not "which of these N dogs is this" but "is this any dog we've seen before, or a new one." Three difficulties compound:

The design response: re-ID as fusion

So the design doesn't lean on appearance alone. Dogs are territorial and their home ranges are small, so a geo and time prior collapses the candidate set from the whole city down to maybe ten nearby dogs before appearance is scored at all. That goes straight at the base-rate problem, and it means a weak appearance signal only has to tell ten dogs apart, not ten thousand.

In practice the whole retrieval is close to a single query — a geographic and temporal prefilter, then a vector ranking on a learned appearance embedding:

SELECT s.individual_id, i.name,
       s.embedding <=> :query AS visual_distance
FROM sightings s
JOIN individuals i ON i.id = s.individual_id
WHERE ST_DWithin(s.geog, :location, 300)          -- geo prior: ~300m
  AND s.captured_at > now() - interval '30 days'   -- time prior
  AND s.species = :species                          -- dogs match dogs, cats match cats
  AND s.individual_id IS NOT NULL
ORDER BY visual_distance
LIMIT 10;

The embeddings come from an animal re-identification foundation model (MegaDescriptor and/or DINOv2), with room to move to calibrated global-plus-local fusion later. And the same prior that narrows the search also hands us training pairs for free.

Routing, and the confirmation loop

Once a new sighting is embedded, its match confidence routes it down one of three paths:

When a neighbour says "that's Kaju," that's recognition-as-love and recognition-as-label at once — the same event in the data model. Every confirmation is also a labelled training pair that sharpens the embedding on the next round. The people who know the dogs end up teaching the model to see, without doing anything they weren't already doing.

Bootstrapping without labels

To beat the cold-start problem before anyone has confirmed anything, the same spatiotemporal structure that narrows search also mines pseudo-labels:

These are noisy but abundant, and they directly attack cold start: enough signal to fine-tune the embedding before humans are in the loop, with human confirmations then supervising on top.

Honest population estimates

Opportunistic crowdsourcing is biased sampling: it over-represents friendly, photogenic, high-footfall dogs and under-counts the shy ones on the quiet lane. So the population estimate can't be a raw sighting count. The real research task is to model where we looked — capture effort and coverage per area, estimate detection probability, then stratify and weight accordingly. The minimum viable signal is "sightings plus a defensible model of where we looked," run per species so dogs and cats are counted separately. And the estimate always carries its uncertainty: the system never returns a point population number without a confidence interval attached.

Principles

Planned architecture

For the pilot, deliberately boring plumbing — so that effort concentrates on the one genuinely hard part, re-identification. A single small server, a single Postgres database doing triple duty (relational, spatial via PostGIS, and vector search via pgvector), and Python end-to-end. The flow:

  1. Intake — a photo arrives over WhatsApp with location and timestamp; the image goes to object storage, a sighting row is created — tagged with species (dog, cat, or the occasional other) from the first photo — and an embedding job is queued.
  2. Embed — a background worker computes the appearance embedding (async, CPU is fine at pilot volume — no GPU on day one).
  3. Retrieve — the geo-and-time prefilter plus vector ranking finds candidate individuals.
  4. Route — the threshold decision auto-links, proposes it to a neighbour, or opens a new slot.
  5. Confirm — a neighbour's verdict links the sighting and becomes a training label.
  6. Estimate — a separate job consumes the resight signal to produce population estimates with intervals; a public map renders aggregate density only.

Everything resists premature scale — no microservices, no GPU, no separate search cluster — until the first experiment tells us how much of the work appearance can actually carry, and how much load the geographic prior must bear.

References