mirror of
https://github.com/azaion/gps-denied-desktop.git
synced 2026-04-22 11:36:36 +00:00
component decomposition is done
This commit is contained in:
@@ -44,5 +44,6 @@
|
|||||||
- Generate draw.io components diagram shows relations between components.
|
- Generate draw.io components diagram shows relations between components.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
Components should be semantically coherents. Do not spread similar functionality across multiple components
|
||||||
Do not put any code yet, only names, input and output.
|
Do not put any code yet, only names, input and output.
|
||||||
Ask as many questions as possible to clarify all uncertainties.
|
Ask as many questions as possible to clarify all uncertainties.
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
<mxfile host="65bd71144e">
|
||||||
|
<diagram name="ASTRAL-Next Components" id="astral-next-components">
|
||||||
|
<mxGraphModel dx="1440" dy="1201" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1800" pageHeight="2800" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0"/>
|
||||||
|
<mxCell id="1" parent="0"/>
|
||||||
|
<mxCell id="title" value="ASTRAL-Next System Architecture GPS-Denied Localization for UAVs 29 Components: Route API (4) + GPS-Denied API (17) + Helpers (8)" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=20;fontStyle=1;fontColor=#ffffff;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="200" y="20" width="1000" height="80" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="route-api-lane" value="Route API (Separate Project)" style="swimlane;horizontal=1;whiteSpace=wrap;html=1;fontSize=14;fontStyle=1;fillColor=#1565C0;strokeColor=#64B5F6;fontColor=#ffffff;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="200" y="110" width="600" height="350" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="r01" value="R01 Route REST API" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#B8860B;strokeColor=#FFD54F;fontColor=#ffffff;" parent="route-api-lane" vertex="1">
|
||||||
|
<mxGeometry x="40" y="50" width="120" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="r02" value="R02 Route Data Manager" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#B8860B;strokeColor=#FFD54F;fontColor=#ffffff;" parent="route-api-lane" vertex="1">
|
||||||
|
<mxGeometry x="200" y="50" width="140" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="r03" value="R03 Waypoint Validator" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#B8860B;strokeColor=#FFD54F;fontColor=#ffffff;" parent="route-api-lane" vertex="1">
|
||||||
|
<mxGeometry x="380" y="50" width="140" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="r04" value="R04 Route Database Layer" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#B8860B;strokeColor=#FFD54F;fontColor=#ffffff;" parent="route-api-lane" vertex="1">
|
||||||
|
<mxGeometry x="200" y="150" width="140" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="r01-r02" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="route-api-lane" source="r01" target="r02" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="r02-r03" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="route-api-lane" source="r02" target="r03" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="r02-r04" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="route-api-lane" source="r02" target="r04" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="route-db" value="Route DB (Separate Schema)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#424242;strokeColor=#BDBDBD;fontColor=#ffffff;" parent="route-api-lane" vertex="1">
|
||||||
|
<mxGeometry x="225" y="240" width="90" height="80" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="r04-db" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="route-api-lane" source="r04" target="route-db" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="gps-denied-lane" value="GPS-Denied API (Main Processing System)" style="swimlane;horizontal=1;whiteSpace=wrap;html=1;fontSize=14;fontStyle=1;fillColor=#388E3C;strokeColor=#66BB6A;fontColor=#ffffff;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="130" y="500" width="1200" height="2200" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="core-layer" value="Core API Layer" style="swimlane;whiteSpace=wrap;html=1;fillColor=#6A1B9A;strokeColor=#BA68C8;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="20" y="40" width="560" height="140" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="g01" value="G01 GPS-Denied REST API" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#8B1A1A;strokeColor=#EF5350;fontColor=#ffffff;" parent="core-layer" vertex="1">
|
||||||
|
<mxGeometry x="20" y="40" width="150" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="g02" value="G02 Flight Manager" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#8B1A1A;strokeColor=#EF5350;fontColor=#ffffff;" parent="core-layer" vertex="1">
|
||||||
|
<mxGeometry x="200" y="40" width="150" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="g03" value="G03 Route API Client" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#8B1A1A;strokeColor=#EF5350;fontColor=#ffffff;" parent="core-layer" vertex="1">
|
||||||
|
<mxGeometry x="380" y="40" width="150" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="g01-g02" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="core-layer" source="g01" target="g02" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="data-layer" value="Data Management" style="swimlane;whiteSpace=wrap;html=1;fillColor=#6A1B9A;strokeColor=#BA68C8;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="600" y="40" width="560" height="140" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="g04" value="G04 Satellite Data Manager (fetch, cache, grid)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#CC6600;strokeColor=#FFB300;fontColor=#ffffff;" parent="data-layer" vertex="1">
|
||||||
|
<mxGeometry x="20" y="40" width="160" height="70" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="g05" value="G05 Image Input Pipeline (queue, validate, store)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#CC6600;strokeColor=#FFB300;fontColor=#ffffff;" parent="data-layer" vertex="1">
|
||||||
|
<mxGeometry x="200" y="40" width="160" height="70" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="g06" value="G06 Image Rotation Mgr (30° sweep, heading)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#CC6600;strokeColor=#FFB300;fontColor=#ffffff;" parent="data-layer" vertex="1">
|
||||||
|
<mxGeometry x="380" y="40" width="160" height="70" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="visual-layer" value="Visual Processing (Tri-Layer Architecture)" style="swimlane;whiteSpace=wrap;html=1;fillColor=#6A1B9A;strokeColor=#BA68C8;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="20" y="200" width="560" height="140" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="g07" value="G07 Sequential VO (SuperPoint+LightGlue)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#1E88E5;strokeColor=#42A5F5;fontColor=#ffffff;" parent="visual-layer" vertex="1">
|
||||||
|
<mxGeometry x="20" y="40" width="160" height="70" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="g08" value="G08 Global Place Recognition (AnyLoc DINOv2)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#1E88E5;strokeColor=#42A5F5;fontColor=#ffffff;" parent="visual-layer" vertex="1">
|
||||||
|
<mxGeometry x="200" y="40" width="160" height="70" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="g09" value="G09 Metric Refinement (LiteSAM)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#1E88E5;strokeColor=#42A5F5;fontColor=#ffffff;" parent="visual-layer" vertex="1">
|
||||||
|
<mxGeometry x="380" y="40" width="160" height="70" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="state-layer" value="State Estimation & Coordination" style="swimlane;whiteSpace=wrap;html=1;fillColor=#6A1B9A;strokeColor=#BA68C8;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="600" y="200" width="560" height="140" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="g10" value="G10 Factor Graph Optimizer (GTSAM iSAM2)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#388E3C;strokeColor=#66BB6A;fontColor=#ffffff;" parent="state-layer" vertex="1">
|
||||||
|
<mxGeometry x="20" y="40" width="160" height="70" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="g11" value="G11 Failure Recovery (Progressive 1→25)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#388E3C;strokeColor=#66BB6A;fontColor=#ffffff;" parent="state-layer" vertex="1">
|
||||||
|
<mxGeometry x="200" y="40" width="160" height="70" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="g12" value="G12 Coordinate Transform (Pixel↔GPS)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#388E3C;strokeColor=#66BB6A;fontColor=#ffffff;" parent="state-layer" vertex="1">
|
||||||
|
<mxGeometry x="380" y="40" width="160" height="70" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="results-layer" value="Results & Communication" style="swimlane;whiteSpace=wrap;html=1;fillColor=#6A1B9A;strokeColor=#BA68C8;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="20" y="360" width="380" height="140" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="g13" value="G13 Result Manager" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E65100;strokeColor=#FFA726;fontColor=#ffffff;" parent="results-layer" vertex="1">
|
||||||
|
<mxGeometry x="20" y="40" width="150" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="g14" value="G14 SSE Event Streamer" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E65100;strokeColor=#FFA726;fontColor=#ffffff;" parent="results-layer" vertex="1">
|
||||||
|
<mxGeometry x="200" y="40" width="150" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="g13-g14" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="results-layer" source="g13" target="g14" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="infra-layer" value="Infrastructure" style="swimlane;whiteSpace=wrap;html=1;fillColor=#6A1B9A;strokeColor=#BA68C8;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="420" y="360" width="740" height="140" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="g15" value="G15 Model Manager (TensorRT/ONNX)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#424242;strokeColor=#BDBDBD;fontColor=#ffffff;" parent="infra-layer" vertex="1">
|
||||||
|
<mxGeometry x="20" y="40" width="160" height="70" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="g16" value="G16 Configuration Mgr" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#424242;strokeColor=#BDBDBD;fontColor=#ffffff;" parent="infra-layer" vertex="1">
|
||||||
|
<mxGeometry x="200" y="40" width="160" height="70" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="g17" value="G17 GPS-Denied DB Layer" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#424242;strokeColor=#BDBDBD;fontColor=#ffffff;" parent="infra-layer" vertex="1">
|
||||||
|
<mxGeometry x="380" y="40" width="160" height="70" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="gps-db" value="GPS-Denied DB (Separate Schema) Flights, Frame Results" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#424242;strokeColor=#BDBDBD;fontColor=#ffffff;" parent="infra-layer" vertex="1">
|
||||||
|
<mxGeometry x="570" y="30" width="140" height="90" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="helpers-lane" value="Helper Components (Shared Utilities)" style="swimlane;whiteSpace=wrap;html=1;fillColor=#424242;strokeColor=#BDBDBD;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="20" y="520" width="1140" height="160" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="h01" value="H01 Camera Model" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#689F38;strokeColor=#9CCC65;fontColor=#ffffff;" parent="helpers-lane" vertex="1">
|
||||||
|
<mxGeometry x="20" y="40" width="120" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="h02" value="H02 GSD Calculator" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#689F38;strokeColor=#9CCC65;fontColor=#ffffff;" parent="helpers-lane" vertex="1">
|
||||||
|
<mxGeometry x="160" y="40" width="120" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="h03" value="H03 Robust Kernels" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#689F38;strokeColor=#9CCC65;fontColor=#ffffff;" parent="helpers-lane" vertex="1">
|
||||||
|
<mxGeometry x="300" y="40" width="120" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="h04" value="H04 Faiss Index Mgr" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#689F38;strokeColor=#9CCC65;fontColor=#ffffff;" parent="helpers-lane" vertex="1">
|
||||||
|
<mxGeometry x="440" y="40" width="120" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="h05" value="H05 Performance Monitor" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#689F38;strokeColor=#9CCC65;fontColor=#ffffff;" parent="helpers-lane" vertex="1">
|
||||||
|
<mxGeometry x="580" y="40" width="130" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="h06" value="H06 Web Mercator Utils" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#689F38;strokeColor=#9CCC65;fontColor=#ffffff;" parent="helpers-lane" vertex="1">
|
||||||
|
<mxGeometry x="730" y="40" width="130" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="h07" value="H07 Image Rotation Utils" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#689F38;strokeColor=#9CCC65;fontColor=#ffffff;" parent="helpers-lane" vertex="1">
|
||||||
|
<mxGeometry x="880" y="40" width="120" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="h08" value="H08 Batch Validator" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#689F38;strokeColor=#9CCC65;fontColor=#ffffff;" parent="helpers-lane" vertex="1">
|
||||||
|
<mxGeometry x="1020" y="40" width="100" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="flow-title" value="Main Processing Flow" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=16;fontStyle=1;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="20" y="700" width="200" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="flow-box" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#1E1E1E;strokeColor=#FFFFFF;dashed=1;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="20" y="740" width="1140" height="600" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="flow-1" value="1. Client uploads batch G01 → G05" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#1565C0;strokeColor=#64B5F6;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="40" y="760" width="160" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="flow-2" value="2. Get next image G05 → G06" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#1565C0;strokeColor=#64B5F6;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="230" y="760" width="160" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="flow-3" value="3. Rotation preprocessing G06 (30° sweep if needed)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#B8860B;strokeColor=#FFD54F;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="420" y="760" width="180" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="flow-4" value="4. Sequential VO G07 (SuperPoint+LightGlue)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#388E3C;strokeColor=#66BB6A;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="630" y="760" width="180" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="flow-5" value="5. Check confidence G11 Failure Recovery" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#8B1A1A;strokeColor=#EF5350;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="840" y="760" width="180" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="flow-6a" value="6a. IF GOOD: LiteSAM (1 tile) G09 drift correction" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#388E3C;strokeColor=#66BB6A;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="40" y="860" width="200" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="flow-6b" value="6b. IF LOST: Progressive search G11 → G04 (1→4→9→16→25) G08 Global PR + G09 LiteSAM" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#CC6600;strokeColor=#FFB300;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="270" y="860" width="220" height="80" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="flow-7" value="7. Factor Graph optimize G10 (fuse VO + GPS)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#6A1B9A;strokeColor=#BA68C8;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="520" y="860" width="180" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="flow-8" value="8. Coordinate transform G12 (Pixel → GPS)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#6A1B9A;strokeColor=#BA68C8;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="730" y="860" width="180" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="flow-9" value="9. Publish results G13 → G03 (Route API) G13 → G14 (SSE to client)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E65100;strokeColor=#FFA726;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="940" y="860" width="180" height="80" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="arrow-1-2" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="flow-1" target="flow-2" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="arrow-2-3" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="flow-2" target="flow-3" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="arrow-3-4" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="flow-3" target="flow-4" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="arrow-4-5" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="flow-4" target="flow-5" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="arrow-5-6a" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="flow-5" target="flow-6a" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="arrow-5-6b" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="flow-5" target="flow-6b" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="arrow-6a-7" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="flow-6a" target="flow-7" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="arrow-6b-7" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="flow-6b" target="flow-7" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="arrow-7-8" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="flow-7" target="flow-8" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="arrow-8-9" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="flow-8" target="flow-9" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="user-input-title" value="User Input Recovery (when all search fails)" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=14;fontStyle=1;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="40" y="980" width="400" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="user-1" value="G11 exhausted (grid=25) → G14 send user_input_needed" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#8B1A1A;strokeColor=#EF5350;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="40" y="1020" width="240" height="70" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="user-2" value="Client responds G01 → G11 apply_user_anchor" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#388E3C;strokeColor=#66BB6A;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="310" y="1020" width="240" height="70" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="user-3" value="G11 → G10 add_absolute_factor (high confidence) Processing resumes" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#6A1B9A;strokeColor=#BA68C8;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="580" y="1020" width="240" height="70" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="arrow-user-1-2" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="user-1" target="user-2" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="arrow-user-2-3" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="user-2" target="user-3" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="async-title" value="Asynchronous Trajectory Refinement" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=14;fontStyle=1;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="40" y="1130" width="350" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="async-1" value="G10 back-propagates new absolute factors" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#6A1B9A;strokeColor=#BA68C8;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="40" y="1170" width="220" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="async-2" value="G13 detect changed frames → G03 batch_update_waypoints" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E65100;strokeColor=#FFA726;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="290" y="1170" width="240" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="async-3" value="G13 → G14 send frame_refined events" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E65100;strokeColor=#FFA726;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
|
||||||
|
<mxGeometry x="560" y="1170" width="220" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="arrow-async-1-2" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="async-1" target="async-2" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="arrow-async-2-3" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="async-2" target="async-3" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="client" value="Client (GPS-Denied UI)" style="shape=actor;whiteSpace=wrap;html=1;fillColor=#1565C0;strokeColor=#64B5F6;fontColor=#ffffff;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="10" y="560" width="90" height="80" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="satellite-provider" value="Satellite Provider API /api/satellite/tiles/..." style="ellipse;shape=cloud;whiteSpace=wrap;html=1;fillColor=#E65100;strokeColor=#FFB300;fontColor=#ffffff;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="900" y="290" width="250" height="140" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="external-detector" value="External<br>Object Detector (Azaion.Inference)<br>(provides pixel coords)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#C62828;strokeColor=#EF5350;dashed=1;fontColor=#ffffff;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="1340" y="800" width="160" height="80" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="client-g01" value="HTTP REST" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="1" source="client" target="g01" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="g03-route" value="Per-frame waypoint updates" style="edgeStyle=orthogonalEdgeStyle;curved=1;dashed=1;strokeColor=#FFFFFF;fontColor=#ffffff;" parent="1" source="g03" target="r01" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="g04-sat" value="Fetch tiles" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="1" source="g04" target="satellite-provider" edge="1">
|
||||||
|
<mxGeometry x="0.3759" y="-10" relative="1" as="geometry">
|
||||||
|
<mxPoint as="offset"/>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="g14-client" value="SSE Events" style="dashed=1;strokeColor=#FFFFFF;fontColor=#ffffff;" parent="1" source="g14" target="client" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="detector-g12" value="Object pixels → GPS" style="dashed=1;strokeColor=#FFFFFF;fontColor=#ffffff;" parent="1" source="external-detector" target="g12" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="legend-title" value="Legend" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=16;fontStyle=1;fontColor=#ffffff;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="100" y="2730" width="100" height="30" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="legend-box" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#1E1E1E;strokeColor=#FFFFFF;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="100" y="2770" width="700" height="120" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="legend-1" value="Route API Components" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#B8860B;strokeColor=#FFD54F;fontColor=#ffffff;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="120" y="2790" width="140" height="40" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="legend-2" value="GPS-Denied Core/API" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#8B1A1A;strokeColor=#EF5350;fontColor=#ffffff;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="280" y="2790" width="140" height="40" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="legend-3" value="Visual Processing" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#1E88E5;strokeColor=#42A5F5;fontColor=#ffffff;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="440" y="2790" width="140" height="40" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="legend-4" value="Helper Components" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#689F38;strokeColor=#9CCC65;fontColor=#ffffff;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="120" y="2840" width="140" height="40" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="legend-5" value="State Estimation" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#388E3C;strokeColor=#66BB6A;fontColor=#ffffff;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="280" y="2840" width="140" height="40" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="legend-6" value="Infrastructure" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#424242;strokeColor=#BDBDBD;fontColor=#ffffff;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="440" y="2840" width="140" height="40" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="legend-7" value="External Systems" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#C62828;strokeColor=#EF5350;fontColor=#ffffff;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="600" y="2790" width="140" height="40" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 333 KiB |
@@ -0,0 +1,364 @@
|
|||||||
|
<!-- 31098ee5-58fb-474a-815e-fd9cbd17c063 9f609f9e-c80d-4c88-b618-3135b96a8333 -->
|
||||||
|
# ASTRAL-Next System Component Decomposition Plan
|
||||||
|
|
||||||
|
## Design Principle: Interface-Based Architecture
|
||||||
|
|
||||||
|
**CRITICAL REQUIREMENT**: Each component MUST implement a well-defined interface to ensure interchangeability with different implementations.
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
|
||||||
|
- Swap implementations (e.g., replace LiteSAM with TransFG, GTSAM with Ceres)
|
||||||
|
- Enable unit testing with mocks
|
||||||
|
- Support multiple backends (TensorRT vs ONNX, different databases)
|
||||||
|
- Facilitate future enhancements without breaking contracts
|
||||||
|
|
||||||
|
**Interface Specification**: Each component spec must define:
|
||||||
|
|
||||||
|
- Interface name (e.g., `ISatelliteDataManager`, `IMetricRefinement`)
|
||||||
|
- All public methods with strict contracts
|
||||||
|
- Input/output data structures
|
||||||
|
- Error conditions and exceptions
|
||||||
|
- Performance guarantees
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## System Architecture Overview
|
||||||
|
|
||||||
|
**Two separate REST APIs in same repository:**
|
||||||
|
|
||||||
|
### Route API (Separate Project)
|
||||||
|
|
||||||
|
- Route/waypoint/geofence CRUD
|
||||||
|
- Shared by GPS-Denied and Mission Planner
|
||||||
|
- Does NOT call satellite provider
|
||||||
|
|
||||||
|
### GPS-Denied API (Main System)
|
||||||
|
|
||||||
|
- Tri-layer localization (SuperPoint+LightGlue, AnyLoc, LiteSAM)
|
||||||
|
- Calls satellite provider for tiles
|
||||||
|
- Rotation preprocessing (LiteSAM 45° limit)
|
||||||
|
- Per-frame Route API updates
|
||||||
|
- Progressive tile search (1→4→9→16→25)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ROUTE API COMPONENTS (4 components)
|
||||||
|
|
||||||
|
### R01_route_rest_api
|
||||||
|
|
||||||
|
**Interface**: `IRouteRestAPI`
|
||||||
|
**Endpoints**: `POST /routes`, `GET /routes/{routeId}`, `PUT /routes/{routeId}/waypoints`, `DELETE /routes/{routeId}`
|
||||||
|
|
||||||
|
### R02_route_data_manager
|
||||||
|
|
||||||
|
**Interface**: `IRouteDataManager`
|
||||||
|
**API**: `save_route()`, `load_route()`, `update_waypoint()`, `delete_waypoint()`, `get_route_metadata()`
|
||||||
|
|
||||||
|
### R03_waypoint_validator
|
||||||
|
|
||||||
|
**Interface**: `IWaypointValidator`
|
||||||
|
**API**: `validate_waypoint()`, `validate_geofence()`, `check_bounds()`, `validate_route_continuity()`
|
||||||
|
|
||||||
|
### R04_route_database_layer
|
||||||
|
|
||||||
|
**Interface**: `IRouteDatabase`
|
||||||
|
**API**: `insert_route()`, `update_route()`, `query_routes()`, `get_waypoints()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GPS-DENIED API COMPONENTS (17 components)
|
||||||
|
|
||||||
|
### Core REST API Layer
|
||||||
|
|
||||||
|
**G01_gps_denied_rest_api**
|
||||||
|
**Interface**: `IGPSDeniedRestAPI`
|
||||||
|
**Endpoints**: `POST /gps-denied/flights`, `POST .../images/batch`, `POST .../user-fix`, `GET .../status`, `GET .../stream`
|
||||||
|
|
||||||
|
**G02_flight_manager**
|
||||||
|
**Interface**: `IFlightManager`
|
||||||
|
**API**: `create_flight()`, `get_flight_state()`, `link_to_route()`, `update_flight_status()`
|
||||||
|
|
||||||
|
**G03_route_api_client**
|
||||||
|
**Interface**: `IRouteAPIClient`
|
||||||
|
**API**: `update_route_waypoint()`, `get_route_info()`, `batch_update_waypoints()`
|
||||||
|
|
||||||
|
### Data Management
|
||||||
|
|
||||||
|
**G04_satellite_data_manager**
|
||||||
|
**Interface**: `ISatelliteDataManager`
|
||||||
|
**API**: `fetch_tile()`, `fetch_tile_grid()`, `prefetch_route_corridor()`, `progressive_fetch()`, `cache_tile()`, `get_cached_tile()`, `compute_tile_coords()`, `expand_search_grid()`
|
||||||
|
**Features**: Progressive retrieval, tile caching, grid calculations
|
||||||
|
|
||||||
|
**G05_image_input_pipeline**
|
||||||
|
**Interface**: `IImageInputPipeline`
|
||||||
|
**API**: `queue_batch()`, `process_next_batch()`, `validate_batch()`, `store_images()`, `get_next_image()`, `get_image_by_sequence()`
|
||||||
|
**Features**: FIFO queuing, validation, storage
|
||||||
|
|
||||||
|
**G06_image_rotation_manager**
|
||||||
|
**Interface**: `IImageRotationManager`
|
||||||
|
**API**: `rotate_image_360()`, `try_rotation_steps()`, `calculate_precise_angle()`, `get_current_heading()`, `update_heading()`, `detect_sharp_turn()`, `requires_rotation_sweep()`
|
||||||
|
**Features**: 30° rotation sweeps, heading tracking
|
||||||
|
|
||||||
|
### Visual Processing
|
||||||
|
|
||||||
|
**G07_sequential_visual_odometry**
|
||||||
|
**Interface**: `ISequentialVO`
|
||||||
|
**API**: `compute_relative_pose()`, `extract_features()`, `match_features()`, `estimate_motion()`
|
||||||
|
|
||||||
|
**G08_global_place_recognition**
|
||||||
|
**Interface**: `IGlobalPlaceRecognition`
|
||||||
|
**API**: `retrieve_candidate_tiles()`, `compute_location_descriptor()`, `query_database()`, `rank_candidates()`
|
||||||
|
|
||||||
|
**G09_metric_refinement**
|
||||||
|
**Interface**: `IMetricRefinement`
|
||||||
|
**API**: `align_to_satellite()`, `compute_homography()`, `extract_gps_from_alignment()`, `compute_match_confidence()`
|
||||||
|
|
||||||
|
### State Estimation
|
||||||
|
|
||||||
|
**G10_factor_graph_optimizer**
|
||||||
|
**Interface**: `IFactorGraphOptimizer`
|
||||||
|
**API**: `add_relative_factor()`, `add_absolute_factor()`, `add_altitude_prior()`, `optimize()`, `get_trajectory()`
|
||||||
|
|
||||||
|
**G11_failure_recovery_coordinator**
|
||||||
|
**Interface**: `IFailureRecoveryCoordinator`
|
||||||
|
**API**: `check_confidence()`, `detect_tracking_loss()`, `start_search()`, `expand_search_radius()`, `try_current_grid()`, `create_user_input_request()`, `apply_user_anchor()`
|
||||||
|
|
||||||
|
**G12_coordinate_transformer**
|
||||||
|
**Interface**: `ICoordinateTransformer`
|
||||||
|
**API**: `pixel_to_gps()`, `gps_to_pixel()`, `image_object_to_gps()`, `compute_gsd()`, `transform_points()`
|
||||||
|
|
||||||
|
### Results & Communication
|
||||||
|
|
||||||
|
**G13_result_manager**
|
||||||
|
**Interface**: `IResultManager`
|
||||||
|
**API**: `update_frame_result()`, `publish_to_route_api()`, `get_flight_results()`, `mark_refined()`
|
||||||
|
|
||||||
|
**G14_sse_event_streamer**
|
||||||
|
**Interface**: `ISSEEventStreamer`
|
||||||
|
**API**: `create_stream()`, `send_frame_result()`, `send_search_progress()`, `send_user_input_request()`, `send_refinement()`
|
||||||
|
|
||||||
|
### Infrastructure
|
||||||
|
|
||||||
|
**G15_model_manager**
|
||||||
|
**Interface**: `IModelManager`
|
||||||
|
**API**: `load_model()`, `get_inference_engine()`, `optimize_to_tensorrt()`, `fallback_to_onnx()`
|
||||||
|
|
||||||
|
**G16_configuration_manager**
|
||||||
|
**Interface**: `IConfigurationManager`
|
||||||
|
**API**: `load_config()`, `get_camera_params()`, `validate_config()`, `get_flight_config()`
|
||||||
|
|
||||||
|
**G17_gps_denied_database_layer**
|
||||||
|
**Interface**: `IGPSDeniedDatabase`
|
||||||
|
**API**: `save_flight_state()`, `load_flight_state()`, `query_processing_history()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## HELPER COMPONENTS (8 components)
|
||||||
|
|
||||||
|
**H01_camera_model** - `ICameraModel`
|
||||||
|
**H02_gsd_calculator** - `IGSDCalculator`
|
||||||
|
**H03_robust_kernels** - `IRobustKernels`
|
||||||
|
**H04_faiss_index_manager** - `IFaissIndexManager`
|
||||||
|
**H05_performance_monitor** - `IPerformanceMonitor`
|
||||||
|
**H06_web_mercator_utils** - `IWebMercatorUtils`
|
||||||
|
**H07_image_rotation_utils** - `IImageRotationUtils`
|
||||||
|
**H08_batch_validator** - `IBatchValidator`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comprehensive Component Interaction Matrix
|
||||||
|
|
||||||
|
### System Initialization
|
||||||
|
|
||||||
|
| Source | Target | Method | Purpose |
|
||||||
|
|--------|--------|--------|---------|
|
||||||
|
| G02 | G15 | `load_model()` × 4 | Load SuperPoint, LightGlue, DINOv2, LiteSAM |
|
||||||
|
| G02 | G16 | `load_config()` | Load system configuration |
|
||||||
|
| G04 | G08 | Satellite tiles | G08 generates descriptors for Faiss |
|
||||||
|
| G08 | H04 | `build_index()` | Build satellite descriptor index |
|
||||||
|
| G08 | G15 | `get_inference_engine("DINOv2")` | Get model for descriptor generation |
|
||||||
|
|
||||||
|
### Flight Creation
|
||||||
|
|
||||||
|
| Source | Target | Method | Purpose |
|
||||||
|
|--------|--------|--------|---------|
|
||||||
|
| Client | G01 | `POST /gps-denied/flights` | Create flight |
|
||||||
|
| G01 | G02 | `create_flight()` | Initialize flight state |
|
||||||
|
| G02 | G16 | `get_flight_config()` | Get camera params, altitude |
|
||||||
|
| G02 | G03 | `get_route_info()` | Fetch route metadata |
|
||||||
|
| G03 | Route API | `GET /routes/{routeId}` | HTTP call |
|
||||||
|
| G02 | G04 | `prefetch_route_corridor()` | Prefetch tiles |
|
||||||
|
| G04 | Satellite Provider | `GET /api/satellite/tiles/batch` | HTTP batch download |
|
||||||
|
| G04 | H06 | `compute_tile_bounds()` | Tile coordinate calculations |
|
||||||
|
| G02 | G17 | `save_flight_state()` | Persist flight metadata |
|
||||||
|
| Client | G01 | `GET .../stream` | Open SSE connection |
|
||||||
|
| G01 | G14 | `create_stream()` | Establish SSE channel |
|
||||||
|
|
||||||
|
### Image Upload
|
||||||
|
|
||||||
|
| Source | Target | Method | Purpose |
|
||||||
|
|--------|--------|--------|---------|
|
||||||
|
| Client | G01 | `POST .../images/batch` | Upload 10-50 images |
|
||||||
|
| G01 | G05 | `queue_batch()` | Queue for processing |
|
||||||
|
| G05 | H08 | `validate_batch()` | Validate sequence, format |
|
||||||
|
| G05 | G17 | `store_images()` | Persist images |
|
||||||
|
|
||||||
|
### Per-Frame Processing (First Frame / Sharp Turn)
|
||||||
|
|
||||||
|
| Source | Target | Method | Purpose |
|
||||||
|
|--------|--------|--------|---------|
|
||||||
|
| G05 | G06 | `get_next_image()` | Get image for processing |
|
||||||
|
| G06 | G06 | `requires_rotation_sweep()` | Check if sweep needed |
|
||||||
|
| G06 | H07 | `rotate_image()` × 12 | Rotate in 30° steps |
|
||||||
|
| G06 | G09 | `align_to_satellite()` × 12 | Try LiteSAM each rotation |
|
||||||
|
| G09 | G04 | `get_cached_tile()` | Get expected tile |
|
||||||
|
| G09 | G15 | `get_inference_engine("LiteSAM")` | Get model |
|
||||||
|
| G06 | H07 | `calculate_rotation_from_points()` | Precise angle from homography |
|
||||||
|
| G06 | Internal | `update_heading()` | Store UAV heading |
|
||||||
|
|
||||||
|
### Per-Frame Processing (Sequential VO)
|
||||||
|
|
||||||
|
| Source | Target | Method | Purpose |
|
||||||
|
|--------|--------|--------|---------|
|
||||||
|
| G05 | G07 | `get_next_image()` | Provide image |
|
||||||
|
| G07 | G15 | `get_inference_engine("SuperPoint")` | Get feature extractor |
|
||||||
|
| G07 | G15 | `get_inference_engine("LightGlue")` | Get matcher |
|
||||||
|
| G07 | H05 | `start_timer()`, `end_timer()` | Monitor timing |
|
||||||
|
| G07 | G10 | `add_relative_factor()` | Add pose measurement |
|
||||||
|
|
||||||
|
### Tracking Good (Drift Correction)
|
||||||
|
|
||||||
|
| Source | Target | Method | Purpose |
|
||||||
|
|--------|--------|--------|---------|
|
||||||
|
| G07 | G11 | `check_confidence()` | Check tracking quality |
|
||||||
|
| G11 | G09 | `align_to_satellite()` | Align to 1 tile |
|
||||||
|
| G09 | G04 | `get_tile_grid(1)` | Get single tile |
|
||||||
|
| G09 | G10 | `add_absolute_factor()` | Add GPS measurement |
|
||||||
|
|
||||||
|
### Tracking Lost (Progressive Search)
|
||||||
|
|
||||||
|
| Source | Target | Method | Purpose |
|
||||||
|
|--------|--------|--------|---------|
|
||||||
|
| G07 | G11 | `check_confidence()` → FAIL | Low confidence |
|
||||||
|
| G11 | G06 | `requires_rotation_sweep()` | Trigger rotation sweep |
|
||||||
|
| G11 | G08 | `retrieve_candidate_tiles()` | Coarse localization |
|
||||||
|
| G08 | G15 | `get_inference_engine("DINOv2")` | Get model |
|
||||||
|
| G08 | H04 | `search()` | Query Faiss index |
|
||||||
|
| G08 | G04 | `get_tile_by_gps()` × 5 | Get candidate tiles |
|
||||||
|
| G11 | G04 | `expand_search_grid(4)` | Get 2×2 grid |
|
||||||
|
| G11 | G09 | `align_to_satellite()` | Try LiteSAM on 4 tiles |
|
||||||
|
| G11 (fail) | G04 | `expand_search_grid(9)` | Expand to 3×3 |
|
||||||
|
| G11 (fail) | G04 | `expand_search_grid(16)` | Expand to 4×4 |
|
||||||
|
| G11 (fail) | G04 | `expand_search_grid(25)` | Expand to 5×5 |
|
||||||
|
| G11 (fail) | G14 | `send_user_input_request()` | Request human help |
|
||||||
|
| G11 | G02 | `update_flight_status("BLOCKED")` | Block processing |
|
||||||
|
|
||||||
|
### Optimization & Results
|
||||||
|
|
||||||
|
| Source | Target | Method | Purpose |
|
||||||
|
|--------|--------|--------|---------|
|
||||||
|
| G10 | H03 | `huber_loss()`, `cauchy_loss()` | Apply robust kernels |
|
||||||
|
| G10 | Internal | `optimize()` | Run iSAM2 optimization |
|
||||||
|
| G10 | G12 | `get_trajectory()` | Get optimized poses |
|
||||||
|
| G12 | H01 | `project()`, `unproject()` | Camera operations |
|
||||||
|
| G12 | H02 | `compute_gsd()` | GSD calculations |
|
||||||
|
| G12 | H06 | `tile_to_latlon()` | Coordinate transforms |
|
||||||
|
| G12 | G13 | Frame GPS + object coords | Provide results |
|
||||||
|
| G13 | G03 | `update_route_waypoint()` | Per-frame Route API update |
|
||||||
|
| G03 | Route API | `PUT /routes/.../waypoints/...` | HTTP call |
|
||||||
|
| G13 | G14 | `send_frame_result()` | Publish to client |
|
||||||
|
| G14 | Client | SSE `frame_processed` | Real-time delivery |
|
||||||
|
| G13 | G17 | `save_flight_state()` | Persist state |
|
||||||
|
|
||||||
|
### User Input Recovery
|
||||||
|
|
||||||
|
| Source | Target | Method | Purpose |
|
||||||
|
|--------|--------|--------|---------|
|
||||||
|
| G14 | Client | SSE `user_input_needed` | Notify client |
|
||||||
|
| Client | G01 | `POST .../user-fix` | Provide anchor |
|
||||||
|
| G01 | G11 | `apply_user_anchor()` | Apply fix |
|
||||||
|
| G11 | G10 | `add_absolute_factor()` (high confidence) | Hard constraint |
|
||||||
|
| G10 | Internal | `optimize()` | Re-optimize |
|
||||||
|
| G11 | G02 | `update_flight_status("PROCESSING")` | Resume |
|
||||||
|
|
||||||
|
### Asynchronous Refinement
|
||||||
|
|
||||||
|
| Source | Target | Method | Purpose |
|
||||||
|
|--------|--------|--------|---------|
|
||||||
|
| G10 | Internal (background) | `optimize()` | Back-propagate anchors |
|
||||||
|
| G10 | G13 | `get_trajectory()` | Get refined poses |
|
||||||
|
| G13 | G03 | `batch_update_waypoints()` | Batch update Route API |
|
||||||
|
| G13 | G14 | `send_refinement()` × N | Send updates |
|
||||||
|
| G14 | Client | SSE `frame_refined` × N | Incremental updates |
|
||||||
|
|
||||||
|
### Cross-Cutting Concerns
|
||||||
|
|
||||||
|
| Source | Target | Method | Purpose |
|
||||||
|
|--------|--------|--------|---------|
|
||||||
|
| G16 | ALL | `get_*_config()` | Provide configuration |
|
||||||
|
| H05 | G07, G08, G09, G10, G11 | `start_timer()`, `end_timer()` | Performance monitoring |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Interaction Coverage Verification
|
||||||
|
|
||||||
|
✅ **Initialization**: G02→G15, G16, G17; G04→G08→H04
|
||||||
|
✅ **Flight creation**: Client→G01→G02→G03,G04,G16,G17,G14
|
||||||
|
✅ **Image upload**: Client→G01→G05→H08,G17
|
||||||
|
✅ **Rotation sweep**: G06→H07,G09 (12 iterations)
|
||||||
|
✅ **Sequential VO**: G07→G15,G10,H05
|
||||||
|
✅ **Drift correction**: G11→G09→G04(1),G10
|
||||||
|
✅ **Tracking loss**: G11→G06,G08,G04(progressive),G09,G14,G02
|
||||||
|
✅ **Global PR**: G08→G15,H04,G04
|
||||||
|
✅ **Optimization**: G10→H03,G12
|
||||||
|
✅ **Coordinate transform**: G12→H01,H02,H06
|
||||||
|
✅ **Results**: G12→G13→G03,G14,G17
|
||||||
|
✅ **User input**: Client→G01→G11→G10,G02
|
||||||
|
✅ **Refinement**: G10→G13→G03,G14
|
||||||
|
✅ **Configuration**: G16→ALL
|
||||||
|
✅ **Performance**: H05→processing components
|
||||||
|
|
||||||
|
**All major component interactions are covered.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deliverables
|
||||||
|
|
||||||
|
**Component Count**: 29 total
|
||||||
|
|
||||||
|
- Route API: 4 (R01-R04)
|
||||||
|
- GPS-Denied API: 17 (G01-G17)
|
||||||
|
- Helpers: 8 (H01-H08)
|
||||||
|
|
||||||
|
**For each component**, create `docs/02_components/[project]_[##]_[component_name]/[component_name]_spec.md`:
|
||||||
|
|
||||||
|
1. **Interface Definition** (interface name, methods, contracts)
|
||||||
|
2. **Component Description** (responsibilities, scope)
|
||||||
|
3. **API Methods** (inputs, outputs, errors, which components call it, test cases)
|
||||||
|
4. **Integration Tests**
|
||||||
|
5. **Non-Functional Requirements** (performance, accuracy targets)
|
||||||
|
6. **Dependencies** (which components it calls)
|
||||||
|
7. **Data Models**
|
||||||
|
|
||||||
|
**Generate draw.io diagram** showing:
|
||||||
|
|
||||||
|
- Two API projects (Route API, GPS-Denied API)
|
||||||
|
- All 29 components
|
||||||
|
- Route API ↔ GPS-Denied API communication
|
||||||
|
- GPS-Denied → Satellite Provider calls
|
||||||
|
- Rotation preprocessing flow
|
||||||
|
- Progressive search expansion (1→4→9→16→25)
|
||||||
|
- Per-frame Route API update flow
|
||||||
|
- Helper component usage
|
||||||
|
|
||||||
|
### To-dos
|
||||||
|
|
||||||
|
- [x] Create 4 Route API specs with interfaces (REST, data manager, validator, DB)
|
||||||
|
- [x] Create GPS-Denied core API specs with interfaces (REST, flight manager, Route client)
|
||||||
|
- [x] Create data management specs with interfaces (satellite, image pipeline, rotation)
|
||||||
|
- [x] Create visual processing specs with interfaces (VO, place recognition, LiteSAM)
|
||||||
|
- [x] Create coordination specs with interfaces (factor graph, failure recovery, transformer)
|
||||||
|
- [x] Create results/infrastructure specs with interfaces (result manager, SSE, models, config, DB)
|
||||||
|
- [x] Create 8 helper specs with interfaces
|
||||||
|
- [x] Generate draw.io with all components, interactions, flows
|
||||||
|
|
||||||
@@ -0,0 +1,387 @@
|
|||||||
|
# GPS-Denied REST API
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `IGPSDeniedRestAPI`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IGPSDeniedRestAPI(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def create_flight(self, flight_data: FlightCreateRequest) -> FlightResponse:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def upload_image_batch(self, flight_id: str, batch: ImageBatch) -> BatchResponse:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def submit_user_fix(self, flight_id: str, fix_data: UserFixRequest) -> UserFixResponse:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_flight_status(self, flight_id: str) -> FlightStatusResponse:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def create_sse_stream(self, flight_id: str) -> SSEStream:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
- Expose REST API endpoints for GPS-Denied image processing pipeline
|
||||||
|
- Handle flight creation with satellite data prefetching
|
||||||
|
- Accept batch image uploads (10-50 images per request)
|
||||||
|
- Accept user-provided GPS fixes for blocked flights
|
||||||
|
- Provide real-time status updates
|
||||||
|
- Stream results via Server-Sent Events (SSE)
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- FastAPI-based REST endpoints
|
||||||
|
- Request/response validation
|
||||||
|
- Coordinate with Flight Manager for processing
|
||||||
|
- Multipart form data handling for image uploads
|
||||||
|
- SSE connection management
|
||||||
|
- Authentication and rate limiting
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `create_flight(flight_data: FlightCreateRequest) -> FlightResponse`
|
||||||
|
|
||||||
|
**REST Endpoint**: `POST /gps-denied/flights`
|
||||||
|
|
||||||
|
**Description**: Creates a new flight processing session, links to Route API, and prefetches satellite data.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Client applications (GPS-Denied UI)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
FlightCreateRequest:
|
||||||
|
route_id: str # UUID from Route API
|
||||||
|
start_gps: GPSPoint # Starting GPS coordinates (approximate)
|
||||||
|
camera_params: CameraParameters
|
||||||
|
rough_waypoints: List[GPSPoint] # Rough route for prefetching
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
FlightResponse:
|
||||||
|
flight_id: str # UUID
|
||||||
|
status: str # "prefetching", "ready", "error"
|
||||||
|
message: Optional[str]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Validate request data
|
||||||
|
2. Call G02 Flight Manager → create_flight()
|
||||||
|
3. Flight Manager triggers satellite prefetch
|
||||||
|
4. Return flight_id immediately (prefetch is async)
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `400 Bad Request`: Invalid input data
|
||||||
|
- `404 Not Found`: route_id doesn't exist in Route API
|
||||||
|
- `500 Internal Server Error`: System error
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Valid flight creation**: Returns 201 with flight_id
|
||||||
|
2. **Invalid route_id**: Returns 404
|
||||||
|
3. **Missing camera_params**: Returns 400
|
||||||
|
4. **Concurrent flight creation**: Multiple flights for same route → all succeed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `upload_image_batch(flight_id: str, batch: ImageBatch) -> BatchResponse`
|
||||||
|
|
||||||
|
**REST Endpoint**: `POST /gps-denied/flights/{flightId}/images/batch`
|
||||||
|
|
||||||
|
**Description**: Uploads a batch of 10-50 UAV images for processing.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Client applications
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str # Path parameter
|
||||||
|
ImageBatch: multipart/form-data
|
||||||
|
images: List[UploadFile] # 10-50 images
|
||||||
|
metadata: BatchMetadata
|
||||||
|
start_sequence: int
|
||||||
|
end_sequence: int
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
BatchResponse:
|
||||||
|
accepted: bool
|
||||||
|
sequences: List[int] # [start, end]
|
||||||
|
next_expected: int
|
||||||
|
message: Optional[str]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Validate flight_id exists
|
||||||
|
2. Validate batch size (10-50 images)
|
||||||
|
3. Validate sequence numbers (strict sequential)
|
||||||
|
4. Pass to G05 Image Input Pipeline
|
||||||
|
5. Return immediately (processing is async)
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `400 Bad Request`: Invalid batch size, out-of-sequence images
|
||||||
|
- `404 Not Found`: flight_id doesn't exist
|
||||||
|
- `413 Payload Too Large`: Batch exceeds size limit
|
||||||
|
- `429 Too Many Requests`: Rate limit exceeded
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Valid batch upload**: 20 images → returns 202 Accepted
|
||||||
|
2. **Out-of-sequence batch**: Sequence gap detected → returns 400
|
||||||
|
3. **Too many images**: 60 images → returns 400
|
||||||
|
4. **Large images**: 50 × 8MB images → successfully uploads
|
||||||
|
5. **Client-side resized images**: 2048×1536 → optimal processing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `submit_user_fix(flight_id: str, fix_data: UserFixRequest) -> UserFixResponse`
|
||||||
|
|
||||||
|
**REST Endpoint**: `POST /gps-denied/flights/{flightId}/user-fix`
|
||||||
|
|
||||||
|
**Description**: Submits user-provided GPS anchor point to unblock failed localization.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Client applications (when user responds to `user_input_needed` event)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
UserFixRequest:
|
||||||
|
frame_id: int # Frame sequence number
|
||||||
|
uav_pixel: Tuple[float, float] # Pixel coordinates in UAV image
|
||||||
|
satellite_gps: GPSPoint # GPS corresponding to pixel location
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
UserFixResponse:
|
||||||
|
accepted: bool
|
||||||
|
processing_resumed: bool
|
||||||
|
message: Optional[str]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Validate flight_id exists and is blocked
|
||||||
|
2. Pass to G11 Failure Recovery Coordinator
|
||||||
|
3. Coordinator applies anchor to Factor Graph
|
||||||
|
4. Resume processing pipeline
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `400 Bad Request`: Invalid fix data
|
||||||
|
- `404 Not Found`: flight_id or frame_id not found
|
||||||
|
- `409 Conflict`: Flight not in blocked state
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Valid user fix**: Blocked flight → returns 200, processing resumes
|
||||||
|
2. **Fix for non-blocked flight**: Returns 409
|
||||||
|
3. **Invalid GPS coordinates**: Returns 400
|
||||||
|
4. **Multiple fixes**: Sequential fixes for different frames → all accepted
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_flight_status(flight_id: str) -> FlightStatusResponse`
|
||||||
|
|
||||||
|
**REST Endpoint**: `GET /gps-denied/flights/{flightId}/status`
|
||||||
|
|
||||||
|
**Description**: Retrieves current processing status of a flight.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Client applications (polling for status)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
FlightStatusResponse:
|
||||||
|
status: str # "prefetching", "processing", "blocked", "completed", "failed"
|
||||||
|
frames_processed: int
|
||||||
|
frames_total: int
|
||||||
|
current_frame: Optional[int]
|
||||||
|
current_heading: Optional[float] # UAV heading in degrees
|
||||||
|
blocked: bool
|
||||||
|
search_grid_size: Optional[int] # 1, 4, 9, 16, or 25
|
||||||
|
message: Optional[str]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `404 Not Found`: flight_id doesn't exist
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Processing flight**: Returns current progress
|
||||||
|
2. **Blocked flight**: Returns blocked=true with search_grid_size
|
||||||
|
3. **Completed flight**: Returns status="completed" with final counts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `create_sse_stream(flight_id: str) -> SSEStream`
|
||||||
|
|
||||||
|
**REST Endpoint**: `GET /gps-denied/flights/{flightId}/stream`
|
||||||
|
|
||||||
|
**Description**: Opens Server-Sent Events connection for real-time result streaming.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Client applications
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
SSE Stream with events:
|
||||||
|
- frame_processed
|
||||||
|
- frame_refined
|
||||||
|
- search_expanded
|
||||||
|
- user_input_needed
|
||||||
|
- processing_blocked
|
||||||
|
- route_api_updated
|
||||||
|
- route_completed
|
||||||
|
```
|
||||||
|
|
||||||
|
**Event Format**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "frame_processed",
|
||||||
|
"data": {
|
||||||
|
"frame_id": 237,
|
||||||
|
"gps": {"lat": 48.123, "lon": 37.456},
|
||||||
|
"altitude": 800.0,
|
||||||
|
"confidence": 0.95,
|
||||||
|
"heading": 87.3,
|
||||||
|
"timestamp": "2025-11-24T10:30:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `404 Not Found`: flight_id doesn't exist
|
||||||
|
- Connection closed on client disconnect
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Connect to stream**: Opens SSE connection successfully
|
||||||
|
2. **Receive frame events**: Process 100 frames → receive 100 events
|
||||||
|
3. **Receive user_input_needed**: Blocked frame → event sent
|
||||||
|
4. **Client reconnect**: Replay missed events from last_event_id
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
### Test 1: Complete Flight Processing Flow
|
||||||
|
1. POST /gps-denied/flights
|
||||||
|
2. GET /gps-denied/flights/{flightId}/stream (open SSE)
|
||||||
|
3. POST /gps-denied/flights/{flightId}/images/batch × 40 (2000 images total)
|
||||||
|
4. Receive frame_processed events via SSE
|
||||||
|
5. Receive route_completed event
|
||||||
|
|
||||||
|
### Test 2: User Fix Flow
|
||||||
|
1. Create flight and process images
|
||||||
|
2. Receive user_input_needed event
|
||||||
|
3. POST /gps-denied/flights/{flightId}/user-fix
|
||||||
|
4. Receive processing_resumed event
|
||||||
|
5. Continue receiving frame_processed events
|
||||||
|
|
||||||
|
### Test 3: Multiple Concurrent Flights
|
||||||
|
1. Create 10 flights concurrently
|
||||||
|
2. Upload batches to all flights in parallel
|
||||||
|
3. Stream results from all flights simultaneously
|
||||||
|
4. Verify no cross-contamination
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **create_flight**: < 500ms response (prefetch is async)
|
||||||
|
- **upload_image_batch**: < 2 seconds for 50 × 2MB images
|
||||||
|
- **submit_user_fix**: < 200ms response
|
||||||
|
- **get_flight_status**: < 100ms
|
||||||
|
- **SSE latency**: < 500ms from event generation to client receipt
|
||||||
|
|
||||||
|
### Scalability
|
||||||
|
- Support 100 concurrent flight processing sessions
|
||||||
|
- Handle 1000+ concurrent SSE connections
|
||||||
|
- Support 10,000 requests per minute
|
||||||
|
|
||||||
|
### Reliability
|
||||||
|
- Request timeout: 30 seconds for batch uploads
|
||||||
|
- SSE keepalive: Ping every 30 seconds
|
||||||
|
- Automatic SSE reconnection with event replay
|
||||||
|
- Graceful handling of client disconnects
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- API key authentication
|
||||||
|
- Rate limiting: 100 requests/minute per client
|
||||||
|
- Max upload size: 500MB per batch
|
||||||
|
- CORS configuration for web clients
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Internal Components
|
||||||
|
- **G02 Flight Manager**: For all flight operations
|
||||||
|
- **G05 Image Input Pipeline**: For batch processing
|
||||||
|
- **G11 Failure Recovery Coordinator**: For user fixes
|
||||||
|
- **G14 SSE Event Streamer**: For real-time streaming
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
- **FastAPI**: Web framework
|
||||||
|
- **Uvicorn**: ASGI server
|
||||||
|
- **Pydantic**: Validation
|
||||||
|
- **python-multipart**: Multipart form handling
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### FlightCreateRequest
|
||||||
|
```python
|
||||||
|
class GPSPoint(BaseModel):
|
||||||
|
lat: float
|
||||||
|
lon: float
|
||||||
|
|
||||||
|
class CameraParameters(BaseModel):
|
||||||
|
focal_length: float # mm
|
||||||
|
sensor_width: float # mm
|
||||||
|
sensor_height: float # mm
|
||||||
|
resolution_width: int # pixels
|
||||||
|
resolution_height: int # pixels
|
||||||
|
distortion_coefficients: Optional[List[float]] = None
|
||||||
|
|
||||||
|
class FlightCreateRequest(BaseModel):
|
||||||
|
route_id: str
|
||||||
|
start_gps: GPSPoint
|
||||||
|
camera_params: CameraParameters
|
||||||
|
rough_waypoints: List[GPSPoint]
|
||||||
|
altitude: float # Predefined altitude in meters
|
||||||
|
```
|
||||||
|
|
||||||
|
### BatchMetadata
|
||||||
|
```python
|
||||||
|
class BatchMetadata(BaseModel):
|
||||||
|
start_sequence: int # e.g., 101
|
||||||
|
end_sequence: int # e.g., 150
|
||||||
|
batch_number: int # e.g., 3
|
||||||
|
```
|
||||||
|
|
||||||
|
### FlightStatusResponse
|
||||||
|
```python
|
||||||
|
class FlightStatusResponse(BaseModel):
|
||||||
|
status: str
|
||||||
|
frames_processed: int
|
||||||
|
frames_total: int
|
||||||
|
current_frame: Optional[int]
|
||||||
|
current_heading: Optional[float]
|
||||||
|
blocked: bool
|
||||||
|
search_grid_size: Optional[int]
|
||||||
|
message: Optional[str]
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
# Flight Manager
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `IFlightManager`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IFlightManager(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def create_flight(self, flight_data: FlightData) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_flight_state(self, flight_id: str) -> Optional[FlightState]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def link_to_route(self, flight_id: str, route_id: str) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def update_flight_status(self, flight_id: str, status: FlightStatus) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def initialize_system(self) -> bool:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
- Manage flight lifecycle (creation, state tracking, completion)
|
||||||
|
- Link flights to Route API routes
|
||||||
|
- Initialize system components (models, configurations, satellite database)
|
||||||
|
- Coordinate satellite data prefetching
|
||||||
|
- Track flight processing status and statistics
|
||||||
|
- Manage flight metadata persistence
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Central coordinator for flight processing sessions
|
||||||
|
- System initialization and resource management
|
||||||
|
- Flight state machine management
|
||||||
|
- Integration point between REST API and processing components
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `create_flight(flight_data: FlightData) -> str`
|
||||||
|
|
||||||
|
**Description**: Creates a new flight processing session, initializes state, and triggers satellite prefetching.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G01 GPS-Denied REST API
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
FlightData:
|
||||||
|
route_id: str
|
||||||
|
start_gps: GPSPoint
|
||||||
|
camera_params: CameraParameters
|
||||||
|
rough_waypoints: List[GPSPoint]
|
||||||
|
altitude: float
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
flight_id: str # UUID
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Generate flight_id (UUID)
|
||||||
|
2. Get flight configuration from G16 Configuration Manager
|
||||||
|
3. Get route info from G03 Route API Client
|
||||||
|
4. Initialize flight state
|
||||||
|
5. Trigger G04 Satellite Data Manager → prefetch_route_corridor()
|
||||||
|
6. Save flight state to G17 Database Layer
|
||||||
|
7. Return flight_id
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `RouteNotFoundError`: route_id doesn't exist
|
||||||
|
- `ConfigurationError`: Invalid camera parameters
|
||||||
|
- `DatabaseError`: Failed to persist state
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Valid flight creation**: Returns flight_id, state persisted
|
||||||
|
2. **Invalid route_id**: Raises RouteNotFoundError
|
||||||
|
3. **Prefetch triggered**: Satellite manager receives prefetch request
|
||||||
|
4. **Concurrent creation**: 10 flights created simultaneously → all succeed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_flight_state(flight_id: str) -> Optional[FlightState]`
|
||||||
|
|
||||||
|
**Description**: Retrieves current flight state including processing statistics.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G01 GPS-Denied REST API (for status endpoint)
|
||||||
|
- G05 Image Input Pipeline (to check flight exists)
|
||||||
|
- G11 Failure Recovery Coordinator (for state updates)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
FlightState:
|
||||||
|
flight_id: str
|
||||||
|
route_id: str
|
||||||
|
status: str # "prefetching", "ready", "processing", "blocked", "completed", "failed"
|
||||||
|
frames_processed: int
|
||||||
|
frames_total: int
|
||||||
|
current_frame: Optional[int]
|
||||||
|
current_heading: Optional[float]
|
||||||
|
blocked: bool
|
||||||
|
search_grid_size: Optional[int]
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
cache_reference: str # Satellite data cache identifier
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `None`: Flight not found (not an error condition)
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Get existing flight**: Returns complete FlightState
|
||||||
|
2. **Get non-existent flight**: Returns None
|
||||||
|
3. **Get during processing**: Returns accurate frame count
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `link_to_route(flight_id: str, route_id: str) -> bool`
|
||||||
|
|
||||||
|
**Description**: Links a flight to its Route API route for waypoint updates.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Internal (during create_flight)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
route_id: str
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
bool: True if linked, False if flight doesn't exist
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Verify flight exists
|
||||||
|
2. Verify route exists via G03 Route API Client
|
||||||
|
3. Store linkage in flight state
|
||||||
|
4. Update database
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `RouteNotFoundError`: route_id invalid
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Valid linkage**: Returns True
|
||||||
|
2. **Invalid flight_id**: Returns False
|
||||||
|
3. **Invalid route_id**: Raises RouteNotFoundError
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `update_flight_status(flight_id: str, status: FlightStatus) -> bool`
|
||||||
|
|
||||||
|
**Description**: Updates flight processing status (processing, blocked, completed, etc.).
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G05 Image Input Pipeline (status transitions)
|
||||||
|
- G11 Failure Recovery Coordinator (blocked/resumed)
|
||||||
|
- G13 Result Manager (completed)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
status: FlightStatus:
|
||||||
|
status_type: str # "processing", "blocked", "completed", "failed"
|
||||||
|
frames_processed: Optional[int]
|
||||||
|
current_frame: Optional[int]
|
||||||
|
current_heading: Optional[float]
|
||||||
|
blocked: Optional[bool]
|
||||||
|
search_grid_size: Optional[int]
|
||||||
|
message: Optional[str]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
bool: True if updated, False if flight not found
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Load current flight state
|
||||||
|
2. Update relevant fields
|
||||||
|
3. Update updated_at timestamp
|
||||||
|
4. Persist to G17 Database Layer
|
||||||
|
5. Return success
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns False if flight not found
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Update to processing**: status="processing" → updates successfully
|
||||||
|
2. **Update to blocked**: blocked=True, search_grid_size=9 → updates
|
||||||
|
3. **Resume from blocked**: blocked=False → processing continues
|
||||||
|
4. **Concurrent updates**: Multiple simultaneous updates → all persist correctly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `initialize_system() -> bool`
|
||||||
|
|
||||||
|
**Description**: Initializes system components on startup (models, configurations, satellite database).
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- System startup (main application)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
- None
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
bool: True if initialization successful
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Load system configuration from G16 Configuration Manager
|
||||||
|
2. Initialize ML models via G15 Model Manager:
|
||||||
|
- Load SuperPoint model
|
||||||
|
- Load LightGlue model
|
||||||
|
- Load DINOv2 model
|
||||||
|
- Load LiteSAM model
|
||||||
|
3. Initialize G08 Global Place Recognition → build satellite descriptor database
|
||||||
|
4. Initialize G04 Satellite Data Manager cache
|
||||||
|
5. Verify all components ready
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `InitializationError`: Component initialization failed
|
||||||
|
- `ModelLoadError`: ML model loading failed
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Clean startup**: All models load successfully
|
||||||
|
2. **Missing model file**: Raises ModelLoadError
|
||||||
|
3. **Configuration error**: Raises InitializationError
|
||||||
|
4. **Partial initialization failure**: Cleanup and raise error
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
### Test 1: Flight Lifecycle
|
||||||
|
1. initialize_system()
|
||||||
|
2. create_flight() with valid data
|
||||||
|
3. get_flight_state() → verify "prefetching"
|
||||||
|
4. Wait for prefetch completion
|
||||||
|
5. update_flight_status("processing")
|
||||||
|
6. get_flight_state() → verify "processing"
|
||||||
|
7. update_flight_status("completed")
|
||||||
|
|
||||||
|
### Test 2: Multiple Concurrent Flights
|
||||||
|
1. create_flight() × 10 concurrently
|
||||||
|
2. update_flight_status() for all flights in parallel
|
||||||
|
3. get_flight_state() for all flights
|
||||||
|
4. Verify no state cross-contamination
|
||||||
|
|
||||||
|
### Test 3: System Initialization
|
||||||
|
1. initialize_system()
|
||||||
|
2. Verify all 4 models loaded
|
||||||
|
3. Verify satellite database ready
|
||||||
|
4. Create flight immediately after init → succeeds
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **create_flight**: < 300ms (excluding prefetch which is async)
|
||||||
|
- **get_flight_state**: < 50ms
|
||||||
|
- **update_flight_status**: < 30ms
|
||||||
|
- **initialize_system**: < 30 seconds (one-time startup cost)
|
||||||
|
|
||||||
|
### Scalability
|
||||||
|
- Support 1000+ concurrent flight sessions
|
||||||
|
- Handle 100 status updates per second
|
||||||
|
- Maintain state for up to 10,000 flights (historical data)
|
||||||
|
|
||||||
|
### Reliability
|
||||||
|
- Graceful handling of component initialization failures
|
||||||
|
- Flight state persistence survives process restarts
|
||||||
|
- Transaction safety for concurrent updates
|
||||||
|
- Automatic cleanup of completed flights after 7 days
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Internal Components
|
||||||
|
- **G03 Route API Client**: For route validation and metadata
|
||||||
|
- **G04 Satellite Data Manager**: For prefetch operations
|
||||||
|
- **G08 Global Place Recognition**: For descriptor database initialization
|
||||||
|
- **G15 Model Manager**: For ML model loading
|
||||||
|
- **G16 Configuration Manager**: For system configuration
|
||||||
|
- **G17 GPS-Denied Database Layer**: For state persistence
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
- None (coordinates with internal components)
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### FlightData
|
||||||
|
```python
|
||||||
|
class FlightData(BaseModel):
|
||||||
|
route_id: str
|
||||||
|
start_gps: GPSPoint
|
||||||
|
camera_params: CameraParameters
|
||||||
|
rough_waypoints: List[GPSPoint]
|
||||||
|
altitude: float
|
||||||
|
```
|
||||||
|
|
||||||
|
### FlightState
|
||||||
|
```python
|
||||||
|
class FlightState(BaseModel):
|
||||||
|
flight_id: str
|
||||||
|
route_id: str
|
||||||
|
status: str
|
||||||
|
frames_processed: int = 0
|
||||||
|
frames_total: int = 0
|
||||||
|
current_frame: Optional[int] = None
|
||||||
|
current_heading: Optional[float] = None
|
||||||
|
blocked: bool = False
|
||||||
|
search_grid_size: Optional[int] = None
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
cache_reference: str
|
||||||
|
camera_params: CameraParameters
|
||||||
|
altitude: float
|
||||||
|
start_gps: GPSPoint
|
||||||
|
```
|
||||||
|
|
||||||
|
### FlightStatus (Update DTO)
|
||||||
|
```python
|
||||||
|
class FlightStatus(BaseModel):
|
||||||
|
status_type: str
|
||||||
|
frames_processed: Optional[int] = None
|
||||||
|
current_frame: Optional[int] = None
|
||||||
|
current_heading: Optional[float] = None
|
||||||
|
blocked: Optional[bool] = None
|
||||||
|
search_grid_size: Optional[int] = None
|
||||||
|
message: Optional[str] = None
|
||||||
|
```
|
||||||
|
|
||||||
|
### SystemState
|
||||||
|
```python
|
||||||
|
class SystemState(BaseModel):
|
||||||
|
initialized: bool
|
||||||
|
models_loaded: Dict[str, bool] # {"SuperPoint": True, "LightGlue": True, ...}
|
||||||
|
satellite_db_ready: bool
|
||||||
|
active_flights_count: int
|
||||||
|
initialization_timestamp: datetime
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
# Route API Client
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `IRouteAPIClient`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IRouteAPIClient(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def update_route_waypoint(self, route_id: str, waypoint_id: str, waypoint: Waypoint) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_route_info(self, route_id: str) -> Optional[RouteInfo]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def batch_update_waypoints(self, route_id: str, waypoints: List[Waypoint]) -> BatchUpdateResult:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
- HTTP client for communicating with Route API
|
||||||
|
- Send per-frame GPS refinements to Route API
|
||||||
|
- Retrieve route metadata and waypoints
|
||||||
|
- Handle batch waypoint updates for trajectory refinements
|
||||||
|
- Manage connection pooling and retry logic
|
||||||
|
- Handle HTTP errors and timeouts
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Synchronous HTTP client (requests library)
|
||||||
|
- Waypoint update operations
|
||||||
|
- Route metadata retrieval
|
||||||
|
- Error handling and retries
|
||||||
|
- Rate limiting and backpressure management
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `update_route_waypoint(route_id: str, waypoint_id: str, waypoint: Waypoint) -> bool`
|
||||||
|
|
||||||
|
**Description**: Updates a single waypoint in Route API. Called per-frame after GPS calculation.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G13 Result Manager (per-frame update)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
route_id: str
|
||||||
|
waypoint_id: str # Frame sequence number
|
||||||
|
waypoint: Waypoint:
|
||||||
|
lat: float
|
||||||
|
lon: float
|
||||||
|
altitude: Optional[float]
|
||||||
|
confidence: float
|
||||||
|
timestamp: datetime
|
||||||
|
refined: bool # Always True for GPS-Denied updates
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
bool: True if updated successfully, False on failure
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP Request**:
|
||||||
|
```
|
||||||
|
PUT /routes/{route_id}/waypoints/{waypoint_id}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"lat": 48.123456,
|
||||||
|
"lon": 37.654321,
|
||||||
|
"altitude": 800.0,
|
||||||
|
"confidence": 0.95,
|
||||||
|
"timestamp": "2025-11-24T10:30:00Z",
|
||||||
|
"refined": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Handling**:
|
||||||
|
- **Retry**: 3 attempts with exponential backoff (1s, 2s, 4s)
|
||||||
|
- **Timeout**: 5 seconds per request
|
||||||
|
- **404 Not Found**: Log warning, return False
|
||||||
|
- **429 Too Many Requests**: Backoff and retry
|
||||||
|
- **500 Server Error**: Retry with backoff
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `False`: Update failed after retries
|
||||||
|
- Logs errors but doesn't raise exceptions (non-critical path)
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Successful update**: Returns True
|
||||||
|
2. **Route API unavailable**: Retries 3 times, returns False
|
||||||
|
3. **Waypoint not found**: Returns False
|
||||||
|
4. **Network timeout**: Retries, returns False if all fail
|
||||||
|
5. **High-frequency updates**: 100 updates/sec sustained
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_route_info(route_id: str) -> Optional[RouteInfo]`
|
||||||
|
|
||||||
|
**Description**: Retrieves route metadata including rough waypoints and geofences.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G02 Flight Manager (during flight creation)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
route_id: str
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
RouteInfo:
|
||||||
|
route_id: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
points: List[GPSPoint] # Rough waypoints
|
||||||
|
geofences: Geofences
|
||||||
|
waypoint_count: int
|
||||||
|
created_at: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP Request**:
|
||||||
|
```
|
||||||
|
GET /routes/{route_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Handling**:
|
||||||
|
- **Retry**: 3 attempts for transient errors
|
||||||
|
- **Timeout**: 10 seconds
|
||||||
|
- **404 Not Found**: Return None
|
||||||
|
- **500 Server Error**: Retry with backoff
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `None`: Route not found or error after retries
|
||||||
|
- `RouteAPIError`: Critical error retrieving route
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Existing route**: Returns complete RouteInfo
|
||||||
|
2. **Non-existent route**: Returns None
|
||||||
|
3. **Large route**: 2000+ waypoints → returns successfully
|
||||||
|
4. **Concurrent requests**: 10 simultaneous requests → all succeed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `batch_update_waypoints(route_id: str, waypoints: List[Waypoint]) -> BatchUpdateResult`
|
||||||
|
|
||||||
|
**Description**: Updates multiple waypoints in a single request. Used for trajectory refinements.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G13 Result Manager (asynchronous refinement updates)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
route_id: str
|
||||||
|
waypoints: List[Waypoint] # Refined waypoints
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
BatchUpdateResult:
|
||||||
|
success: bool
|
||||||
|
updated_count: int
|
||||||
|
failed_ids: List[str]
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP Request**:
|
||||||
|
```
|
||||||
|
PUT /routes/{route_id}/waypoints/batch
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"waypoints": [
|
||||||
|
{
|
||||||
|
"id": "AD000237",
|
||||||
|
"lat": 48.123,
|
||||||
|
"lon": 37.654,
|
||||||
|
"altitude": 800.0,
|
||||||
|
"confidence": 0.97,
|
||||||
|
"timestamp": "2025-11-24T10:30:00Z",
|
||||||
|
"refined": true
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Handling**:
|
||||||
|
- **Partial success**: Some waypoints update, some fail
|
||||||
|
- **Retry**: 3 attempts for complete batch
|
||||||
|
- **Timeout**: 30 seconds (larger batches)
|
||||||
|
- Returns updated_count and failed_ids
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `success=False` with failed_ids list
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Batch update 100 waypoints**: All succeed
|
||||||
|
2. **Partial failure**: 5 waypoints fail → returns failed_ids
|
||||||
|
3. **Empty batch**: Returns success=True, updated_count=0
|
||||||
|
4. **Large batch**: 500 waypoints → splits into sub-batches
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
### Test 1: Per-Frame Update Flow
|
||||||
|
1. Create flight and process 100 frames
|
||||||
|
2. update_route_waypoint() × 100 sequentially
|
||||||
|
3. Verify all updates successful via get_route_info()
|
||||||
|
4. Verify waypoints marked as refined=True
|
||||||
|
|
||||||
|
### Test 2: Refinement Batch Update
|
||||||
|
1. Process route, track 200 frames needing refinement
|
||||||
|
2. batch_update_waypoints() with 200 waypoints
|
||||||
|
3. Verify all updates applied
|
||||||
|
4. Handle partial failures gracefully
|
||||||
|
|
||||||
|
### Test 3: Error Recovery
|
||||||
|
1. Simulate Route API downtime
|
||||||
|
2. Attempt update_route_waypoint() → retries 3 times
|
||||||
|
3. Route API comes back online
|
||||||
|
4. Next update succeeds
|
||||||
|
|
||||||
|
### Test 4: High-Frequency Updates
|
||||||
|
1. Send 200 waypoint updates sequentially
|
||||||
|
2. Measure throughput and success rate
|
||||||
|
3. Verify no rate limiting issues
|
||||||
|
4. Verify all updates persisted
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **update_route_waypoint**: < 100ms average latency (critical path)
|
||||||
|
- **get_route_info**: < 200ms
|
||||||
|
- **batch_update_waypoints**: < 2 seconds for 100 waypoints
|
||||||
|
- **Throughput**: Support 100 waypoint updates per second
|
||||||
|
|
||||||
|
### Scalability
|
||||||
|
- Connection pool: 20-50 connections
|
||||||
|
- Handle 1000+ waypoint updates per flight (2000+ frame flight)
|
||||||
|
- Support concurrent updates from multiple flights
|
||||||
|
|
||||||
|
### Reliability
|
||||||
|
- **Retry strategy**: 3 attempts with exponential backoff
|
||||||
|
- **Circuit breaker**: Temporarily stop requests after 5 consecutive failures
|
||||||
|
- **Timeout management**: Progressive timeouts (5s, 10s, 30s)
|
||||||
|
- **Graceful degradation**: Continue processing even if Route API unavailable
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
- Track success/failure rates
|
||||||
|
- Monitor latency percentiles (p50, p95, p99)
|
||||||
|
- Alert on high failure rates
|
||||||
|
- Log all HTTP errors
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Internal Components
|
||||||
|
- None (external HTTP client)
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
- **Route API**: External REST API service
|
||||||
|
- **requests** or **httpx**: HTTP client library
|
||||||
|
- **tenacity**: Retry library
|
||||||
|
- **urllib3**: Connection pooling
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### RouteInfo
|
||||||
|
```python
|
||||||
|
class GPSPoint(BaseModel):
|
||||||
|
lat: float
|
||||||
|
lon: float
|
||||||
|
|
||||||
|
class Geofences(BaseModel):
|
||||||
|
polygons: List[Polygon]
|
||||||
|
|
||||||
|
class RouteInfo(BaseModel):
|
||||||
|
route_id: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
points: List[GPSPoint]
|
||||||
|
geofences: Geofences
|
||||||
|
waypoint_count: int
|
||||||
|
created_at: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
### Waypoint
|
||||||
|
```python
|
||||||
|
class Waypoint(BaseModel):
|
||||||
|
id: str
|
||||||
|
lat: float
|
||||||
|
lon: float
|
||||||
|
altitude: Optional[float]
|
||||||
|
confidence: float
|
||||||
|
timestamp: datetime
|
||||||
|
refined: bool
|
||||||
|
```
|
||||||
|
|
||||||
|
### BatchUpdateResult
|
||||||
|
```python
|
||||||
|
class BatchUpdateResult(BaseModel):
|
||||||
|
success: bool
|
||||||
|
updated_count: int
|
||||||
|
failed_ids: List[str]
|
||||||
|
errors: Optional[Dict[str, str]] # waypoint_id -> error_message
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTPConfig
|
||||||
|
```python
|
||||||
|
class HTTPConfig(BaseModel):
|
||||||
|
route_api_base_url: str # e.g., "http://localhost:8000"
|
||||||
|
timeout: int = 5 # seconds
|
||||||
|
max_retries: int = 3
|
||||||
|
retry_backoff: float = 1.0 # seconds
|
||||||
|
connection_pool_size: int = 50
|
||||||
|
max_batch_size: int = 500
|
||||||
|
```
|
||||||
|
|
||||||
|
### Retry Strategy
|
||||||
|
```python
|
||||||
|
retry_strategy = {
|
||||||
|
"stop": "stop_after_attempt(3)",
|
||||||
|
"wait": "wait_exponential(multiplier=1, min=1, max=10)",
|
||||||
|
"retry": "retry_if_exception_type((ConnectionError, Timeout, HTTPError))",
|
||||||
|
"reraise": True
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
+551
@@ -0,0 +1,551 @@
|
|||||||
|
# Satellite Data Manager
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `ISatelliteDataManager`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ISatelliteDataManager(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def fetch_tile(self, lat: float, lon: float, zoom: int) -> Optional[np.ndarray]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def fetch_tile_grid(self, center_lat: float, center_lon: float, grid_size: int, zoom: int) -> Dict[str, np.ndarray]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def prefetch_route_corridor(self, waypoints: List[GPSPoint], corridor_width_m: float, zoom: int) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def progressive_fetch(self, center_lat: float, center_lon: float, grid_sizes: List[int], zoom: int) -> Iterator[Dict[str, np.ndarray]]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def cache_tile(self, tile_coords: TileCoords, tile_data: np.ndarray) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_cached_tile(self, tile_coords: TileCoords) -> Optional[np.ndarray]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_tile_grid(self, center: TileCoords, grid_size: int) -> List[TileCoords]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def compute_tile_coords(self, lat: float, lon: float, zoom: int) -> TileCoords:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def expand_search_grid(self, center: TileCoords, current_size: int, new_size: int) -> List[TileCoords]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def compute_tile_bounds(self, tile_coords: TileCoords) -> TileBounds:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def clear_flight_cache(self, flight_id: str) -> bool:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
- Fetch satellite tiles from external provider API
|
||||||
|
- Manage local tile cache per flight
|
||||||
|
- Calculate tile coordinates and grid layouts
|
||||||
|
- Support progressive tile grid expansion (1→4→9→16→25)
|
||||||
|
- Handle Web Mercator projection calculations
|
||||||
|
- Coordinate corridor prefetching for flight routes
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- **HTTP client** for Satellite Provider API
|
||||||
|
- **Local caching** with disk storage
|
||||||
|
- **Grid calculations** for search patterns
|
||||||
|
- **Tile coordinate transformations** (GPS↔Tile coordinates)
|
||||||
|
- **Progressive retrieval** for "kidnapped robot" recovery
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `fetch_tile(lat: float, lon: float, zoom: int) -> Optional[np.ndarray]`
|
||||||
|
|
||||||
|
**Description**: Fetches a single satellite tile by GPS coordinates.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G09 Metric Refinement (single tile for drift correction)
|
||||||
|
- Internal (during prefetching)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
lat: float # Latitude
|
||||||
|
lon: float # Longitude
|
||||||
|
zoom: int # Zoom level (19 for 0.3m/pixel at Ukraine latitude)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
np.ndarray: Tile image (H×W×3 RGB) or None if failed
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP Request**:
|
||||||
|
```
|
||||||
|
GET /api/satellite/tiles/latlon?lat={lat}&lon={lon}&zoom={zoom}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Convert GPS to tile coordinates
|
||||||
|
2. Check cache
|
||||||
|
3. If not cached, fetch from satellite provider
|
||||||
|
4. Cache tile
|
||||||
|
5. Return tile image
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `None`: Tile unavailable, HTTP error
|
||||||
|
- Logs errors for monitoring
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Cache hit**: Tile in cache → returns immediately
|
||||||
|
2. **Cache miss**: Fetches from API → caches → returns
|
||||||
|
3. **API error**: Returns None
|
||||||
|
4. **Invalid coordinates**: Returns None
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `fetch_tile_grid(center_lat: float, center_lon: float, grid_size: int, zoom: int) -> Dict[str, np.ndarray]`
|
||||||
|
|
||||||
|
**Description**: Fetches NxN grid of tiles centered on GPS coordinates.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G09 Metric Refinement (for progressive search)
|
||||||
|
- G11 Failure Recovery Coordinator
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
center_lat: float
|
||||||
|
center_lon: float
|
||||||
|
grid_size: int # 1, 4 (2×2), 9 (3×3), 16 (4×4), or 25 (5×5)
|
||||||
|
zoom: int
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
Dict[str, np.ndarray] # tile_id -> tile_image
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Compute tile grid centered on coordinates
|
||||||
|
2. For each tile in grid:
|
||||||
|
- Check cache
|
||||||
|
- If not cached, fetch from API
|
||||||
|
3. Return dict of tiles
|
||||||
|
|
||||||
|
**HTTP Request** (if using batch endpoint):
|
||||||
|
```
|
||||||
|
GET /api/satellite/tiles/batch?tiles=[...]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns partial dict if some tiles fail
|
||||||
|
- Empty dict if all tiles fail
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **2×2 grid**: Returns 4 tiles
|
||||||
|
2. **3×3 grid**: Returns 9 tiles
|
||||||
|
3. **5×5 grid**: Returns 25 tiles
|
||||||
|
4. **Partial failure**: Some tiles unavailable → returns available tiles
|
||||||
|
5. **All cached**: Fast retrieval without HTTP requests
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `prefetch_route_corridor(waypoints: List[GPSPoint], corridor_width_m: float, zoom: int) -> bool`
|
||||||
|
|
||||||
|
**Description**: Prefetches satellite tiles along route corridor for a flight.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G02 Flight Manager (during flight creation)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
waypoints: List[GPSPoint] # Rough route waypoints
|
||||||
|
corridor_width_m: float # Corridor width in meters (e.g., 1000m)
|
||||||
|
zoom: int
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
bool: True if prefetch completed, False on error
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. For each waypoint pair:
|
||||||
|
- Calculate corridor polygon
|
||||||
|
- Determine tiles covering corridor
|
||||||
|
2. Fetch tiles (async, parallel)
|
||||||
|
3. Cache all tiles with flight_id reference
|
||||||
|
|
||||||
|
**Algorithm**:
|
||||||
|
- Use H06 Web Mercator Utils for tile calculations
|
||||||
|
- Parallel fetching (10-20 concurrent requests)
|
||||||
|
- Progress tracking for monitoring
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `False`: Major error preventing prefetch
|
||||||
|
- Logs warnings for individual tile failures
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Simple route**: 10 waypoints → prefetches 50-100 tiles
|
||||||
|
2. **Long route**: 50 waypoints → prefetches 200-500 tiles
|
||||||
|
3. **Partial failure**: Some tiles fail → continues, returns True
|
||||||
|
4. **Complete failure**: All tiles fail → returns False
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `progressive_fetch(center_lat: float, center_lon: float, grid_sizes: List[int], zoom: int) -> Iterator[Dict[str, np.ndarray]]`
|
||||||
|
|
||||||
|
**Description**: Progressively fetches expanding tile grids for "kidnapped robot" recovery.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G11 Failure Recovery Coordinator (progressive search)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
center_lat: float
|
||||||
|
center_lon: float
|
||||||
|
grid_sizes: List[int] # e.g., [1, 4, 9, 16, 25]
|
||||||
|
zoom: int
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
Iterator yielding Dict[str, np.ndarray] for each grid size
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. For each grid_size in sequence:
|
||||||
|
- Fetch tile grid
|
||||||
|
- Yield tiles
|
||||||
|
- If match found by caller, iterator can be stopped
|
||||||
|
|
||||||
|
**Usage Pattern**:
|
||||||
|
```python
|
||||||
|
for tiles in progressive_fetch(lat, lon, [1, 4, 9, 16, 25], 19):
|
||||||
|
if litesam_match_found(tiles):
|
||||||
|
break # Stop expanding search
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Progressive search**: Yields 1, then 4, then 9 tiles
|
||||||
|
2. **Early termination**: Match on 4 tiles → doesn't fetch 9, 16, 25
|
||||||
|
3. **Full search**: No match → fetches all grid sizes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `cache_tile(tile_coords: TileCoords, tile_data: np.ndarray) -> bool`
|
||||||
|
|
||||||
|
**Description**: Caches a satellite tile to disk.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Internal (after fetching tiles)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
tile_coords: TileCoords:
|
||||||
|
x: int
|
||||||
|
y: int
|
||||||
|
zoom: int
|
||||||
|
tile_data: np.ndarray
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
bool: True if cached successfully
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Generate cache key from tile_coords
|
||||||
|
2. Serialize tile_data (PNG format)
|
||||||
|
3. Write to disk cache directory
|
||||||
|
4. Update cache index
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `False`: Disk write error, space full
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Cache new tile**: Writes successfully
|
||||||
|
2. **Overwrite existing**: Updates tile
|
||||||
|
3. **Disk full**: Returns False
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_cached_tile(tile_coords: TileCoords) -> Optional[np.ndarray]`
|
||||||
|
|
||||||
|
**Description**: Retrieves a cached tile from disk.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Internal (before fetching from API)
|
||||||
|
- G09 Metric Refinement (direct cache lookup)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
tile_coords: TileCoords
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
Optional[np.ndarray]: Tile image or None if not cached
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Generate cache key
|
||||||
|
2. Check if file exists
|
||||||
|
3. Load and deserialize
|
||||||
|
4. Return tile_data
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `None`: Not cached, corrupted file
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Cache hit**: Returns tile quickly
|
||||||
|
2. **Cache miss**: Returns None
|
||||||
|
3. **Corrupted cache**: Returns None, logs warning
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_tile_grid(center: TileCoords, grid_size: int) -> List[TileCoords]`
|
||||||
|
|
||||||
|
**Description**: Calculates tile coordinates for NxN grid centered on a tile.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Internal (for grid fetching)
|
||||||
|
- G11 Failure Recovery Coordinator
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
center: TileCoords
|
||||||
|
grid_size: int # 1, 4, 9, 16, 25
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
List[TileCoords] # List of tile coordinates in grid
|
||||||
|
```
|
||||||
|
|
||||||
|
**Algorithm**:
|
||||||
|
- For grid_size=9 (3×3): tiles from center-1 to center+1 in both x and y
|
||||||
|
- For grid_size=16 (4×4): asymmetric grid with center slightly off-center
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **1-tile grid**: Returns [center]
|
||||||
|
2. **4-tile grid (2×2)**: Returns 4 tiles
|
||||||
|
3. **9-tile grid (3×3)**: Returns 9 tiles centered
|
||||||
|
4. **25-tile grid (5×5)**: Returns 25 tiles
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `compute_tile_coords(lat: float, lon: float, zoom: int) -> TileCoords`
|
||||||
|
|
||||||
|
**Description**: Converts GPS coordinates to tile coordinates.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- All methods that need tile coordinates from GPS
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
lat: float
|
||||||
|
lon: float
|
||||||
|
zoom: int
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
TileCoords:
|
||||||
|
x: int
|
||||||
|
y: int
|
||||||
|
zoom: int
|
||||||
|
```
|
||||||
|
|
||||||
|
**Algorithm** (Web Mercator):
|
||||||
|
```python
|
||||||
|
n = 2 ** zoom
|
||||||
|
x = int((lon + 180) / 360 * n)
|
||||||
|
lat_rad = lat * π / 180
|
||||||
|
y = int((1 - log(tan(lat_rad) + sec(lat_rad)) / π) / 2 * n)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Ukraine coordinates**: Produces valid tile coords
|
||||||
|
2. **Edge cases**: lat=0, lon=0, lat=90, lon=180
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `expand_search_grid(center: TileCoords, current_size: int, new_size: int) -> List[TileCoords]`
|
||||||
|
|
||||||
|
**Description**: Returns only NEW tiles when expanding from current grid to larger grid.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G11 Failure Recovery Coordinator (progressive search optimization)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
center: TileCoords
|
||||||
|
current_size: int # e.g., 4
|
||||||
|
new_size: int # e.g., 9
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
List[TileCoords] # Only tiles not in current_size grid
|
||||||
|
```
|
||||||
|
|
||||||
|
**Purpose**: Avoid re-fetching tiles already tried in smaller grid.
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **4→9 expansion**: Returns 5 new tiles (9-4)
|
||||||
|
2. **9→16 expansion**: Returns 7 new tiles
|
||||||
|
3. **1→4 expansion**: Returns 3 new tiles
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `compute_tile_bounds(tile_coords: TileCoords) -> TileBounds`
|
||||||
|
|
||||||
|
**Description**: Computes GPS bounding box of a tile.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G09 Metric Refinement (for homography calculations)
|
||||||
|
- H06 Web Mercator Utils (shared calculation)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
tile_coords: TileCoords
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
TileBounds:
|
||||||
|
nw: GPSPoint # North-West corner
|
||||||
|
ne: GPSPoint # North-East corner
|
||||||
|
sw: GPSPoint # South-West corner
|
||||||
|
se: GPSPoint # South-East corner
|
||||||
|
center: GPSPoint
|
||||||
|
gsd: float # Ground Sampling Distance (meters/pixel)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Algorithm**:
|
||||||
|
- Inverse Web Mercator projection
|
||||||
|
- GSD calculation: `156543.03392 * cos(lat * π/180) / 2^zoom`
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Zoom 19 at Ukraine**: GSD ≈ 0.3 m/pixel
|
||||||
|
2. **Tile bounds**: Valid GPS coordinates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `clear_flight_cache(flight_id: str) -> bool`
|
||||||
|
|
||||||
|
**Description**: Clears cached tiles for a completed flight.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G02 Flight Manager (cleanup after flight completion)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
bool: True if cleared successfully
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Find all tiles associated with flight_id
|
||||||
|
2. Delete tile files
|
||||||
|
3. Update cache index
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Clear flight cache**: Removes all associated tiles
|
||||||
|
2. **Non-existent flight**: Returns True (no-op)
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
### Test 1: Prefetch and Retrieval
|
||||||
|
1. prefetch_route_corridor() with 20 waypoints
|
||||||
|
2. Verify tiles cached
|
||||||
|
3. get_cached_tile() for each tile → all hit cache
|
||||||
|
4. clear_flight_cache() → cache cleared
|
||||||
|
|
||||||
|
### Test 2: Progressive Search Simulation
|
||||||
|
1. progressive_fetch() with [1, 4, 9, 16, 25]
|
||||||
|
2. Simulate match on 9 tiles
|
||||||
|
3. Verify only 1, 4, 9 fetched (not 16, 25)
|
||||||
|
|
||||||
|
### Test 3: Grid Expansion
|
||||||
|
1. fetch_tile_grid(4) → 4 tiles
|
||||||
|
2. expand_search_grid(4, 9) → 5 new tiles
|
||||||
|
3. Verify no duplicate fetches
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **fetch_tile**: < 200ms (cached: < 10ms)
|
||||||
|
- **fetch_tile_grid(9)**: < 1 second
|
||||||
|
- **prefetch_route_corridor**: < 30 seconds for 500 tiles
|
||||||
|
- **Cache lookup**: < 5ms
|
||||||
|
|
||||||
|
### Scalability
|
||||||
|
- Cache 10,000+ tiles per flight
|
||||||
|
- Support 100 concurrent tile fetches
|
||||||
|
- Handle 10GB+ cache size
|
||||||
|
|
||||||
|
### Reliability
|
||||||
|
- Retry failed HTTP requests (3 attempts)
|
||||||
|
- Graceful degradation on partial failures
|
||||||
|
- Cache corruption recovery
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Internal Components
|
||||||
|
- **H06 Web Mercator Utils**: Tile coordinate calculations
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
- **Satellite Provider API**: HTTP tile source
|
||||||
|
- **requests** or **httpx**: HTTP client
|
||||||
|
- **numpy**: Image handling
|
||||||
|
- **opencv-python**: Image I/O
|
||||||
|
- **diskcache**: Persistent cache
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### TileCoords
|
||||||
|
```python
|
||||||
|
class TileCoords(BaseModel):
|
||||||
|
x: int
|
||||||
|
y: int
|
||||||
|
zoom: int
|
||||||
|
```
|
||||||
|
|
||||||
|
### TileBounds
|
||||||
|
```python
|
||||||
|
class TileBounds(BaseModel):
|
||||||
|
nw: GPSPoint
|
||||||
|
ne: GPSPoint
|
||||||
|
sw: GPSPoint
|
||||||
|
se: GPSPoint
|
||||||
|
center: GPSPoint
|
||||||
|
gsd: float # meters/pixel
|
||||||
|
```
|
||||||
|
|
||||||
|
### CacheConfig
|
||||||
|
```python
|
||||||
|
class CacheConfig(BaseModel):
|
||||||
|
cache_dir: str = "./satellite_cache"
|
||||||
|
max_size_gb: int = 50
|
||||||
|
eviction_policy: str = "lru"
|
||||||
|
ttl_days: int = 30
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,450 @@
|
|||||||
|
# Image Input Pipeline
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `IImageInputPipeline`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IImageInputPipeline(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def queue_batch(self, flight_id: str, batch: ImageBatch) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def process_next_batch(self, flight_id: str) -> Optional[ProcessedBatch]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def validate_batch(self, batch: ImageBatch) -> ValidationResult:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def store_images(self, flight_id: str, images: List[ImageData]) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_next_image(self, flight_id: str) -> Optional[ImageData]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_image_by_sequence(self, flight_id: str, sequence: int) -> Optional[ImageData]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_image_metadata(self, flight_id: str, sequence: int) -> Optional[ImageMetadata]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_processing_status(self, flight_id: str) -> ProcessingStatus:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
- Unified image ingestion, validation, storage, and retrieval
|
||||||
|
- FIFO batch queuing for processing
|
||||||
|
- Validate consecutive naming (AD000001, AD000002, etc.)
|
||||||
|
- Validate sequence integrity (strict sequential ordering)
|
||||||
|
- Image persistence with indexed retrieval
|
||||||
|
- Metadata extraction (EXIF, dimensions)
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Batch queue management
|
||||||
|
- Image validation
|
||||||
|
- Disk storage management
|
||||||
|
- Sequential processing coordination
|
||||||
|
- Metadata management
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `queue_batch(flight_id: str, batch: ImageBatch) -> bool`
|
||||||
|
|
||||||
|
**Description**: Queues a batch of images for processing (FIFO).
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G01 GPS-Denied REST API (after upload)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
batch: ImageBatch:
|
||||||
|
images: List[bytes] # Raw image data
|
||||||
|
filenames: List[str] # e.g., ["AD000101.jpg", "AD000102.jpg", ...]
|
||||||
|
start_sequence: int # 101
|
||||||
|
end_sequence: int # 150
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
bool: True if queued successfully
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Validate batch using H08 Batch Validator
|
||||||
|
2. Check sequence continuity (no gaps)
|
||||||
|
3. Add to FIFO queue for flight_id
|
||||||
|
4. Return immediately (async processing)
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `ValidationError`: Sequence gap, invalid naming
|
||||||
|
- `QueueFullError`: Queue capacity exceeded
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Valid batch**: Queued successfully
|
||||||
|
2. **Sequence gap**: Batch 101-150, expecting 51-100 → error
|
||||||
|
3. **Invalid naming**: Non-consecutive names → error
|
||||||
|
4. **Queue full**: Returns error with backpressure signal
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `process_next_batch(flight_id: str) -> Optional[ProcessedBatch]`
|
||||||
|
|
||||||
|
**Description**: Dequeues and processes the next batch from FIFO queue.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Internal processing loop (background worker)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
ProcessedBatch:
|
||||||
|
images: List[ImageData]
|
||||||
|
batch_id: str
|
||||||
|
start_sequence: int
|
||||||
|
end_sequence: int
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Dequeue next batch
|
||||||
|
2. Decompress/decode images
|
||||||
|
3. Extract metadata (EXIF, dimensions)
|
||||||
|
4. Store images to disk
|
||||||
|
5. Return ProcessedBatch for pipeline
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `None`: Queue empty
|
||||||
|
- `ImageCorruptionError`: Invalid image data
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Process batch**: Dequeues, returns ImageData list
|
||||||
|
2. **Empty queue**: Returns None
|
||||||
|
3. **Corrupted image**: Logs error, skips image
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `validate_batch(batch: ImageBatch) -> ValidationResult`
|
||||||
|
|
||||||
|
**Description**: Validates batch integrity and sequence continuity.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Internal (before queuing)
|
||||||
|
- H08 Batch Validator (delegated validation)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
batch: ImageBatch
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
ValidationResult:
|
||||||
|
valid: bool
|
||||||
|
errors: List[str]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validation Rules**:
|
||||||
|
1. **Batch size**: 10 <= len(images) <= 50
|
||||||
|
2. **Naming convention**: ADxxxxxx.jpg (6 digits)
|
||||||
|
3. **Sequence continuity**: Consecutive numbers
|
||||||
|
4. **File format**: JPEG or PNG
|
||||||
|
5. **Image dimensions**: 640x480 to 6252x4168
|
||||||
|
6. **File size**: < 10MB per image
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Valid batch**: Returns valid=True
|
||||||
|
2. **Too few images**: 5 images → invalid
|
||||||
|
3. **Too many images**: 60 images → invalid
|
||||||
|
4. **Non-consecutive**: AD000101, AD000103 → invalid
|
||||||
|
5. **Invalid naming**: IMG_0001.jpg → invalid
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `store_images(flight_id: str, images: List[ImageData]) -> bool`
|
||||||
|
|
||||||
|
**Description**: Persists images to disk with indexed storage.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Internal (after processing batch)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
images: List[ImageData]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
bool: True if stored successfully
|
||||||
|
```
|
||||||
|
|
||||||
|
**Storage Structure**:
|
||||||
|
```
|
||||||
|
/image_storage/
|
||||||
|
{flight_id}/
|
||||||
|
AD000001.jpg
|
||||||
|
AD000002.jpg
|
||||||
|
metadata.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Create flight directory if not exists
|
||||||
|
2. Write each image to disk
|
||||||
|
3. Update metadata index
|
||||||
|
4. Persist to G17 Database Layer (metadata only)
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `StorageError`: Disk full, permission error
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Store batch**: All images written successfully
|
||||||
|
2. **Disk full**: Returns False
|
||||||
|
3. **Verify storage**: Images retrievable after storage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_next_image(flight_id: str) -> Optional[ImageData]`
|
||||||
|
|
||||||
|
**Description**: Gets the next image in sequence for processing.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G06 Image Rotation Manager
|
||||||
|
- G07 Sequential VO
|
||||||
|
- Processing pipeline (main loop)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
ImageData:
|
||||||
|
flight_id: str
|
||||||
|
sequence: int
|
||||||
|
filename: str
|
||||||
|
image: np.ndarray # Loaded image
|
||||||
|
metadata: ImageMetadata
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Track current sequence number for flight
|
||||||
|
2. Load next image from disk
|
||||||
|
3. Increment sequence counter
|
||||||
|
4. Return ImageData
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `None`: No more images
|
||||||
|
- `ImageNotFoundError`: Expected image missing
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Get sequential images**: Returns images in order
|
||||||
|
2. **End of sequence**: Returns None
|
||||||
|
3. **Missing image**: Handles gracefully
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_image_by_sequence(flight_id: str, sequence: int) -> Optional[ImageData]`
|
||||||
|
|
||||||
|
**Description**: Retrieves a specific image by sequence number.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G11 Failure Recovery Coordinator (for user fix)
|
||||||
|
- G13 Result Manager (for refinement)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
sequence: int
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
Optional[ImageData]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Construct filename from sequence (ADxxxxxx.jpg)
|
||||||
|
2. Load from disk
|
||||||
|
3. Load metadata
|
||||||
|
4. Return ImageData
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `None`: Image not found
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Get specific image**: Returns correct image
|
||||||
|
2. **Invalid sequence**: Returns None
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_image_metadata(flight_id: str, sequence: int) -> Optional[ImageMetadata]`
|
||||||
|
|
||||||
|
**Description**: Retrieves metadata without loading full image (lightweight).
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G02 Flight Manager (status checks)
|
||||||
|
- G13 Result Manager (metadata-only queries)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
sequence: int
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
ImageMetadata:
|
||||||
|
sequence: int
|
||||||
|
filename: str
|
||||||
|
dimensions: Tuple[int, int] # (width, height)
|
||||||
|
file_size: int # bytes
|
||||||
|
timestamp: datetime
|
||||||
|
exif_data: Optional[Dict]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Get metadata**: Returns quickly without loading image
|
||||||
|
2. **Missing image**: Returns None
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_processing_status(flight_id: str) -> ProcessingStatus`
|
||||||
|
|
||||||
|
**Description**: Gets current processing status for a flight.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G01 GPS-Denied REST API (status endpoint)
|
||||||
|
- G02 Flight Manager
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
ProcessingStatus:
|
||||||
|
flight_id: str
|
||||||
|
total_images: int
|
||||||
|
processed_images: int
|
||||||
|
current_sequence: int
|
||||||
|
queued_batches: int
|
||||||
|
processing_rate: float # images/second
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Get status**: Returns accurate counts
|
||||||
|
2. **During processing**: Updates in real-time
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
### Test 1: Batch Processing Flow
|
||||||
|
1. queue_batch() with 50 images
|
||||||
|
2. process_next_batch() → returns batch
|
||||||
|
3. store_images() → persists to disk
|
||||||
|
4. get_next_image() × 50 → retrieves all sequentially
|
||||||
|
5. Verify metadata
|
||||||
|
|
||||||
|
### Test 2: Multiple Batches
|
||||||
|
1. queue_batch() × 5 (250 images total)
|
||||||
|
2. process_next_batch() × 5
|
||||||
|
3. Verify FIFO order maintained
|
||||||
|
4. Verify sequence continuity
|
||||||
|
|
||||||
|
### Test 3: Error Handling
|
||||||
|
1. Queue batch with sequence gap
|
||||||
|
2. Verify validation error
|
||||||
|
3. Queue valid batch → succeeds
|
||||||
|
4. Simulate disk full → storage fails gracefully
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **queue_batch**: < 100ms
|
||||||
|
- **process_next_batch**: < 2 seconds for 50 images
|
||||||
|
- **get_next_image**: < 50ms
|
||||||
|
- **get_image_by_sequence**: < 50ms
|
||||||
|
- **Processing throughput**: 10-20 images/second
|
||||||
|
|
||||||
|
### Scalability
|
||||||
|
- Support 3000 images per flight
|
||||||
|
- Handle 10 concurrent flights
|
||||||
|
- Manage 100GB+ image storage
|
||||||
|
|
||||||
|
### Reliability
|
||||||
|
- Crash recovery (resume processing from last sequence)
|
||||||
|
- Atomic batch operations
|
||||||
|
- Data integrity validation
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Internal Components
|
||||||
|
- **H08 Batch Validator**: For validation logic
|
||||||
|
- **G17 Database Layer**: For metadata persistence
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
- **opencv-python**: Image I/O
|
||||||
|
- **Pillow**: Image processing
|
||||||
|
- **numpy**: Image arrays
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### ImageBatch
|
||||||
|
```python
|
||||||
|
class ImageBatch(BaseModel):
|
||||||
|
images: List[bytes]
|
||||||
|
filenames: List[str]
|
||||||
|
start_sequence: int
|
||||||
|
end_sequence: int
|
||||||
|
batch_number: int
|
||||||
|
```
|
||||||
|
|
||||||
|
### ImageData
|
||||||
|
```python
|
||||||
|
class ImageData(BaseModel):
|
||||||
|
flight_id: str
|
||||||
|
sequence: int
|
||||||
|
filename: str
|
||||||
|
image: np.ndarray
|
||||||
|
metadata: ImageMetadata
|
||||||
|
```
|
||||||
|
|
||||||
|
### ImageMetadata
|
||||||
|
```python
|
||||||
|
class ImageMetadata(BaseModel):
|
||||||
|
sequence: int
|
||||||
|
filename: str
|
||||||
|
dimensions: Tuple[int, int]
|
||||||
|
file_size: int
|
||||||
|
timestamp: datetime
|
||||||
|
exif_data: Optional[Dict]
|
||||||
|
```
|
||||||
|
|
||||||
|
### ProcessingStatus
|
||||||
|
```python
|
||||||
|
class ProcessingStatus(BaseModel):
|
||||||
|
flight_id: str
|
||||||
|
total_images: int
|
||||||
|
processed_images: int
|
||||||
|
current_sequence: int
|
||||||
|
queued_batches: int
|
||||||
|
processing_rate: float
|
||||||
|
```
|
||||||
|
|
||||||
+410
@@ -0,0 +1,410 @@
|
|||||||
|
# Image Rotation Manager
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `IImageRotationManager`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IImageRotationManager(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def rotate_image_360(self, image: np.ndarray, angle: float) -> np.ndarray:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def try_rotation_steps(self, flight_id: str, image: np.ndarray, satellite_tile: np.ndarray) -> Optional[RotationResult]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def calculate_precise_angle(self, homography: np.ndarray, initial_angle: float) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_current_heading(self, flight_id: str) -> Optional[float]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def update_heading(self, flight_id: str, heading: float) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def detect_sharp_turn(self, flight_id: str, new_heading: float) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def requires_rotation_sweep(self, flight_id: str) -> bool:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
- Handle UAV image rotation preprocessing for LiteSAM
|
||||||
|
- **Critical**: LiteSAM fails if images rotated >45°, requires preprocessing
|
||||||
|
- Perform 30° step rotation sweeps (12 rotations: 0°, 30°, 60°, ..., 330°)
|
||||||
|
- Track UAV heading angle across flight
|
||||||
|
- Calculate precise rotation angle from homography point correspondences
|
||||||
|
- Detect sharp turns requiring rotation sweep
|
||||||
|
- Pre-rotate images to known heading for subsequent frames
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Image rotation operations
|
||||||
|
- UAV heading tracking and history
|
||||||
|
- Sharp turn detection
|
||||||
|
- Rotation sweep coordination with LiteSAM matching
|
||||||
|
- Precise angle calculation from homography
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `rotate_image_360(image: np.ndarray, angle: float) -> np.ndarray`
|
||||||
|
|
||||||
|
**Description**: Rotates an image by specified angle around center.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Internal (during rotation sweep)
|
||||||
|
- H07 Image Rotation Utils (may delegate to)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
image: np.ndarray # Input image (H×W×3)
|
||||||
|
angle: float # Rotation angle in degrees (0-360)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
np.ndarray # Rotated image (same dimensions)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Details**:
|
||||||
|
- Rotation around image center
|
||||||
|
- Preserves image dimensions
|
||||||
|
- Fills borders with black or extrapolation
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- None (always returns rotated image)
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Rotate 90°**: Image rotated correctly
|
||||||
|
2. **Rotate 0°**: Image unchanged
|
||||||
|
3. **Rotate 180°**: Image inverted
|
||||||
|
4. **Rotate 45°**: Diagonal rotation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `try_rotation_steps(flight_id: str, image: np.ndarray, satellite_tile: np.ndarray) -> Optional[RotationResult]`
|
||||||
|
|
||||||
|
**Description**: Performs 30° rotation sweep, trying LiteSAM match for each rotation.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Internal (when requires_rotation_sweep() returns True)
|
||||||
|
- Main processing loop (first frame or sharp turn)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
image: np.ndarray # UAV image
|
||||||
|
satellite_tile: np.ndarray # Satellite reference tile
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
RotationResult:
|
||||||
|
matched: bool
|
||||||
|
initial_angle: float # Best matching step angle (0, 30, 60, ...)
|
||||||
|
precise_angle: float # Refined angle from homography
|
||||||
|
confidence: float
|
||||||
|
homography: np.ndarray
|
||||||
|
```
|
||||||
|
|
||||||
|
**Algorithm**:
|
||||||
|
```
|
||||||
|
For angle in [0°, 30°, 60°, 90°, 120°, 150°, 180°, 210°, 240°, 270°, 300°, 330°]:
|
||||||
|
rotated_image = rotate_image_360(image, angle)
|
||||||
|
result = LiteSAM.align_to_satellite(rotated_image, satellite_tile)
|
||||||
|
if result.matched and result.confidence > threshold:
|
||||||
|
precise_angle = calculate_precise_angle(result.homography, angle)
|
||||||
|
update_heading(flight_id, precise_angle)
|
||||||
|
return RotationResult(matched=True, initial_angle=angle, precise_angle=precise_angle, ...)
|
||||||
|
return None # No match found
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. For each 30° step:
|
||||||
|
- Rotate image
|
||||||
|
- Call G09 Metric Refinement (LiteSAM)
|
||||||
|
- Check if match found
|
||||||
|
2. If match found:
|
||||||
|
- Calculate precise angle from homography
|
||||||
|
- Update UAV heading
|
||||||
|
- Return result
|
||||||
|
3. If no match:
|
||||||
|
- Return None (triggers progressive search expansion)
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `None`: No match found in any rotation
|
||||||
|
- This is expected behavior (leads to progressive search)
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Match at 60°**: Finds match, returns result
|
||||||
|
2. **Match at 0°**: No rotation needed, finds match
|
||||||
|
3. **No match**: All 12 rotations tried, returns None
|
||||||
|
4. **Multiple matches**: Returns best confidence
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `calculate_precise_angle(homography: np.ndarray, initial_angle: float) -> float`
|
||||||
|
|
||||||
|
**Description**: Calculates precise rotation angle from homography matrix point shifts.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Internal (after LiteSAM match in rotation sweep)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
homography: np.ndarray # 3×3 homography matrix from LiteSAM
|
||||||
|
initial_angle: float # 30° step angle that matched
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
float: Precise rotation angle (e.g., 62.3° refined from 60° step)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Algorithm**:
|
||||||
|
1. Extract rotation component from homography
|
||||||
|
2. Calculate angle from rotation matrix
|
||||||
|
3. Refine initial_angle with delta from homography
|
||||||
|
|
||||||
|
**Uses**: H07 Image Rotation Utils for angle calculation
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Falls back to initial_angle if calculation fails
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Refine 60°**: Returns 62.5° (small delta)
|
||||||
|
2. **Refine 0°**: Returns 3.2° (small rotation)
|
||||||
|
3. **Invalid homography**: Returns initial_angle
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_current_heading(flight_id: str) -> Optional[float]`
|
||||||
|
|
||||||
|
**Description**: Gets current UAV heading angle for a flight.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G06 Internal (to check if pre-rotation needed)
|
||||||
|
- Main processing loop (before LiteSAM)
|
||||||
|
- G11 Failure Recovery Coordinator (logging)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
Optional[float]: Heading angle in degrees (0-360), or None if not initialized
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `None`: First frame, heading not yet determined
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **After first frame**: Returns heading angle
|
||||||
|
2. **Before first frame**: Returns None
|
||||||
|
3. **During flight**: Returns current heading
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `update_heading(flight_id: str, heading: float) -> bool`
|
||||||
|
|
||||||
|
**Description**: Updates UAV heading angle after successful match.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Internal (after rotation sweep match)
|
||||||
|
- Internal (after normal LiteSAM match with small rotation delta)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
heading: float # New heading angle (0-360)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
bool: True if updated successfully
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Normalize angle to 0-360 range
|
||||||
|
2. Add to heading history (last 10 headings)
|
||||||
|
3. Update current_heading for flight
|
||||||
|
4. Persist to database (optional)
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Update heading**: Sets new heading
|
||||||
|
2. **Angle normalization**: 370° → 10°
|
||||||
|
3. **History tracking**: Maintains last 10 headings
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `detect_sharp_turn(flight_id: str, new_heading: float) -> bool`
|
||||||
|
|
||||||
|
**Description**: Detects if UAV made a sharp turn (>45° heading change).
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Internal (before deciding if rotation sweep needed)
|
||||||
|
- Main processing loop
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
new_heading: float # Proposed new heading
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
bool: True if sharp turn detected (>45° change)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Algorithm**:
|
||||||
|
```python
|
||||||
|
current = get_current_heading(flight_id)
|
||||||
|
if current is None:
|
||||||
|
return False
|
||||||
|
delta = abs(new_heading - current)
|
||||||
|
if delta > 180: # Handle wraparound
|
||||||
|
delta = 360 - delta
|
||||||
|
return delta > 45
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Small turn**: 60° → 75° → False (15° delta)
|
||||||
|
2. **Sharp turn**: 60° → 120° → True (60° delta)
|
||||||
|
3. **Wraparound**: 350° → 20° → False (30° delta)
|
||||||
|
4. **180° turn**: 0° → 180° → True
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `requires_rotation_sweep(flight_id: str) -> bool`
|
||||||
|
|
||||||
|
**Description**: Determines if rotation sweep is needed for current frame.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Main processing loop (before each frame)
|
||||||
|
- G11 Failure Recovery Coordinator (after tracking loss)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
bool: True if rotation sweep required
|
||||||
|
```
|
||||||
|
|
||||||
|
**Conditions for sweep**:
|
||||||
|
1. **First frame**: heading not initialized
|
||||||
|
2. **Sharp turn detected**: >45° heading change from VO
|
||||||
|
3. **Tracking loss**: LiteSAM failed to match in previous frame
|
||||||
|
4. **User flag**: Manual trigger (rare)
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **First frame**: Returns True
|
||||||
|
2. **Second frame, no turn**: Returns False
|
||||||
|
3. **Sharp turn detected**: Returns True
|
||||||
|
4. **Tracking loss**: Returns True
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
### Test 1: First Frame Rotation Sweep
|
||||||
|
1. First frame arrives (no heading set)
|
||||||
|
2. requires_rotation_sweep() → True
|
||||||
|
3. try_rotation_steps() → rotates 12 times
|
||||||
|
4. Match found at 60° step
|
||||||
|
5. calculate_precise_angle() → 62.3°
|
||||||
|
6. update_heading(62.3°)
|
||||||
|
7. Subsequent frames use 62.3° heading
|
||||||
|
|
||||||
|
### Test 2: Normal Frame Processing
|
||||||
|
1. Heading known (90°)
|
||||||
|
2. requires_rotation_sweep() → False
|
||||||
|
3. Pre-rotate image to 90°
|
||||||
|
4. LiteSAM match succeeds with small delta (+2.5°)
|
||||||
|
5. update_heading(92.5°)
|
||||||
|
|
||||||
|
### Test 3: Sharp Turn Detection
|
||||||
|
1. UAV heading 45°
|
||||||
|
2. Next frame shows 120° heading (from VO estimate)
|
||||||
|
3. detect_sharp_turn() → True (75° delta)
|
||||||
|
4. requires_rotation_sweep() → True
|
||||||
|
5. Perform rotation sweep → find match at 120° step
|
||||||
|
|
||||||
|
### Test 4: Tracking Loss Recovery
|
||||||
|
1. LiteSAM fails to match (no overlap after turn)
|
||||||
|
2. requires_rotation_sweep() → True
|
||||||
|
3. try_rotation_steps() with all 12 rotations
|
||||||
|
4. Match found → heading updated
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **rotate_image_360**: < 20ms per rotation
|
||||||
|
- **try_rotation_steps**: < 1.2 seconds (12 rotations × 100ms LiteSAM)
|
||||||
|
- **calculate_precise_angle**: < 10ms
|
||||||
|
- **get_current_heading**: < 1ms
|
||||||
|
- **update_heading**: < 5ms
|
||||||
|
|
||||||
|
### Accuracy
|
||||||
|
- **Angle precision**: ±0.5° for precise angle calculation
|
||||||
|
- **Sharp turn detection**: 100% accuracy for >45° turns
|
||||||
|
|
||||||
|
### Reliability
|
||||||
|
- Rotation sweep always completes all 12 steps
|
||||||
|
- Graceful handling of no-match scenarios
|
||||||
|
- Heading history preserved across failures
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Internal Components
|
||||||
|
- **G09 Metric Refinement**: For LiteSAM matching during rotation sweep
|
||||||
|
- **H07 Image Rotation Utils**: For image rotation and angle calculations
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
- **opencv-python**: Image rotation (`cv2.warpAffine`)
|
||||||
|
- **numpy**: Matrix operations
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### RotationResult
|
||||||
|
```python
|
||||||
|
class RotationResult(BaseModel):
|
||||||
|
matched: bool
|
||||||
|
initial_angle: float # 30° step angle (0, 30, 60, ...)
|
||||||
|
precise_angle: float # Refined angle from homography
|
||||||
|
confidence: float
|
||||||
|
homography: np.ndarray
|
||||||
|
inlier_count: int
|
||||||
|
```
|
||||||
|
|
||||||
|
### HeadingHistory
|
||||||
|
```python
|
||||||
|
class HeadingHistory(BaseModel):
|
||||||
|
flight_id: str
|
||||||
|
current_heading: float
|
||||||
|
heading_history: List[float] # Last 10 headings
|
||||||
|
last_update: datetime
|
||||||
|
sharp_turns: int # Count of sharp turns detected
|
||||||
|
```
|
||||||
|
|
||||||
|
### RotationConfig
|
||||||
|
```python
|
||||||
|
class RotationConfig(BaseModel):
|
||||||
|
step_angle: float = 30.0 # Degrees
|
||||||
|
sharp_turn_threshold: float = 45.0 # Degrees
|
||||||
|
confidence_threshold: float = 0.7 # For accepting match
|
||||||
|
history_size: int = 10 # Number of headings to track
|
||||||
|
```
|
||||||
|
|
||||||
+316
@@ -0,0 +1,316 @@
|
|||||||
|
# Sequential Visual Odometry
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `ISequentialVO`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ISequentialVO(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def compute_relative_pose(self, prev_image: np.ndarray, curr_image: np.ndarray) -> Optional[RelativePose]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def extract_features(self, image: np.ndarray) -> Features:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def match_features(self, features1: Features, features2: Features) -> Matches:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def estimate_motion(self, matches: Matches, camera_params: CameraParameters) -> Optional[Motion]:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
- SuperPoint feature extraction from UAV images
|
||||||
|
- LightGlue feature matching between consecutive frames
|
||||||
|
- Handle <5% overlap scenarios
|
||||||
|
- Estimate relative pose (translation + rotation) between frames
|
||||||
|
- Return relative pose factors for Factor Graph Optimizer
|
||||||
|
- Detect tracking loss (low inlier count)
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Frame-to-frame visual odometry
|
||||||
|
- Feature-based motion estimation
|
||||||
|
- Handles low overlap and challenging agricultural environments
|
||||||
|
- Provides relative measurements for trajectory optimization
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `compute_relative_pose(prev_image: np.ndarray, curr_image: np.ndarray) -> Optional[RelativePose]`
|
||||||
|
|
||||||
|
**Description**: Computes relative camera pose between consecutive frames.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Main processing loop (per-frame)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
prev_image: np.ndarray # Previous frame (t-1)
|
||||||
|
curr_image: np.ndarray # Current frame (t)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
RelativePose:
|
||||||
|
translation: np.ndarray # (x, y, z) in meters
|
||||||
|
rotation: np.ndarray # 3×3 rotation matrix or quaternion
|
||||||
|
confidence: float # 0.0 to 1.0
|
||||||
|
inlier_count: int
|
||||||
|
total_matches: int
|
||||||
|
tracking_good: bool
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. extract_features(prev_image) → features1
|
||||||
|
2. extract_features(curr_image) → features2
|
||||||
|
3. match_features(features1, features2) → matches
|
||||||
|
4. estimate_motion(matches, camera_params) → motion
|
||||||
|
5. Return RelativePose
|
||||||
|
|
||||||
|
**Tracking Quality Indicators**:
|
||||||
|
- **Good tracking**: inlier_count > 50, inlier_ratio > 0.5
|
||||||
|
- **Degraded tracking**: inlier_count 20-50
|
||||||
|
- **Tracking loss**: inlier_count < 20
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `None`: Tracking lost (insufficient matches)
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Good overlap (>50%)**: Returns reliable pose
|
||||||
|
2. **Low overlap (5-10%)**: Still succeeds with LightGlue
|
||||||
|
3. **<5% overlap**: May return None (tracking loss)
|
||||||
|
4. **Agricultural texture**: Handles repetitive patterns
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `extract_features(image: np.ndarray) -> Features`
|
||||||
|
|
||||||
|
**Description**: Extracts SuperPoint keypoints and descriptors from image.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Internal (during compute_relative_pose)
|
||||||
|
- G08 Global Place Recognition (for descriptor caching)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
image: np.ndarray # Input image (H×W×3 or H×W)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
Features:
|
||||||
|
keypoints: np.ndarray # (N, 2) - (x, y) coordinates
|
||||||
|
descriptors: np.ndarray # (N, 256) - 256-dim descriptors
|
||||||
|
scores: np.ndarray # (N,) - detection confidence scores
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Details**:
|
||||||
|
- Uses G15 Model Manager to get SuperPoint model
|
||||||
|
- Converts to grayscale if needed
|
||||||
|
- Non-maximum suppression for keypoint selection
|
||||||
|
- Typically extracts 500-2000 keypoints per image
|
||||||
|
|
||||||
|
**Performance**:
|
||||||
|
- Inference time: ~15ms with TensorRT on RTX 2060
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Never fails (returns empty features if image invalid)
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **FullHD image**: Extracts ~1000 keypoints
|
||||||
|
2. **High-res image (6252×4168)**: Extracts ~2000 keypoints
|
||||||
|
3. **Low-texture image**: Extracts fewer keypoints
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `match_features(features1: Features, features2: Features) -> Matches`
|
||||||
|
|
||||||
|
**Description**: Matches features using LightGlue attention-based matcher.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Internal (during compute_relative_pose)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
features1: Features # Previous frame features
|
||||||
|
features2: Features # Current frame features
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
Matches:
|
||||||
|
matches: np.ndarray # (M, 2) - indices [idx1, idx2]
|
||||||
|
scores: np.ndarray # (M,) - match confidence scores
|
||||||
|
keypoints1: np.ndarray # (M, 2) - matched keypoints from frame 1
|
||||||
|
keypoints2: np.ndarray # (M, 2) - matched keypoints from frame 2
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Details**:
|
||||||
|
- Uses G15 Model Manager to get LightGlue model
|
||||||
|
- Transformer-based attention mechanism
|
||||||
|
- "Dustbin" mechanism for unmatched features
|
||||||
|
- Adaptive depth (exits early for easy matches)
|
||||||
|
- **Critical**: Handles <5% overlap better than RANSAC
|
||||||
|
|
||||||
|
**Performance**:
|
||||||
|
- Inference time: ~35-100ms (adaptive depth)
|
||||||
|
- Faster for high-overlap, slower for low-overlap
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **High overlap**: Fast matching (~35ms), 500+ matches
|
||||||
|
2. **Low overlap (<5%)**: Slower (~100ms), 20-50 matches
|
||||||
|
3. **No overlap**: Few or no matches (< 10)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `estimate_motion(matches: Matches, camera_params: CameraParameters) -> Optional[Motion]`
|
||||||
|
|
||||||
|
**Description**: Estimates camera motion from matched keypoints using Essential Matrix.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Internal (during compute_relative_pose)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
matches: Matches
|
||||||
|
camera_params: CameraParameters:
|
||||||
|
focal_length: float
|
||||||
|
principal_point: Tuple[float, float]
|
||||||
|
resolution: Tuple[int, int]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
Motion:
|
||||||
|
translation: np.ndarray # (x, y, z) - unit vector (scale ambiguous)
|
||||||
|
rotation: np.ndarray # 3×3 rotation matrix
|
||||||
|
inliers: np.ndarray # Boolean mask of inlier matches
|
||||||
|
inlier_count: int
|
||||||
|
```
|
||||||
|
|
||||||
|
**Algorithm**:
|
||||||
|
1. Normalize keypoint coordinates using camera intrinsics
|
||||||
|
2. Estimate Essential Matrix using RANSAC
|
||||||
|
3. Decompose Essential Matrix → [R, t]
|
||||||
|
4. Return motion with inlier mask
|
||||||
|
|
||||||
|
**Scale Ambiguity**:
|
||||||
|
- Monocular VO has inherent scale ambiguity
|
||||||
|
- Translation is unit vector (direction only)
|
||||||
|
- Scale resolved by:
|
||||||
|
- Altitude prior (from G10 Factor Graph)
|
||||||
|
- Absolute GPS measurements (from G09 LiteSAM)
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `None`: Insufficient inliers (< 8 points for Essential Matrix)
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Good matches**: Returns motion with high inlier count
|
||||||
|
2. **Low inliers**: May return None
|
||||||
|
3. **Degenerate motion**: Handles pure rotation
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
### Test 1: Normal Flight Sequence
|
||||||
|
1. Load consecutive frames with 50% overlap
|
||||||
|
2. compute_relative_pose() → returns valid pose
|
||||||
|
3. Verify translation direction reasonable
|
||||||
|
4. Verify inlier_count > 100
|
||||||
|
|
||||||
|
### Test 2: Low Overlap Scenario
|
||||||
|
1. Load frames with 5% overlap
|
||||||
|
2. compute_relative_pose() → still succeeds
|
||||||
|
3. Verify inlier_count > 20
|
||||||
|
4. Verify LightGlue finds matches despite low overlap
|
||||||
|
|
||||||
|
### Test 3: Tracking Loss
|
||||||
|
1. Load frames with 0% overlap (sharp turn)
|
||||||
|
2. compute_relative_pose() → returns None
|
||||||
|
3. Verify tracking_good = False
|
||||||
|
4. Trigger global place recognition
|
||||||
|
|
||||||
|
### Test 4: Agricultural Texture
|
||||||
|
1. Load images of wheat fields (repetitive texture)
|
||||||
|
2. compute_relative_pose() → SuperPoint handles better than SIFT
|
||||||
|
3. Verify match quality
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **compute_relative_pose**: < 200ms total
|
||||||
|
- SuperPoint extraction: ~15ms × 2 = 30ms
|
||||||
|
- LightGlue matching: ~50ms
|
||||||
|
- Motion estimation: ~10ms
|
||||||
|
- **Frame rate**: 5-10 FPS processing (meets <5s requirement)
|
||||||
|
|
||||||
|
### Accuracy
|
||||||
|
- **Relative rotation**: ±2° error
|
||||||
|
- **Relative translation direction**: ±5° error
|
||||||
|
- **Inlier ratio**: >50% for good tracking
|
||||||
|
|
||||||
|
### Reliability
|
||||||
|
- Handle 100m spacing between frames
|
||||||
|
- Survive temporary tracking degradation
|
||||||
|
- Recover from brief occlusions
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Internal Components
|
||||||
|
- **G15 Model Manager**: For SuperPoint and LightGlue models
|
||||||
|
- **G16 Configuration Manager**: For camera parameters
|
||||||
|
- **H01 Camera Model**: For coordinate normalization
|
||||||
|
- **H05 Performance Monitor**: For timing measurements
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
- **SuperPoint**: Feature extraction model
|
||||||
|
- **LightGlue**: Feature matching model
|
||||||
|
- **opencv-python**: Essential Matrix estimation
|
||||||
|
- **numpy**: Matrix operations
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### Features
|
||||||
|
```python
|
||||||
|
class Features(BaseModel):
|
||||||
|
keypoints: np.ndarray # (N, 2)
|
||||||
|
descriptors: np.ndarray # (N, 256)
|
||||||
|
scores: np.ndarray # (N,)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Matches
|
||||||
|
```python
|
||||||
|
class Matches(BaseModel):
|
||||||
|
matches: np.ndarray # (M, 2) - pairs of indices
|
||||||
|
scores: np.ndarray # (M,) - match confidence
|
||||||
|
keypoints1: np.ndarray # (M, 2)
|
||||||
|
keypoints2: np.ndarray # (M, 2)
|
||||||
|
```
|
||||||
|
|
||||||
|
### RelativePose
|
||||||
|
```python
|
||||||
|
class RelativePose(BaseModel):
|
||||||
|
translation: np.ndarray # (3,) - unit vector
|
||||||
|
rotation: np.ndarray # (3, 3) or (4,) quaternion
|
||||||
|
confidence: float
|
||||||
|
inlier_count: int
|
||||||
|
total_matches: int
|
||||||
|
tracking_good: bool
|
||||||
|
scale_ambiguous: bool = True
|
||||||
|
```
|
||||||
|
|
||||||
|
### Motion
|
||||||
|
```python
|
||||||
|
class Motion(BaseModel):
|
||||||
|
translation: np.ndarray # (3,)
|
||||||
|
rotation: np.ndarray # (3, 3)
|
||||||
|
inliers: np.ndarray # Boolean mask
|
||||||
|
inlier_count: int
|
||||||
|
```
|
||||||
|
|
||||||
+310
@@ -0,0 +1,310 @@
|
|||||||
|
# Global Place Recognition
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `IGlobalPlaceRecognition`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IGlobalPlaceRecognition(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def retrieve_candidate_tiles(self, image: np.ndarray, top_k: int) -> List[TileCandidate]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def compute_location_descriptor(self, image: np.ndarray) -> np.ndarray:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def query_database(self, descriptor: np.ndarray, top_k: int) -> List[DatabaseMatch]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def rank_candidates(self, candidates: List[TileCandidate]) -> List[TileCandidate]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def initialize_database(self, satellite_tiles: List[SatelliteTile]) -> bool:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
- AnyLoc (DINOv2 + VLAD) for coarse localization after tracking loss
|
||||||
|
- "Kidnapped robot" recovery after sharp turns
|
||||||
|
- Compute image descriptors robust to season/appearance changes
|
||||||
|
- Query Faiss index of satellite tile descriptors
|
||||||
|
- Return top-k candidate tile regions for progressive refinement
|
||||||
|
- Initialize satellite descriptor database during system startup
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Global localization (not frame-to-frame)
|
||||||
|
- Appearance-based place recognition
|
||||||
|
- Handles domain gap (UAV vs satellite imagery)
|
||||||
|
- Semantic feature extraction (DINOv2)
|
||||||
|
- Efficient similarity search (Faiss)
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `retrieve_candidate_tiles(image: np.ndarray, top_k: int) -> List[TileCandidate]`
|
||||||
|
|
||||||
|
**Description**: Retrieves top-k candidate satellite tiles for a UAV image.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G11 Failure Recovery Coordinator (after tracking loss)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
image: np.ndarray # UAV image
|
||||||
|
top_k: int # Number of candidates (typically 5)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
List[TileCandidate]:
|
||||||
|
tile_id: str
|
||||||
|
gps_center: GPSPoint
|
||||||
|
similarity_score: float
|
||||||
|
rank: int
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. compute_location_descriptor(image) → descriptor
|
||||||
|
2. query_database(descriptor, top_k) → database_matches
|
||||||
|
3. Retrieve tile metadata for matches
|
||||||
|
4. rank_candidates() → sorted by similarity
|
||||||
|
5. Return top-k candidates
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns empty list: Database not initialized, query failed
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **UAV image over Ukraine**: Returns relevant tiles
|
||||||
|
2. **Different season**: DINOv2 handles appearance change
|
||||||
|
3. **Top-1 accuracy**: Correct tile in top-5 > 85%
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `compute_location_descriptor(image: np.ndarray) -> np.ndarray`
|
||||||
|
|
||||||
|
**Description**: Computes global descriptor using DINOv2 + VLAD aggregation.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Internal (during retrieve_candidate_tiles)
|
||||||
|
- System initialization (for satellite database)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
image: np.ndarray # UAV or satellite image
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
np.ndarray: Descriptor vector (4096-dim or 8192-dim)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Algorithm (AnyLoc)**:
|
||||||
|
1. Extract DINOv2 features (dense feature map)
|
||||||
|
2. Apply VLAD (Vector of Locally Aggregated Descriptors) aggregation
|
||||||
|
3. L2-normalize descriptor
|
||||||
|
4. Return compact global descriptor
|
||||||
|
|
||||||
|
**Processing Details**:
|
||||||
|
- Uses G15 Model Manager to get DINOv2 model
|
||||||
|
- Dense features: extracts from multiple spatial locations
|
||||||
|
- VLAD codebook: pre-trained cluster centers
|
||||||
|
- Semantic features: invariant to texture/color changes
|
||||||
|
|
||||||
|
**Performance**:
|
||||||
|
- Inference time: ~150ms for DINOv2 + VLAD
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Same location, different season**: Similar descriptors
|
||||||
|
2. **Different locations**: Dissimilar descriptors
|
||||||
|
3. **UAV vs satellite**: Domain-invariant features
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `query_database(descriptor: np.ndarray, top_k: int) -> List[DatabaseMatch]`
|
||||||
|
|
||||||
|
**Description**: Queries Faiss index for most similar satellite tiles.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Internal (during retrieve_candidate_tiles)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
descriptor: np.ndarray # Query descriptor
|
||||||
|
top_k: int
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
List[DatabaseMatch]:
|
||||||
|
index: int # Tile index in database
|
||||||
|
distance: float # L2 distance
|
||||||
|
similarity_score: float # Normalized score
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Details**:
|
||||||
|
- Uses H04 Faiss Index Manager
|
||||||
|
- Index type: IVF (Inverted File) or HNSW for fast search
|
||||||
|
- Distance metric: L2 (Euclidean)
|
||||||
|
- Query time: ~10-50ms for 10,000+ tiles
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns empty list: Query failed
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Query satellite database**: Returns top-5 matches
|
||||||
|
2. **Large database (10,000 tiles)**: Fast retrieval (<50ms)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `rank_candidates(candidates: List[TileCandidate]) -> List[TileCandidate]`
|
||||||
|
|
||||||
|
**Description**: Re-ranks candidates based on additional heuristics.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Internal (during retrieve_candidate_tiles)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
candidates: List[TileCandidate] # Initial ranking by similarity
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
List[TileCandidate] # Re-ranked list
|
||||||
|
```
|
||||||
|
|
||||||
|
**Re-ranking Factors**:
|
||||||
|
1. **Similarity score**: Primary factor
|
||||||
|
2. **Spatial proximity**: Prefer tiles near dead-reckoning estimate
|
||||||
|
3. **Previous trajectory**: Favor continuation of route
|
||||||
|
4. **Geofence constraints**: Within operational area
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Spatial re-ranking**: Closer tile promoted
|
||||||
|
2. **Similar scores**: Spatial proximity breaks tie
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `initialize_database(satellite_tiles: List[SatelliteTile]) -> bool`
|
||||||
|
|
||||||
|
**Description**: Initializes satellite descriptor database during system startup.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G02 Flight Manager (during system initialization)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
List[SatelliteTile]:
|
||||||
|
tile_id: str
|
||||||
|
image: np.ndarray
|
||||||
|
gps_center: GPSPoint
|
||||||
|
bounds: TileBounds
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
bool: True if database initialized successfully
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. For each satellite tile:
|
||||||
|
- compute_location_descriptor(tile.image) → descriptor
|
||||||
|
- Store descriptor with tile metadata
|
||||||
|
2. Build Faiss index using H04 Faiss Index Manager
|
||||||
|
3. Persist index to disk for fast startup
|
||||||
|
|
||||||
|
**Performance**:
|
||||||
|
- Initialization time: ~10-30 minutes for 10,000 tiles (one-time cost)
|
||||||
|
- Can be done offline and loaded at startup
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Initialize with 1000 tiles**: Completes successfully
|
||||||
|
2. **Load pre-built index**: Fast startup (<10s)
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
### Test 1: Place Recognition Flow
|
||||||
|
1. Load UAV image from sharp turn
|
||||||
|
2. retrieve_candidate_tiles(top_k=5)
|
||||||
|
3. Verify correct tile in top-5
|
||||||
|
4. Pass candidates to G11 Failure Recovery
|
||||||
|
|
||||||
|
### Test 2: Season Invariance
|
||||||
|
1. Satellite tiles from summer
|
||||||
|
2. UAV images from autumn
|
||||||
|
3. retrieve_candidate_tiles() → correct match despite appearance change
|
||||||
|
|
||||||
|
### Test 3: Database Initialization
|
||||||
|
1. Prepare 500 satellite tiles
|
||||||
|
2. initialize_database(tiles)
|
||||||
|
3. Verify Faiss index built
|
||||||
|
4. Query with test image → returns matches
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **retrieve_candidate_tiles**: < 200ms total
|
||||||
|
- Descriptor computation: ~150ms
|
||||||
|
- Database query: ~50ms
|
||||||
|
- **compute_location_descriptor**: ~150ms
|
||||||
|
- **query_database**: ~10-50ms
|
||||||
|
|
||||||
|
### Accuracy
|
||||||
|
- **Recall@5**: > 85% (correct tile in top-5)
|
||||||
|
- **Recall@1**: > 60% (correct tile is top-1)
|
||||||
|
|
||||||
|
### Scalability
|
||||||
|
- Support 10,000+ satellite tiles in database
|
||||||
|
- Fast query even with large database
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Internal Components
|
||||||
|
- **G15 Model Manager**: For DINOv2 model
|
||||||
|
- **H04 Faiss Index Manager**: For similarity search
|
||||||
|
- **G04 Satellite Data Manager**: For tile metadata
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
- **DINOv2**: Foundation vision model
|
||||||
|
- **Faiss**: Similarity search library
|
||||||
|
- **numpy**: Array operations
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### TileCandidate
|
||||||
|
```python
|
||||||
|
class TileCandidate(BaseModel):
|
||||||
|
tile_id: str
|
||||||
|
gps_center: GPSPoint
|
||||||
|
bounds: TileBounds
|
||||||
|
similarity_score: float
|
||||||
|
rank: int
|
||||||
|
spatial_score: Optional[float]
|
||||||
|
```
|
||||||
|
|
||||||
|
### DatabaseMatch
|
||||||
|
```python
|
||||||
|
class DatabaseMatch(BaseModel):
|
||||||
|
index: int
|
||||||
|
tile_id: str
|
||||||
|
distance: float
|
||||||
|
similarity_score: float
|
||||||
|
```
|
||||||
|
|
||||||
|
### SatelliteTile
|
||||||
|
```python
|
||||||
|
class SatelliteTile(BaseModel):
|
||||||
|
tile_id: str
|
||||||
|
image: np.ndarray
|
||||||
|
gps_center: GPSPoint
|
||||||
|
bounds: TileBounds
|
||||||
|
descriptor: Optional[np.ndarray]
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
# Metric Refinement
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `IMetricRefinement`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IMetricRefinement(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def align_to_satellite(self, uav_image: np.ndarray, satellite_tile: np.ndarray) -> Optional[AlignmentResult]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def compute_homography(self, uav_image: np.ndarray, satellite_tile: np.ndarray) -> Optional[np.ndarray]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def extract_gps_from_alignment(self, homography: np.ndarray, tile_bounds: TileBounds, image_center: Tuple[int, int]) -> GPSPoint:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def compute_match_confidence(self, alignment: AlignmentResult) -> float:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
- LiteSAM for precise UAV-to-satellite cross-view matching
|
||||||
|
- **Requires pre-rotated images** from Image Rotation Manager
|
||||||
|
- Compute homography mapping UAV image to satellite tile
|
||||||
|
- Extract absolute GPS coordinates from alignment
|
||||||
|
- Process against single tile (drift correction) or tile grid (progressive search)
|
||||||
|
- Achieve <20m accuracy requirement
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Cross-view geo-localization (UAV↔satellite)
|
||||||
|
- Handles altitude variations (<1km)
|
||||||
|
- Multi-scale processing for different GSDs
|
||||||
|
- Domain gap (UAV downward vs satellite nadir view)
|
||||||
|
- **Critical**: Fails if rotation >45° (handled by G06)
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `align_to_satellite(uav_image: np.ndarray, satellite_tile: np.ndarray) -> Optional[AlignmentResult]`
|
||||||
|
|
||||||
|
**Description**: Aligns UAV image to satellite tile, returning GPS location.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G06 Image Rotation Manager (during rotation sweep)
|
||||||
|
- G11 Failure Recovery Coordinator (progressive search)
|
||||||
|
- Main processing loop (drift correction with single tile)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
uav_image: np.ndarray # Pre-rotated UAV image
|
||||||
|
satellite_tile: np.ndarray # Reference satellite tile
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
AlignmentResult:
|
||||||
|
matched: bool
|
||||||
|
homography: np.ndarray # 3×3 transformation matrix
|
||||||
|
gps_center: GPSPoint # UAV image center GPS
|
||||||
|
confidence: float
|
||||||
|
inlier_count: int
|
||||||
|
total_correspondences: int
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Extract features from both images using LiteSAM encoder
|
||||||
|
2. Compute dense correspondence field
|
||||||
|
3. Estimate homography from correspondences
|
||||||
|
4. Validate match quality (inlier count, reprojection error)
|
||||||
|
5. If valid match:
|
||||||
|
- Extract GPS from homography
|
||||||
|
- Return AlignmentResult
|
||||||
|
6. If no match:
|
||||||
|
- Return None
|
||||||
|
|
||||||
|
**Match Criteria**:
|
||||||
|
- **Good match**: inlier_count > 30, confidence > 0.7
|
||||||
|
- **Weak match**: inlier_count 15-30, confidence 0.5-0.7
|
||||||
|
- **No match**: inlier_count < 15
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `None`: No match found, rotation >45° (should be pre-rotated)
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Good alignment**: Returns GPS within 20m of ground truth
|
||||||
|
2. **Altitude variation**: Handles GSD mismatch
|
||||||
|
3. **Rotation >45°**: Fails (by design, requires pre-rotation)
|
||||||
|
4. **Multi-scale**: Processes at multiple scales
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `compute_homography(uav_image: np.ndarray, satellite_tile: np.ndarray) -> Optional[np.ndarray]`
|
||||||
|
|
||||||
|
**Description**: Computes homography transformation from UAV to satellite.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Internal (during align_to_satellite)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
uav_image: np.ndarray
|
||||||
|
satellite_tile: np.ndarray
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
Optional[np.ndarray]: 3×3 homography matrix or None
|
||||||
|
```
|
||||||
|
|
||||||
|
**Algorithm (LiteSAM)**:
|
||||||
|
1. Extract multi-scale features using TAIFormer
|
||||||
|
2. Compute correlation via Convolutional Token Mixer (CTM)
|
||||||
|
3. Generate dense correspondences
|
||||||
|
4. Estimate homography using RANSAC
|
||||||
|
5. Refine with non-linear optimization
|
||||||
|
|
||||||
|
**Homography Properties**:
|
||||||
|
- Maps pixels from UAV image to satellite image
|
||||||
|
- Accounts for: scale, rotation, perspective
|
||||||
|
- 8 DoF (degrees of freedom)
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `None`: Insufficient correspondences
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Valid correspondence**: Returns 3×3 matrix
|
||||||
|
2. **Insufficient features**: Returns None
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `extract_gps_from_alignment(homography: np.ndarray, tile_bounds: TileBounds, image_center: Tuple[int, int]) -> GPSPoint`
|
||||||
|
|
||||||
|
**Description**: Extracts GPS coordinates from homography and tile georeferencing.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Internal (during align_to_satellite)
|
||||||
|
- G06 Image Rotation Manager (for precise angle calculation)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
homography: np.ndarray # 3×3 matrix
|
||||||
|
tile_bounds: TileBounds # GPS bounds of satellite tile
|
||||||
|
image_center: Tuple[int, int] # Center pixel of UAV image
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
GPSPoint:
|
||||||
|
lat: float
|
||||||
|
lon: float
|
||||||
|
```
|
||||||
|
|
||||||
|
**Algorithm**:
|
||||||
|
1. Apply homography to UAV image center point
|
||||||
|
2. Get pixel coordinates in satellite tile
|
||||||
|
3. Convert satellite pixel to GPS using tile_bounds and GSD
|
||||||
|
4. Return GPS coordinates
|
||||||
|
|
||||||
|
**Uses**: G04 Satellite Data Manager for tile_bounds, H02 GSD Calculator
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Center alignment**: UAV center → correct GPS
|
||||||
|
2. **Corner alignment**: UAV corner → correct GPS
|
||||||
|
3. **Multiple points**: All points consistent
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `compute_match_confidence(alignment: AlignmentResult) -> float`
|
||||||
|
|
||||||
|
**Description**: Computes match confidence score from alignment quality.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Internal (during align_to_satellite)
|
||||||
|
- G11 Failure Recovery Coordinator (to decide if match acceptable)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
alignment: AlignmentResult
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
float: Confidence score (0.0 to 1.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Confidence Factors**:
|
||||||
|
1. **Inlier ratio**: inliers / total_correspondences
|
||||||
|
2. **Inlier count**: Absolute number of inliers
|
||||||
|
3. **Reprojection error**: Mean error of inliers (in pixels)
|
||||||
|
4. **Spatial distribution**: Inliers well-distributed vs clustered
|
||||||
|
|
||||||
|
**Thresholds**:
|
||||||
|
- **High confidence (>0.8)**: inlier_ratio > 0.6, inlier_count > 50, MRE < 0.5px
|
||||||
|
- **Medium confidence (0.5-0.8)**: inlier_ratio > 0.4, inlier_count > 30
|
||||||
|
- **Low confidence (<0.5)**: Reject match
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Good match**: confidence > 0.8
|
||||||
|
2. **Weak match**: confidence 0.5-0.7
|
||||||
|
3. **Poor match**: confidence < 0.5
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
### Test 1: Single Tile Drift Correction
|
||||||
|
1. Load UAV image and expected satellite tile
|
||||||
|
2. Pre-rotate UAV image to known heading
|
||||||
|
3. align_to_satellite() → returns GPS
|
||||||
|
4. Verify GPS within 20m of ground truth
|
||||||
|
|
||||||
|
### Test 2: Progressive Search (4 tiles)
|
||||||
|
1. Load UAV image from sharp turn
|
||||||
|
2. Get 2×2 tile grid from G04
|
||||||
|
3. align_to_satellite() for each tile
|
||||||
|
4. First 3 tiles: No match
|
||||||
|
5. 4th tile: Match found → GPS extracted
|
||||||
|
|
||||||
|
### Test 3: Rotation Sensitivity
|
||||||
|
1. Rotate UAV image by 60° (not pre-rotated)
|
||||||
|
2. align_to_satellite() → returns None (fails as expected)
|
||||||
|
3. Pre-rotate to 60°
|
||||||
|
4. align_to_satellite() → succeeds
|
||||||
|
|
||||||
|
### Test 4: Multi-Scale Robustness
|
||||||
|
1. UAV at 500m altitude (GSD=0.1m/pixel)
|
||||||
|
2. Satellite at zoom 19 (GSD=0.3m/pixel)
|
||||||
|
3. LiteSAM handles scale difference → match succeeds
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **align_to_satellite**: ~60ms per tile (TensorRT optimized)
|
||||||
|
- **Progressive search 25 tiles**: ~1.5 seconds total (25 × 60ms)
|
||||||
|
- Meets <5s per frame requirement
|
||||||
|
|
||||||
|
### Accuracy
|
||||||
|
- **GPS accuracy**: 60% of frames < 20m error, 80% < 50m error
|
||||||
|
- **Mean Reprojection Error (MRE)**: < 1.0 pixels
|
||||||
|
- **Alignment success rate**: > 95% when rotation correct
|
||||||
|
|
||||||
|
### Reliability
|
||||||
|
- Graceful failure when no match
|
||||||
|
- Robust to altitude variations (<1km)
|
||||||
|
- Handles seasonal appearance changes (to extent possible)
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Internal Components
|
||||||
|
- **G15 Model Manager**: For LiteSAM model
|
||||||
|
- **G04 Satellite Data Manager**: For tile_bounds and GSD
|
||||||
|
- **H01 Camera Model**: For projection operations
|
||||||
|
- **H02 GSD Calculator**: For coordinate transformations
|
||||||
|
- **H05 Performance Monitor**: For timing
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
- **LiteSAM**: Cross-view matching model
|
||||||
|
- **opencv-python**: Homography estimation
|
||||||
|
- **numpy**: Matrix operations
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### AlignmentResult
|
||||||
|
```python
|
||||||
|
class AlignmentResult(BaseModel):
|
||||||
|
matched: bool
|
||||||
|
homography: np.ndarray # (3, 3)
|
||||||
|
gps_center: GPSPoint
|
||||||
|
confidence: float
|
||||||
|
inlier_count: int
|
||||||
|
total_correspondences: int
|
||||||
|
reprojection_error: float # Mean error in pixels
|
||||||
|
```
|
||||||
|
|
||||||
|
### GPSPoint
|
||||||
|
```python
|
||||||
|
class GPSPoint(BaseModel):
|
||||||
|
lat: float
|
||||||
|
lon: float
|
||||||
|
```
|
||||||
|
|
||||||
|
### TileBounds
|
||||||
|
```python
|
||||||
|
class TileBounds(BaseModel):
|
||||||
|
nw: GPSPoint
|
||||||
|
ne: GPSPoint
|
||||||
|
sw: GPSPoint
|
||||||
|
se: GPSPoint
|
||||||
|
center: GPSPoint
|
||||||
|
gsd: float # Ground Sampling Distance (m/pixel)
|
||||||
|
```
|
||||||
|
|
||||||
|
### LiteSAMConfig
|
||||||
|
```python
|
||||||
|
class LiteSAMConfig(BaseModel):
|
||||||
|
model_path: str
|
||||||
|
confidence_threshold: float = 0.7
|
||||||
|
min_inliers: int = 15
|
||||||
|
max_reprojection_error: float = 2.0 # pixels
|
||||||
|
multi_scale_levels: int = 3
|
||||||
|
```
|
||||||
|
|
||||||
+362
@@ -0,0 +1,362 @@
|
|||||||
|
# Factor Graph Optimizer
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `IFactorGraphOptimizer`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IFactorGraphOptimizer(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def add_relative_factor(self, frame_i: int, frame_j: int, relative_pose: RelativePose, covariance: np.ndarray) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def add_absolute_factor(self, frame_id: int, gps: GPSPoint, covariance: np.ndarray, is_user_anchor: bool) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def add_altitude_prior(self, frame_id: int, altitude: float, covariance: float) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def optimize(self, iterations: int) -> OptimizationResult:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_trajectory(self) -> Dict[int, Pose]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_marginal_covariance(self, frame_id: int) -> np.ndarray:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
- GTSAM-based fusion of relative and absolute measurements
|
||||||
|
- Incremental optimization (iSAM2) for real-time performance
|
||||||
|
- Robust kernels (Huber/Cauchy) for 350m outlier handling
|
||||||
|
- Scale resolution through altitude priors and absolute GPS
|
||||||
|
- Trajectory smoothing and global consistency
|
||||||
|
- Back-propagation of refinements to previous frames
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Non-linear least squares optimization
|
||||||
|
- Factor graph representation of SLAM problem
|
||||||
|
- Handles monocular scale ambiguity
|
||||||
|
- Real-time incremental updates
|
||||||
|
- Asynchronous batch refinement
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `add_relative_factor(frame_i: int, frame_j: int, relative_pose: RelativePose, covariance: np.ndarray) -> bool`
|
||||||
|
|
||||||
|
**Description**: Adds relative pose measurement between consecutive frames.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G07 Sequential VO (frame-to-frame odometry)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
frame_i: int # Previous frame ID
|
||||||
|
frame_j: int # Current frame ID (typically frame_i + 1)
|
||||||
|
relative_pose: RelativePose:
|
||||||
|
translation: np.ndarray # (3,) - in meters (scale from altitude prior)
|
||||||
|
rotation: np.ndarray # (3, 3) or quaternion
|
||||||
|
covariance: np.ndarray # (6, 6) - uncertainty
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
bool: True if factor added successfully
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Create BetweenFactor in GTSAM
|
||||||
|
2. Apply robust kernel (Huber) to handle outliers
|
||||||
|
3. Add to factor graph
|
||||||
|
4. Mark graph as needing optimization
|
||||||
|
|
||||||
|
**Robust Kernel**:
|
||||||
|
- **Huber loss**: Downweights large errors (>threshold)
|
||||||
|
- **Critical** for 350m outlier handling from tilt
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Normal motion**: Factor added, contributes to optimization
|
||||||
|
2. **Large displacement** (350m outlier): Huber kernel reduces weight
|
||||||
|
3. **Consecutive factors**: Chain of relative factors builds trajectory
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `add_absolute_factor(frame_id: int, gps: GPSPoint, covariance: np.ndarray, is_user_anchor: bool) -> bool`
|
||||||
|
|
||||||
|
**Description**: Adds absolute GPS measurement for drift correction or user anchor.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G09 Metric Refinement (after LiteSAM alignment)
|
||||||
|
- G11 Failure Recovery Coordinator (user-provided anchors)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
frame_id: int
|
||||||
|
gps: GPSPoint:
|
||||||
|
lat: float
|
||||||
|
lon: float
|
||||||
|
covariance: np.ndarray # (2, 2) or (3, 3) - GPS uncertainty
|
||||||
|
is_user_anchor: bool # True for user-provided fixes (high confidence)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
bool: True if factor added
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Convert GPS to local ENU coordinates (East-North-Up)
|
||||||
|
2. Create PriorFactor or UnaryFactor
|
||||||
|
3. Set covariance (low for user anchors, higher for LiteSAM)
|
||||||
|
4. Add to factor graph
|
||||||
|
5. Trigger optimization (immediate for user anchors)
|
||||||
|
|
||||||
|
**Covariance Settings**:
|
||||||
|
- **User anchor**: σ = 5m (high confidence)
|
||||||
|
- **LiteSAM match**: σ = 20-50m (depends on confidence)
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **LiteSAM GPS**: Adds absolute factor, corrects drift
|
||||||
|
2. **User anchor**: High confidence, immediately refines trajectory
|
||||||
|
3. **Multiple absolute factors**: Graph optimizes to balance all
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `add_altitude_prior(frame_id: int, altitude: float, covariance: float) -> bool`
|
||||||
|
|
||||||
|
**Description**: Adds altitude constraint to resolve monocular scale ambiguity.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Main processing loop (for each frame)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
frame_id: int
|
||||||
|
altitude: float # Predefined altitude in meters
|
||||||
|
covariance: float # Altitude uncertainty (e.g., 50m)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
bool: True if prior added
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Create UnaryFactor for Z-coordinate
|
||||||
|
2. Set as soft constraint (not hard constraint)
|
||||||
|
3. Add to factor graph
|
||||||
|
|
||||||
|
**Purpose**:
|
||||||
|
- Resolves scale ambiguity in monocular VO
|
||||||
|
- Prevents scale drift (trajectory collapsing or exploding)
|
||||||
|
- Soft constraint allows adjustment based on absolute GPS
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Without altitude prior**: Scale drifts over time
|
||||||
|
2. **With altitude prior**: Scale stabilizes
|
||||||
|
3. **Conflicting measurements**: Optimizer balances VO and altitude
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `optimize(iterations: int) -> OptimizationResult`
|
||||||
|
|
||||||
|
**Description**: Runs optimization to refine trajectory.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Main processing loop (incremental after each frame)
|
||||||
|
- Asynchronous refinement thread (batch optimization)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
iterations: int # Max iterations (typically 5-10 for incremental, 50-100 for batch)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
OptimizationResult:
|
||||||
|
converged: bool
|
||||||
|
final_error: float
|
||||||
|
iterations_used: int
|
||||||
|
optimized_frames: List[int] # Frames with updated poses
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Details**:
|
||||||
|
- **Incremental** (iSAM2): Updates only affected nodes
|
||||||
|
- **Batch**: Re-optimizes entire trajectory when new absolute factors added
|
||||||
|
- **Robust M-estimation**: Automatically downweights outliers
|
||||||
|
|
||||||
|
**Optimization Algorithm** (Levenberg-Marquardt):
|
||||||
|
1. Linearize factor graph around current estimate
|
||||||
|
2. Solve linear system
|
||||||
|
3. Update pose estimates
|
||||||
|
4. Check convergence (error reduction < threshold)
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Incremental optimization**: Fast (<100ms), local update
|
||||||
|
2. **Batch optimization**: Slower (~500ms), refines entire trajectory
|
||||||
|
3. **Convergence**: Error reduces, converges within iterations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_trajectory() -> Dict[int, Pose]`
|
||||||
|
|
||||||
|
**Description**: Retrieves complete optimized trajectory.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G13 Result Manager (for publishing results)
|
||||||
|
- G12 Coordinate Transformer (for GPS conversion)
|
||||||
|
|
||||||
|
**Input**: None
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
Dict[int, Pose]:
|
||||||
|
frame_id -> Pose:
|
||||||
|
position: np.ndarray # (x, y, z) in ENU
|
||||||
|
orientation: np.ndarray # Quaternion or rotation matrix
|
||||||
|
timestamp: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Extract all pose estimates from graph
|
||||||
|
2. Convert to appropriate coordinate system
|
||||||
|
3. Return dictionary
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **After optimization**: Returns all frame poses
|
||||||
|
2. **Refined trajectory**: Poses updated after batch optimization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_marginal_covariance(frame_id: int) -> np.ndarray`
|
||||||
|
|
||||||
|
**Description**: Gets uncertainty (covariance) of a pose estimate.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G11 Failure Recovery Coordinator (to detect high uncertainty)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
frame_id: int
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
np.ndarray: (6, 6) covariance matrix [x, y, z, roll, pitch, yaw]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Purpose**:
|
||||||
|
- Uncertainty quantification
|
||||||
|
- Trigger user input when uncertainty too high (> 50m radius)
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Well-constrained pose**: Small covariance
|
||||||
|
2. **Unconstrained pose**: Large covariance
|
||||||
|
3. **After absolute factor**: Covariance reduces
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
### Test 1: Incremental Trajectory Building
|
||||||
|
1. Initialize graph with first frame
|
||||||
|
2. Add relative factors from VO × 100
|
||||||
|
3. Add altitude priors × 100
|
||||||
|
4. Optimize incrementally after each frame
|
||||||
|
5. Verify smooth trajectory
|
||||||
|
|
||||||
|
### Test 2: Drift Correction with Absolute GPS
|
||||||
|
1. Build trajectory with VO only (will drift)
|
||||||
|
2. Add absolute GPS factor at frame 50
|
||||||
|
3. Optimize → trajectory corrects
|
||||||
|
4. Verify frames 1-49 also corrected (back-propagation)
|
||||||
|
|
||||||
|
### Test 3: Outlier Handling
|
||||||
|
1. Add normal relative factors
|
||||||
|
2. Add 350m outlier factor (tilt error)
|
||||||
|
3. Optimize with robust kernel
|
||||||
|
4. Verify outlier downweighted, trajectory smooth
|
||||||
|
|
||||||
|
### Test 4: User Anchor Integration
|
||||||
|
1. Processing blocked at frame 237
|
||||||
|
2. User provides anchor (high confidence)
|
||||||
|
3. add_absolute_factor(is_user_anchor=True)
|
||||||
|
4. Optimize → trajectory snaps to anchor
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **Incremental optimize**: < 100ms per frame (iSAM2)
|
||||||
|
- **Batch optimize**: < 500ms for 100 frames
|
||||||
|
- **get_trajectory**: < 10ms
|
||||||
|
- Real-time capable: 10 FPS processing
|
||||||
|
|
||||||
|
### Accuracy
|
||||||
|
- **Mean Reprojection Error (MRE)**: < 1.0 pixels
|
||||||
|
- **GPS accuracy**: Meet 80% < 50m, 60% < 20m criteria
|
||||||
|
- **Trajectory smoothness**: No sudden jumps (except user anchors)
|
||||||
|
|
||||||
|
### Reliability
|
||||||
|
- Numerical stability for 2000+ frame trajectories
|
||||||
|
- Graceful handling of degenerate configurations
|
||||||
|
- Robust to missing/corrupted measurements
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Internal Components
|
||||||
|
- **H03 Robust Kernels**: For Huber/Cauchy loss functions
|
||||||
|
- **H02 GSD Calculator**: For coordinate conversions
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
- **GTSAM**: Graph optimization library
|
||||||
|
- **numpy**: Matrix operations
|
||||||
|
- **scipy**: Sparse matrix operations (optional)
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### Pose
|
||||||
|
```python
|
||||||
|
class Pose(BaseModel):
|
||||||
|
frame_id: int
|
||||||
|
position: np.ndarray # (3,) - [x, y, z] in ENU
|
||||||
|
orientation: np.ndarray # (4,) quaternion or (3,3) rotation matrix
|
||||||
|
timestamp: datetime
|
||||||
|
covariance: Optional[np.ndarray] # (6, 6)
|
||||||
|
```
|
||||||
|
|
||||||
|
### RelativePose
|
||||||
|
```python
|
||||||
|
class RelativePose(BaseModel):
|
||||||
|
translation: np.ndarray # (3,)
|
||||||
|
rotation: np.ndarray # (3, 3) or (4,)
|
||||||
|
covariance: np.ndarray # (6, 6)
|
||||||
|
```
|
||||||
|
|
||||||
|
### OptimizationResult
|
||||||
|
```python
|
||||||
|
class OptimizationResult(BaseModel):
|
||||||
|
converged: bool
|
||||||
|
final_error: float
|
||||||
|
iterations_used: int
|
||||||
|
optimized_frames: List[int]
|
||||||
|
mean_reprojection_error: float
|
||||||
|
```
|
||||||
|
|
||||||
|
### FactorGraphConfig
|
||||||
|
```python
|
||||||
|
class FactorGraphConfig(BaseModel):
|
||||||
|
robust_kernel_type: str = "Huber" # or "Cauchy"
|
||||||
|
huber_threshold: float = 1.0 # pixels
|
||||||
|
cauchy_k: float = 0.1
|
||||||
|
isam2_relinearize_threshold: float = 0.1
|
||||||
|
isam2_relinearize_skip: int = 1
|
||||||
|
```
|
||||||
|
|
||||||
+404
@@ -0,0 +1,404 @@
|
|||||||
|
# Failure Recovery Coordinator
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `IFailureRecoveryCoordinator`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IFailureRecoveryCoordinator(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def check_confidence(self, vo_result: RelativePose, litesam_result: Optional[AlignmentResult]) -> ConfidenceAssessment:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def detect_tracking_loss(self, confidence: ConfidenceAssessment) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def start_search(self, flight_id: str, frame_id: int, estimated_gps: GPSPoint) -> SearchSession:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def expand_search_radius(self, session: SearchSession) -> List[TileCoords]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def try_current_grid(self, session: SearchSession, tiles: Dict[str, np.ndarray]) -> Optional[AlignmentResult]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def mark_found(self, session: SearchSession, result: AlignmentResult) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_search_status(self, session: SearchSession) -> SearchStatus:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def create_user_input_request(self, flight_id: str, frame_id: int, candidate_tiles: List[TileCandidate]) -> UserInputRequest:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def apply_user_anchor(self, flight_id: str, frame_id: int, anchor: UserAnchor) -> bool:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
- Monitor confidence metrics (inlier count, MRE, covariance)
|
||||||
|
- Detect tracking loss and trigger recovery
|
||||||
|
- Coordinate progressive tile search (1→4→9→16→25)
|
||||||
|
- Handle human-in-the-loop when all strategies exhausted
|
||||||
|
- Block flight processing when awaiting user input
|
||||||
|
- Apply user-provided anchors to Factor Graph
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Confidence monitoring
|
||||||
|
- Progressive search coordination
|
||||||
|
- User input request/response handling
|
||||||
|
- Recovery strategy orchestration
|
||||||
|
- Integration point for G04, G06, G08, G09, G10
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `check_confidence(vo_result: RelativePose, litesam_result: Optional[AlignmentResult]) -> ConfidenceAssessment`
|
||||||
|
|
||||||
|
**Description**: Assesses tracking confidence from VO and LiteSAM results.
|
||||||
|
|
||||||
|
**Called By**: Main processing loop (per frame)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
vo_result: RelativePose
|
||||||
|
litesam_result: Optional[AlignmentResult]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
ConfidenceAssessment:
|
||||||
|
overall_confidence: float # 0-1
|
||||||
|
vo_confidence: float
|
||||||
|
litesam_confidence: float
|
||||||
|
inlier_count: int
|
||||||
|
tracking_status: str # "good", "degraded", "lost"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Confidence Metrics**:
|
||||||
|
- VO inlier count and ratio
|
||||||
|
- LiteSAM match confidence
|
||||||
|
- Factor graph marginal covariance
|
||||||
|
- Reprojection error
|
||||||
|
|
||||||
|
**Thresholds**:
|
||||||
|
- **Good**: VO inliers > 50, LiteSAM confidence > 0.7
|
||||||
|
- **Degraded**: VO inliers 20-50
|
||||||
|
- **Lost**: VO inliers < 20
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Good tracking → "good" status
|
||||||
|
2. Low overlap → "degraded"
|
||||||
|
3. Sharp turn → "lost"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `detect_tracking_loss(confidence: ConfidenceAssessment) -> bool`
|
||||||
|
|
||||||
|
**Description**: Determines if tracking is lost.
|
||||||
|
|
||||||
|
**Called By**: Main processing loop
|
||||||
|
|
||||||
|
**Input**: `ConfidenceAssessment`
|
||||||
|
|
||||||
|
**Output**: `bool` - True if tracking lost
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Confidence good → False
|
||||||
|
2. Confidence lost → True
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `start_search(flight_id: str, frame_id: int, estimated_gps: GPSPoint) -> SearchSession`
|
||||||
|
|
||||||
|
**Description**: Initiates progressive search session.
|
||||||
|
|
||||||
|
**Called By**: Main processing loop (when tracking lost)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
frame_id: int
|
||||||
|
estimated_gps: GPSPoint # Dead-reckoning estimate
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
SearchSession:
|
||||||
|
session_id: str
|
||||||
|
flight_id: str
|
||||||
|
frame_id: int
|
||||||
|
center_gps: GPSPoint
|
||||||
|
current_grid_size: int # Starts at 1
|
||||||
|
max_grid_size: int # 25
|
||||||
|
found: bool
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Create search session
|
||||||
|
2. Set center from estimated_gps
|
||||||
|
3. Set current_grid_size = 1
|
||||||
|
4. Return session
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Start search → session created with grid_size=1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `expand_search_radius(session: SearchSession) -> List[TileCoords]`
|
||||||
|
|
||||||
|
**Description**: Expands search grid to next size (1→4→9→16→25).
|
||||||
|
|
||||||
|
**Called By**: Internal (after try_current_grid fails)
|
||||||
|
|
||||||
|
**Input**: `SearchSession`
|
||||||
|
|
||||||
|
**Output**: `List[TileCoords]` - Tiles for next grid size
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Increment current_grid_size (1→4→9→16→25)
|
||||||
|
2. Call G04.expand_search_grid() to get new tiles only
|
||||||
|
3. Return new tile coordinates
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Expand 1→4 → returns 3 new tiles
|
||||||
|
2. Expand 4→9 → returns 5 new tiles
|
||||||
|
3. At grid_size=25 → no more expansion
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `try_current_grid(session: SearchSession, tiles: Dict[str, np.ndarray]) -> Optional[AlignmentResult]`
|
||||||
|
|
||||||
|
**Description**: Tries LiteSAM matching on current tile grid.
|
||||||
|
|
||||||
|
**Called By**: Internal (progressive search loop)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
session: SearchSession
|
||||||
|
tiles: Dict[str, np.ndarray] # From G04
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**: `Optional[AlignmentResult]` - Match result or None
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Get UAV image for frame_id
|
||||||
|
2. For each tile in grid:
|
||||||
|
- Call G09.align_to_satellite(uav_image, tile)
|
||||||
|
- If match found with confidence > threshold:
|
||||||
|
- mark_found(session, result)
|
||||||
|
- Return result
|
||||||
|
3. Return None if no match
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Match on 3rd tile → returns result
|
||||||
|
2. No match in grid → returns None
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `mark_found(session: SearchSession, result: AlignmentResult) -> bool`
|
||||||
|
|
||||||
|
**Description**: Marks search session as successful.
|
||||||
|
|
||||||
|
**Called By**: Internal
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
session: SearchSession
|
||||||
|
result: AlignmentResult
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**: `bool` - True
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Set session.found = True
|
||||||
|
2. Log success (grid_size where found)
|
||||||
|
3. Resume processing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_search_status(session: SearchSession) -> SearchStatus`
|
||||||
|
|
||||||
|
**Description**: Gets current search status.
|
||||||
|
|
||||||
|
**Called By**: G01 REST API (for status endpoint)
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
SearchStatus:
|
||||||
|
current_grid_size: int
|
||||||
|
found: bool
|
||||||
|
exhausted: bool # Reached grid_size=25 without match
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `create_user_input_request(flight_id: str, frame_id: int, candidate_tiles: List[TileCandidate]) -> UserInputRequest`
|
||||||
|
|
||||||
|
**Description**: Creates user input request when all search strategies exhausted.
|
||||||
|
|
||||||
|
**Called By**: Internal (when grid_size=25 and no match)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
frame_id: int
|
||||||
|
candidate_tiles: List[TileCandidate] # Top-5 from G08
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
UserInputRequest:
|
||||||
|
request_id: str
|
||||||
|
flight_id: str
|
||||||
|
frame_id: int
|
||||||
|
uav_image: np.ndarray
|
||||||
|
candidate_tiles: List[TileCandidate]
|
||||||
|
message: str
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Get UAV image for frame_id
|
||||||
|
2. Get top-5 candidates from G08
|
||||||
|
3. Create request
|
||||||
|
4. Send via G14 SSE → "user_input_needed" event
|
||||||
|
5. Update G02 flight_status("BLOCKED")
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. All search failed → creates request
|
||||||
|
2. Request sent to client via SSE
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `apply_user_anchor(flight_id: str, frame_id: int, anchor: UserAnchor) -> bool`
|
||||||
|
|
||||||
|
**Description**: Applies user-provided GPS anchor.
|
||||||
|
|
||||||
|
**Called By**: G01 REST API (user-fix endpoint)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
frame_id: int
|
||||||
|
anchor: UserAnchor:
|
||||||
|
uav_pixel: Tuple[float, float]
|
||||||
|
satellite_gps: GPSPoint
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**: `bool` - True if applied
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Validate anchor data
|
||||||
|
2. Call G10.add_absolute_factor(frame_id, gps, is_user_anchor=True)
|
||||||
|
3. G10.optimize() → refines trajectory
|
||||||
|
4. Update G02 flight_status("PROCESSING")
|
||||||
|
5. Resume processing from next frame
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Valid anchor → applied, processing resumes
|
||||||
|
2. Invalid anchor → rejected
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
### Test 1: Progressive Search Flow
|
||||||
|
1. Tracking lost detected
|
||||||
|
2. start_search() → grid_size=1
|
||||||
|
3. try_current_grid(1 tile) → no match
|
||||||
|
4. expand_search_radius() → grid_size=4
|
||||||
|
5. try_current_grid(4 tiles) → match found
|
||||||
|
6. mark_found() → success
|
||||||
|
|
||||||
|
### Test 2: Full Search Exhaustion
|
||||||
|
1. start_search()
|
||||||
|
2. try grids: 1→4→9→16→25, all fail
|
||||||
|
3. create_user_input_request()
|
||||||
|
4. User provides anchor
|
||||||
|
5. apply_user_anchor() → processing resumes
|
||||||
|
|
||||||
|
### Test 3: Confidence Monitoring
|
||||||
|
1. Normal frames → confidence good
|
||||||
|
2. Low overlap frame → confidence degraded
|
||||||
|
3. Sharp turn → tracking lost, trigger search
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **check_confidence**: < 10ms
|
||||||
|
- **Progressive search (25 tiles)**: < 1.5s total
|
||||||
|
- **User input latency**: < 500ms from creation to SSE event
|
||||||
|
|
||||||
|
### Reliability
|
||||||
|
- Always exhausts all search strategies before requesting user input
|
||||||
|
- Guarantees processing block when awaiting user input
|
||||||
|
- Graceful recovery from all failure modes
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Internal Components
|
||||||
|
- G04 Satellite Data Manager (tile grids)
|
||||||
|
- G06 Image Rotation Manager (rotation sweep)
|
||||||
|
- G08 Global Place Recognition (candidates)
|
||||||
|
- G09 Metric Refinement (LiteSAM)
|
||||||
|
- G10 Factor Graph Optimizer (anchor application)
|
||||||
|
- G02 Flight Manager (status updates)
|
||||||
|
- G14 SSE Event Streamer (user input events)
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
- None
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### ConfidenceAssessment
|
||||||
|
```python
|
||||||
|
class ConfidenceAssessment(BaseModel):
|
||||||
|
overall_confidence: float
|
||||||
|
vo_confidence: float
|
||||||
|
litesam_confidence: float
|
||||||
|
inlier_count: int
|
||||||
|
tracking_status: str
|
||||||
|
```
|
||||||
|
|
||||||
|
### SearchSession
|
||||||
|
```python
|
||||||
|
class SearchSession(BaseModel):
|
||||||
|
session_id: str
|
||||||
|
flight_id: str
|
||||||
|
frame_id: int
|
||||||
|
center_gps: GPSPoint
|
||||||
|
current_grid_size: int
|
||||||
|
max_grid_size: int
|
||||||
|
found: bool
|
||||||
|
exhausted: bool
|
||||||
|
```
|
||||||
|
|
||||||
|
### UserInputRequest
|
||||||
|
```python
|
||||||
|
class UserInputRequest(BaseModel):
|
||||||
|
request_id: str
|
||||||
|
flight_id: str
|
||||||
|
frame_id: int
|
||||||
|
uav_image: np.ndarray
|
||||||
|
candidate_tiles: List[TileCandidate]
|
||||||
|
message: str
|
||||||
|
created_at: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
### UserAnchor
|
||||||
|
```python
|
||||||
|
class UserAnchor(BaseModel):
|
||||||
|
uav_pixel: Tuple[float, float]
|
||||||
|
satellite_gps: GPSPoint
|
||||||
|
confidence: float = 1.0
|
||||||
|
```
|
||||||
|
|
||||||
+333
@@ -0,0 +1,333 @@
|
|||||||
|
# Coordinate Transformer
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `ICoordinateTransformer`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ICoordinateTransformer(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def pixel_to_gps(self, pixel: Tuple[float, float], frame_pose: Pose, camera_params: CameraParameters, altitude: float) -> GPSPoint:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def gps_to_pixel(self, gps: GPSPoint, frame_pose: Pose, camera_params: CameraParameters, altitude: float) -> Tuple[float, float]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def image_object_to_gps(self, object_pixel: Tuple[float, float], frame_id: int) -> GPSPoint:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def compute_gsd(self, altitude: float, focal_length: float, sensor_width: float, image_width: int) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def transform_points(self, points: List[Tuple[float, float]], transformation: np.ndarray) -> List[Tuple[float, float]]:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
- Pixel-to-GPS coordinate conversions
|
||||||
|
- GPS-to-pixel inverse projections
|
||||||
|
- **Critical**: Convert object pixel coordinates (from external detection system) to GPS
|
||||||
|
- Ground Sampling Distance (GSD) calculations
|
||||||
|
- Handle multiple coordinate systems: WGS84, Web Mercator, ENU, image pixels, rotated coordinates
|
||||||
|
- Camera model integration for projection operations
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Coordinate system transformations
|
||||||
|
- Camera projection mathematics
|
||||||
|
- Integration with Factor Graph poses
|
||||||
|
- Object localization (pixel → GPS)
|
||||||
|
- Support for external object detection system
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `pixel_to_gps(pixel: Tuple[float, float], frame_pose: Pose, camera_params: CameraParameters, altitude: float) -> GPSPoint`
|
||||||
|
|
||||||
|
**Description**: Converts pixel coordinates to GPS using camera pose and ground plane assumption.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G13 Result Manager (for frame center GPS)
|
||||||
|
- Internal (for image_object_to_gps)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
pixel: Tuple[float, float] # (x, y) in image coordinates
|
||||||
|
frame_pose: Pose # From Factor Graph (ENU coordinates)
|
||||||
|
camera_params: CameraParameters
|
||||||
|
altitude: float # Ground altitude
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
GPSPoint:
|
||||||
|
lat: float
|
||||||
|
lon: float
|
||||||
|
```
|
||||||
|
|
||||||
|
**Algorithm**:
|
||||||
|
1. Unproject pixel to 3D ray using H01 Camera Model
|
||||||
|
2. Intersect ray with ground plane at altitude
|
||||||
|
3. Transform 3D point from camera frame to ENU using frame_pose
|
||||||
|
4. Convert ENU to WGS84 GPS using H06 Web Mercator Utils
|
||||||
|
|
||||||
|
**Assumptions**:
|
||||||
|
- Ground plane assumption (terrain height negligible)
|
||||||
|
- Downward-pointing camera
|
||||||
|
- Known altitude
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- None (always returns GPS, may be inaccurate if assumptions violated)
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Image center**: Returns frame center GPS
|
||||||
|
2. **Image corner**: Returns GPS at corner
|
||||||
|
3. **Object pixel**: Returns object GPS
|
||||||
|
4. **Altitude variation**: Correct GPS at different altitudes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `gps_to_pixel(gps: GPSPoint, frame_pose: Pose, camera_params: CameraParameters, altitude: float) -> Tuple[float, float]`
|
||||||
|
|
||||||
|
**Description**: Inverse projection from GPS to image pixel coordinates.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Visualization tools (overlay GPS annotations)
|
||||||
|
- Testing/validation
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
gps: GPSPoint
|
||||||
|
frame_pose: Pose
|
||||||
|
camera_params: CameraParameters
|
||||||
|
altitude: float
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
Tuple[float, float]: (x, y) pixel coordinates
|
||||||
|
```
|
||||||
|
|
||||||
|
**Algorithm**:
|
||||||
|
1. Convert GPS to ENU using H06
|
||||||
|
2. Transform ENU point to camera frame using frame_pose
|
||||||
|
3. Project 3D point to image plane using H01 Camera Model
|
||||||
|
4. Return pixel coordinates
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Frame center GPS**: Returns image center pixel
|
||||||
|
2. **Out of view GPS**: Returns pixel outside image bounds
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `image_object_to_gps(object_pixel: Tuple[float, float], frame_id: int) -> GPSPoint`
|
||||||
|
|
||||||
|
**Description**: **Critical method** - Converts object pixel coordinates to GPS. Used by external object detection system.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- External object detection system (provides pixel coordinates)
|
||||||
|
- G13 Result Manager (converts objects to GPS for output)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
object_pixel: Tuple[float, float] # Pixel coordinates from object detector
|
||||||
|
frame_id: int # Frame containing object
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
GPSPoint: GPS coordinates of object center
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Get frame_pose from G10 Factor Graph
|
||||||
|
2. Get camera_params from G16 Configuration Manager
|
||||||
|
3. Get altitude from configuration
|
||||||
|
4. Call pixel_to_gps(object_pixel, frame_pose, camera_params, altitude)
|
||||||
|
5. Return GPS
|
||||||
|
|
||||||
|
**User Story**:
|
||||||
|
- External system detects object in UAV image at pixel (1024, 768)
|
||||||
|
- Calls image_object_to_gps(frame_id=237, object_pixel=(1024, 768))
|
||||||
|
- Returns GPSPoint(lat=48.123, lon=37.456)
|
||||||
|
- Object GPS can be used for navigation, targeting, etc.
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Object at image center**: Returns frame center GPS
|
||||||
|
2. **Object at corner**: Returns GPS with offset
|
||||||
|
3. **Multiple objects**: Each gets correct GPS
|
||||||
|
4. **Refined trajectory**: Object GPS updates after refinement
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `compute_gsd(altitude: float, focal_length: float, sensor_width: float, image_width: int) -> float`
|
||||||
|
|
||||||
|
**Description**: Computes Ground Sampling Distance (meters per pixel).
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Internal (for pixel_to_gps)
|
||||||
|
- G09 Metric Refinement (for scale calculations)
|
||||||
|
- H02 GSD Calculator (may delegate to)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
altitude: float # meters
|
||||||
|
focal_length: float # mm
|
||||||
|
sensor_width: float # mm
|
||||||
|
image_width: int # pixels
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
float: GSD in meters/pixel
|
||||||
|
```
|
||||||
|
|
||||||
|
**Formula**:
|
||||||
|
```
|
||||||
|
GSD = (altitude * sensor_width) / (focal_length * image_width)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
- altitude = 800m
|
||||||
|
- focal_length = 24mm
|
||||||
|
- sensor_width = 36mm
|
||||||
|
- image_width = 6000px
|
||||||
|
- GSD = (800 * 36) / (24 * 6000) = 0.2 m/pixel
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Standard parameters**: Returns reasonable GSD (~0.1-0.3 m/pixel)
|
||||||
|
2. **Higher altitude**: GSD increases
|
||||||
|
3. **Longer focal length**: GSD decreases
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `transform_points(points: List[Tuple[float, float]], transformation: np.ndarray) -> List[Tuple[float, float]]`
|
||||||
|
|
||||||
|
**Description**: Applies homography or affine transformation to list of points.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G06 Image Rotation Manager (for rotation transforms)
|
||||||
|
- G09 Metric Refinement (homography application)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
points: List[Tuple[float, float]]
|
||||||
|
transformation: np.ndarray # 3×3 homography or 2×3 affine
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
List[Tuple[float, float]]: Transformed points
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Convert points to homogeneous coordinates
|
||||||
|
2. Apply transformation matrix
|
||||||
|
3. Normalize and return
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Identity transform**: Points unchanged
|
||||||
|
2. **Rotation**: Points rotated correctly
|
||||||
|
3. **Homography**: Points transformed with perspective
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
### Test 1: Frame Center GPS Calculation
|
||||||
|
1. Get frame_pose from Factor Graph
|
||||||
|
2. pixel_to_gps(image_center) → GPS
|
||||||
|
3. Verify GPS matches expected location
|
||||||
|
4. Verify accuracy < 50m
|
||||||
|
|
||||||
|
### Test 2: Object Localization
|
||||||
|
1. External detector finds object at pixel (1500, 2000)
|
||||||
|
2. image_object_to_gps(frame_id, pixel) → GPS
|
||||||
|
3. Verify GPS correct
|
||||||
|
4. Multiple objects → all get correct GPS
|
||||||
|
|
||||||
|
### Test 3: Round-Trip Conversion
|
||||||
|
1. Start with GPS point
|
||||||
|
2. gps_to_pixel() → pixel
|
||||||
|
3. pixel_to_gps() → GPS
|
||||||
|
4. Verify GPS matches original (within tolerance)
|
||||||
|
|
||||||
|
### Test 4: GSD Calculation
|
||||||
|
1. compute_gsd() with known parameters
|
||||||
|
2. Verify matches expected value
|
||||||
|
3. Test at different altitudes
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **pixel_to_gps**: < 5ms
|
||||||
|
- **gps_to_pixel**: < 5ms
|
||||||
|
- **image_object_to_gps**: < 10ms
|
||||||
|
- **compute_gsd**: < 1ms
|
||||||
|
|
||||||
|
### Accuracy
|
||||||
|
- **GPS accuracy**: Inherits from Factor Graph accuracy (~20m)
|
||||||
|
- **GSD calculation**: ±1% precision
|
||||||
|
- **Projection accuracy**: < 1 pixel error
|
||||||
|
|
||||||
|
### Reliability
|
||||||
|
- Handle edge cases (points outside image)
|
||||||
|
- Graceful handling of degenerate configurations
|
||||||
|
- Numerical stability
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Internal Components
|
||||||
|
- **G10 Factor Graph Optimizer**: For frame poses
|
||||||
|
- **G16 Configuration Manager**: For camera parameters
|
||||||
|
- **H01 Camera Model**: For projection operations
|
||||||
|
- **H02 GSD Calculator**: For GSD calculations
|
||||||
|
- **H06 Web Mercator Utils**: For coordinate conversions
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
- **numpy**: Matrix operations
|
||||||
|
- **opencv-python**: Homography operations (optional)
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### Pose (from Factor Graph)
|
||||||
|
```python
|
||||||
|
class Pose(BaseModel):
|
||||||
|
position: np.ndarray # (3,) - [x, y, z] in ENU
|
||||||
|
orientation: np.ndarray # (4,) quaternion or (3,3) rotation
|
||||||
|
timestamp: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
### CameraParameters
|
||||||
|
```python
|
||||||
|
class CameraParameters(BaseModel):
|
||||||
|
focal_length: float # mm
|
||||||
|
sensor_width: float # mm
|
||||||
|
sensor_height: float # mm
|
||||||
|
resolution_width: int # pixels
|
||||||
|
resolution_height: int # pixels
|
||||||
|
principal_point: Tuple[float, float] # (cx, cy)
|
||||||
|
distortion_coefficients: Optional[List[float]]
|
||||||
|
```
|
||||||
|
|
||||||
|
### GPSPoint
|
||||||
|
```python
|
||||||
|
class GPSPoint(BaseModel):
|
||||||
|
lat: float
|
||||||
|
lon: float
|
||||||
|
```
|
||||||
|
|
||||||
|
### CoordinateFrame
|
||||||
|
```python
|
||||||
|
class CoordinateFrame(Enum):
|
||||||
|
WGS84 = "wgs84" # GPS coordinates
|
||||||
|
ENU = "enu" # East-North-Up local frame
|
||||||
|
ECEF = "ecef" # Earth-Centered Earth-Fixed
|
||||||
|
IMAGE = "image" # Image pixel coordinates
|
||||||
|
CAMERA = "camera" # Camera frame
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
# Result Manager
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `IResultManager`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IResultManager(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def update_frame_result(self, flight_id: str, frame_id: int, result: FrameResult) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def publish_to_route_api(self, flight_id: str, frame_id: int) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_flight_results(self, flight_id: str) -> FlightResults:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def mark_refined(self, flight_id: str, frame_ids: List[int]) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_changed_frames(self, flight_id: str, since: datetime) -> List[int]:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
- Manage trajectory results per flight
|
||||||
|
- Track frame refinements and changes
|
||||||
|
- Trigger per-frame Route API updates via G03
|
||||||
|
- Send incremental updates via G14 SSE
|
||||||
|
- Maintain result versioning for audit trail
|
||||||
|
- Convert optimized poses to GPS coordinates
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Result state management
|
||||||
|
- Route API integration
|
||||||
|
- SSE event triggering
|
||||||
|
- Incremental update detection
|
||||||
|
- Result persistence
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `update_frame_result(flight_id: str, frame_id: int, result: FrameResult) -> bool`
|
||||||
|
|
||||||
|
**Description**: Updates result for a processed frame.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Main processing loop (after each frame)
|
||||||
|
- G10 Factor Graph (after refinement)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
frame_id: int
|
||||||
|
result: FrameResult:
|
||||||
|
gps_center: GPSPoint
|
||||||
|
altitude: float
|
||||||
|
heading: float
|
||||||
|
confidence: float
|
||||||
|
timestamp: datetime
|
||||||
|
refined: bool
|
||||||
|
objects: List[ObjectLocation] # From external detector
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**: `bool` - True if updated
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Store result in memory/database
|
||||||
|
2. Call publish_to_route_api()
|
||||||
|
3. Call G14.send_frame_result()
|
||||||
|
4. Update flight statistics
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. New frame result → stored and published
|
||||||
|
2. Refined result → updates existing, marks refined=True
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `publish_to_route_api(flight_id: str, frame_id: int) -> bool`
|
||||||
|
|
||||||
|
**Description**: Sends frame GPS to Route API via G03 client.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Internal (after update_frame_result)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
frame_id: int
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**: `bool` - True if published successfully
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Get result for frame_id
|
||||||
|
2. Convert to Waypoint format
|
||||||
|
3. Call G03.update_route_waypoint()
|
||||||
|
4. Handle errors (retry if transient)
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Successful publish → Route API updated
|
||||||
|
2. Route API unavailable → logs error, continues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_flight_results(flight_id: str) -> FlightResults`
|
||||||
|
|
||||||
|
**Description**: Retrieves all results for a flight.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G01 REST API (results endpoint)
|
||||||
|
- Testing/validation
|
||||||
|
|
||||||
|
**Input**: `flight_id: str`
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
FlightResults:
|
||||||
|
flight_id: str
|
||||||
|
frames: List[FrameResult]
|
||||||
|
statistics: FlightStatistics
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Get all results → returns complete trajectory
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `mark_refined(flight_id: str, frame_ids: List[int]) -> bool`
|
||||||
|
|
||||||
|
**Description**: Marks frames as refined after batch optimization.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G10 Factor Graph (after asynchronous refinement)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
frame_ids: List[int] # Frames with updated poses
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**: `bool`
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. For each frame_id:
|
||||||
|
- Get refined pose from G10
|
||||||
|
- Convert to GPS via G12
|
||||||
|
- Update result with refined=True
|
||||||
|
- publish_to_route_api()
|
||||||
|
- Call G14.send_refinement()
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Batch refinement → all frames updated and published
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_changed_frames(flight_id: str, since: datetime) -> List[int]`
|
||||||
|
|
||||||
|
**Description**: Gets frames changed since timestamp (for incremental updates).
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G14 SSE Event Streamer (for reconnection replay)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
flight_id: str
|
||||||
|
since: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**: `List[int]` - Frame IDs changed since timestamp
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Get changes → returns only modified frames
|
||||||
|
2. No changes → returns empty list
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
### Test 1: Per-Frame Processing
|
||||||
|
1. Process frame 237
|
||||||
|
2. update_frame_result() → stores result
|
||||||
|
3. Verify publish_to_route_api() called
|
||||||
|
4. Verify G14 SSE event sent
|
||||||
|
|
||||||
|
### Test 2: Batch Refinement
|
||||||
|
1. Process 100 frames
|
||||||
|
2. Factor graph refines frames 10-50
|
||||||
|
3. mark_refined([10-50]) → updates all
|
||||||
|
4. Verify Route API updated
|
||||||
|
5. Verify SSE refinement events sent
|
||||||
|
|
||||||
|
### Test 3: Incremental Updates
|
||||||
|
1. Process frames 1-100
|
||||||
|
2. Client disconnects at frame 50
|
||||||
|
3. Client reconnects
|
||||||
|
4. get_changed_frames(since=frame_50_time)
|
||||||
|
5. Client receives frames 51-100
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **update_frame_result**: < 50ms
|
||||||
|
- **publish_to_route_api**: < 100ms (non-blocking)
|
||||||
|
- **get_flight_results**: < 200ms for 2000 frames
|
||||||
|
|
||||||
|
### Reliability
|
||||||
|
- Result persistence survives crashes
|
||||||
|
- Guaranteed at-least-once delivery to Route API
|
||||||
|
- Idempotent updates
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Internal Components
|
||||||
|
- G03 Route API Client
|
||||||
|
- G10 Factor Graph Optimizer
|
||||||
|
- G12 Coordinate Transformer
|
||||||
|
- G14 SSE Event Streamer
|
||||||
|
- G17 Database Layer
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
- None
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### FrameResult
|
||||||
|
```python
|
||||||
|
class ObjectLocation(BaseModel):
|
||||||
|
object_id: str
|
||||||
|
pixel: Tuple[float, float]
|
||||||
|
gps: GPSPoint
|
||||||
|
class_name: str
|
||||||
|
confidence: float
|
||||||
|
|
||||||
|
class FrameResult(BaseModel):
|
||||||
|
frame_id: int
|
||||||
|
gps_center: GPSPoint
|
||||||
|
altitude: float
|
||||||
|
heading: float
|
||||||
|
confidence: float
|
||||||
|
timestamp: datetime
|
||||||
|
refined: bool
|
||||||
|
objects: List[ObjectLocation]
|
||||||
|
updated_at: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
### FlightResults
|
||||||
|
```python
|
||||||
|
class FlightStatistics(BaseModel):
|
||||||
|
total_frames: int
|
||||||
|
processed_frames: int
|
||||||
|
refined_frames: int
|
||||||
|
mean_confidence: float
|
||||||
|
processing_time: float
|
||||||
|
|
||||||
|
class FlightResults(BaseModel):
|
||||||
|
flight_id: str
|
||||||
|
frames: List[FrameResult]
|
||||||
|
statistics: FlightStatistics
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
# SSE Event Streamer
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `ISSEEventStreamer`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ISSEEventStreamer(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def create_stream(self, flight_id: str, client_id: str) -> StreamConnection:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def send_frame_result(self, flight_id: str, frame_result: FrameResult) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def send_search_progress(self, flight_id: str, search_status: SearchStatus) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def send_user_input_request(self, flight_id: str, request: UserInputRequest) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def send_refinement(self, flight_id: str, frame_id: int, updated_result: FrameResult) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def close_stream(self, flight_id: str, client_id: str) -> bool:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
- Server-Sent Events broadcaster for real-time results
|
||||||
|
- Stream per-frame processing results to clients
|
||||||
|
- Send refinement updates asynchronously
|
||||||
|
- Request user input when processing blocked
|
||||||
|
- Handle client connections and reconnections
|
||||||
|
- Event replay from last received event
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- SSE protocol implementation
|
||||||
|
- Event formatting and sending
|
||||||
|
- Connection management
|
||||||
|
- Client reconnection handling
|
||||||
|
- Multiple concurrent streams per flight
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `create_stream(flight_id: str, client_id: str) -> StreamConnection`
|
||||||
|
|
||||||
|
**Description**: Creates SSE connection for a client.
|
||||||
|
|
||||||
|
**Called By**: G01 REST API (GET /stream endpoint)
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
StreamConnection:
|
||||||
|
stream_id: str
|
||||||
|
flight_id: str
|
||||||
|
client_id: str
|
||||||
|
last_event_id: Optional[str]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Event Types**:
|
||||||
|
- `frame_processed`
|
||||||
|
- `frame_refined`
|
||||||
|
- `search_expanded`
|
||||||
|
- `user_input_needed`
|
||||||
|
- `processing_blocked`
|
||||||
|
- `route_api_updated`
|
||||||
|
- `route_completed`
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Create stream → client receives keepalive pings
|
||||||
|
2. Multiple clients → each gets own stream
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `send_frame_result(flight_id: str, frame_result: FrameResult) -> bool`
|
||||||
|
|
||||||
|
**Description**: Sends frame_processed event.
|
||||||
|
|
||||||
|
**Called By**: G13 Result Manager
|
||||||
|
|
||||||
|
**Event Format**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "frame_processed",
|
||||||
|
"id": "frame_237",
|
||||||
|
"data": {
|
||||||
|
"frame_id": 237,
|
||||||
|
"gps": {"lat": 48.123, "lon": 37.456},
|
||||||
|
"altitude": 800.0,
|
||||||
|
"confidence": 0.95,
|
||||||
|
"heading": 87.3,
|
||||||
|
"timestamp": "2025-11-24T10:30:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Send event → all clients receive
|
||||||
|
2. Client disconnected → event buffered for replay
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `send_search_progress(flight_id: str, search_status: SearchStatus) -> bool`
|
||||||
|
|
||||||
|
**Description**: Sends search_expanded event.
|
||||||
|
|
||||||
|
**Called By**: G11 Failure Recovery Coordinator
|
||||||
|
|
||||||
|
**Event Format**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "search_expanded",
|
||||||
|
"data": {
|
||||||
|
"frame_id": 237,
|
||||||
|
"grid_size": 9,
|
||||||
|
"status": "searching"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `send_user_input_request(flight_id: str, request: UserInputRequest) -> bool`
|
||||||
|
|
||||||
|
**Description**: Sends user_input_needed event.
|
||||||
|
|
||||||
|
**Called By**: G11 Failure Recovery Coordinator
|
||||||
|
|
||||||
|
**Event Format**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "user_input_needed",
|
||||||
|
"data": {
|
||||||
|
"request_id": "uuid",
|
||||||
|
"frame_id": 237,
|
||||||
|
"candidate_tiles": [...]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `send_refinement(flight_id: str, frame_id: int, updated_result: FrameResult) -> bool`
|
||||||
|
|
||||||
|
**Description**: Sends frame_refined event.
|
||||||
|
|
||||||
|
**Called By**: G13 Result Manager
|
||||||
|
|
||||||
|
**Event Format**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "frame_refined",
|
||||||
|
"data": {
|
||||||
|
"frame_id": 237,
|
||||||
|
"gps": {"lat": 48.1235, "lon": 37.4562},
|
||||||
|
"refined": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `close_stream(flight_id: str, client_id: str) -> bool`
|
||||||
|
|
||||||
|
**Description**: Closes SSE connection.
|
||||||
|
|
||||||
|
**Called By**: G01 REST API (on client disconnect)
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
### Test 1: Real-Time Streaming
|
||||||
|
1. Client connects
|
||||||
|
2. Process 100 frames
|
||||||
|
3. Client receives 100 frame_processed events
|
||||||
|
4. Verify order and completeness
|
||||||
|
|
||||||
|
### Test 2: Reconnection with Replay
|
||||||
|
1. Client connects
|
||||||
|
2. Process 50 frames
|
||||||
|
3. Client disconnects
|
||||||
|
4. Process 50 more frames
|
||||||
|
5. Client reconnects with last_event_id
|
||||||
|
6. Client receives frames 51-100
|
||||||
|
|
||||||
|
### Test 3: User Input Flow
|
||||||
|
1. Processing blocks
|
||||||
|
2. send_user_input_request()
|
||||||
|
3. Client receives event
|
||||||
|
4. Client responds with fix
|
||||||
|
5. Processing resumes
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **Event latency**: < 500ms from generation to client
|
||||||
|
- **Throughput**: 100 events/second
|
||||||
|
- **Concurrent connections**: 1000+ clients
|
||||||
|
|
||||||
|
### Reliability
|
||||||
|
- Event buffering for disconnected clients
|
||||||
|
- Automatic reconnection support
|
||||||
|
- Keepalive pings every 30 seconds
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Internal Components
|
||||||
|
- None (receives calls from other components)
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
- **FastAPI** or **Flask** SSE support
|
||||||
|
- **asyncio**: For async event streaming
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### StreamConnection
|
||||||
|
```python
|
||||||
|
class StreamConnection(BaseModel):
|
||||||
|
stream_id: str
|
||||||
|
flight_id: str
|
||||||
|
client_id: str
|
||||||
|
created_at: datetime
|
||||||
|
last_event_id: Optional[str]
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSEEvent
|
||||||
|
```python
|
||||||
|
class SSEEvent(BaseModel):
|
||||||
|
event: str
|
||||||
|
id: Optional[str]
|
||||||
|
data: Dict[str, Any]
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
# Model Manager
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `IModelManager`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IModelManager(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def load_model(self, model_name: str, model_format: str) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_inference_engine(self, model_name: str) -> InferenceEngine:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def optimize_to_tensorrt(self, model_name: str, onnx_path: str) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def fallback_to_onnx(self, model_name: str) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def warmup_model(self, model_name: str) -> bool:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
- Load ML models (TensorRT primary, ONNX fallback)
|
||||||
|
- Manage model lifecycle (loading, unloading, warmup)
|
||||||
|
- Provide inference engines for:
|
||||||
|
- SuperPoint (feature extraction)
|
||||||
|
- LightGlue (feature matching)
|
||||||
|
- DINOv2 (global descriptors)
|
||||||
|
- LiteSAM (cross-view matching)
|
||||||
|
- Handle TensorRT optimization and ONNX fallback
|
||||||
|
- Ensure <5s processing requirement through acceleration
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Model loading and caching
|
||||||
|
- TensorRT optimization
|
||||||
|
- ONNX fallback handling
|
||||||
|
- Inference engine abstraction
|
||||||
|
- GPU memory management
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `load_model(model_name: str, model_format: str) -> bool`
|
||||||
|
|
||||||
|
**Description**: Loads model in specified format.
|
||||||
|
|
||||||
|
**Called By**: G02 Flight Manager (during initialization)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
model_name: str # "SuperPoint", "LightGlue", "DINOv2", "LiteSAM"
|
||||||
|
model_format: str # "tensorrt", "onnx", "pytorch"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**: `bool` - True if loaded
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Check if model already loaded
|
||||||
|
2. Load model file
|
||||||
|
3. Initialize inference engine
|
||||||
|
4. Warm up model
|
||||||
|
5. Cache for reuse
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Load TensorRT model → succeeds
|
||||||
|
2. TensorRT unavailable → fallback to ONNX
|
||||||
|
3. Load all 4 models → all succeed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_inference_engine(model_name: str) -> InferenceEngine`
|
||||||
|
|
||||||
|
**Description**: Gets inference engine for a model.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G07 Sequential VO (SuperPoint, LightGlue)
|
||||||
|
- G08 Global Place Recognition (DINOv2)
|
||||||
|
- G09 Metric Refinement (LiteSAM)
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
InferenceEngine:
|
||||||
|
model_name: str
|
||||||
|
format: str
|
||||||
|
infer(input: np.ndarray) -> np.ndarray
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Get SuperPoint engine → returns engine
|
||||||
|
2. Call infer() → returns features
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `optimize_to_tensorrt(model_name: str, onnx_path: str) -> str`
|
||||||
|
|
||||||
|
**Description**: Converts ONNX model to TensorRT for acceleration.
|
||||||
|
|
||||||
|
**Called By**: System initialization (one-time)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
model_name: str
|
||||||
|
onnx_path: str # Path to ONNX model
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**: `str` - Path to TensorRT engine
|
||||||
|
|
||||||
|
**Processing Details**:
|
||||||
|
- FP16 precision (2-3x speedup)
|
||||||
|
- Graph fusion and kernel optimization
|
||||||
|
- One-time conversion, cached for reuse
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Convert ONNX to TensorRT → engine created
|
||||||
|
2. Load TensorRT engine → inference faster than ONNX
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `fallback_to_onnx(model_name: str) -> bool`
|
||||||
|
|
||||||
|
**Description**: Falls back to ONNX if TensorRT fails.
|
||||||
|
|
||||||
|
**Called By**: Internal (during load_model)
|
||||||
|
|
||||||
|
**Processing Flow**:
|
||||||
|
1. Detect TensorRT failure
|
||||||
|
2. Load ONNX model
|
||||||
|
3. Log warning
|
||||||
|
4. Continue with ONNX
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. TensorRT fails → ONNX loaded automatically
|
||||||
|
2. System continues functioning
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `warmup_model(model_name: str) -> bool`
|
||||||
|
|
||||||
|
**Description**: Warms up model with dummy input.
|
||||||
|
|
||||||
|
**Called By**: Internal (after load_model)
|
||||||
|
|
||||||
|
**Purpose**: Initialize CUDA kernels, allocate GPU memory
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Warmup → first real inference fast
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
### Test 1: Model Loading
|
||||||
|
1. load_model("SuperPoint", "tensorrt")
|
||||||
|
2. load_model("LightGlue", "tensorrt")
|
||||||
|
3. load_model("DINOv2", "tensorrt")
|
||||||
|
4. load_model("LiteSAM", "tensorrt")
|
||||||
|
5. Verify all loaded
|
||||||
|
|
||||||
|
### Test 2: Inference Performance
|
||||||
|
1. Get inference engine
|
||||||
|
2. Run inference 100 times
|
||||||
|
3. Measure average latency
|
||||||
|
4. Verify meets performance targets
|
||||||
|
|
||||||
|
### Test 3: Fallback Scenario
|
||||||
|
1. Simulate TensorRT failure
|
||||||
|
2. Verify fallback to ONNX
|
||||||
|
3. Verify inference still works
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **SuperPoint**: ~15ms (TensorRT), ~50ms (ONNX)
|
||||||
|
- **LightGlue**: ~50ms (TensorRT), ~150ms (ONNX)
|
||||||
|
- **DINOv2**: ~150ms (TensorRT), ~500ms (ONNX)
|
||||||
|
- **LiteSAM**: ~60ms (TensorRT), ~200ms (ONNX)
|
||||||
|
|
||||||
|
### Memory
|
||||||
|
- GPU memory: ~4GB for all 4 models
|
||||||
|
|
||||||
|
### Reliability
|
||||||
|
- Graceful fallback to ONNX
|
||||||
|
- Automatic retry on transient errors
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
- **TensorRT**: NVIDIA inference optimization
|
||||||
|
- **ONNX Runtime**: ONNX inference
|
||||||
|
- **PyTorch**: Model weights (optional)
|
||||||
|
- **CUDA**: GPU acceleration
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### InferenceEngine
|
||||||
|
```python
|
||||||
|
class InferenceEngine(ABC):
|
||||||
|
model_name: str
|
||||||
|
format: str
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def infer(self, input: np.ndarray) -> np.ndarray:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### ModelConfig
|
||||||
|
```python
|
||||||
|
class ModelConfig(BaseModel):
|
||||||
|
model_name: str
|
||||||
|
model_path: str
|
||||||
|
format: str
|
||||||
|
precision: str # "fp16", "fp32"
|
||||||
|
warmup_iterations: int = 3
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
# Configuration Manager
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `IConfigurationManager`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IConfigurationManager(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def load_config(self, config_path: str) -> SystemConfig:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_camera_params(self, camera_id: Optional[str] = None) -> CameraParameters:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def validate_config(self, config: SystemConfig) -> ValidationResult:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_flight_config(self, flight_id: str) -> FlightConfig:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def update_config(self, section: str, key: str, value: Any) -> bool:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
- Load system configuration from files/environment
|
||||||
|
- Provide camera parameters (focal length, sensor size, resolution)
|
||||||
|
- Manage flight-specific configurations
|
||||||
|
- Validate configuration integrity
|
||||||
|
- Support configuration updates at runtime
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- System-wide configuration
|
||||||
|
- Camera parameter management
|
||||||
|
- Operational area bounds
|
||||||
|
- Model paths and settings
|
||||||
|
- Database connections
|
||||||
|
- API endpoints
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `load_config(config_path: str) -> SystemConfig`
|
||||||
|
|
||||||
|
**Description**: Loads system configuration.
|
||||||
|
|
||||||
|
**Called By**: System startup
|
||||||
|
|
||||||
|
**Input**: `config_path: str` - Path to config file (YAML/JSON)
|
||||||
|
|
||||||
|
**Output**: `SystemConfig` - Complete configuration
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Load valid config → succeeds
|
||||||
|
2. Missing file → uses defaults
|
||||||
|
3. Invalid config → raises error
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_camera_params(camera_id: Optional[str] = None) -> CameraParameters`
|
||||||
|
|
||||||
|
**Description**: Gets camera parameters.
|
||||||
|
|
||||||
|
**Called By**: All processing components
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
CameraParameters:
|
||||||
|
focal_length: float
|
||||||
|
sensor_width: float
|
||||||
|
sensor_height: float
|
||||||
|
resolution_width: int
|
||||||
|
resolution_height: int
|
||||||
|
principal_point: Tuple[float, float]
|
||||||
|
distortion_coefficients: List[float]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Get default camera → returns params
|
||||||
|
2. Get specific camera → returns params
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `validate_config(config: SystemConfig) -> ValidationResult`
|
||||||
|
|
||||||
|
**Description**: Validates configuration.
|
||||||
|
|
||||||
|
**Called By**: After load_config
|
||||||
|
|
||||||
|
**Validation Rules**:
|
||||||
|
- Camera parameters sensible
|
||||||
|
- Paths exist
|
||||||
|
- Operational area valid
|
||||||
|
- Database connection string valid
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Valid config → passes
|
||||||
|
2. Invalid focal length → fails
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_flight_config(flight_id: str) -> FlightConfig`
|
||||||
|
|
||||||
|
**Description**: Gets flight-specific configuration.
|
||||||
|
|
||||||
|
**Called By**: G02 Flight Manager
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
FlightConfig:
|
||||||
|
camera_params: CameraParameters
|
||||||
|
altitude: float
|
||||||
|
operational_area: OperationalArea
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Get flight config → returns params
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `update_config(section: str, key: str, value: Any) -> bool`
|
||||||
|
|
||||||
|
**Description**: Updates config at runtime.
|
||||||
|
|
||||||
|
**Called By**: Admin tools
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Update value → succeeds
|
||||||
|
2. Invalid key → fails
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### SystemConfig
|
||||||
|
```python
|
||||||
|
class SystemConfig(BaseModel):
|
||||||
|
camera: CameraParameters
|
||||||
|
operational_area: OperationalArea
|
||||||
|
models: ModelPaths
|
||||||
|
database: DatabaseConfig
|
||||||
|
api: APIConfig
|
||||||
|
```
|
||||||
|
|
||||||
|
### CameraParameters
|
||||||
|
```python
|
||||||
|
class CameraParameters(BaseModel):
|
||||||
|
focal_length: float # mm
|
||||||
|
sensor_width: float # mm
|
||||||
|
sensor_height: float # mm
|
||||||
|
resolution_width: int
|
||||||
|
resolution_height: int
|
||||||
|
principal_point: Tuple[float, float]
|
||||||
|
distortion_coefficients: List[float]
|
||||||
|
```
|
||||||
|
|
||||||
|
### OperationalArea
|
||||||
|
```python
|
||||||
|
class OperationalArea(BaseModel):
|
||||||
|
name: str = "Eastern Ukraine"
|
||||||
|
min_lat: float = 45.0
|
||||||
|
max_lat: float = 52.0
|
||||||
|
min_lon: float = 22.0
|
||||||
|
max_lon: float = 40.0
|
||||||
|
```
|
||||||
|
|
||||||
+193
@@ -0,0 +1,193 @@
|
|||||||
|
# GPS-Denied Database Layer
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `IGPSDeniedDatabase`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IGPSDeniedDatabase(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def save_flight_state(self, flight_state: FlightState) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def load_flight_state(self, flight_id: str) -> Optional[FlightState]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def query_processing_history(self, filters: Dict[str, Any]) -> List[FlightState]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def save_frame_result(self, flight_id: str, frame_result: FrameResult) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_frame_results(self, flight_id: str) -> List[FrameResult]:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
- Database access for GPS-Denied processing state
|
||||||
|
- Separate schema from Route API database
|
||||||
|
- Persist flight state, frame results
|
||||||
|
- Query processing history
|
||||||
|
- Support crash recovery
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Flight state persistence
|
||||||
|
- Frame result storage
|
||||||
|
- Processing history queries
|
||||||
|
- Connection management
|
||||||
|
- Transaction handling
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `save_flight_state(flight_state: FlightState) -> bool`
|
||||||
|
|
||||||
|
**Description**: Saves flight processing state.
|
||||||
|
|
||||||
|
**Called By**: G02 Flight Manager
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
FlightState:
|
||||||
|
flight_id: str
|
||||||
|
route_id: str
|
||||||
|
status: str
|
||||||
|
frames_processed: int
|
||||||
|
frames_total: int
|
||||||
|
current_heading: float
|
||||||
|
blocked: bool
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**: `bool` - True if saved
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Save state → persisted
|
||||||
|
2. Update state → overwrites
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `load_flight_state(flight_id: str) -> Optional[FlightState]`
|
||||||
|
|
||||||
|
**Description**: Loads flight state.
|
||||||
|
|
||||||
|
**Called By**: G02 Flight Manager (crash recovery)
|
||||||
|
|
||||||
|
**Output**: `Optional[FlightState]`
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Load existing → returns state
|
||||||
|
2. Load non-existent → returns None
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `query_processing_history(filters: Dict[str, Any]) -> List[FlightState]`
|
||||||
|
|
||||||
|
**Description**: Queries historical processing data.
|
||||||
|
|
||||||
|
**Called By**: Analytics, admin tools
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Query by date range → returns flights
|
||||||
|
2. Query by status → returns filtered
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `save_frame_result(flight_id: str, frame_result: FrameResult) -> bool`
|
||||||
|
|
||||||
|
**Description**: Saves frame processing result.
|
||||||
|
|
||||||
|
**Called By**: G13 Result Manager
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Save result → persisted
|
||||||
|
2. Update result (refinement) → overwrites
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_frame_results(flight_id: str) -> List[FrameResult]`
|
||||||
|
|
||||||
|
**Description**: Gets all frame results for flight.
|
||||||
|
|
||||||
|
**Called By**: G13 Result Manager
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. Get results → returns all frames
|
||||||
|
2. No results → returns empty list
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Flights table
|
||||||
|
CREATE TABLE gps_denied_flights (
|
||||||
|
flight_id VARCHAR(36) PRIMARY KEY,
|
||||||
|
route_id VARCHAR(36) NOT NULL,
|
||||||
|
status VARCHAR(50),
|
||||||
|
frames_processed INT,
|
||||||
|
frames_total INT,
|
||||||
|
current_heading FLOAT,
|
||||||
|
blocked BOOLEAN,
|
||||||
|
created_at TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Frame results table
|
||||||
|
CREATE TABLE frame_results (
|
||||||
|
id VARCHAR(36) PRIMARY KEY,
|
||||||
|
flight_id VARCHAR(36) NOT NULL,
|
||||||
|
frame_id INT NOT NULL,
|
||||||
|
gps_lat DECIMAL(10, 7),
|
||||||
|
gps_lon DECIMAL(11, 7),
|
||||||
|
altitude FLOAT,
|
||||||
|
heading FLOAT,
|
||||||
|
confidence FLOAT,
|
||||||
|
refined BOOLEAN,
|
||||||
|
timestamp TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
FOREIGN KEY (flight_id) REFERENCES gps_denied_flights(flight_id) ON DELETE CASCADE,
|
||||||
|
UNIQUE KEY (flight_id, frame_id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
- **PostgreSQL** or **MySQL**
|
||||||
|
- **SQLAlchemy** or **psycopg2**
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### FlightState
|
||||||
|
```python
|
||||||
|
class FlightState(BaseModel):
|
||||||
|
flight_id: str
|
||||||
|
route_id: str
|
||||||
|
status: str
|
||||||
|
frames_processed: int
|
||||||
|
frames_total: int
|
||||||
|
current_heading: Optional[float]
|
||||||
|
blocked: bool
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
### FrameResult
|
||||||
|
```python
|
||||||
|
class FrameResult(BaseModel):
|
||||||
|
frame_id: int
|
||||||
|
gps_center: GPSPoint
|
||||||
|
altitude: float
|
||||||
|
heading: float
|
||||||
|
confidence: float
|
||||||
|
refined: bool
|
||||||
|
timestamp: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# Camera Model Helper
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `ICameraModel`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ICameraModel(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def project(self, point_3d: np.ndarray, camera_params: CameraParameters) -> Tuple[float, float]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def unproject(self, pixel: Tuple[float, float], depth: float, camera_params: CameraParameters) -> np.ndarray:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_focal_length(self, camera_params: CameraParameters) -> Tuple[float, float]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def apply_distortion(self, pixel: Tuple[float, float], distortion_coeffs: List[float]) -> Tuple[float, float]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def remove_distortion(self, pixel: Tuple[float, float], distortion_coeffs: List[float]) -> Tuple[float, float]:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
Pinhole camera projection model with Brown-Conrady distortion handling.
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `project(point_3d: np.ndarray, camera_params: CameraParameters) -> Tuple[float, float]`
|
||||||
|
|
||||||
|
**Description**: Projects 3D point to 2D image pixel.
|
||||||
|
|
||||||
|
**Formula**:
|
||||||
|
```
|
||||||
|
x = fx * X/Z + cx
|
||||||
|
y = fy * Y/Z + cy
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `unproject(pixel: Tuple[float, float], depth: float, camera_params: CameraParameters) -> np.ndarray`
|
||||||
|
|
||||||
|
**Description**: Unprojects pixel to 3D ray at given depth.
|
||||||
|
|
||||||
|
**Formula**:
|
||||||
|
```
|
||||||
|
X = (x - cx) * depth / fx
|
||||||
|
Y = (y - cy) * depth / fy
|
||||||
|
Z = depth
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_focal_length(camera_params: CameraParameters) -> Tuple[float, float]`
|
||||||
|
|
||||||
|
**Description**: Returns (fx, fy) in pixels.
|
||||||
|
|
||||||
|
**Formula**:
|
||||||
|
```
|
||||||
|
fx = focal_length_mm * image_width / sensor_width_mm
|
||||||
|
fy = focal_length_mm * image_height / sensor_height_mm
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `apply_distortion(pixel: Tuple[float, float], distortion_coeffs: List[float]) -> Tuple[float, float]`
|
||||||
|
|
||||||
|
**Description**: Applies radial and tangential distortion (Brown-Conrady model).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `remove_distortion(pixel: Tuple[float, float], distortion_coeffs: List[float]) -> Tuple[float, float]`
|
||||||
|
|
||||||
|
**Description**: Removes distortion from observed pixel.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
**External**: opencv-python, numpy
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
```python
|
||||||
|
class CameraParameters(BaseModel):
|
||||||
|
focal_length: float # mm
|
||||||
|
sensor_width: float # mm
|
||||||
|
sensor_height: float # mm
|
||||||
|
resolution_width: int
|
||||||
|
resolution_height: int
|
||||||
|
principal_point: Tuple[float, float] # (cx, cy) pixels
|
||||||
|
distortion_coefficients: List[float] # [k1, k2, p1, p2, k3]
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# GSD Calculator Helper
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `IGSDCalculator`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IGSDCalculator(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def compute_gsd(self, altitude: float, camera_params: CameraParameters) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def altitude_to_scale(self, altitude: float, focal_length: float) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def meters_per_pixel(self, lat: float, zoom: int) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def gsd_from_camera(self, altitude: float, focal_length: float, sensor_width: float, image_width: int) -> float:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
Ground Sampling Distance computations for altitude and coordinate systems.
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `compute_gsd(altitude: float, camera_params: CameraParameters) -> float`
|
||||||
|
|
||||||
|
**Description**: Computes GSD from altitude and camera parameters.
|
||||||
|
|
||||||
|
**Formula**:
|
||||||
|
```
|
||||||
|
GSD = (altitude * sensor_width) / (focal_length * image_width)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example**: altitude=800m, focal=24mm, sensor=36mm, width=6000px → GSD=0.2 m/pixel
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `altitude_to_scale(altitude: float, focal_length: float) -> float`
|
||||||
|
|
||||||
|
**Description**: Converts altitude to scale factor for VO.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `meters_per_pixel(lat: float, zoom: int) -> float`
|
||||||
|
|
||||||
|
**Description**: Computes GSD for Web Mercator tiles at zoom level.
|
||||||
|
|
||||||
|
**Formula**:
|
||||||
|
```
|
||||||
|
meters_per_pixel = 156543.03392 * cos(lat * π/180) / 2^zoom
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example**: lat=48°N, zoom=19 → ~0.3 m/pixel
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `gsd_from_camera(altitude: float, focal_length: float, sensor_width: float, image_width: int) -> float`
|
||||||
|
|
||||||
|
**Description**: Direct GSD calculation from parameters.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
**External**: numpy
|
||||||
|
|
||||||
|
## Test Cases
|
||||||
|
|
||||||
|
1. Standard camera at 800m → GSD ~0.1-0.3 m/pixel
|
||||||
|
2. Web Mercator zoom 19 at Ukraine → ~0.3 m/pixel
|
||||||
|
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Robust Kernels Helper
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `IRobustKernels`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IRobustKernels(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def huber_loss(self, error: float, threshold: float) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def cauchy_loss(self, error: float, k: float) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def compute_weight(self, error: float, kernel_type: str, params: Dict[str, float]) -> float:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
Huber/Cauchy loss functions for outlier rejection in optimization.
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `huber_loss(error: float, threshold: float) -> float`
|
||||||
|
|
||||||
|
**Description**: Huber robust loss function.
|
||||||
|
|
||||||
|
**Formula**:
|
||||||
|
```
|
||||||
|
if |error| <= threshold:
|
||||||
|
loss = 0.5 * error^2
|
||||||
|
else:
|
||||||
|
loss = threshold * (|error| - 0.5 * threshold)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Purpose**: Quadratic for small errors, linear for large errors (outliers).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `cauchy_loss(error: float, k: float) -> float`
|
||||||
|
|
||||||
|
**Description**: Cauchy robust loss function.
|
||||||
|
|
||||||
|
**Formula**:
|
||||||
|
```
|
||||||
|
loss = (k^2 / 2) * log(1 + (error/k)^2)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Purpose**: More aggressive outlier rejection than Huber.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `compute_weight(error: float, kernel_type: str, params: Dict[str, float]) -> float`
|
||||||
|
|
||||||
|
**Description**: Computes robust weight for error.
|
||||||
|
|
||||||
|
**Usage**: Factor Graph applies weights to downweight outliers.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
**External**: numpy
|
||||||
|
|
||||||
|
## Test Cases
|
||||||
|
|
||||||
|
1. Small error → weight ≈ 1.0
|
||||||
|
2. Large error (350m outlier) → weight ≈ 0.1 (downweighted)
|
||||||
|
3. Huber vs Cauchy → Cauchy more aggressive
|
||||||
|
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# Faiss Index Manager Helper
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `IFaissIndexManager`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IFaissIndexManager(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def build_index(self, descriptors: np.ndarray, index_type: str) -> FaissIndex:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def add_descriptors(self, index: FaissIndex, descriptors: np.ndarray) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def search(self, index: FaissIndex, query: np.ndarray, k: int) -> Tuple[np.ndarray, np.ndarray]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def save_index(self, index: FaissIndex, path: str) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def load_index(self, path: str) -> FaissIndex:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
Manages Faiss indices for AnyLoc retrieval (IVF, HNSW options).
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `build_index(descriptors: np.ndarray, index_type: str) -> FaissIndex`
|
||||||
|
|
||||||
|
**Description**: Builds Faiss index from descriptors.
|
||||||
|
|
||||||
|
**Index Types**:
|
||||||
|
- **"IVF"**: Inverted File (fast for large databases)
|
||||||
|
- **"HNSW"**: Hierarchical Navigable Small World (best accuracy/speed trade-off)
|
||||||
|
- **"Flat"**: Brute force (exact, slow for large datasets)
|
||||||
|
|
||||||
|
**Input**: (N, D) descriptors array
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `add_descriptors(index: FaissIndex, descriptors: np.ndarray) -> bool`
|
||||||
|
|
||||||
|
**Description**: Adds more descriptors to existing index.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `search(index: FaissIndex, query: np.ndarray, k: int) -> Tuple[np.ndarray, np.ndarray]`
|
||||||
|
|
||||||
|
**Description**: Searches for k nearest neighbors.
|
||||||
|
|
||||||
|
**Output**: (distances, indices) - shape (k,)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `save_index(index: FaissIndex, path: str) -> bool`
|
||||||
|
|
||||||
|
**Description**: Saves index to disk for fast startup.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `load_index(path: str) -> FaissIndex`
|
||||||
|
|
||||||
|
**Description**: Loads pre-built index from disk.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
**External**: faiss-gpu or faiss-cpu
|
||||||
|
|
||||||
|
## Test Cases
|
||||||
|
|
||||||
|
1. Build index with 10,000 descriptors → succeeds
|
||||||
|
2. Search query → returns top-k matches
|
||||||
|
3. Save/load index → index restored correctly
|
||||||
|
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# Performance Monitor Helper
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `IPerformanceMonitor`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IPerformanceMonitor(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def start_timer(self, operation: str) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def end_timer(self, timer_id: str) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_statistics(self, operation: str) -> PerformanceStats:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def check_sla(self, operation: str, threshold: float) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_bottlenecks(self) -> List[Tuple[str, float]]:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
Tracks processing times, ensures <5s constraint per frame.
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `start_timer(operation: str) -> str`
|
||||||
|
|
||||||
|
**Description**: Starts timing an operation.
|
||||||
|
|
||||||
|
**Returns**: timer_id (UUID)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `end_timer(timer_id: str) -> float`
|
||||||
|
|
||||||
|
**Description**: Ends timer and records duration.
|
||||||
|
|
||||||
|
**Returns**: Duration in seconds
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_statistics(operation: str) -> PerformanceStats`
|
||||||
|
|
||||||
|
**Description**: Gets statistics for an operation.
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
PerformanceStats:
|
||||||
|
operation: str
|
||||||
|
count: int
|
||||||
|
mean: float
|
||||||
|
p50: float
|
||||||
|
p95: float
|
||||||
|
p99: float
|
||||||
|
max: float
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `check_sla(operation: str, threshold: float) -> bool`
|
||||||
|
|
||||||
|
**Description**: Checks if operation meets SLA threshold.
|
||||||
|
|
||||||
|
**Example**: check_sla("frame_processing", 5.0) → True if < 5s
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_bottlenecks() -> List[Tuple[str, float]]`
|
||||||
|
|
||||||
|
**Description**: Returns slowest operations.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
**External**: time, statistics
|
||||||
|
|
||||||
|
## Test Cases
|
||||||
|
|
||||||
|
1. Start/end timer → records duration
|
||||||
|
2. Get statistics → returns percentiles
|
||||||
|
3. Check SLA → returns True if meeting targets
|
||||||
|
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# Web Mercator Utils Helper
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `IWebMercatorUtils`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IWebMercatorUtils(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def latlon_to_tile(self, lat: float, lon: float, zoom: int) -> Tuple[int, int]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def tile_to_latlon(self, x: int, y: int, zoom: int) -> Tuple[float, float]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def compute_tile_bounds(self, x: int, y: int, zoom: int) -> TileBounds:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_zoom_gsd(self, lat: float, zoom: int) -> float:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
Web Mercator projection (EPSG:3857) for tile coordinates. Used for Google Maps tiles.
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `latlon_to_tile(lat: float, lon: float, zoom: int) -> Tuple[int, int]`
|
||||||
|
|
||||||
|
**Description**: Converts GPS to tile coordinates.
|
||||||
|
|
||||||
|
**Formula**:
|
||||||
|
```
|
||||||
|
n = 2^zoom
|
||||||
|
x = floor((lon + 180) / 360 * n)
|
||||||
|
lat_rad = lat * π / 180
|
||||||
|
y = floor((1 - log(tan(lat_rad) + sec(lat_rad)) / π) / 2 * n)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns**: (x, y) tile coordinates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `tile_to_latlon(x: int, y: int, zoom: int) -> Tuple[float, float]`
|
||||||
|
|
||||||
|
**Description**: Converts tile coordinates to GPS (NW corner).
|
||||||
|
|
||||||
|
**Formula** (inverse of above)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `compute_tile_bounds(x: int, y: int, zoom: int) -> TileBounds`
|
||||||
|
|
||||||
|
**Description**: Computes GPS bounding box of tile.
|
||||||
|
|
||||||
|
**Returns**:
|
||||||
|
```python
|
||||||
|
TileBounds:
|
||||||
|
nw: GPSPoint # North-West
|
||||||
|
ne: GPSPoint # North-East
|
||||||
|
sw: GPSPoint # South-West
|
||||||
|
se: GPSPoint # South-East
|
||||||
|
center: GPSPoint
|
||||||
|
gsd: float
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_zoom_gsd(lat: float, zoom: int) -> float`
|
||||||
|
|
||||||
|
**Description**: Gets GSD for zoom level at latitude.
|
||||||
|
|
||||||
|
**Formula**:
|
||||||
|
```
|
||||||
|
gsd = 156543.03392 * cos(lat * π/180) / 2^zoom
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
**External**: numpy
|
||||||
|
|
||||||
|
## Test Cases
|
||||||
|
|
||||||
|
1. GPS to tile at zoom 19 → valid tile coords
|
||||||
|
2. Tile to GPS → inverse correct
|
||||||
|
3. Compute bounds → 4 corners valid
|
||||||
|
4. GSD at zoom 19, Ukraine → ~0.3 m/pixel
|
||||||
|
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# Image Rotation Utils Helper
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `IImageRotationUtils`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IImageRotationUtils(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def rotate_image(self, image: np.ndarray, angle: float, center: Optional[Tuple[int, int]] = None) -> np.ndarray:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def calculate_rotation_from_points(self, src_points: np.ndarray, dst_points: np.ndarray) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def normalize_angle(self, angle: float) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def compute_rotation_matrix(self, angle: float, center: Tuple[int, int]) -> np.ndarray:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
Image rotation operations, angle calculations from point shifts.
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `rotate_image(image: np.ndarray, angle: float, center: Optional[Tuple[int, int]] = None) -> np.ndarray`
|
||||||
|
|
||||||
|
**Description**: Rotates image around center.
|
||||||
|
|
||||||
|
**Implementation**: Uses cv2.getRotationMatrix2D + cv2.warpAffine
|
||||||
|
|
||||||
|
**Parameters**:
|
||||||
|
- **angle**: Degrees (0-360)
|
||||||
|
- **center**: Rotation center (default: image center)
|
||||||
|
|
||||||
|
**Returns**: Rotated image (same dimensions)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `calculate_rotation_from_points(src_points: np.ndarray, dst_points: np.ndarray) -> float`
|
||||||
|
|
||||||
|
**Description**: Calculates rotation angle from point correspondences.
|
||||||
|
|
||||||
|
**Input**: (N, 2) arrays of matching points
|
||||||
|
|
||||||
|
**Algorithm**:
|
||||||
|
1. Compute centroids
|
||||||
|
2. Calculate angle from centroid shifts
|
||||||
|
3. Return angle in degrees
|
||||||
|
|
||||||
|
**Use Case**: Extract precise angle from LiteSAM homography
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `normalize_angle(angle: float) -> float`
|
||||||
|
|
||||||
|
**Description**: Normalizes angle to 0-360 range.
|
||||||
|
|
||||||
|
**Formula**:
|
||||||
|
```
|
||||||
|
angle = angle % 360
|
||||||
|
if angle < 0:
|
||||||
|
angle += 360
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `compute_rotation_matrix(angle: float, center: Tuple[int, int]) -> np.ndarray`
|
||||||
|
|
||||||
|
**Description**: Computes 2D rotation matrix.
|
||||||
|
|
||||||
|
**Returns**: 2×3 affine transformation matrix
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
**External**: opencv-python, numpy
|
||||||
|
|
||||||
|
## Test Cases
|
||||||
|
|
||||||
|
1. Rotate 90° → image rotated correctly
|
||||||
|
2. Calculate angle from points → accurate angle
|
||||||
|
3. Normalize 370° → 10°
|
||||||
|
4. Rotation matrix → correct transformation
|
||||||
|
|
||||||
@@ -0,0 +1,329 @@
|
|||||||
|
# Batch Validator Helper
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `IBatchValidator`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IBatchValidator(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def validate_batch_size(self, batch: ImageBatch) -> ValidationResult:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def check_sequence_continuity(self, batch: ImageBatch, expected_start: int) -> ValidationResult:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def validate_naming_convention(self, filenames: List[str]) -> ValidationResult:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def validate_format(self, image_data: bytes) -> ValidationResult:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
- Validate image batch integrity
|
||||||
|
- Check sequence continuity and naming conventions
|
||||||
|
- Validate image format and dimensions
|
||||||
|
- Ensure batch size constraints (10-50 images)
|
||||||
|
- Support strict sequential ordering (ADxxxxxx.jpg)
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Batch validation for G05 Image Input Pipeline
|
||||||
|
- Image format validation
|
||||||
|
- Filename pattern matching
|
||||||
|
- Sequence gap detection
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `validate_batch_size(batch: ImageBatch) -> ValidationResult`
|
||||||
|
|
||||||
|
**Description**: Validates batch contains 10-50 images.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G05 Image Input Pipeline (before queuing)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
batch: ImageBatch:
|
||||||
|
images: List[bytes]
|
||||||
|
filenames: List[str]
|
||||||
|
start_sequence: int
|
||||||
|
end_sequence: int
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
ValidationResult:
|
||||||
|
valid: bool
|
||||||
|
errors: List[str]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validation Rules**:
|
||||||
|
- **Minimum batch size**: 10 images
|
||||||
|
- **Maximum batch size**: 50 images
|
||||||
|
- **Reason**: Balance between upload overhead and processing granularity
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `valid=False` with error message (not an exception)
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Valid batch (20 images)**: Returns `valid=True`
|
||||||
|
2. **Too few images (5)**: Returns `valid=False`, error="Batch size 5 below minimum 10"
|
||||||
|
3. **Too many images (60)**: Returns `valid=False`, error="Batch size 60 exceeds maximum 50"
|
||||||
|
4. **Empty batch**: Returns `valid=False`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `check_sequence_continuity(batch: ImageBatch, expected_start: int) -> ValidationResult`
|
||||||
|
|
||||||
|
**Description**: Validates images form consecutive sequence with no gaps.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G05 Image Input Pipeline (before queuing)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
batch: ImageBatch
|
||||||
|
expected_start: int # Expected starting sequence number
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
ValidationResult:
|
||||||
|
valid: bool
|
||||||
|
errors: List[str]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validation Rules**:
|
||||||
|
1. **Sequence starts at expected_start**: First image sequence == expected_start
|
||||||
|
2. **Consecutive numbers**: No gaps in sequence (AD000101, AD000102, AD000103, ...)
|
||||||
|
3. **Filename extraction**: Parse sequence from ADxxxxxx.jpg pattern
|
||||||
|
4. **Strict ordering**: Images must be in sequential order
|
||||||
|
|
||||||
|
**Algorithm**:
|
||||||
|
```python
|
||||||
|
sequences = [extract_sequence(filename) for filename in batch.filenames]
|
||||||
|
if sequences[0] != expected_start:
|
||||||
|
return invalid("Expected start {expected_start}, got {sequences[0]}")
|
||||||
|
for i in range(len(sequences) - 1):
|
||||||
|
if sequences[i+1] != sequences[i] + 1:
|
||||||
|
return invalid(f"Gap detected: {sequences[i]} -> {sequences[i+1]}")
|
||||||
|
return valid()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `valid=False` with specific gap information
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Valid sequence (101-150)**: expected_start=101 → valid=True
|
||||||
|
2. **Wrong start**: expected_start=101, got 102 → valid=False
|
||||||
|
3. **Gap in sequence**: AD000101, AD000103 (missing 102) → valid=False
|
||||||
|
4. **Out of order**: AD000102, AD000101 → valid=False
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `validate_naming_convention(filenames: List[str]) -> ValidationResult`
|
||||||
|
|
||||||
|
**Description**: Validates filenames match ADxxxxxx.jpg pattern.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Internal (during check_sequence_continuity)
|
||||||
|
- G05 Image Input Pipeline
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
filenames: List[str]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
ValidationResult:
|
||||||
|
valid: bool
|
||||||
|
errors: List[str]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validation Rules**:
|
||||||
|
1. **Pattern**: `AD\d{6}\.(jpg|JPG|png|PNG)`
|
||||||
|
2. **Examples**: AD000001.jpg, AD000237.JPG, AD002000.png
|
||||||
|
3. **Case insensitive**: Accepts .jpg, .JPG, .Jpg
|
||||||
|
4. **6 digits required**: Zero-padded to 6 digits
|
||||||
|
|
||||||
|
**Regex Pattern**: `^AD\d{6}\.(jpg|JPG|png|PNG)$`
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `valid=False` listing invalid filenames
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Valid names**: ["AD000001.jpg", "AD000002.jpg"] → valid=True
|
||||||
|
2. **Invalid prefix**: "IMG_0001.jpg" → valid=False
|
||||||
|
3. **Wrong digit count**: "AD001.jpg" (3 digits) → valid=False
|
||||||
|
4. **Missing extension**: "AD000001" → valid=False
|
||||||
|
5. **Invalid extension**: "AD000001.bmp" → valid=False
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `validate_format(image_data: bytes) -> ValidationResult`
|
||||||
|
|
||||||
|
**Description**: Validates image file format and properties.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G05 Image Input Pipeline (per-image validation)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
image_data: bytes # Raw image file bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
ValidationResult:
|
||||||
|
valid: bool
|
||||||
|
errors: List[str]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validation Rules**:
|
||||||
|
1. **Format**: Valid JPEG or PNG
|
||||||
|
2. **Dimensions**: 640×480 to 6252×4168 pixels
|
||||||
|
3. **File size**: < 10MB per image
|
||||||
|
4. **Image readable**: Not corrupted
|
||||||
|
5. **Color channels**: RGB (3 channels)
|
||||||
|
|
||||||
|
**Algorithm**:
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
image = PIL.Image.open(BytesIO(image_data))
|
||||||
|
width, height = image.size
|
||||||
|
|
||||||
|
if image.format not in ['JPEG', 'PNG']:
|
||||||
|
return invalid("Format must be JPEG or PNG")
|
||||||
|
|
||||||
|
if width < 640 or height < 480:
|
||||||
|
return invalid("Dimensions too small")
|
||||||
|
|
||||||
|
if width > 6252 or height > 4168:
|
||||||
|
return invalid("Dimensions too large")
|
||||||
|
|
||||||
|
if len(image_data) > 10 * 1024 * 1024:
|
||||||
|
return invalid("File size exceeds 10MB")
|
||||||
|
|
||||||
|
return valid()
|
||||||
|
except Exception as e:
|
||||||
|
return invalid(f"Corrupted image: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `valid=False` with specific error
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Valid JPEG (2048×1536)**: valid=True
|
||||||
|
2. **Valid PNG (6252×4168)**: valid=True
|
||||||
|
3. **Too small (320×240)**: valid=False
|
||||||
|
4. **Too large (8000×6000)**: valid=False
|
||||||
|
5. **File too big (15MB)**: valid=False
|
||||||
|
6. **Corrupted file**: valid=False
|
||||||
|
7. **BMP format**: valid=False
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
### Test 1: Complete Batch Validation
|
||||||
|
1. Create batch with 20 images, AD000101.jpg - AD000120.jpg
|
||||||
|
2. validate_batch_size() → valid
|
||||||
|
3. validate_naming_convention() → valid
|
||||||
|
4. check_sequence_continuity(expected_start=101) → valid
|
||||||
|
5. validate_format() for each image → all valid
|
||||||
|
|
||||||
|
### Test 2: Invalid Batch Detection
|
||||||
|
1. Create batch with 60 images → validate_batch_size() fails
|
||||||
|
2. Create batch with gap (AD000101, AD000103) → check_sequence_continuity() fails
|
||||||
|
3. Create batch with IMG_0001.jpg → validate_naming_convention() fails
|
||||||
|
4. Create batch with corrupted image → validate_format() fails
|
||||||
|
|
||||||
|
### Test 3: Edge Cases
|
||||||
|
1. Batch with exactly 10 images → valid
|
||||||
|
2. Batch with exactly 50 images → valid
|
||||||
|
3. Batch with 51 images → invalid
|
||||||
|
4. Batch starting at AD999995.jpg (near max) → valid
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **validate_batch_size**: < 1ms
|
||||||
|
- **check_sequence_continuity**: < 10ms for 50 images
|
||||||
|
- **validate_naming_convention**: < 5ms for 50 filenames
|
||||||
|
- **validate_format**: < 20ms per image (with PIL)
|
||||||
|
- **Total batch validation**: < 100ms for 50 images
|
||||||
|
|
||||||
|
### Reliability
|
||||||
|
- Never raises exceptions (returns ValidationResult with errors)
|
||||||
|
- Handles edge cases gracefully
|
||||||
|
- Clear, actionable error messages
|
||||||
|
|
||||||
|
### Maintainability
|
||||||
|
- Configurable validation rules (min/max batch size, dimensions)
|
||||||
|
- Easy to add new validation rules
|
||||||
|
- Comprehensive error reporting
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Internal Components
|
||||||
|
- None (pure utility, no internal dependencies)
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
- **Pillow (PIL)**: Image format validation and dimension checking
|
||||||
|
- **re** (regex): Filename pattern matching
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### ImageBatch
|
||||||
|
```python
|
||||||
|
class ImageBatch(BaseModel):
|
||||||
|
images: List[bytes] # Raw image data
|
||||||
|
filenames: List[str] # e.g., ["AD000101.jpg", ...]
|
||||||
|
start_sequence: int # 101
|
||||||
|
end_sequence: int # 150
|
||||||
|
batch_number: int # Sequential batch number
|
||||||
|
```
|
||||||
|
|
||||||
|
### ValidationResult
|
||||||
|
```python
|
||||||
|
class ValidationResult(BaseModel):
|
||||||
|
valid: bool
|
||||||
|
errors: List[str] = [] # Empty if valid
|
||||||
|
warnings: List[str] = [] # Optional warnings
|
||||||
|
```
|
||||||
|
|
||||||
|
### ValidationRules (Configuration)
|
||||||
|
```python
|
||||||
|
class ValidationRules(BaseModel):
|
||||||
|
min_batch_size: int = 10
|
||||||
|
max_batch_size: int = 50
|
||||||
|
min_width: int = 640
|
||||||
|
min_height: int = 480
|
||||||
|
max_width: int = 6252
|
||||||
|
max_height: int = 4168
|
||||||
|
max_file_size_mb: int = 10
|
||||||
|
allowed_formats: List[str] = ["JPEG", "PNG"]
|
||||||
|
filename_pattern: str = r"^AD\d{6}\.(jpg|JPG|png|PNG)$"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sequence Extraction
|
||||||
|
```python
|
||||||
|
def extract_sequence(filename: str) -> int:
|
||||||
|
"""
|
||||||
|
Extracts sequence number from filename.
|
||||||
|
|
||||||
|
Example: "AD000237.jpg" -> 237
|
||||||
|
"""
|
||||||
|
match = re.match(r"AD(\d{6})\.", filename)
|
||||||
|
if match:
|
||||||
|
return int(match.group(1))
|
||||||
|
raise ValueError(f"Invalid filename format: {filename}")
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
# Route REST API
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `IRouteRestAPI`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IRouteRestAPI(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def create_route(self, route_data: RouteCreateRequest) -> RouteResponse:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_route(self, route_id: str) -> RouteResponse:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def update_waypoints(self, route_id: str, waypoints: List[Waypoint]) -> UpdateResponse:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def delete_route(self, route_id: str) -> DeleteResponse:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
- Expose REST API endpoints for route lifecycle management
|
||||||
|
- Handle HTTP request validation and routing
|
||||||
|
- Coordinate with Route Data Manager for persistence operations
|
||||||
|
- Validate incoming requests through Waypoint Validator
|
||||||
|
- Return appropriate HTTP responses with proper status codes
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- CRUD operations for routes
|
||||||
|
- Waypoint management within routes
|
||||||
|
- Geofence management
|
||||||
|
- Route metadata retrieval
|
||||||
|
- Used by both GPS-Denied system and Mission Planner
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `create_route(route_data: RouteCreateRequest) -> RouteResponse`
|
||||||
|
|
||||||
|
**Description**: Creates a new route with initial waypoints and geofences.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Client applications (GPS-Denied UI, Mission Planner UI)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
RouteCreateRequest:
|
||||||
|
id: Optional[str] # UUID, generated if not provided
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
points: List[GPSPoint] # Initial rough waypoints
|
||||||
|
geofences: Geofences
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
RouteResponse:
|
||||||
|
route_id: str
|
||||||
|
created: bool
|
||||||
|
timestamp: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `400 Bad Request`: Invalid input data (missing required fields, invalid GPS coordinates)
|
||||||
|
- `409 Conflict`: Route with same ID already exists
|
||||||
|
- `500 Internal Server Error`: Database or internal error
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Valid route creation**: Provide valid route data → returns 201 with routeId
|
||||||
|
2. **Missing required field**: Omit name → returns 400 with error message
|
||||||
|
3. **Invalid GPS coordinates**: Provide lat > 90 → returns 400
|
||||||
|
4. **Duplicate route ID**: Create route with existing ID → returns 409
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_route(route_id: str) -> RouteResponse`
|
||||||
|
|
||||||
|
**Description**: Retrieves complete route information including all waypoints and geofences.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Client applications
|
||||||
|
- G03 Route API Client (from GPS-Denied system)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
route_id: str # UUID
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
RouteResponse:
|
||||||
|
route_id: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
points: List[Waypoint] # All waypoints with metadata
|
||||||
|
geofences: Geofences
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `404 Not Found`: Route ID does not exist
|
||||||
|
- `500 Internal Server Error`: Database error
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Existing route**: Valid routeId → returns 200 with complete route data
|
||||||
|
2. **Non-existent route**: Invalid routeId → returns 404
|
||||||
|
3. **Route with many waypoints**: Route with 2000+ waypoints → returns 200 with all data
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `update_waypoints(route_id: str, waypoint_id: str, waypoint_data: Waypoint) -> UpdateResponse`
|
||||||
|
|
||||||
|
**Description**: Updates a specific waypoint within a route. Used for per-frame GPS refinement from GPS-Denied system.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- G03 Route API Client (per-frame updates)
|
||||||
|
- Client applications (manual corrections)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
route_id: str
|
||||||
|
waypoint_id: str # Frame sequence number or waypoint ID
|
||||||
|
waypoint_data: Waypoint:
|
||||||
|
lat: float
|
||||||
|
lon: float
|
||||||
|
altitude: Optional[float]
|
||||||
|
confidence: float
|
||||||
|
timestamp: datetime
|
||||||
|
refined: bool # True if updated by GPS-Denied refinement
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
UpdateResponse:
|
||||||
|
updated: bool
|
||||||
|
waypoint_id: str
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `404 Not Found`: Route or waypoint not found
|
||||||
|
- `400 Bad Request`: Invalid waypoint data
|
||||||
|
- `500 Internal Server Error`: Database error
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Update existing waypoint**: Valid data → returns 200
|
||||||
|
2. **Refinement update**: GPS-Denied sends refined coordinates → updates successfully
|
||||||
|
3. **Invalid coordinates**: lat > 90 → returns 400
|
||||||
|
4. **Non-existent waypoint**: Invalid waypoint_id → returns 404
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `delete_route(route_id: str) -> DeleteResponse`
|
||||||
|
|
||||||
|
**Description**: Deletes a route and all associated waypoints.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- Client applications
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
route_id: str
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
DeleteResponse:
|
||||||
|
deleted: bool
|
||||||
|
route_id: str
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `404 Not Found`: Route does not exist
|
||||||
|
- `500 Internal Server Error`: Database error
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Delete existing route**: Valid routeId → returns 200
|
||||||
|
2. **Delete non-existent route**: Invalid routeId → returns 404
|
||||||
|
3. **Delete route with active flight**: Route linked to processing flight → returns 200 (cascade handling in DB)
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
### Test 1: Route Creation and Retrieval Flow
|
||||||
|
1. POST `/routes` with valid data
|
||||||
|
2. Verify 201 response with routeId
|
||||||
|
3. GET `/routes/{routeId}`
|
||||||
|
4. Verify returned data matches created data
|
||||||
|
|
||||||
|
### Test 2: GPS-Denied Integration Flow
|
||||||
|
1. Create route via POST
|
||||||
|
2. Simulate GPS-Denied per-frame updates via PUT `/routes/{routeId}/waypoints/{waypointId}` × 100
|
||||||
|
3. GET route and verify all waypoints updated
|
||||||
|
4. Verify `refined: true` flag set
|
||||||
|
|
||||||
|
### Test 3: Concurrent Waypoint Updates
|
||||||
|
1. Create route
|
||||||
|
2. Send 50 concurrent PUT requests to different waypoints
|
||||||
|
3. Verify all updates succeed
|
||||||
|
4. Verify data consistency
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **Create route**: < 500ms response time
|
||||||
|
- **Get route**: < 200ms for routes with < 2000 waypoints
|
||||||
|
- **Update waypoint**: < 100ms (critical for GPS-Denied real-time updates)
|
||||||
|
- **Delete route**: < 300ms
|
||||||
|
- **Throughput**: Handle 100 concurrent waypoint updates per second
|
||||||
|
|
||||||
|
### Scalability
|
||||||
|
- Support 1000+ concurrent route processing sessions
|
||||||
|
- Handle routes with up to 3000 waypoints
|
||||||
|
|
||||||
|
### Availability
|
||||||
|
- 99.9% uptime SLA
|
||||||
|
- Graceful degradation under load
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Input validation on all endpoints
|
||||||
|
- SQL injection prevention
|
||||||
|
- Rate limiting: 1000 requests/minute per client
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Internal Components
|
||||||
|
- **R02 Route Data Manager**: For all data persistence operations
|
||||||
|
- **R03 Waypoint Validator**: For input validation
|
||||||
|
- **R04 Route Database Layer**: Indirectly through Data Manager
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
- **FastAPI/Flask**: Web framework
|
||||||
|
- **Pydantic**: Request/response validation
|
||||||
|
- **Uvicorn**: ASGI server
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### RouteCreateRequest
|
||||||
|
```python
|
||||||
|
class GPSPoint(BaseModel):
|
||||||
|
lat: float # Latitude -90 to 90
|
||||||
|
lon: float # Longitude -180 to 180
|
||||||
|
|
||||||
|
class Polygon(BaseModel):
|
||||||
|
north_west: GPSPoint
|
||||||
|
south_east: GPSPoint
|
||||||
|
|
||||||
|
class Geofences(BaseModel):
|
||||||
|
polygons: List[Polygon]
|
||||||
|
|
||||||
|
class RouteCreateRequest(BaseModel):
|
||||||
|
id: Optional[str] = None # UUID
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
points: List[GPSPoint] # Initial rough waypoints
|
||||||
|
geofences: Geofences
|
||||||
|
```
|
||||||
|
|
||||||
|
### Waypoint
|
||||||
|
```python
|
||||||
|
class Waypoint(BaseModel):
|
||||||
|
id: str # Sequence number or UUID
|
||||||
|
lat: float
|
||||||
|
lon: float
|
||||||
|
altitude: Optional[float] = None
|
||||||
|
confidence: float # 0.0 to 1.0
|
||||||
|
timestamp: datetime
|
||||||
|
refined: bool = False
|
||||||
|
```
|
||||||
|
|
||||||
|
### RouteResponse
|
||||||
|
```python
|
||||||
|
class RouteResponse(BaseModel):
|
||||||
|
route_id: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
points: List[Waypoint]
|
||||||
|
geofences: Geofences
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
# Route Data Manager
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `IRouteDataManager`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IRouteDataManager(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def save_route(self, route: Route) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def load_route(self, route_id: str) -> Optional[Route]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def update_waypoint(self, route_id: str, waypoint_id: str, waypoint: Waypoint) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def delete_waypoint(self, route_id: str, waypoint_id: str) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_route_metadata(self, route_id: str) -> Optional[RouteMetadata]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def delete_route(self, route_id: str) -> bool:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
- Manage route persistence and retrieval
|
||||||
|
- Coordinate with Route Database Layer for data operations
|
||||||
|
- Handle waypoint CRUD operations within routes
|
||||||
|
- Manage route metadata (timestamps, statistics)
|
||||||
|
- Ensure data consistency and transaction management
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Business logic layer between REST API and Database Layer
|
||||||
|
- Route lifecycle management
|
||||||
|
- Waypoint batch operations
|
||||||
|
- Query optimization for large route datasets
|
||||||
|
- Caching layer for frequently accessed routes (optional)
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `save_route(route: Route) -> str`
|
||||||
|
|
||||||
|
**Description**: Persists a new route with initial waypoints and geofences.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- R01 Route REST API
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
Route:
|
||||||
|
id: Optional[str] # Generated if not provided
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
points: List[Waypoint]
|
||||||
|
geofences: Geofences
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
route_id: str # UUID of saved route
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `DuplicateRouteError`: Route with same ID exists
|
||||||
|
- `ValidationError`: Invalid route data
|
||||||
|
- `DatabaseError`: Database connection or constraint violation
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **New route**: Valid route → returns routeId, verifies in DB
|
||||||
|
2. **Route with 1000 waypoints**: Large route → saves successfully
|
||||||
|
3. **Duplicate ID**: Existing route ID → raises DuplicateRouteError
|
||||||
|
4. **Transaction rollback**: DB error mid-save → no partial data persisted
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `load_route(route_id: str) -> Optional[Route]`
|
||||||
|
|
||||||
|
**Description**: Retrieves complete route data including all waypoints.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- R01 Route REST API
|
||||||
|
- R03 Waypoint Validator (for context validation)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
route_id: str
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
Route or None if not found
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `DatabaseError`: Database connection error
|
||||||
|
- Returns `None`: Route not found (not an error condition)
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Existing route**: Valid ID → returns complete Route object
|
||||||
|
2. **Non-existent route**: Invalid ID → returns None
|
||||||
|
3. **Large route**: 3000 waypoints → returns all data efficiently
|
||||||
|
4. **Concurrent reads**: Multiple simultaneous loads → all succeed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `update_waypoint(route_id: str, waypoint_id: str, waypoint: Waypoint) -> bool`
|
||||||
|
|
||||||
|
**Description**: Updates a single waypoint within a route. Optimized for high-frequency GPS-Denied updates.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- R01 Route REST API
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
route_id: str
|
||||||
|
waypoint_id: str
|
||||||
|
waypoint: Waypoint
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
bool: True if updated, False if route/waypoint not found
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `ValidationError`: Invalid waypoint data
|
||||||
|
- `DatabaseError`: Database error
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Update existing waypoint**: Valid data → returns True
|
||||||
|
2. **Non-existent waypoint**: Invalid waypoint_id → returns False
|
||||||
|
3. **Concurrent updates**: 100 simultaneous updates to different waypoints → all succeed
|
||||||
|
4. **Update timestamp**: Automatically updates route.updated_at
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `delete_waypoint(route_id: str, waypoint_id: str) -> bool`
|
||||||
|
|
||||||
|
**Description**: Deletes a specific waypoint from a route.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- R01 Route REST API (rare, for manual corrections)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
route_id: str
|
||||||
|
waypoint_id: str
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
bool: True if deleted, False if not found
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `DatabaseError`: Database error
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Delete existing waypoint**: Valid IDs → returns True
|
||||||
|
2. **Delete non-existent waypoint**: Invalid ID → returns False
|
||||||
|
3. **Delete all waypoints**: Delete all waypoints one by one → succeeds
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_route_metadata(route_id: str) -> Optional[RouteMetadata]`
|
||||||
|
|
||||||
|
**Description**: Retrieves route metadata without loading all waypoints (lightweight operation).
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- R01 Route REST API
|
||||||
|
- Client applications (route listing)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
route_id: str
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
RouteMetadata:
|
||||||
|
route_id: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
waypoint_count: int
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `None`: Route not found
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Get metadata**: Valid ID → returns metadata without waypoints
|
||||||
|
2. **Performance**: Metadata retrieval < 50ms even for 3000-waypoint route
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `delete_route(route_id: str) -> bool`
|
||||||
|
|
||||||
|
**Description**: Deletes a route and all associated waypoints.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- R01 Route REST API
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
route_id: str
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
bool: True if deleted, False if not found
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `DatabaseError`: Database error
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Delete route**: Valid ID → deletes route and all waypoints
|
||||||
|
2. **Cascade delete**: Verify all waypoints deleted
|
||||||
|
3. **Non-existent route**: Invalid ID → returns False
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
### Test 1: Complete Route Lifecycle
|
||||||
|
1. save_route() with 100 waypoints
|
||||||
|
2. load_route() and verify all data
|
||||||
|
3. update_waypoint() for 50 waypoints
|
||||||
|
4. delete_waypoint() for 10 waypoints
|
||||||
|
5. get_route_metadata() and verify count
|
||||||
|
6. delete_route() and verify removal
|
||||||
|
|
||||||
|
### Test 2: High-Frequency Update Simulation (GPS-Denied Pattern)
|
||||||
|
1. save_route() with 2000 waypoints
|
||||||
|
2. Simulate per-frame updates: update_waypoint() × 2000 in sequence
|
||||||
|
3. Verify all updates persisted correctly
|
||||||
|
4. Measure total time < 200 seconds (100ms per update)
|
||||||
|
|
||||||
|
### Test 3: Concurrent Route Operations
|
||||||
|
1. Create 10 routes concurrently
|
||||||
|
2. Update different waypoints in parallel (100 concurrent updates)
|
||||||
|
3. Delete 5 routes concurrently while updating others
|
||||||
|
4. Verify data consistency
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **save_route**: < 300ms for routes with 100 waypoints
|
||||||
|
- **load_route**: < 150ms for routes with 2000 waypoints
|
||||||
|
- **update_waypoint**: < 50ms (critical path for GPS-Denied)
|
||||||
|
- **get_route_metadata**: < 30ms
|
||||||
|
- **delete_route**: < 200ms
|
||||||
|
|
||||||
|
### Scalability
|
||||||
|
- Support 1000+ concurrent route operations
|
||||||
|
- Handle routes with up to 3000 waypoints efficiently
|
||||||
|
- Optimize for read-heavy workload (90% reads, 10% writes)
|
||||||
|
|
||||||
|
### Reliability
|
||||||
|
- ACID transaction guarantees
|
||||||
|
- Automatic retry on transient database errors (3 attempts)
|
||||||
|
- Data validation before persistence
|
||||||
|
|
||||||
|
### Maintainability
|
||||||
|
- Clear separation from database implementation
|
||||||
|
- Support for future caching layer integration
|
||||||
|
- Comprehensive error handling and logging
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Internal Components
|
||||||
|
- **R03 Waypoint Validator**: Validates waypoints before persistence
|
||||||
|
- **R04 Route Database Layer**: For all database operations
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
- None (pure business logic layer)
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### Route
|
||||||
|
```python
|
||||||
|
class Route(BaseModel):
|
||||||
|
id: str # UUID
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
points: List[Waypoint]
|
||||||
|
geofences: Geofences
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
### RouteMetadata
|
||||||
|
```python
|
||||||
|
class RouteMetadata(BaseModel):
|
||||||
|
route_id: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
waypoint_count: int
|
||||||
|
geofence_count: int
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
### Waypoint
|
||||||
|
```python
|
||||||
|
class Waypoint(BaseModel):
|
||||||
|
id: str
|
||||||
|
lat: float
|
||||||
|
lon: float
|
||||||
|
altitude: Optional[float]
|
||||||
|
confidence: float
|
||||||
|
timestamp: datetime
|
||||||
|
refined: bool
|
||||||
|
```
|
||||||
|
|
||||||
|
### Geofences
|
||||||
|
```python
|
||||||
|
class Polygon(BaseModel):
|
||||||
|
north_west: GPSPoint
|
||||||
|
south_east: GPSPoint
|
||||||
|
|
||||||
|
class Geofences(BaseModel):
|
||||||
|
polygons: List[Polygon]
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
# Waypoint Validator
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `IWaypointValidator`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IWaypointValidator(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def validate_waypoint(self, waypoint: Waypoint) -> ValidationResult:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def validate_geofence(self, geofence: Geofences) -> ValidationResult:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def check_bounds(self, waypoint: Waypoint, geofences: Geofences) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def validate_route_continuity(self, waypoints: List[Waypoint]) -> ValidationResult:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
- Validate individual waypoint data (GPS coordinates, altitude, confidence)
|
||||||
|
- Validate geofence definitions (polygon bounds, topology)
|
||||||
|
- Check waypoints against geofence boundaries
|
||||||
|
- Validate route continuity (detect large gaps, validate sequencing)
|
||||||
|
- Provide detailed validation error messages
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Input validation for Route API
|
||||||
|
- Business rule enforcement (operational area restrictions for Ukraine)
|
||||||
|
- Geospatial boundary checking
|
||||||
|
- Data quality assurance
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `validate_waypoint(waypoint: Waypoint) -> ValidationResult`
|
||||||
|
|
||||||
|
**Description**: Validates a single waypoint's data integrity and constraints.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- R01 Route REST API (before creating/updating)
|
||||||
|
- R02 Route Data Manager (pre-persistence validation)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
Waypoint:
|
||||||
|
id: str
|
||||||
|
lat: float
|
||||||
|
lon: float
|
||||||
|
altitude: Optional[float]
|
||||||
|
confidence: float
|
||||||
|
timestamp: datetime
|
||||||
|
refined: bool
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
ValidationResult:
|
||||||
|
valid: bool
|
||||||
|
errors: List[str]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validation Rules**:
|
||||||
|
1. **Latitude**: -90.0 <= lat <= 90.0
|
||||||
|
2. **Longitude**: -180.0 <= lon <= 180.0
|
||||||
|
3. **Altitude**: 0 <= altitude <= 1000 meters (if provided)
|
||||||
|
4. **Confidence**: 0.0 <= confidence <= 1.0
|
||||||
|
5. **Timestamp**: Not in future, not older than 1 year
|
||||||
|
6. **Operational area** (Ukraine restriction): Latitude ~45-52N, Longitude ~22-40E
|
||||||
|
7. **ID**: Non-empty string
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `ValidationResult` with `valid=False` and error list (not an exception)
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Valid waypoint**: All fields correct → returns `valid=True`
|
||||||
|
2. **Invalid latitude**: lat=100 → returns `valid=False`, error="Latitude out of range"
|
||||||
|
3. **Invalid longitude**: lon=200 → returns `valid=False`
|
||||||
|
4. **Invalid confidence**: confidence=1.5 → returns `valid=False`
|
||||||
|
5. **Future timestamp**: timestamp=tomorrow → returns `valid=False`
|
||||||
|
6. **Outside operational area**: lat=10 (not Ukraine) → returns `valid=False`
|
||||||
|
7. **Valid altitude**: altitude=500 → returns `valid=True`
|
||||||
|
8. **Invalid altitude**: altitude=1500 → returns `valid=False`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `validate_geofence(geofence: Geofences) -> ValidationResult`
|
||||||
|
|
||||||
|
**Description**: Validates geofence polygon definitions.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- R01 Route REST API (during route creation)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
Geofences:
|
||||||
|
polygons: List[Polygon]
|
||||||
|
|
||||||
|
Polygon:
|
||||||
|
north_west: GPSPoint
|
||||||
|
south_east: GPSPoint
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
ValidationResult:
|
||||||
|
valid: bool
|
||||||
|
errors: List[str]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validation Rules**:
|
||||||
|
1. **North-West corner**: NW.lat > SE.lat
|
||||||
|
2. **North-West corner**: NW.lon < SE.lon (for Eastern Ukraine)
|
||||||
|
3. **Polygon size**: Max 500km × 500km
|
||||||
|
4. **Polygon count**: 1 <= len(polygons) <= 10
|
||||||
|
5. **No self-intersection**: Polygons should not overlap
|
||||||
|
6. **Within operational area**: All corners within Ukraine bounds
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `ValidationResult` with validation errors
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Valid geofence**: Single polygon in Ukraine → valid=True
|
||||||
|
2. **Invalid corners**: NW.lat < SE.lat → valid=False
|
||||||
|
3. **Too large**: 600km × 600km → valid=False
|
||||||
|
4. **Too many polygons**: 15 polygons → valid=False
|
||||||
|
5. **Overlapping polygons**: Two overlapping → valid=False (warning)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `check_bounds(waypoint: Waypoint, geofences: Geofences) -> bool`
|
||||||
|
|
||||||
|
**Description**: Checks if a waypoint falls within geofence boundaries.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- R01 Route REST API (optional check during waypoint updates)
|
||||||
|
- R02 Route Data Manager (business rule enforcement)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
waypoint: Waypoint
|
||||||
|
geofences: Geofences
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
bool: True if waypoint is within any geofence polygon
|
||||||
|
```
|
||||||
|
|
||||||
|
**Algorithm**:
|
||||||
|
- Point-in-polygon test for each geofence polygon
|
||||||
|
- Returns True if point is inside at least one polygon
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- None (returns False if outside all geofences)
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Inside geofence**: Waypoint in polygon center → returns True
|
||||||
|
2. **Outside geofence**: Waypoint 10km outside → returns False
|
||||||
|
3. **On boundary**: Waypoint on polygon edge → returns True
|
||||||
|
4. **Multiple geofences**: Waypoint in second polygon → returns True
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `validate_route_continuity(waypoints: List[Waypoint]) -> ValidationResult`
|
||||||
|
|
||||||
|
**Description**: Validates route continuity, detecting large gaps and sequence issues.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- R01 Route REST API (during route creation)
|
||||||
|
- R02 Route Data Manager (route quality check)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
waypoints: List[Waypoint] # Should be ordered by sequence/timestamp
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
ValidationResult:
|
||||||
|
valid: bool
|
||||||
|
errors: List[str]
|
||||||
|
warnings: List[str]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validation Rules**:
|
||||||
|
1. **Minimum waypoints**: len(waypoints) >= 2
|
||||||
|
2. **Maximum waypoints**: len(waypoints) <= 3000
|
||||||
|
3. **Timestamp ordering**: waypoints[i].timestamp < waypoints[i+1].timestamp
|
||||||
|
4. **Distance gaps**: Consecutive waypoints < 500 meters apart (warning if violated)
|
||||||
|
5. **Large gap detection**: Flag gaps > 1km (warning for potential data loss)
|
||||||
|
6. **No duplicate timestamps**: All timestamps unique
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- Returns `ValidationResult` with errors and warnings
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Valid route**: 100 waypoints, 100m spacing → valid=True
|
||||||
|
2. **Too few waypoints**: 1 waypoint → valid=False
|
||||||
|
3. **Too many waypoints**: 3500 waypoints → valid=False
|
||||||
|
4. **Unordered timestamps**: waypoints out of order → valid=False
|
||||||
|
5. **Large gap**: 2km gap between waypoints → valid=True with warning
|
||||||
|
6. **Duplicate timestamps**: Two waypoints same time → valid=False
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
### Test 1: Complete Validation Pipeline
|
||||||
|
1. Create waypoint with all valid data
|
||||||
|
2. validate_waypoint() → passes
|
||||||
|
3. Create geofence for Eastern Ukraine
|
||||||
|
4. validate_geofence() → passes
|
||||||
|
5. check_bounds() → waypoint inside geofence
|
||||||
|
|
||||||
|
### Test 2: Route Validation Flow
|
||||||
|
1. Create 500 waypoints with 100m spacing
|
||||||
|
2. validate_route_continuity() → passes
|
||||||
|
3. Add waypoint 2km away
|
||||||
|
4. validate_route_continuity() → passes with warning
|
||||||
|
5. Add waypoint with past timestamp
|
||||||
|
6. validate_route_continuity() → fails
|
||||||
|
|
||||||
|
### Test 3: Edge Cases
|
||||||
|
1. Waypoint on geofence boundary
|
||||||
|
2. Waypoint at North Pole (lat=90)
|
||||||
|
3. Waypoint at dateline (lon=180)
|
||||||
|
4. Route with exactly 3000 waypoints
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **validate_waypoint**: < 1ms per waypoint
|
||||||
|
- **validate_geofence**: < 10ms per geofence
|
||||||
|
- **check_bounds**: < 2ms per check
|
||||||
|
- **validate_route_continuity**: < 100ms for 2000 waypoints
|
||||||
|
|
||||||
|
### Accuracy
|
||||||
|
- GPS coordinate validation: 6 decimal places precision (0.1m)
|
||||||
|
- Geofence boundary check: 1-meter precision
|
||||||
|
|
||||||
|
### Maintainability
|
||||||
|
- Validation rules configurable via configuration file
|
||||||
|
- Easy to add new validation rules
|
||||||
|
- Clear error messages for debugging
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Internal Components
|
||||||
|
- **R04 Route Database Layer**: For loading existing route data (optional context)
|
||||||
|
- **H06 Web Mercator Utils**: For distance calculations (optional)
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
- **Shapely** (optional): For advanced polygon operations
|
||||||
|
- **Geopy**: For geodesic distance calculations
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### ValidationResult
|
||||||
|
```python
|
||||||
|
class ValidationResult(BaseModel):
|
||||||
|
valid: bool
|
||||||
|
errors: List[str] = []
|
||||||
|
warnings: List[str] = []
|
||||||
|
```
|
||||||
|
|
||||||
|
### OperationalArea (Configuration)
|
||||||
|
```python
|
||||||
|
class OperationalArea(BaseModel):
|
||||||
|
name: str = "Eastern Ukraine"
|
||||||
|
min_lat: float = 45.0
|
||||||
|
max_lat: float = 52.0
|
||||||
|
min_lon: float = 22.0
|
||||||
|
max_lon: float = 40.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### ValidationRules (Configuration)
|
||||||
|
```python
|
||||||
|
class ValidationRules(BaseModel):
|
||||||
|
max_altitude: float = 1000.0 # meters
|
||||||
|
max_waypoint_gap: float = 500.0 # meters
|
||||||
|
max_route_waypoints: int = 3000
|
||||||
|
min_route_waypoints: int = 2
|
||||||
|
max_geofence_size: float = 500000.0 # meters (500km)
|
||||||
|
max_geofences: int = 10
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,475 @@
|
|||||||
|
# Route Database Layer
|
||||||
|
|
||||||
|
## Interface Definition
|
||||||
|
|
||||||
|
**Interface Name**: `IRouteDatabase`
|
||||||
|
|
||||||
|
### Interface Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
class IRouteDatabase(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def insert_route(self, route: Route) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def update_route(self, route: Route) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def query_routes(self, filters: Dict[str, Any], limit: int, offset: int) -> List[Route]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_route_by_id(self, route_id: str) -> Optional[Route]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_waypoints(self, route_id: str, limit: Optional[int] = None) -> List[Waypoint]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def insert_waypoint(self, route_id: str, waypoint: Waypoint) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def update_waypoint(self, route_id: str, waypoint_id: str, waypoint: Waypoint) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def delete_route(self, route_id: str) -> bool:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Description
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
- Direct database access layer for route data
|
||||||
|
- Execute SQL queries and commands
|
||||||
|
- Manage database connections and transactions
|
||||||
|
- Handle connection pooling and retry logic
|
||||||
|
- Provide database abstraction for potential migration (PostgreSQL, MySQL, etc.)
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- CRUD operations on routes table
|
||||||
|
- CRUD operations on waypoints table
|
||||||
|
- CRUD operations on geofences table
|
||||||
|
- Query optimization for large datasets
|
||||||
|
- Database schema management
|
||||||
|
- Separate schema from GPS-Denied API database
|
||||||
|
|
||||||
|
## API Methods
|
||||||
|
|
||||||
|
### `insert_route(route: Route) -> str`
|
||||||
|
|
||||||
|
**Description**: Inserts a new route with initial waypoints and geofences into the database.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- R02 Route Data Manager
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
Route:
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
points: List[Waypoint]
|
||||||
|
geofences: Geofences
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
route_id: str # Inserted route ID
|
||||||
|
```
|
||||||
|
|
||||||
|
**Database Operations**:
|
||||||
|
1. Begin transaction
|
||||||
|
2. INSERT INTO routes (id, name, description, created_at, updated_at)
|
||||||
|
3. INSERT INTO waypoints (route_id, ...) for each waypoint
|
||||||
|
4. INSERT INTO geofences (route_id, ...) for each polygon
|
||||||
|
5. Commit transaction
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `IntegrityError`: Duplicate route_id (unique constraint violation)
|
||||||
|
- `DatabaseError`: Connection error, transaction failure
|
||||||
|
- Automatic rollback on any error
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Insert route with 100 waypoints**: Successful insertion, all waypoints persisted
|
||||||
|
2. **Duplicate route_id**: Raises IntegrityError
|
||||||
|
3. **Transaction rollback**: Error on waypoint insertion → route also rolled back
|
||||||
|
4. **Connection loss**: Mid-transaction error → graceful rollback
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `update_route(route: Route) -> bool`
|
||||||
|
|
||||||
|
**Description**: Updates route metadata (name, description, updated_at).
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- R02 Route Data Manager
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
Route with updated fields
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
bool: True if updated, False if route not found
|
||||||
|
```
|
||||||
|
|
||||||
|
**Database Operations**:
|
||||||
|
```sql
|
||||||
|
UPDATE routes
|
||||||
|
SET name = ?, description = ?, updated_at = ?
|
||||||
|
WHERE id = ?
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `DatabaseError`: Connection or query error
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Update existing route**: Returns True
|
||||||
|
2. **Update non-existent route**: Returns False
|
||||||
|
3. **Update with same data**: Succeeds, updates timestamp
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `query_routes(filters: Dict[str, Any], limit: int, offset: int) -> List[Route]`
|
||||||
|
|
||||||
|
**Description**: Queries routes with filtering, pagination for route listing.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- R02 Route Data Manager
|
||||||
|
- R01 Route REST API (list endpoints)
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
filters: Dict[str, Any] # e.g., {"name": "Mission%", "created_after": datetime}
|
||||||
|
limit: int # Max results
|
||||||
|
offset: int # For pagination
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
List[Route] # Routes without full waypoint data (metadata only)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Database Operations**:
|
||||||
|
```sql
|
||||||
|
SELECT * FROM routes
|
||||||
|
WHERE name LIKE ? AND created_at > ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `DatabaseError`: Query error
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Filter by name**: Returns matching routes
|
||||||
|
2. **Pagination**: offset=100, limit=50 → returns routes 100-149
|
||||||
|
3. **Empty result**: No matches → returns []
|
||||||
|
4. **No filters**: Returns all routes (with limit)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_route_by_id(route_id: str) -> Optional[Route]`
|
||||||
|
|
||||||
|
**Description**: Retrieves complete route with all waypoints by ID.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- R02 Route Data Manager
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
route_id: str
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
Optional[Route] # Complete route with all waypoints, or None
|
||||||
|
```
|
||||||
|
|
||||||
|
**Database Operations**:
|
||||||
|
1. SELECT FROM routes WHERE id = ?
|
||||||
|
2. SELECT FROM waypoints WHERE route_id = ? ORDER BY timestamp
|
||||||
|
3. SELECT FROM geofences WHERE route_id = ?
|
||||||
|
4. Assemble Route object
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `DatabaseError`: Query error
|
||||||
|
- Returns None if route not found
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Existing route**: Returns complete Route object
|
||||||
|
2. **Non-existent route**: Returns None
|
||||||
|
3. **Large route (3000 waypoints)**: Returns all data within 150ms
|
||||||
|
4. **Route with no waypoints**: Returns route with empty points list
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get_waypoints(route_id: str, limit: Optional[int] = None) -> List[Waypoint]`
|
||||||
|
|
||||||
|
**Description**: Retrieves waypoints for a route, optionally limited.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- R02 Route Data Manager
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
route_id: str
|
||||||
|
limit: Optional[int] # For pagination or preview
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
List[Waypoint]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Database Operations**:
|
||||||
|
```sql
|
||||||
|
SELECT * FROM waypoints
|
||||||
|
WHERE route_id = ?
|
||||||
|
ORDER BY timestamp ASC
|
||||||
|
LIMIT ? -- if limit provided
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `DatabaseError`: Query error
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **All waypoints**: limit=None → returns all
|
||||||
|
2. **Limited waypoints**: limit=100 → returns first 100
|
||||||
|
3. **No waypoints**: Empty list
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `insert_waypoint(route_id: str, waypoint: Waypoint) -> str`
|
||||||
|
|
||||||
|
**Description**: Inserts a new waypoint into a route.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- R02 Route Data Manager
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
route_id: str
|
||||||
|
waypoint: Waypoint
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
waypoint_id: str
|
||||||
|
```
|
||||||
|
|
||||||
|
**Database Operations**:
|
||||||
|
```sql
|
||||||
|
INSERT INTO waypoints (id, route_id, lat, lon, altitude, confidence, timestamp, refined)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `ForeignKeyError`: route_id doesn't exist
|
||||||
|
- `IntegrityError`: Duplicate waypoint_id
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Valid insertion**: Returns waypoint_id
|
||||||
|
2. **Non-existent route**: Raises ForeignKeyError
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `update_waypoint(route_id: str, waypoint_id: str, waypoint: Waypoint) -> bool`
|
||||||
|
|
||||||
|
**Description**: Updates a waypoint. Critical path for GPS-Denied per-frame updates.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- R02 Route Data Manager
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
route_id: str
|
||||||
|
waypoint_id: str
|
||||||
|
waypoint: Waypoint
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
bool: True if updated, False if not found
|
||||||
|
```
|
||||||
|
|
||||||
|
**Database Operations**:
|
||||||
|
```sql
|
||||||
|
UPDATE waypoints
|
||||||
|
SET lat = ?, lon = ?, altitude = ?, confidence = ?, refined = ?
|
||||||
|
WHERE id = ? AND route_id = ?
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optimization**:
|
||||||
|
- Prepared statement caching
|
||||||
|
- Connection pooling for high throughput
|
||||||
|
- Indexed on (route_id, id) for fast lookups
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `DatabaseError`: Query error
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Update existing waypoint**: Returns True, updates data
|
||||||
|
2. **Non-existent waypoint**: Returns False
|
||||||
|
3. **High-frequency updates**: 100 updates/sec sustained for 20 seconds
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `delete_route(route_id: str) -> bool`
|
||||||
|
|
||||||
|
**Description**: Deletes a route and cascades to waypoints and geofences.
|
||||||
|
|
||||||
|
**Called By**:
|
||||||
|
- R02 Route Data Manager
|
||||||
|
|
||||||
|
**Input**:
|
||||||
|
```python
|
||||||
|
route_id: str
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
```python
|
||||||
|
bool: True if deleted, False if not found
|
||||||
|
```
|
||||||
|
|
||||||
|
**Database Operations**:
|
||||||
|
```sql
|
||||||
|
DELETE FROM routes WHERE id = ?
|
||||||
|
-- Cascade deletes from waypoints and geofences via FK constraints
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Conditions**:
|
||||||
|
- `DatabaseError`: Query error
|
||||||
|
|
||||||
|
**Test Cases**:
|
||||||
|
1. **Delete route with waypoints**: Deletes route and all waypoints
|
||||||
|
2. **Verify cascade**: Check waypoints table empty for route_id
|
||||||
|
3. **Non-existent route**: Returns False
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
### Test 1: Complete Database Lifecycle
|
||||||
|
1. insert_route() with 500 waypoints
|
||||||
|
2. get_route_by_id() and verify all data
|
||||||
|
3. update_waypoint() × 100
|
||||||
|
4. query_routes() with filters
|
||||||
|
5. delete_route() and verify removal
|
||||||
|
|
||||||
|
### Test 2: High-Frequency Update Pattern (GPS-Denied Simulation)
|
||||||
|
1. insert_route() with 2000 waypoints
|
||||||
|
2. update_waypoint() × 2000 sequentially
|
||||||
|
3. Measure total time and throughput
|
||||||
|
4. Verify all updates persisted correctly
|
||||||
|
|
||||||
|
### Test 3: Concurrent Access
|
||||||
|
1. Insert 10 routes concurrently
|
||||||
|
2. Update waypoints in parallel (100 concurrent connections)
|
||||||
|
3. Query routes while updates occurring
|
||||||
|
4. Verify no deadlocks or data corruption
|
||||||
|
|
||||||
|
### Test 4: Transaction Integrity
|
||||||
|
1. Begin insert_route() transaction
|
||||||
|
2. Simulate error mid-waypoint insertion
|
||||||
|
3. Verify complete rollback (no partial data)
|
||||||
|
|
||||||
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **insert_route**: < 200ms for 100 waypoints
|
||||||
|
- **update_waypoint**: < 30ms (critical path)
|
||||||
|
- **get_route_by_id**: < 100ms for 2000 waypoints
|
||||||
|
- **query_routes**: < 150ms for pagination queries
|
||||||
|
- **Throughput**: 200+ waypoint updates per second
|
||||||
|
|
||||||
|
### Scalability
|
||||||
|
- Connection pool: 50-100 connections
|
||||||
|
- Support 1000+ concurrent operations
|
||||||
|
- Handle tables with millions of waypoints
|
||||||
|
|
||||||
|
### Reliability
|
||||||
|
- ACID transaction guarantees
|
||||||
|
- Automatic retry on transient errors (3 attempts with exponential backoff)
|
||||||
|
- Connection health checks
|
||||||
|
- Graceful degradation on connection pool exhaustion
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- SQL injection prevention (parameterized queries only)
|
||||||
|
- Principle of least privilege (database user permissions)
|
||||||
|
- Connection string encryption
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Internal Components
|
||||||
|
- None (lowest layer)
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
- **PostgreSQL** or **MySQL**: Relational database
|
||||||
|
- **SQLAlchemy** or **psycopg2**: Database driver
|
||||||
|
- **Alembic**: Schema migration tool
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### Database Schema
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Routes table
|
||||||
|
CREATE TABLE routes (
|
||||||
|
id VARCHAR(36) PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_created_at (created_at),
|
||||||
|
INDEX idx_name (name)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Waypoints table
|
||||||
|
CREATE TABLE waypoints (
|
||||||
|
id VARCHAR(36) PRIMARY KEY,
|
||||||
|
route_id VARCHAR(36) NOT NULL,
|
||||||
|
lat DECIMAL(10, 7) NOT NULL,
|
||||||
|
lon DECIMAL(11, 7) NOT NULL,
|
||||||
|
altitude DECIMAL(7, 2),
|
||||||
|
confidence DECIMAL(3, 2) NOT NULL,
|
||||||
|
timestamp TIMESTAMP NOT NULL,
|
||||||
|
refined BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
FOREIGN KEY (route_id) REFERENCES routes(id) ON DELETE CASCADE,
|
||||||
|
INDEX idx_route_timestamp (route_id, timestamp),
|
||||||
|
INDEX idx_route_id (route_id, id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Geofences table
|
||||||
|
CREATE TABLE geofences (
|
||||||
|
id VARCHAR(36) PRIMARY KEY,
|
||||||
|
route_id VARCHAR(36) NOT NULL,
|
||||||
|
nw_lat DECIMAL(10, 7) NOT NULL,
|
||||||
|
nw_lon DECIMAL(11, 7) NOT NULL,
|
||||||
|
se_lat DECIMAL(10, 7) NOT NULL,
|
||||||
|
se_lon DECIMAL(11, 7) NOT NULL,
|
||||||
|
FOREIGN KEY (route_id) REFERENCES routes(id) ON DELETE CASCADE,
|
||||||
|
INDEX idx_route_id (route_id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connection Configuration
|
||||||
|
```python
|
||||||
|
class DatabaseConfig(BaseModel):
|
||||||
|
host: str
|
||||||
|
port: int
|
||||||
|
database: str
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
pool_size: int = 50
|
||||||
|
max_overflow: int = 50
|
||||||
|
pool_timeout: int = 30
|
||||||
|
pool_recycle: int = 3600
|
||||||
|
```
|
||||||
|
|
||||||
@@ -92,6 +92,13 @@
|
|||||||
### Revise
|
### Revise
|
||||||
- Revise the plan, answer questions, put detailed descriptions
|
- Revise the plan, answer questions, put detailed descriptions
|
||||||
- Make sure stored components are coherent and make sense
|
- Make sure stored components are coherent and make sense
|
||||||
|
- Ask AI
|
||||||
|
```
|
||||||
|
analyze carefully interaction between components. All covered? also, state that each component should implement interface, so that they could be interchangeable with different implementations
|
||||||
|
```
|
||||||
|
|
||||||
|
### Store plan
|
||||||
|
save plan to `docs/02_components/00_decomposition_plan.md`
|
||||||
|
|
||||||
|
|
||||||
## 2.2 **🤖📋AI plan**: Generate tests
|
## 2.2 **🤖📋AI plan**: Generate tests
|
||||||
|
|||||||
Reference in New Issue
Block a user