feat(simulation): add Worker AI polygon runtime and terrain navigation
This commit is contained in:
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user