-
-
The agent looks again at 10 m, then checks rain: 37.5 mm in 3 days explains it, so it logs the plume as expected.
-
The agent sees the cloud at 10 m and stops there: no rain check, no sample, marked as an image artifact.
-
Pililla: dark water flagged by the scan. The 10 m crop shows a cloud edge.
-
Binangonan: 1.2 mm of rain and plumes seen here before. The agent proposes field sampling, which waits for a person to approve.
-
A satellite can't tell what's in the water. It can tell where a sample is worth taking.
-
Each recorded run can be replayed step by step.
-
Calamba, Sep 18 vs Sep 25: OpenCV 5 flags brown water off the shore after a week of rain.
-
Sampling requests stop at "pending approval". Only a signed-in researcher approves or rejects.
-
Works on a phone, for checking a sighting from the field.
-
OpenCV 5 flags brown water off the shore after a week of rain.
-
Every plume found in Laguna de Bay. Red means dry weather (suspicious, verify), blue means after rain.
-
Runtime on AWS: a daily OpenCV 5 scan of Sentinel-2 feeds an agent on Bedrock AgentCore.
-
Delivery: CI checks every change, including an agent replay of three real cases, before a scripted deploy.
Inspiration
Laguna de Bay is the largest lake in the Philippines. It is a source of drinking water for parts of Metro Manila, supports a large fishing community, and receives runoff from factories, farms and towns along its shore. When a discharge happens, the water changes color long before anyone files a report. Often nobody files one at all.
Field sampling is the only way to confirm pollution, but it is slow and costly, and nobody can sample roughly 870 km² of water every day. Sentinel-2 passes over the lake every few days, for free. So we asked: can a satellite and an agent decide where a sampling team should go?
What it does
Lawata watches the lake from orbit and sorts each suspicious plume into one of four outcomes:
- Discard it as an artifact (cloud edge, sun glint, haze)
- Log it as expected, for example runoff after heavy rain
- Recheck it on the next satellite pass
- Propose field sampling, which a person must approve before any team goes out
Lawata never calls a finding "illegal". It says suspicious, verify. A satellite sees color, not PFAS or heavy metals, so Lawata is a triage tool, not a verdict.
Try it at https://www.lawata.org. Researchers sign in to see each decision, run the agent live on any part of the lake, and approve or reject sampling.
How we built it
Perception (OpenCV 5). We read only the lake window from Sentinel-2 Cloud-Optimized GeoTIFFs on AWS Open Data, then:
Build a water mask with MNDWI instead of NDWI, because the lake is so green that NDWI misses it: $$\text{MNDWI} = \frac{G - \text{SWIR}_1}{G + \text{SWIR}_1}$$ where G is green (band 3) and SWIR₁ is shortwave infrared (band 11). We clean it with
cv2.morphologyEx, keep the largest component withcv2.connectedComponentsWithStats(this drops fishponds), and erode the shoreline.Mask clouds from the scene classification layer and buffer them by 300 m with
cv2.dilate. Flag sun glint with a local-texture filter (cv2.blur) and haze with a robust z-score on blue.Score every pixel by its color distance from open water more than 2 km from shore (
cv2.distanceTransform), using brightness-independent features $$\mathbf{x} = \left[\ln\tfrac{R}{G},\ \ln\tfrac{B}{G},\ \ln G\right], \qquad D = \sqrt{(\mathbf{x}-\boldsymbol{\mu})^\top \Sigma^{-1} (\mathbf{x}-\boldsymbol{\mu})}$$ This catches both brown (sediment) and dark (organic) plumes.Compare each pixel with its own median over many dates, so we detect change at that spot, not just "brown water".
Segment with
cv2.findContoursand keep blobs of at least 30 ha that touch the shore within 1 km, have a smooth outline and sit mostly away from cloud.
Decision (agent). Each candidate goes to Claude Haiku 4.5 on Amazon Bedrock (Converse tool use), hosted on Bedrock AgentCore Runtime. The agent has three tools: rainfall (Open-Meteo), plume history (Neon Postgres/PostGIS) and, most importantly, inspect_plume. That tool re-reads the spot at 10 m, reruns OpenCV, and returns clear-water, cloud-edge, glint and shape measurements plus a crop image the model can see. What it sees changes what it does next: a cloud edge at 10 m ends the run in mark_artifact with no rain check, and a real plume in dry weather at a spot with a history of unusual water ends in a sampling request.
Safety rails in code. Hard limits of 6 turns, 8 tool calls and 20k tokens; any limit ends in schedule_recheck, never in sampling. Each decision needs its evidence (no sampling request without inspection, rain and history). With too little clear water, only a recheck is accepted. Sampling tasks always stop at pending_approval, and approving one needs a signed-in researcher, a note and a confirmation.
Cloud. A FastAPI Lambda behind CloudFront serves the React dashboard and API; Amazon Cognito handles researcher sign-in; evidence crops go to S3 and secrets to SSM. Everything is in Terraform with an AWS Budget alarm, ARM64 containers and no idle servers, so it costs a few dollars a month. The whole-lake scan currently runs as a batch job; the scheduled scan Lambda is deployed and is the next piece to wire up.
Results on real data
We ran the scan on 980 Sentinel-2 scenes from January 2024 to October 2026: 221 dates, 86 of them clear enough to use.
- 34 plumes on the map, of which 3 were artifacts (haze, cloud shadow, cloud edge)
- 25 of the 31 real plumes followed rain
- 6 appeared in dry weather. Five, at Lumban and Paete, sit near the Pagsanjan River mouth and the Kalayaan power-plant outflow and are likely explained. One, at Binangonan on 2026-05-06, is unexplained: that spot showed unusual water on 4 of 9 clear passes that spring.
The same agent with the same prompt handles three real cases and takes three different actions:
| Case | What the evidence shows | Agent action |
|---|---|---|
| Pililla, 2026-04-21 | At 10 m the blob hugs a cloud edge (77% near cloud) | mark_artifact |
| Calamba, 2026-09-25 | 806 ha plume after ~150 mm of rain in a week | log_expected |
| Binangonan, 2026-05-06 | 92 ha shore-attached plume, about 1 mm of rain in 7 days, unusual water there before | create_sampling_task, awaiting human approval |
All three pass in our replay tests, with real OpenCV and real rainfall, and on the deployed agent (11 s for the Binangonan case).
Challenges
- A bug that cost us weeks. We applied Sentinel-2's reflectance offset twice, so every value went negative and nothing made sense. The fix was one
if. Finding it took weeks. - Fixed thresholds fail. The lake's color shifts a lot between seasons, so we score each pixel against the scene and against its own history.
- Everything looks like a plume. Sun glint, cloud edges, cloud shadow and shoreline pixels each needed their own filter, and the agent's 10 m look catches what the filters miss.
- Agents that don't stop. Without hard limits in code, a model can loop forever. Our tests include a fake model that never decides, and the loop still ends safely in a recheck.
Accomplishments that we're proud of
- A live, public site at lawata.org with a working agent you can run on any part of the lake.
- Over 400 automated tests (Python and web) plus end-to-end tests, all run in CI along with the three-case replay.
- Wording and permissions that never let the system claim more than a satellite can see, or act without a person.
What we learned
- Computer vision finds candidates, but context (rain, history, a closer look) decides what they mean. Splitting perception from decision made both easier to test.
- Relative measures beat absolute ones in real-world imagery.
- Responsible AI here means a person always makes the final call, and the wording never claims more than the data shows.
What's next
- Wire the daily scan into the scheduled Lambda, run after scenes publish, and follow up rechecks automatically.
- Model wind, which stirs up sediment in a shallow lake, and use seasonal baselines.
- Store each live area analysis so it can open a sampling task, still pending approval.
- Add more lakes, and work with the Laguna Lake Development Authority so approved tasks reach real field teams, with lab results feeding back as ground truth.
Built With
- amazon-bedrock
- amazon-cloudfront
- amazon-eventbridge
- amazon-web-services
- aws-lambda
- aws-open-data
- bedrock-agentcore
- claude
- fastapi
- numpy
- opencv
- postgresql
- python
- rasterio
- react
- sentinel-2
- typescript
Log in or sign up for Devpost to join the conversation.