-
-
Phragmites vs cattail: split at 0.95 vs 0.70, refused, with how to tell them apart
-
Spotted lanternfly near Peterborough: 0 records within 50 km, 39 within 200, refused as unverifiable
-
Three views of the same plant: 3 of 3 agree, reported
-
he record as a place: every tile is a verified sighting, drag to turn the sphere
-
Tap a tile: the sighting's evidence, nearest records and the query links behind it
-
Record screen for public reports
-
Spotted lanternfly in Thunder Bay: nothing within 200 km, refused, hotline number on the card
Inspiration
Ontario spends real money fighting invasive species, and the first line of defence is people reporting what they see. The problem is that most reports are wrong. Wild parsnip looks like golden alexanders. Giant hogweed looks like cow parsnip. Phragmites looks like cattail. Every "AI plant identifier" we tried answers confidently either way, and a confident wrong report wastes the hotline's time and teaches people not to bother.
We wanted an app that knows when it doesn't know.
What it does
You photograph a plant, insect, mussel or fish. A vision model proposes up to three candidates. Then four deterministic rules decide whether the sighting can be reported:
- Agreement. Send up to three photos of the same subject. Two thirds of the views, never fewer than two, have to name the same species. One photo has to beat its runner-up by 2x.
- The Ontario list. The species has to be on our list of 16 Ontario invasives, each mapped to its iNaturalist taxon id.
- Range. At least 3 research-grade iNaturalist records within 50 km in the last 3 years, corroborated by GBIF as an independent second database. Zero records within 200 km is a "new range" refusal that points you to the Invading Species Hotline, because a first sighting needs a person, not an app.
- Season. The photo's month has to exist in the species' Ontario month histogram.
The first rule that fails wins, and the app shows you which one, with the raw evidence: the model's confidence numbers, the nearest real sightings with distances, the month histogram, and the exact iNaturalist and GBIF query URLs so anyone can re-run them. When the refusal is a native lookalike, the card shows how to tell the two apart, sourced from the Ontario government's own pages.
If every rule passes, the sighting goes into a shared ledger with its evidence snapshot. The same species within 100 m in the last 30 days is answered with the existing record instead of a duplicate. The ledger exports as CSV and GeoJSON. A batch endpoint takes a trail survey's worth of photos and returns every verdict at once, each photo at its own camera GPS position. Photos are checked once and never stored; the ledger keeps a hash and a location rounded to about 100 m, and any record can be deleted for real.
The number we're most honest about
Before writing the identifier we tested the model on ten real iNaturalist photos, one per species.
| Prompt | Top-1 correct |
|---|---|
| "Name this species" | 2 / 10 |
| Choose from our catalogue of 16 invasives, 9 native lookalikes and Other | 6 / 10 |
Six out of ten is not good enough to trust. So the model never produces the verdict. In our tests its misses were exactly the lookalike confusions the app exists for: wild parsnip called golden alexanders, giant hogweed called cow parsnip. The gate refused both. With three views of the same plant, a Phragmites photo that split 0.95 vs 0.70 against cattail on its own became 3 of 3 agreeing.
How we built it
- Backend: Java 25, Spring Boot 4.1, Maven, H2.
Gate.javais the four rules, deterministic, with 17 unit tests that use a fake data source and no network. 22 backend tests and 19 frontend tests run on GitHub Actions on every push. - Two proposers, one gate. The proposer is an interface that names candidates and nothing more. The local build runs the 9B vision model (the whole catalogue, 6 of 10 on our test photos). The hosted copy has no GPU, so it runs Pl@ntNet's free plant API through the same interface (plants only, 6 of 7 on our plant photos). Same rules, same ledger, same evidence. A photo the hosted proposer can't name gets an honest "this server covers plants only" instead of a guess. One Docker image serves the API and the app from one origin.
- Identification: a local 9B vision model (qwen3.5) through Ollama, JSON mode, temperature zero and a fixed seed, so the same photo gives the same answer every time. The kill check is itself a JUnit test tagged
livethat anyone can re-run. - Data: iNaturalist and GBIF public APIs, no keys, every response cached on disk by URL so a recorded demo works offline.
- Photo integrity: camera EXIF read on the server, so GPS, date and camera can't be edited in the browser. Evidence only, never a refusal.
- Frontend: React 18, TypeScript, Vite, EXIF extraction in the browser, bilingual EN/FR, PWA.
- The record sphere is the open-source InfiniteMenu component (React Bits, MIT), which we'd used in an earlier project; here it's wired to the live ledger so every tile is a real verified sighting with its evidence a tap away.
Challenges
The first version of the identifier scored 2 out of 10 and we almost switched projects. Giving the model a closed catalogue that includes the native lookalikes is what made it usable, and it made the refusals meaningful: when the model says "cow parsnip", the gate can say "not on the list, but here's the listed lookalike you might be looking at."
Spring Boot 4 moved Jackson to a new package, iNaturalist returns location as one string, and a network failure used to look like "zero records," which the gate would have read as a new range. That last one became a rule: a source that can't be reached is "unavailable," never zero.
What we learned
That the useful part of a model is knowing exactly where its confidence stops being worth anything, and building the thing that decides outside it. Also Java 25 records, Spring Boot 4, Jackson 3, JPA on H2, and how iNaturalist and GBIF actually differ.
What's next
Photos taken by the app itself with GPS. Running both proposers on the same photo, so agreement means two different models, not two views (the gate already accepts any number of proposers). Reference photos of each species in the record, with their licences. A free insect identifier for the hosted copy when one exists.
Where this goes
The people who would pay for this are the ones who pay for wrong reports today: the Invading Species Hotline (OFAH and the Ontario government), conservation authorities, and municipalities that fund removal. What they get is a report that arrives with its proof attached, deduplicated, exportable as CSV or GeoJSON into the systems they already use. The model can be swapped as better ones arrive; the rules and the ledger are the product.
Built With
- docker
- exif
- gbif-api
- github-actions
- h2
- inaturalist-api
- java
- junit
- maven
- ollama
- plantnet
- pwa
- qwen
- react
- spring-boot
- typescript
- vite


Log in or sign up for Devpost to join the conversation.