BUSH FOREVER SITE {bushland.bf_sites}
@@ -379,9 +372,61 @@ function MapOverlay({
{bushland.name}
-
- {bushland.description}
-
+
+
+ Site Description
+
+
+ {bushland.description}
+
+
+
+
+
+
+ Recent Sightings
+
+
+ {bushland.reportedSightings?.length ? (
+
+ {bushland.reportedSightings.slice(0, 3).map((sighting) => (
+ -
+
+ {new Date(`${sighting.date}T00:00:00`).toLocaleDateString(
+ "en-AU",
+ { day: "numeric", month: "short", year: "numeric" },
+ )}
+
+
+ {sighting.species} reported
+
+
+ {sighting.count}{" "}
+ {sighting.count === 1 ? "report" : "reports"}
+
+
+ ))}
+
+ ) : (
+
+ No weed sightings have been reported here yet.
+
+ )}
+
{bushland.distanceMetres !== undefined ? (
{formatBushlandDistance(bushland.distanceMetres)}
@@ -429,6 +474,7 @@ export default function BushlandMap({
onLocationChange,
onLocationStatusChange,
}: BushlandMapProps) {
+ const queryClient = useQueryClient();
const [selectedEvent, setSelectedEvent] = useState(null);
const [selectedBushland, setSelectedBushland] =
useState(null);
@@ -439,6 +485,38 @@ export default function BushlandMap({
const { data } = useEvents();
const events = useMemo(() => data?.events ?? [], [data?.events]);
+ useEffect(() => {
+ const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL;
+ if (!backendUrl || typeof window === "undefined") {
+ return;
+ }
+
+ const source = new EventSource(`${backendUrl}/weeds/stream/`);
+ source.onmessage = (message) => {
+ const update = JSON.parse(message.data) as {
+ bushland_id: number | null;
+ sighting: NonNullable[number];
+ };
+ queryClient.invalidateQueries({ queryKey: ["bushlands"] });
+ if (update.bushland_id === null) {
+ return;
+ }
+ setSelectedBushland((current) =>
+ current?.objectid === update.bushland_id
+ ? {
+ ...current,
+ reportedSightings: [
+ update.sighting,
+ ...(current.reportedSightings ?? []),
+ ].slice(0, 3),
+ }
+ : current,
+ );
+ };
+
+ return () => source.close();
+ }, [queryClient]);
+
useEffect(() => {
if (!nearestBushland) {
return;
diff --git a/client/src/components/event-preview-sheet.tsx b/client/src/components/event-preview-sheet.tsx
index b24c27f..bc59fa4 100644
--- a/client/src/components/event-preview-sheet.tsx
+++ b/client/src/components/event-preview-sheet.tsx
@@ -18,7 +18,10 @@ export function EventPreviewSheet({ event, onClose }: EventPreviewSheetProps) {
return (
-
+
diff --git a/client/src/components/weed-results.tsx b/client/src/components/weed-results.tsx
index 4872f09..a7040dc 100644
--- a/client/src/components/weed-results.tsx
+++ b/client/src/components/weed-results.tsx
@@ -298,7 +298,9 @@ export function WeedResults({
if (!data) return null;
const topCandidate = data.candidates[0];
- const isUnknown = data.candidates.length === 0;
+ const hasNoConfidentMatch = !data.candidates.some(
+ (candidate) => candidate.confidence > 0.2,
+ );
const isLowConfidence = topCandidate?.confidence_level === "low";
return (
@@ -318,7 +320,7 @@ export function WeedResults({
{data.disclaimer}
- {isUnknown ? (
+ {hasNoConfidentMatch ? (
No confident match found
diff --git a/client/src/hooks/bushlands.ts b/client/src/hooks/bushlands.ts
index 5e71953..50e8579 100644
--- a/client/src/hooks/bushlands.ts
+++ b/client/src/hooks/bushlands.ts
@@ -10,9 +10,16 @@ export type BushlandProperties = {
name: string;
description: string;
sourceUrl: string;
+ reportedSightings?: ReportedWeedSighting[];
distanceMetres?: number;
};
+export type ReportedWeedSighting = {
+ date: string;
+ species: string;
+ count: number;
+};
+
export type BushlandCollection = FeatureCollection<
Polygon | MultiPolygon,
BushlandProperties
diff --git a/client/src/hooks/weeds.ts b/client/src/hooks/weeds.ts
index 79aaa86..8a4d5c8 100644
--- a/client/src/hooks/weeds.ts
+++ b/client/src/hooks/weeds.ts
@@ -11,6 +11,60 @@ export interface IdentifyWeedVariables {
longitude?: number;
}
+export interface ReportSightingVariables {
+ bushlandId?: number;
+ observedOn: string;
+ abundance: string;
+ latitude?: number;
+ longitude?: number;
+ notes?: string;
+ confirmedSpecies: string;
+ topScientificName?: string;
+ topCommonName?: string;
+ topConfidence?: number;
+ modelId?: string;
+}
+
+export interface ReportSightingResponse {
+ id: number;
+ ticket_id: string;
+ confirmed_species: string;
+ observed_on: string | null;
+ abundance: string;
+ created_at: string;
+}
+
+export const useReportSighting = (
+ args?: Omit<
+ UseMutationOptions<
+ ReportSightingResponse,
+ unknown,
+ ReportSightingVariables
+ >,
+ "mutationFn"
+ >,
+) => {
+ return useMutation({
+ ...args,
+ mutationFn: async (variables: ReportSightingVariables) => {
+ const res = await api.post("/weeds/report/", {
+ bushland_id: variables.bushlandId,
+ observed_on: variables.observedOn,
+ abundance: variables.abundance,
+ latitude: variables.latitude,
+ longitude: variables.longitude,
+ notes: variables.notes,
+ confirmed_species: variables.confirmedSpecies,
+ top_scientific_name: variables.topScientificName,
+ top_common_name: variables.topCommonName,
+ top_confidence: variables.topConfidence,
+ model_id: variables.modelId,
+ });
+ return res.data;
+ },
+ });
+};
+
/**
* Backend for `POST /api/weeds/identify/` (issue #9) isn't implemented yet.
* In non-production builds, if the real request can't reach the server at
diff --git a/client/src/pages/identify.tsx b/client/src/pages/identify.tsx
index d46052e..9b46d57 100644
--- a/client/src/pages/identify.tsx
+++ b/client/src/pages/identify.tsx
@@ -5,7 +5,6 @@ import { useEffect, useState } from "react";
import { AppShell } from "@/components/app-shell";
import {
- generateTicketId,
type ReportDetails,
ReportSightingForm,
} from "@/components/report-sighting-form";
@@ -16,7 +15,7 @@ import {
WeedResults,
type WeedResultsStatus,
} from "@/components/weed-results";
-import { useIdentifyWeed, useSubmitWeedReport } from "@/hooks/weeds";
+import { useIdentifyWeed, useReportSighting } from "@/hooks/weeds";
import { BUSHLAND_FIXTURES, UNSURE_BUSHLAND_ID } from "@/lib/bushland-fixtures";
import type { WeedCandidate } from "@/types/weeds";
@@ -79,7 +78,7 @@ export default function IdentifyPage() {
const [photoPreviewUrl, setPhotoPreviewUrl] = useState(null);
const identifyMutation = useIdentifyWeed();
- const reportMutation = useSubmitWeedReport();
+ const reportMutation = useReportSighting();
// Revoke the captured-photo preview URL whenever it changes or the page unmounts.
useEffect(() => {
@@ -132,41 +131,44 @@ export default function IdentifyPage() {
};
const handleSubmitReport = (details: ReportDetails) => {
- const topCandidate =
- confirmedOutcome?.kind === "candidate"
- ? confirmedOutcome.candidate
+ // The bushland select stores fixture indices as "0"-"3" when no real
+ // nearby list is available (no location) — only forward numeric IDs
+ // that came from the real backend list (see ReportSightingForm).
+ const bushlandId =
+ details.bushlandId !== "manual" &&
+ details.bushlandId !== UNSURE_BUSHLAND_ID &&
+ /^\d+$/.test(details.bushlandId)
+ ? Number(details.bushlandId)
: undefined;
reportMutation.mutate(
{
- bushlandId: details.bushlandId,
- bushlandName: details.bushlandName,
- observationDate: details.observationDate,
+ bushlandId,
+ observedOn: details.observationDate,
abundance: details.abundance,
latitude: details.latitude,
longitude: details.longitude,
notes: details.notes,
confirmedSpecies: confirmedOutcome
? subjectLabelFor(confirmedOutcome)
- : undefined,
- candidates: identifyMutation.data?.candidates,
+ : "",
+ topScientificName:
+ confirmedOutcome?.kind === "candidate"
+ ? confirmedOutcome.candidate.scientific_name
+ : undefined,
+ topCommonName:
+ confirmedOutcome?.kind === "candidate"
+ ? confirmedOutcome.candidate.common_name
+ : undefined,
+ topConfidence:
+ confirmedOutcome?.kind === "candidate"
+ ? confirmedOutcome.candidate.confidence
+ : undefined,
modelId: identifyMutation.data?.model_id,
- topScientificName: topCandidate?.scientific_name,
- topConfidence: topCandidate?.confidence,
},
{
onSuccess: (data) => {
- setReportDetails({
- ...details,
- ticketId: data.ticket_id,
- });
- setStep("success");
- },
- onError: () => {
- setReportDetails({
- ...details,
- ticketId: details.ticketId || generateTicketId(),
- });
+ setReportDetails({ ...details, ticketId: data.ticket_id });
setStep("success");
},
},
@@ -212,15 +214,23 @@ export default function IdentifyPage() {
)}
{step === "report" && confirmedOutcome && (
-
+ <>
+
+ {reportMutation.isError ? (
+
+ We couldn't submit this report. Check your connection and try
+ again.
+
+ ) : null}
+ >
)}
{step === "success" && confirmedOutcome && reportDetails && (
@@ -231,13 +241,11 @@ export default function IdentifyPage() {
- Demo report created
+ Report submitted
- This demonstration report is stored only for the current session
- and has not been submitted to an official reporting service.
- Persisting real sightings depends on the backend WeedScan
- integration (issue #9).
+ Your sighting has been recorded and can now appear in the
+ bushland's recent sightings.
diff --git a/server/api/bushlands/tests.py b/server/api/bushlands/tests.py
index 15b8676..27c16f7 100644
--- a/server/api/bushlands/tests.py
+++ b/server/api/bushlands/tests.py
@@ -1,5 +1,7 @@
from django.test import TestCase
+from api.weeds.models import WeedSighting
+
from .models import BushlandArea
@@ -89,6 +91,34 @@ def test_returns_nearest_area_and_bounds(self):
)
self.assertEqual(response.json()["bounds"], [115.81, -31.99, 115.83, -31.97])
+ def test_returns_recent_reported_sightings_for_nearest_area(self):
+ area = BushlandArea.objects.create(
+ source_object_id=74,
+ site_number=402,
+ name="Pelican Point, Crawley",
+ geometry={"type": "Polygon", "coordinates": []},
+ bbox_west=115.81,
+ bbox_south=-31.99,
+ bbox_east=115.83,
+ bbox_north=-31.97,
+ source_url="https://catalogue.data.wa.gov.au/",
+ )
+ WeedSighting.objects.create(
+ model_id="weedscan19",
+ top_common_name="Bridal creeper",
+ bushland_area=area,
+ observed_on="2026-08-30",
+ )
+
+ response = self.client.get(
+ "/api/bushlands/nearest/?latitude=-31.98&longitude=115.82"
+ )
+
+ self.assertEqual(
+ response.json()["reportedSightings"],
+ [{"date": "2026-08-30", "species": "Bridal creeper", "count": 1}],
+ )
+
def test_nearest_area_requires_valid_coordinates(self):
response = self.client.get(
"/api/bushlands/nearest/?latitude=not-a-number&longitude=115.82"
diff --git a/server/api/bushlands/views.py b/server/api/bushlands/views.py
index b7a14ea..19ad6a1 100644
--- a/server/api/bushlands/views.py
+++ b/server/api/bushlands/views.py
@@ -2,6 +2,7 @@
from rest_framework.decorators import api_view
from .models import BushlandArea
+from api.weeds.models import WeedSighting
def _parse_bbox(raw_bbox):
@@ -42,6 +43,23 @@ def _distance_to_bbox(area, longitude, latitude):
return longitude_distance**2 + latitude_distance**2
+def _reported_sightings(area):
+ sightings = WeedSighting.objects.filter(bushland_area=area).order_by(
+ "-observed_on", "-identified_at"
+ )[:3]
+ return [
+ {
+ "date": (sighting.observed_on or sighting.identified_at.date()).isoformat(),
+ "species": sighting.confirmed_species
+ or sighting.top_common_name
+ or sighting.top_scientific_name
+ or "Unidentified weed",
+ "count": 1,
+ }
+ for sighting in sightings
+ ]
+
+
@api_view(["GET"])
def bushland_areas(request):
try:
@@ -72,6 +90,7 @@ def bushland_areas(request):
"name": area.name,
"description": area.description,
"sourceUrl": area.source_url,
+ "reportedSightings": _reported_sightings(area),
},
"geometry": area.geometry,
}
@@ -124,6 +143,7 @@ def nearest_bushland(request):
"name": area.name,
"description": area.description,
"sourceUrl": area.source_url,
+ "reportedSightings": _reported_sightings(area),
"bounds": [
area.bbox_west,
area.bbox_south,
diff --git a/server/api/weeds/apps.py b/server/api/weeds/apps.py
index 6fff2b8..8dfa8a9 100644
--- a/server/api/weeds/apps.py
+++ b/server/api/weeds/apps.py
@@ -4,3 +4,15 @@
class WeedsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "api.weeds"
+
+ def ready(self):
+ from .models import WeedSighting
+
+ if not hasattr(WeedSighting, "ticket_id"):
+ WeedSighting.ticket_id = property(
+ lambda sighting: (
+ f"TKT-{sighting.identified_at.strftime('%Y%m%d')}-{sighting.pk:04d}"
+ if sighting.pk and sighting.identified_at
+ else ""
+ )
+ )
diff --git a/server/api/weeds/management/commands/seed_demo_sightings.py b/server/api/weeds/management/commands/seed_demo_sightings.py
new file mode 100644
index 0000000..9059228
--- /dev/null
+++ b/server/api/weeds/management/commands/seed_demo_sightings.py
@@ -0,0 +1,78 @@
+from datetime import datetime
+from zoneinfo import ZoneInfo
+
+from django.core.management.base import BaseCommand
+from django.db import transaction
+
+from api.bushlands.models import BushlandArea
+from api.weeds.models import WeedSighting
+
+PERTH_TIMEZONE = ZoneInfo("Australia/Perth")
+PLACEHOLDER_SIGHTINGS = (
+ ("2026-05-08", "Bridal creeper", "Asparagus asparagoides"),
+ ("2026-06-19", "Blackberry", "Rubus fruticosus"),
+ ("2026-07-11", "Boneseed", "Chrysanthemoides monilifera"),
+ ("2026-08-03", "Watsonia", "Watsonia meriana"),
+ ("2026-08-30", "Arum lily", "Zantedeschia aethiopica"),
+)
+DEMO_NOTE_PREFIX = "Demo placeholder sighting"
+
+
+class Command(BaseCommand):
+ help = "Create five dated placeholder weed sightings for every bushland area."
+
+ def handle(self, *args, **options):
+ bushlands = list(BushlandArea.objects.all())
+ created = 0
+ updated = 0
+
+ with transaction.atomic():
+ for bushland in bushlands:
+ expected_notes = {
+ f"{DEMO_NOTE_PREFIX} {index}/5"
+ for index in range(1, len(PLACEHOLDER_SIGHTINGS) + 1)
+ }
+ WeedSighting.objects.filter(
+ bushland_area=bushland,
+ notes__startswith=DEMO_NOTE_PREFIX,
+ ).exclude(notes__in=expected_notes).delete()
+
+ for index, (date_value, common_name, scientific_name) in enumerate(
+ PLACEHOLDER_SIGHTINGS, start=1
+ ):
+ note = f"{DEMO_NOTE_PREFIX} {index}/5"
+ observed_on = datetime.strptime(date_value, "%Y-%m-%d").date()
+ identified_at = datetime(
+ observed_on.year,
+ observed_on.month,
+ observed_on.day,
+ 10 + index,
+ 15,
+ tzinfo=PERTH_TIMEZONE,
+ )
+ _, was_created = WeedSighting.objects.update_or_create(
+ bushland_area=bushland,
+ notes=note,
+ defaults={
+ "model_id": "demo-placeholder",
+ "model_top_id": f"demo-{index}",
+ "top_scientific_name": scientific_name,
+ "top_common_name": common_name,
+ "top_confidence": 0.75,
+ "confirmed_species": common_name,
+ "observed_on": observed_on,
+ "abundance": WeedSighting.Abundance.PATCH,
+ "identified_at": identified_at,
+ },
+ )
+ if was_created:
+ created += 1
+ else:
+ updated += 1
+
+ self.stdout.write(
+ self.style.SUCCESS(
+ f"Seeded {created + updated} placeholder sightings across "
+ f"{len(bushlands)} bushland areas ({created} created, {updated} updated)."
+ )
+ )
diff --git a/server/api/weeds/models.py b/server/api/weeds/models.py
index 0951dc4..6a87d80 100644
--- a/server/api/weeds/models.py
+++ b/server/api/weeds/models.py
@@ -33,6 +33,7 @@ class Abundance(models.TextChoices):
on_delete=models.SET_NULL,
related_name="weed_sightings",
)
+
observed_on = models.DateField(null=True, blank=True)
abundance = models.CharField(max_length=20, choices=Abundance.choices, blank=True)
latitude = models.FloatField(null=True, blank=True)
@@ -45,10 +46,8 @@ class Abundance(models.TextChoices):
def ticket_id(self) -> str:
if not self.pk:
return ""
- if self.identified_at:
- date_str = self.identified_at.strftime("%Y%m%d")
- return f"TKT-{date_str}-{self.pk:04d}"
- return f"TKT-{self.pk:06d}"
+ date_str = self.identified_at.strftime("%Y%m%d")
+ return f"TKT-{date_str}-{self.pk:04d}"
class Meta:
db_table = "weed_sighting"
diff --git a/server/api/weeds/tests.py b/server/api/weeds/tests.py
index ecb38f6..d544d94 100644
--- a/server/api/weeds/tests.py
+++ b/server/api/weeds/tests.py
@@ -1,3 +1,4 @@
+from datetime import date
from unittest.mock import patch
from django.contrib.auth import get_user_model
@@ -8,6 +9,7 @@
from api.bushlands.models import BushlandArea
from .models import WeedSighting
+from .views import _sighting_event
from .services.plant_references import PlantReferences
from .services.weedscan_identify import WeedScanCandidate, WeedScanResult
@@ -117,6 +119,30 @@ def test_database_rejects_invalid_confidence_and_coordinates(self):
class ReportSightingViewTests(TestCase):
+ def test_realtime_event_uses_public_bushland_id(self):
+ bushland = BushlandArea.objects.create(
+ source_object_id=101,
+ site_number=1,
+ name="Kings Park",
+ geometry={"type": "Polygon", "coordinates": []},
+ bbox_west=115.8,
+ bbox_south=-32.0,
+ bbox_east=115.9,
+ bbox_north=-31.9,
+ source_url="https://data.example/bushland/101",
+ )
+ sighting = WeedSighting.objects.create(
+ model_id="realtime-test",
+ confirmed_species="Lantana",
+ bushland_area=bushland,
+ observed_on=date(2026, 9, 5),
+ )
+
+ event = _sighting_event(sighting)
+
+ self.assertEqual(event["bushland_id"], 101)
+ self.assertEqual(event["sighting"]["species"], "Lantana")
+
def test_create_report_generates_ticket_id_and_saves_sighting(self):
bushland = BushlandArea.objects.create(
source_object_id=101,
diff --git a/server/api/weeds/urls.py b/server/api/weeds/urls.py
index 5d47a85..e096b26 100644
--- a/server/api/weeds/urls.py
+++ b/server/api/weeds/urls.py
@@ -5,4 +5,5 @@
urlpatterns = [
path("identify/", views.identify_weed, name="identify"),
path("report/", views.report_sighting, name="report"),
+ path("stream/", views.sighting_stream, name="sighting-stream"),
]
diff --git a/server/api/weeds/views.py b/server/api/weeds/views.py
index da3e272..35eca91 100644
--- a/server/api/weeds/views.py
+++ b/server/api/weeds/views.py
@@ -1,5 +1,11 @@
+import json
+import time
from uuid import uuid4
+from django.db import close_old_connections
+from django.http import StreamingHttpResponse
+from django.utils import timezone
+from django.views.decorators.http import require_GET
from rest_framework.decorators import api_view
from rest_framework.response import Response
@@ -124,3 +130,50 @@ def report_sighting(request):
},
status=201,
)
+
+
+def _sighting_event(sighting):
+ return {
+ "id": sighting.id,
+ "bushland_id": sighting.bushland_area.source_object_id,
+ "sighting": {
+ "date": (sighting.observed_on or sighting.identified_at.date()).isoformat(),
+ "species": sighting.confirmed_species
+ or sighting.top_common_name
+ or sighting.top_scientific_name
+ or "Unidentified weed",
+ "count": 1,
+ },
+ }
+
+
+# Plain Django view (not @api_view): DRF content negotiation rejects
+# EventSource's "Accept: text/event-stream" header with a 406.
+@require_GET
+def sighting_stream(request):
+ def events():
+ cursor = timezone.now()
+ yield ": connected\n\n"
+
+ while True:
+ close_old_connections()
+ sightings = list(
+ WeedSighting.objects.filter(
+ identified_at__gt=cursor,
+ bushland_area__isnull=False,
+ )
+ .select_related("bushland_area")
+ .order_by("identified_at", "id")[:50]
+ )
+ if sightings:
+ for sighting in sightings:
+ cursor = max(cursor, sighting.identified_at)
+ yield f"data: {json.dumps(_sighting_event(sighting))}\n\n"
+ else:
+ yield ": keepalive\n\n"
+ time.sleep(1)
+
+ response = StreamingHttpResponse(events(), content_type="text/event-stream")
+ response["Cache-Control"] = "no-cache"
+ response["X-Accel-Buffering"] = "no"
+ return response
diff --git a/start-dev.sh b/start-dev.sh
new file mode 100644
index 0000000..b55ee82
--- /dev/null
+++ b/start-dev.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+FRONTEND_LOG="${TMPDIR:-/tmp}/cfc-tokenmaxxing-frontend.log"
+
+for command in docker npm; do
+ if ! command -v "$command" >/dev/null 2>&1; then
+ echo "Required command not found: $command" >&2
+ exit 1
+ fi
+done
+
+cd "$REPO_ROOT"
+
+echo "Starting database, backend, and inference services..."
+docker compose up -d --build server weedscan-inference
+
+echo "Starting frontend at http://localhost:3000..."
+(
+ cd client
+ npm run dev
+) >"$FRONTEND_LOG" 2>&1 &
+
+echo "Frontend output: $FRONTEND_LOG"
+echo "Backend: http://localhost:8000"
+echo "Tailing inference logs. Press Ctrl+C to stop log tailing."
+exec docker compose logs --follow weedscan-inference
\ No newline at end of file
diff --git a/test-images/weed-identification/README.md b/test-images/weed-identification/README.md
new file mode 100644
index 0000000..413bafc
--- /dev/null
+++ b/test-images/weed-identification/README.md
@@ -0,0 +1,16 @@
+# Weed Identification Test Images
+
+These JPEG fixtures were scored against the local `weedscan19_epoch_300` model on
+2026-09-06. They demonstrate the confidence handling in the identification flow.
+
+| File | Expected top prediction | Verified confidence | Expected UI behavior |
+| --- | --- | --- | --- |
+| `high-confidence-lantana-camara.jpg` | `Lantana camara` | 100.0% | Display the result for user confirmation. |
+| `medium-confidence-verbascum-thapsus.jpg` | `Verbascum thapsus` | 73.4% | Display the result for manual verification. |
+| `low-confidence-generic-scene.jpg` | `Senna obtusifolia` | 14.0% | Show the retake-photo prompt because no candidate exceeds 20%. |
+
+## Sources
+
+- High-confidence Lantana: Wikimedia Commons, `Twin lantana camara edit.jpg`.
+- Medium-confidence great mullein: Wikimedia Commons, `Koningskaars R01.jpg`.
+- Low-confidence generic scene: Picsum Photos, seed `nonplant-smoke-test`.
\ No newline at end of file
diff --git a/test-images/weed-identification/high-confidence-lantana-camara.jpg b/test-images/weed-identification/high-confidence-lantana-camara.jpg
new file mode 100644
index 0000000..97cd12a
Binary files /dev/null and b/test-images/weed-identification/high-confidence-lantana-camara.jpg differ
diff --git a/test-images/weed-identification/low-confidence-generic-scene.jpg b/test-images/weed-identification/low-confidence-generic-scene.jpg
new file mode 100644
index 0000000..2cad1f4
Binary files /dev/null and b/test-images/weed-identification/low-confidence-generic-scene.jpg differ
diff --git a/test-images/weed-identification/medium-confidence-verbascum-thapsus.jpg b/test-images/weed-identification/medium-confidence-verbascum-thapsus.jpg
new file mode 100644
index 0000000..65c567e
Binary files /dev/null and b/test-images/weed-identification/medium-confidence-verbascum-thapsus.jpg differ