Add pool condition detection #9
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
I want you to look at the current design of nemish and design out how we'll add pool condition detection to the daemon subcommand. Right now the system works by downloading a satellite image, calculating a pool mask, and then translating that mask into a geometry. All of this data is then sent back to nidus-sync. Nidus-sync recently developed the ability to track not just pool location, but pool condition:
In general we detect these by looking at the color of the pool. I'd like you to design two things. First, how would we change the training system of nemish to not just find pools but to learn to categorize them by their condition. Second, once we have that trained model, how would we update the daemon process to use the model to also output the pool condition?
Pool Condition Detection in Nemish — Design Proposal
Context
Current nemish pipeline
nidus-sync pool condition model
feature_pool_statetracks condition as an enum with values already defined:bluedrygreenmurkycoveredfalse poolunknownThe detectable conditions by satellite imagery color analysis are: blue, dry, green, murky, covered. I'll design for these five + background.
Part 1: Training System Changes
Analysis of approaches
Option A: Multi-class semantic segmentation (pixel-level)
Change the model from binary (pool vs background) to N-class where each class = condition:
What changes in the code:
default.yaml):data.num_classes→ 6 (or N+1)PoolDatasetvalidates for binary 0/255 and exits on unexpected values — a new dataset variant or a mode flag is needed.CombinedLoss(CrossEntropy + Dice)already works for multi-class. Likely adjustce_weight/dice_weightsince Dice can be noisy with many rare classes.compute_iou,compute_dice): already handlenum_classes> 2. Each class gets its own IoU/Dice report.predict_array): still works — returns per-pixel argmax over N classes.Challenges:
Option B: Two-stage (pool detection → condition classifier) ★ RECOMMENDED ★
Stage 1 (unchanged): binary pool segmentation as today.
Stage 2: for each detected pool polygon, classify its condition.
Two viable sub-approaches for Stage 2:
B1 — Lightweight color-statistics classifier:
B2 — CNN patch classifier:
Why two-stage is better than multi-class segmentation:
Option C: Multi-task segmentation (shared encoder, two decoders)
One shared encoder (ResNet-34) with two decoder heads:
This preserves the binary mask quality while learning condition features, but it's the most complex to implement, debug, and train. The codebase doesn't support multi-head architectures yet, and this design doc isn't the place for that scope.
Recommended approach: Two-stage (Option B2 + B1 hybrid)
Start with B1 (color-statistics MLP) as the MVP — it's the quickest path to a working condition classifier:
As a fallback/Baseline for covered pools (which are very distinct — usually a dark rectangle/blue tarp), add a simple heuristic pre-classifier: if mean pixel brightness < threshold and edge density > threshold → "covered". This can be a 20-line function and catch most covered pools with near-perfect accuracy.
The MLP classifier training pipeline would be a new subcommand:
Part 2: Daemon Integration
What the daemon currently does (in
_inference_worker)What needs to change for condition detection
Changes by file
src/daemon.pyLoad condition classifier during inference worker init (alongside the pool detection model):
New function —
classify_pool_condition():Modified
_submit_result()— also include condition in the payload:Where
conditionsis a list of per-pool conditions matching the order of polygons in the geometry.src/inference/predict.pyAdd a new condition classifier module (or a new file
src/classify.py):src/training/condition_classifier.py(new file)Training pipeline for the condition MLP classifier:
nidus-sync side
The
/api/vision/task/{id}/inferenceendpoint needs to accept aconditionsfield. Currently it readsconfidence(form field) andmask(file). Adding:The
vision_analysis_tasktable already hasconfidence. We'd add a new migration to store per-task conditions. However, since a task (tile) may contain multiple pools with different conditions, the condition belongs on the geometry features or in a separatetask_pool_conditionstable.Recommended approach: Store conditions as a property on the geometry GeoJSON. Each polygon Feature gets a
conditionproperty. The existing GeoJSON pipeline already attaches per-polygon properties (id,area_pixels,num_vertices). Just addcondition.The
postVisionTaskInferencehandler already receivesgeometryin form data (optional from the daemon, though currently unused in that handler — geometry is separately PUT via/vision/task/{id}). We should:conditionsform field to the inference POSTvision_analysis.goflow where tasks becomefeature_poolentries)Implementation plan
Phase 1: Condition classifier training pipeline (nemish)
src/training/condition.py—PoolConditionDataset,ConditionMLP,train_condition_classifier()nemish train-conditionCLI subcommand — takes pool-level CSV labels + existing pool detection modelextract_color_features()to image_utils or new classify modulePhase 2: Daemon integration
classify_pool_condition()to the daemon_inference_worker_submit_result()payload/data/model/)Phase 3: nidus-sync changes
conditionsform field topostVisionTaskInferencehandlervision_analysis_taskschema if needed for condition trackingOut of scope (Phase 1)
Risks and mitigations
Great, this is a good plan. I agree that option B is preferrable, and I like your recommendation of Option B2 + B1 hybrid. I also think that including the condition in the GeoJSON payload properties is an elegant solution.
Please create a pull request detailing the design and implementation plan with steps for validation that things are working at each phase. Put it in doc/pool-condition-detection.md
PR #10 created with the design document:
doc/pool-condition-detection.md.#10
Covers:
Ready for review, neighborino!
CI failure on main after merge
PR #10 was merged, but the merge CI run (run 188) will fail — there is a pre-existing lint issue in
src/web/server.py: an unused# noqa: N802directive ondo_GET(). Ruff 0.15.x doesn't flag N802 onBaseHTTPRequestHandlermethod names, so the noqa is flagged as unused (RUF100). This existed before the design doc commit, but wasn't caught because the CI config was iterated on main separately.Created PR #11 as a one-line fix (remove the stale noqa comment): #11
Once #11 is merged, main CI should pass cleanly again.