feat(simulation): add Worker AI polygon runtime and terrain navigation

This commit is contained in:
DCCONSTRUCTIONS
2026-09-25 16:40:45 +03:00
parent a7c64e009d
commit f01bd39037
88 changed files with 9918 additions and 108 deletions
@@ -0,0 +1,10 @@
FROM ndc/mission-core-ai-module-ddrnet:20260904-v8
RUN /opt/conda/envs/goose/bin/python -m pip install --no-cache-dir --no-deps \
transformers==4.44.2 tokenizers==0.19.1 safetensors==0.4.5 \
huggingface-hub==0.24.6 regex==2024.9.11 packaging==24.1 \
filelock==3.16.1 fsspec==2024.9.0 PyYAML==6.0.2 \
requests==2.32.3 tqdm==4.66.5 typing-extensions==4.12.2
ENV HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1 TOKENIZERS_PARALLELISM=false
LABEL com.nodedc.product=mission-core com.nodedc.stack=ai-polygon \
com.nodedc.role=ai-module com.nodedc.managed-by=ai-polygon-worker
ENTRYPOINT ["/opt/conda/envs/goose/bin/python", "-B", "/adapter/segformer/server.py"]
@@ -0,0 +1,37 @@
param([string]$Root = 'D:\NDC_MISSIONCORE\runtime\simulation')
$ErrorActionPreference='Stop'
$ProgressPreference='SilentlyContinue'
[Console]::OutputEncoding=[System.Text.Encoding]::UTF8
$revision='de01bae28967510f9ddd496c60a969357195400c'
$out=Join-Path $Root ('assets\segformer-b2-ade\'+$revision)
New-Item -ItemType Directory -Force $out | Out-Null
$files=@{
'config.json'='ee7400840fdb1e5045f0b2eba78bf053df8e33a309c4acec31705a48c8cc5c00'
'preprocessor_config.json'='8039d1d210abaa7117ad78e58cdfd6141a2ec72c03dae891b3cd76737e422c6c'
'README.md'='7b532a0053fc1769553386090fbc928ed8d0f5b5d5b20cfa8f51e46f56ef3c6d'
'pytorch_model.bin'='187ca07bea003a5717c63d04ea90b07f33cd033c0ebf44b4b89fce5070d6c8f3'
}
foreach($name in $files.Keys) {
$path=Join-Path $out $name
if (!(Test-Path $path)) {
$part=$path+'.part'
Invoke-WebRequest -UseBasicParsing -Uri "https://huggingface.co/nvidia/segformer-b2-finetuned-ade-512-512/resolve/$revision/$name" -OutFile $part
if ((Get-FileHash $part -Algorithm SHA256).Hash.ToLower() -ne $files[$name]) { throw "Downloaded model checksum mismatch: $name" }
Move-Item $part $path
}
if ((Get-FileHash $path -Algorithm SHA256).Hash.ToLower() -ne $files[$name]) { throw "Installed model checksum mismatch: $name" }
}
$ErrorActionPreference='Continue'
$base=docker image inspect ndc/mission-core-ai-module-ddrnet:20260904-v8 --format '{{.Id}}'
if ($LASTEXITCODE -ne 0 -or $base.Trim() -ne 'sha256:a3b7d22f5d3bfdf2d84444b936c8b01abf8243be652387d7e2024ba7bda587f5') {throw 'Pinned base image changed'}
$tag='ndc/mission-core-ai-module-segformer:de01bae2-v1'
$image=docker image inspect $tag --format '{{.Id}}' 2>$null
if ($LASTEXITCODE -ne 0) {
docker build --pull=false --progress plain -t $tag -f (Join-Path $PSScriptRoot 'Dockerfile') $PSScriptRoot
if ($LASTEXITCODE -ne 0) {throw 'SegFormer image build failed'}
$image=docker image inspect $tag --format '{{.Id}}'
if ($LASTEXITCODE -ne 0) {throw 'SegFormer image unavailable'}
}
$receipt=@{schema_version='missioncore.ai-polygon-segformer-install/v1';revision=$revision;image=$image.Trim();tag=$tag;files=$files;assets=$out;installed_at=[DateTime]::UtcNow.ToString('o');license='NVIDIA SegFormer research/evaluation; see upstream model card'}
[IO.File]::WriteAllText((Join-Path $out 'installation.json'),($receipt|ConvertTo-Json -Depth 4),[Text.UTF8Encoding]::new($false))
$receipt|ConvertTo-Json -Depth 4 -Compress
@@ -0,0 +1,52 @@
"""Worker-only offline SegFormer comparison; no live control authority."""
import argparse
import hashlib
import json
import time
from pathlib import Path
import numpy as np
import torch
from PIL import Image
from transformers import SegformerForSemanticSegmentation, SegformerImageProcessor
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--image", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
processor = SegformerImageProcessor.from_pretrained("/assets", local_files_only=True)
model = (
SegformerForSemanticSegmentation.from_pretrained("/assets", local_files_only=True)
.cuda()
.eval()
)
image = Image.open(args.image).convert("RGB").crop((100, 0, 700, 600))
tensor = processor(images=image, return_tensors="pt")["pixel_values"].cuda()
times = []
with torch.inference_mode():
for _ in range(6):
torch.cuda.synchronize()
start = time.monotonic()
logits = model(tensor).logits
logits = torch.nn.functional.interpolate(
logits, size=(512, 512), mode="bilinear", align_corners=False
)
labels = logits.argmax(1)[0].cpu().numpy().astype(np.uint8)
times.append((time.monotonic() - start) * 1000)
args.output.mkdir(parents=True, exist_ok=True)
Image.fromarray(labels).save(args.output / "labels.png")
ids, counts = np.unique(labels, return_counts=True)
report = {
"source_sha256": hashlib.sha256(args.image.read_bytes()).hexdigest(),
"classes": {model.config.id2label[int(i)]: int(counts[n]) for n, i in enumerate(ids)},
"inference_ms": times,
}
(args.output / "report.json").write_text(json.dumps(report, indent=2), encoding="utf-8")
print(json.dumps(report))
if __name__ == "__main__":
main()
+87
View File
@@ -0,0 +1,87 @@
"""Resident, pinned ADE20K surface provider. RGB only; no scene truth input."""
import hashlib
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
import numpy as np
import torch
from PIL import Image
from transformers import SegformerForSemanticSegmentation, SegformerImageProcessor
# These are surface candidates, not permission to drive. The metric terrain
# planner still checks step height, slope and the full rover footprint.
SURFACES = (3, 6, 9, 11, 13, 29, 34, 46, 52, 91)
FILES = {
"config.json": "ee7400840fdb1e5045f0b2eba78bf053df8e33a309c4acec31705a48c8cc5c00",
"preprocessor_config.json": "8039d1d210abaa7117ad78e58cdfd6141a2ec72c03dae891b3cd76737e422c6c",
"pytorch_model.bin": "187ca07bea003a5717c63d04ea90b07f33cd033c0ebf44b4b89fce5070d6c8f3",
}
def main():
for name, expected in FILES.items():
if hashlib.sha256((Path("/assets") / name).read_bytes()).hexdigest() != expected:
raise RuntimeError("SegFormer asset identity changed: " + name)
processor = SegformerImageProcessor.from_pretrained("/assets", local_files_only=True)
model = (
SegformerForSemanticSegmentation.from_pretrained("/assets", local_files_only=True)
.cuda()
.eval()
)
torch.set_num_threads(2)
def infer(rgb):
image = Image.fromarray(rgb).crop((100, 0, 700, 600))
tensor = processor(images=image, return_tensors="pt")["pixel_values"].cuda()
with torch.inference_mode():
logits = model(tensor).logits
logits = torch.nn.functional.interpolate(
logits, size=(512, 512), mode="bilinear", align_corners=False
)
confidence, labels = logits.softmax(1).max(1)
labels = labels[0].cpu().numpy().astype(np.uint8)
candidate = np.isin(labels, SURFACES) & (confidence[0].cpu().numpy() >= 0.55)
return labels.tobytes() + candidate.astype(np.uint8).tobytes()
infer(np.zeros((600, 800, 3), dtype=np.uint8))
class Handler(BaseHTTPRequestHandler):
def setup(self):
super().setup()
self.connection.settimeout(10)
def reply(self, status, body):
self.send_response(status)
self.send_header("Content-Type", "application/octet-stream")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
self.reply(
200 if self.path == "/ready" else 404,
json.dumps({"model": "segformer-b2-ade150"}).encode(),
)
def do_POST(self):
if self.path != "/infer" or self.headers.get("Content-Length") != "1440000":
self.reply(400, b"Expected 800x600 raw RGB uint8")
return
try:
raw = self.rfile.read(1440000)
if len(raw) != 1440000:
raise ValueError("Incomplete camera frame")
self.reply(200, infer(np.frombuffer(raw, np.uint8).reshape(600, 800, 3)))
except (TimeoutError, ValueError, RuntimeError):
self.reply(500, b"Surface inference failed")
def log_message(self, *_):
pass
HTTPServer(("0.0.0.0", 8010), Handler).serve_forever()
if __name__ == "__main__":
main()