feat(perception): add recorded replay maturation labs
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SourceRoot,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$OutputRoot,
|
||||
|
||||
[string]$LogPath = ''
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
$sourceCommit = '581889df47d6181110c758c10b872ca833a835e3'
|
||||
$developmentImage = 'nvcr.io/nvidia/deepstream:9.1-triton-multiarch@sha256:fd31f5b44ababdbdee8cd397a375e888191b49e402ac237254a4cdc239130f5b'
|
||||
$runtimeImage = 'nvcr.io/nvidia/deepstream:9.1-samples-multiarch@sha256:10eca409b3894e91c1bac915c9f1346307e56695e552487cbe8cf2f58a3f998f'
|
||||
$libraryName = 'libnvds_infercustomparser_tao.so'
|
||||
$expectedSources = [ordered]@{
|
||||
'Makefile' = '0265f470354e60c6d719bde68c7b74b1879eed9b4552b8fe7b39416af5ce6835'
|
||||
'debug_logger_raii.cpp' = '1d388509e1ff9008de6ccd6451db9c6433273ed78a94e95a1df84585b8dc2915'
|
||||
'debug_logger_raii.hpp' = '6efdce1874468848664a18ceb613f2384b8c079cb12baa888a433d6c0f81b7ec'
|
||||
'debug_logger_tensor.hpp' = 'c9999fcf92536bbb36498ddd4485f213fc2f5ddc70d24f8d408195a48680b94f'
|
||||
'nvdsinfer_custombboxparser_tao.cpp' = '1794e3ee5152f25eff31454c6181368676f6659c68fc25b4b1933f6cbb63158b'
|
||||
}
|
||||
|
||||
function Get-Sha256([string]$Path) {
|
||||
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
}
|
||||
|
||||
function Invoke-Docker([string[]]$Arguments, [string]$Label) {
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& docker @Arguments
|
||||
$dockerExitCode = $LASTEXITCODE
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if ($dockerExitCode -ne 0) {
|
||||
throw "$Label failed with exit code $dockerExitCode"
|
||||
}
|
||||
}
|
||||
|
||||
$source = (Resolve-Path -LiteralPath $SourceRoot).Path
|
||||
New-Item -ItemType Directory -Force -Path $OutputRoot | Out-Null
|
||||
$output = (Resolve-Path -LiteralPath $OutputRoot).Path
|
||||
if ($LogPath) {
|
||||
$logParent = Split-Path -Parent $LogPath
|
||||
if ($logParent) { New-Item -ItemType Directory -Force -Path $logParent | Out-Null }
|
||||
Start-Transcript -LiteralPath $LogPath -Append | Out-Null
|
||||
}
|
||||
|
||||
try {
|
||||
$sourceLibrary = Join-Path $source $libraryName
|
||||
if (Test-Path -LiteralPath $sourceLibrary -PathType Leaf) {
|
||||
Remove-Item -LiteralPath $sourceLibrary -Force
|
||||
}
|
||||
$actualSourceFiles = @(Get-ChildItem -LiteralPath $source -File | ForEach-Object { $_.Name })
|
||||
if (@($actualSourceFiles | Where-Object { -not $expectedSources.Contains($_) }).Count -ne 0 -or
|
||||
@($expectedSources.Keys | Where-Object { $actualSourceFiles -notcontains $_ }).Count -ne 0) {
|
||||
throw 'NVIDIA TAO parser source inventory changed.'
|
||||
}
|
||||
foreach ($entry in $expectedSources.GetEnumerator()) {
|
||||
$path = Join-Path $source $entry.Key
|
||||
$actualSha = Get-Sha256 $path
|
||||
if ($actualSha -ne $entry.Value) {
|
||||
throw "NVIDIA TAO parser source changed: $($entry.Key)"
|
||||
}
|
||||
}
|
||||
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& docker image inspect $developmentImage *> $null
|
||||
$developmentImageCached = $LASTEXITCODE -eq 0
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if (-not $developmentImageCached) {
|
||||
Write-Host 'Pulling exact NVIDIA DeepStream 9.1 Triton development image...'
|
||||
Invoke-Docker @('pull', $developmentImage) 'DeepStream development image pull'
|
||||
}
|
||||
|
||||
$containerSource = '/opt/nvidia/deepstream/deepstream/sources/apps/sample_apps/deepstream_tao_apps/post_processor'
|
||||
Invoke-Docker @(
|
||||
'run', '--rm', '--gpus', 'all', '--network', 'none', '--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges',
|
||||
'--mount', "type=bind,src=$source,dst=$containerSource",
|
||||
'--workdir', $containerSource,
|
||||
'--entrypoint', 'make', $developmentImage, 'CUDA_VER=13.2'
|
||||
) 'official NVIDIA TAO parser build'
|
||||
if (-not (Test-Path -LiteralPath $sourceLibrary -PathType Leaf) -or
|
||||
(Get-Item -LiteralPath $sourceLibrary).Length -eq 0) {
|
||||
throw 'NVIDIA TAO parser build did not produce a library.'
|
||||
}
|
||||
|
||||
$destination = Join-Path $output $libraryName
|
||||
$temporary = "$destination.$([Guid]::NewGuid().ToString('N')).tmp"
|
||||
Copy-Item -LiteralPath $sourceLibrary -Destination $temporary
|
||||
Move-Item -LiteralPath $temporary -Destination $destination -Force
|
||||
$librarySha = Get-Sha256 $destination
|
||||
|
||||
Invoke-Docker @(
|
||||
'run', '--rm', '--gpus', 'all', '--network', 'none', '--read-only',
|
||||
'--cap-drop', 'ALL', '--security-opt', 'no-new-privileges',
|
||||
'--mount', "type=bind,src=$output,dst=/workspace/parser,readonly",
|
||||
'--entrypoint', '/bin/bash', $runtimeImage, '-lc',
|
||||
"ldd /workspace/parser/$libraryName && nm -D /workspace/parser/$libraryName | grep -q NvDsInferParseCustomDDETRTAO"
|
||||
) 'NVIDIA TAO parser runtime verification'
|
||||
|
||||
$manifest = [ordered]@{
|
||||
schema_version = 'missioncore.e46e-nvidia-tao-parser/v1'
|
||||
status = 'completed'
|
||||
source_repository = 'https://github.com/NVIDIA/DeepStream.git'
|
||||
source_commit = $sourceCommit
|
||||
source_files = @($expectedSources.GetEnumerator() | ForEach-Object {
|
||||
[ordered]@{ path = $_.Key; sha256 = $_.Value }
|
||||
})
|
||||
development_image = $developmentImage
|
||||
runtime_image = $runtimeImage
|
||||
cuda_version = '13.2'
|
||||
symbol = 'NvDsInferParseCustomDDETRTAO'
|
||||
library_file = $libraryName
|
||||
library_sha256 = $librarySha
|
||||
completed_at_utc = (Get-Date).ToUniversalTime().ToString('o')
|
||||
}
|
||||
$manifest | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'parser-runtime.json') -Encoding UTF8
|
||||
Write-Host "E46E_NVIDIA_TAO_PARSER_COMPLETED sha256=$librarySha output=$output"
|
||||
}
|
||||
finally {
|
||||
if ($LogPath) { Stop-Transcript | Out-Null }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$BuildScript,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SourceRoot,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$OutputRoot
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$taskName = 'MissionCore-E46ENvidiaTaoParser'
|
||||
$script = (Resolve-Path -LiteralPath $BuildScript).Path
|
||||
$source = (Resolve-Path -LiteralPath $SourceRoot).Path
|
||||
New-Item -ItemType Directory -Force -Path $OutputRoot | Out-Null
|
||||
$output = (Resolve-Path -LiteralPath $OutputRoot).Path
|
||||
$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||
if ($existing -and $existing.State -eq 'Running') {
|
||||
throw "$taskName is already running."
|
||||
}
|
||||
$logsRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46e\logs'
|
||||
New-Item -ItemType Directory -Force -Path $logsRoot | Out-Null
|
||||
$stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ')
|
||||
$logPath = Join-Path $logsRoot "e46e-parser-build-$stamp.log"
|
||||
$powerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||
$arguments = @(
|
||||
'-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
|
||||
'-File', "`"$script`"", '-SourceRoot', "`"$source`"",
|
||||
'-OutputRoot', "`"$output`"", '-LogPath', "`"$logPath`""
|
||||
) -join ' '
|
||||
$userId = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
$action = New-ScheduledTaskAction -Execute $powerShell -Argument $arguments -WorkingDirectory $source
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited
|
||||
$trigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).AddMinutes(30))
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
|
||||
-StartWhenAvailable -ExecutionTimeLimit ([TimeSpan]::FromHours(3))
|
||||
Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal `
|
||||
-Trigger $trigger -Settings $settings `
|
||||
-Description 'Build the pinned official NVIDIA DeepStream TAO RT-DETR parser.' -Force | Out-Null
|
||||
Start-ScheduledTask -TaskName $taskName
|
||||
Write-Host "E46E_PARSER_TASK_STARTED task=$taskName log=$logPath"
|
||||
@@ -0,0 +1,319 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PackageRoot,
|
||||
|
||||
[string]$SourceJobRoot = 'D:\NDC_MISSIONCORE\runtime\jobs\recorded-camera-602ac89026ed12978619801d',
|
||||
|
||||
[string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46e',
|
||||
|
||||
[string]$LogPath = ''
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
function Get-Sha256([string]$Path) {
|
||||
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
}
|
||||
|
||||
function Assert-Sha256([string]$Path, [string]$Expected, [string]$Label) {
|
||||
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
||||
throw "$Label is missing: $Path"
|
||||
}
|
||||
$actual = Get-Sha256 $Path
|
||||
if ($actual -ne $Expected) {
|
||||
throw "$Label SHA-256 changed: expected $Expected, got $actual"
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Docker([string[]]$Arguments, [string]$Label) {
|
||||
& docker @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "$Label failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
}
|
||||
|
||||
$package = (Resolve-Path -LiteralPath $PackageRoot).Path
|
||||
$sourceJob = (Resolve-Path -LiteralPath $SourceJobRoot).Path
|
||||
$profilePath = Join-Path $package 'profile.json'
|
||||
$manifestPath = Join-Path $package 'manifest.json'
|
||||
if (-not (Test-Path -LiteralPath $profilePath -PathType Leaf) -or
|
||||
-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) {
|
||||
throw 'E46E package is incomplete.'
|
||||
}
|
||||
$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
|
||||
if ($manifest.schema_version -ne 'missioncore.e46e-worker-package/v1' -or
|
||||
$manifest.package_id -ne (Split-Path -Leaf $package)) {
|
||||
throw 'E46E package identity is invalid.'
|
||||
}
|
||||
$expectedPaths = @($manifest.identity.artifact_paths)
|
||||
foreach ($artifact in @($manifest.artifacts)) {
|
||||
if ($expectedPaths -notcontains [string]$artifact.path) {
|
||||
throw "Unexpected E46E package artifact: $($artifact.path)"
|
||||
}
|
||||
$artifactPath = Join-Path $package ([string]$artifact.path)
|
||||
Assert-Sha256 $artifactPath ([string]$artifact.sha256) "package artifact $($artifact.path)"
|
||||
if ((Get-Item -LiteralPath $artifactPath).Length -ne [int64]$artifact.byte_length) {
|
||||
throw "Package artifact length changed: $($artifact.path)"
|
||||
}
|
||||
}
|
||||
$actualPaths = @(Get-ChildItem -LiteralPath $package -Recurse -File | ForEach-Object {
|
||||
$_.FullName.Substring($package.Length + 1).Replace('\', '/')
|
||||
})
|
||||
if (@($actualPaths | Where-Object { $_ -ne 'manifest.json' -and $expectedPaths -notcontains $_ }).Count -ne 0 -or
|
||||
@($expectedPaths | Where-Object { $actualPaths -notcontains $_ }).Count -ne 0) {
|
||||
throw 'E46E package file set changed.'
|
||||
}
|
||||
|
||||
$profile = Get-Content -LiteralPath $profilePath -Raw | ConvertFrom-Json
|
||||
if ($profile.schema_version -ne 'missioncore.e46e-ready-stack-profile/v1') {
|
||||
throw 'E46E profile is incompatible.'
|
||||
}
|
||||
$image = [string]$profile.runtime.container_image
|
||||
$imageDigestMatch = [regex]::Match($image, '@sha256:([0-9a-f]{64})$')
|
||||
if (-not $imageDigestMatch.Success) {
|
||||
throw 'E46E runtime image must be pinned by a full SHA-256 digest.'
|
||||
}
|
||||
$imageDigest = $imageDigestMatch.Groups[1].Value
|
||||
$modelSha = [string]$profile.detector.model_sha256
|
||||
$streamSha = [string]$profile.source.stream_sha256
|
||||
$modelFile = [string]$profile.detector.model_file
|
||||
$modelUrl = [string]$profile.detector.model_url
|
||||
$parserFile = [string]$profile.parser.library_file
|
||||
$parserSha = [string]$profile.parser.library_sha256
|
||||
$parserLibraryPath = Join-Path $package "runtime\$parserFile"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $RuntimeRoot | Out-Null
|
||||
$logsRoot = Join-Path $RuntimeRoot 'logs'
|
||||
$modelsRoot = Join-Path $RuntimeRoot 'models\trafficcamnet_transformer_lite\deployable_resnet50_v2.0'
|
||||
$inputsRoot = Join-Path $RuntimeRoot 'inputs'
|
||||
$runsRoot = Join-Path $RuntimeRoot 'runs'
|
||||
$resultsRoot = Join-Path $RuntimeRoot 'ready-stack-results'
|
||||
foreach ($path in @($logsRoot, $modelsRoot, $inputsRoot, $runsRoot, $resultsRoot)) {
|
||||
New-Item -ItemType Directory -Force -Path $path | Out-Null
|
||||
}
|
||||
if ($LogPath) {
|
||||
$logParent = Split-Path -Parent $LogPath
|
||||
if ($logParent) { New-Item -ItemType Directory -Force -Path $logParent | Out-Null }
|
||||
Start-Transcript -LiteralPath $LogPath -Append | Out-Null
|
||||
}
|
||||
|
||||
try {
|
||||
Write-Host "E46E package: $($manifest.package_id)"
|
||||
Write-Host "E46E source: $sourceJob"
|
||||
Write-Host "E46E image: $image"
|
||||
Assert-Sha256 $parserLibraryPath $parserSha 'official NVIDIA DeepStream TAO parser'
|
||||
|
||||
$modelPath = Join-Path $modelsRoot $modelFile
|
||||
if (Test-Path -LiteralPath $modelPath -PathType Leaf) {
|
||||
Assert-Sha256 $modelPath $modelSha 'TrafficCamNet Transformer Lite model'
|
||||
}
|
||||
else {
|
||||
$modelTemp = "$modelPath.$([Guid]::NewGuid().ToString('N')).download"
|
||||
Write-Host 'Downloading exact NVIDIA TrafficCamNet Transformer Lite model...'
|
||||
& curl.exe --fail --location --retry 3 --output $modelTemp $modelUrl
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "NVIDIA model download failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
Assert-Sha256 $modelTemp $modelSha 'downloaded model'
|
||||
Move-Item -LiteralPath $modelTemp -Destination $modelPath
|
||||
}
|
||||
|
||||
$jobPath = Join-Path $sourceJob 'job.json'
|
||||
$job = Get-Content -LiteralPath $jobPath -Raw | ConvertFrom-Json
|
||||
if ($job.job_id -ne $profile.source.job_id -or
|
||||
$job.input.archive_index_sha256 -ne $profile.source.archive_index_sha256 -or
|
||||
$job.input.archive_summary_sha256 -ne $profile.source.archive_summary_sha256) {
|
||||
throw 'Exact E46E source job binding changed.'
|
||||
}
|
||||
$cameraRoot = Join-Path $sourceJob 'input\camera\sensor.camera.right\epoch-1'
|
||||
$indexPath = Join-Path $cameraRoot 'index.jsonl'
|
||||
$summaryPath = Join-Path $cameraRoot 'summary.json'
|
||||
Assert-Sha256 $indexPath ([string]$profile.source.archive_index_sha256) 'source index'
|
||||
Assert-Sha256 $summaryPath ([string]$profile.source.archive_summary_sha256) 'source summary'
|
||||
$summary = Get-Content -LiteralPath $summaryPath -Raw | ConvertFrom-Json
|
||||
if ($summary.stream_sha256 -ne $streamSha -or
|
||||
[int]$summary.segment_count -ne [int]$profile.source.segment_count) {
|
||||
throw 'Source stream identity changed.'
|
||||
}
|
||||
|
||||
$inputPath = Join-Path $inputsRoot "right-$streamSha.mp4"
|
||||
if (Test-Path -LiteralPath $inputPath -PathType Leaf) {
|
||||
Assert-Sha256 $inputPath $streamSha 'reconstructed RIGHT stream'
|
||||
}
|
||||
else {
|
||||
$inputTemp = "$inputPath.$([Guid]::NewGuid().ToString('N')).tmp"
|
||||
$destinationStream = [System.IO.File]::Open(
|
||||
$inputTemp,
|
||||
[System.IO.FileMode]::CreateNew,
|
||||
[System.IO.FileAccess]::Write,
|
||||
[System.IO.FileShare]::None
|
||||
)
|
||||
$incremental = [System.Security.Cryptography.IncrementalHash]::CreateHash(
|
||||
[System.Security.Cryptography.HashAlgorithmName]::SHA256
|
||||
)
|
||||
try {
|
||||
$sourceParts = [System.Collections.Generic.List[string]]::new()
|
||||
$sourceParts.Add((Join-Path $cameraRoot 'init.mp4'))
|
||||
foreach ($line in [System.IO.File]::ReadLines($indexPath)) {
|
||||
$row = $line | ConvertFrom-Json
|
||||
$sourceParts.Add((Join-Path $cameraRoot ([string]$row.path)))
|
||||
}
|
||||
if ($sourceParts.Count -ne ([int]$profile.source.segment_count + 1)) {
|
||||
throw 'Source stream part count changed.'
|
||||
}
|
||||
$buffer = New-Object byte[] (4MB)
|
||||
foreach ($part in $sourceParts) {
|
||||
$inputStream = [System.IO.File]::OpenRead($part)
|
||||
try {
|
||||
while (($read = $inputStream.Read($buffer, 0, $buffer.Length)) -gt 0) {
|
||||
$destinationStream.Write($buffer, 0, $read)
|
||||
$incremental.AppendData($buffer, 0, $read)
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$inputStream.Dispose()
|
||||
}
|
||||
}
|
||||
$destinationStream.Flush($true)
|
||||
$actualStreamSha = ([BitConverter]::ToString(
|
||||
$incremental.GetHashAndReset()
|
||||
)).Replace('-', '').ToLowerInvariant()
|
||||
}
|
||||
finally {
|
||||
$incremental.Dispose()
|
||||
$destinationStream.Dispose()
|
||||
}
|
||||
if ($actualStreamSha -ne $streamSha) {
|
||||
throw "Reconstructed stream SHA-256 changed: $actualStreamSha"
|
||||
}
|
||||
Move-Item -LiteralPath $inputTemp -Destination $inputPath
|
||||
}
|
||||
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& docker image inspect $image *> $null
|
||||
$imageCached = $LASTEXITCODE -eq 0
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if ($imageCached) {
|
||||
Write-Host 'Using the exact cached NVIDIA DeepStream image.'
|
||||
}
|
||||
else {
|
||||
Write-Host 'Pulling exact NVIDIA DeepStream image in the interactive user session...'
|
||||
Invoke-Docker @('pull', $image) 'DeepStream image pull'
|
||||
}
|
||||
|
||||
$runId = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ')
|
||||
$runRoot = Join-Path $runsRoot $runId
|
||||
$rawRoot = Join-Path $runRoot 'raw'
|
||||
$inputMount = Join-Path $runRoot 'input'
|
||||
New-Item -ItemType Directory -Force -Path $rawRoot | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $rawRoot 'detections') | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $rawRoot 'tracks') | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $inputMount | Out-Null
|
||||
Copy-Item -LiteralPath $inputPath -Destination (Join-Path $inputMount 'right.mp4')
|
||||
$deepstreamLog = Join-Path $rawRoot 'deepstream.log'
|
||||
$trackerCopy = Join-Path $rawRoot 'tracker-config.yml'
|
||||
$startedAt = (Get-Date).ToUniversalTime().ToString('o')
|
||||
|
||||
$containerCommand = @"
|
||||
set -euo pipefail
|
||||
cp /opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml /workspace/output/tracker-config.yml
|
||||
deepstream-app -c /workspace/package/runtime/e46e_deepstream_app.txt
|
||||
"@
|
||||
$dockerArguments = @(
|
||||
'run', '--rm', '--name', "ndc-mission-core-deepstream-e46e-$runId",
|
||||
'--gpus', 'all', '--network', 'none', '--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges', '--shm-size', '4g',
|
||||
'--label', 'com.nodedc.product=mission-core',
|
||||
'--label', 'com.nodedc.stack=perception',
|
||||
'--label', 'com.nodedc.role=deepstream-ready-stack-e46e',
|
||||
'--label', 'com.nodedc.managed-by=mission-core-worker',
|
||||
'--mount', "type=bind,src=$inputMount,dst=/workspace/input,readonly",
|
||||
'--mount', "type=bind,src=$package,dst=/workspace/package,readonly",
|
||||
'--mount', "type=bind,src=$modelsRoot,dst=/workspace/model",
|
||||
'--mount', "type=bind,src=$rawRoot,dst=/workspace/output",
|
||||
'--entrypoint', '/bin/bash', $image, '-lc', $containerCommand
|
||||
)
|
||||
Write-Host 'Running full 4489-frame NVIDIA detector + NvDCF replay...'
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& docker @dockerArguments 2>&1 | Tee-Object -LiteralPath $deepstreamLog
|
||||
$deepstreamExit = $LASTEXITCODE
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if ($deepstreamExit -ne 0) {
|
||||
throw "DeepStream replay failed with exit code $deepstreamExit"
|
||||
}
|
||||
|
||||
$overlayPath = Join-Path $rawRoot 'overlay.mp4'
|
||||
$enginePath = Join-Path $modelsRoot "$modelFile`_b1_gpu0_fp16.engine"
|
||||
if (-not (Test-Path -LiteralPath $overlayPath -PathType Leaf) -or
|
||||
(Get-Item -LiteralPath $overlayPath).Length -eq 0) {
|
||||
throw 'DeepStream did not produce an overlay video.'
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $enginePath -PathType Leaf)) {
|
||||
throw 'DeepStream did not produce the exact TensorRT engine.'
|
||||
}
|
||||
$detectionFiles = @(Get-ChildItem -LiteralPath (Join-Path $rawRoot 'detections') -File)
|
||||
$trackFiles = @(Get-ChildItem -LiteralPath (Join-Path $rawRoot 'tracks') -File)
|
||||
if ($detectionFiles.Count -ne [int]$profile.source.segment_count -or
|
||||
$trackFiles.Count -ne [int]$profile.source.segment_count) {
|
||||
throw "DeepStream frame coverage changed: detections=$($detectionFiles.Count), tracks=$($trackFiles.Count)"
|
||||
}
|
||||
$runtime = [ordered]@{
|
||||
schema_version = 'missioncore.e46e-deepstream-runtime/v1'
|
||||
status = 'completed'
|
||||
worker_host = $env:COMPUTERNAME
|
||||
gpu_name = ((& nvidia-smi --query-gpu=name --format=csv,noheader | Select-Object -First 1).Trim())
|
||||
started_at_utc = $startedAt
|
||||
completed_at_utc = (Get-Date).ToUniversalTime().ToString('o')
|
||||
container_image = $image
|
||||
container_image_digest = $imageDigest
|
||||
model_sha256 = Get-Sha256 $modelPath
|
||||
model_engine_sha256 = Get-Sha256 $enginePath
|
||||
deepstream_config_sha256 = Get-Sha256 (Join-Path $package 'runtime\e46e_deepstream_app.txt')
|
||||
detector_config_sha256 = Get-Sha256 (Join-Path $package 'runtime\e46e_trafficcamnet_rtdetr.txt')
|
||||
parser_library_sha256 = Get-Sha256 $parserLibraryPath
|
||||
tracker_config_sha256 = Get-Sha256 $trackerCopy
|
||||
input_stream_sha256 = Get-Sha256 $inputPath
|
||||
overlay_sha256 = Get-Sha256 $overlayPath
|
||||
frame_count = [int]$profile.source.segment_count
|
||||
deepstream_exit_code = $deepstreamExit
|
||||
}
|
||||
$runtimePath = Join-Path $rawRoot 'runtime.json'
|
||||
$runtime | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $runtimePath -Encoding UTF8
|
||||
|
||||
$consolidatorImage = 'nvcr.io/nvidia/tritonserver:26.06-py3@sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794'
|
||||
$consolidatorArguments = @(
|
||||
'run', '--rm', '--name', "ndc-mission-core-e46e-consolidator-$runId",
|
||||
'--network', 'none', '--read-only', '--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges', '--tmpfs', '/tmp:rw,noexec,nosuid,size=64m',
|
||||
'--mount', "type=bind,src=$package,dst=/workspace/package,readonly",
|
||||
'--mount', "type=bind,src=$sourceJob,dst=/workspace/source-job,readonly",
|
||||
'--mount', "type=bind,src=$rawRoot,dst=/workspace/raw,readonly",
|
||||
'--mount', "type=bind,src=$resultsRoot,dst=/workspace/results",
|
||||
'-e', 'PYTHONPATH=/workspace/package/runtime',
|
||||
'-e', 'PYTHONDONTWRITEBYTECODE=1',
|
||||
$consolidatorImage,
|
||||
'python3', '/workspace/package/runtime/run_e46e_ready_stack.py',
|
||||
'--source-job', '/workspace/source-job',
|
||||
'--raw-root', '/workspace/raw',
|
||||
'--profile', '/workspace/package/profile.json',
|
||||
'--output-root', '/workspace/results'
|
||||
)
|
||||
Write-Host 'Freezing immutable E46E evidence...'
|
||||
Invoke-Docker $consolidatorArguments 'E46E consolidation'
|
||||
Write-Host "E46E_READY_STACK_COMPLETED run=$runId results=$resultsRoot"
|
||||
}
|
||||
finally {
|
||||
if ($LogPath) { Stop-Transcript | Out-Null }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PackageRoot,
|
||||
|
||||
[string]$SourceJobRoot = 'D:\NDC_MISSIONCORE\runtime\jobs\recorded-camera-602ac89026ed12978619801d',
|
||||
|
||||
[string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46e'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$taskName = 'MissionCore-E46EReadyStack'
|
||||
$package = (Resolve-Path -LiteralPath $PackageRoot).Path
|
||||
$script = Join-Path $package 'runtime\Invoke-E46EReadyStack.ps1'
|
||||
if (-not (Test-Path -LiteralPath $script -PathType Leaf)) {
|
||||
throw "E46E runner is missing: $script"
|
||||
}
|
||||
$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||
if ($existing -and $existing.State -eq 'Running') {
|
||||
throw "$taskName is already running."
|
||||
}
|
||||
$logsRoot = Join-Path $RuntimeRoot 'logs'
|
||||
New-Item -ItemType Directory -Force -Path $logsRoot | Out-Null
|
||||
$stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ')
|
||||
$logPath = Join-Path $logsRoot "e46e-ready-stack-$stamp.log"
|
||||
$powerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||
$arguments = @(
|
||||
'-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
|
||||
'-File', "`"$script`"",
|
||||
'-PackageRoot', "`"$package`"",
|
||||
'-SourceJobRoot', "`"$SourceJobRoot`"",
|
||||
'-RuntimeRoot', "`"$RuntimeRoot`"",
|
||||
'-LogPath', "`"$logPath`""
|
||||
) -join ' '
|
||||
$userId = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
$action = New-ScheduledTaskAction -Execute $powerShell -Argument $arguments -WorkingDirectory $package
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited
|
||||
$trigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).AddMinutes(30))
|
||||
$settings = New-ScheduledTaskSettingsSet `
|
||||
-AllowStartIfOnBatteries `
|
||||
-DontStopIfGoingOnBatteries `
|
||||
-StartWhenAvailable `
|
||||
-ExecutionTimeLimit ([TimeSpan]::FromHours(6))
|
||||
Register-ScheduledTask `
|
||||
-TaskName $taskName `
|
||||
-Action $action `
|
||||
-Principal $principal `
|
||||
-Trigger $trigger `
|
||||
-Settings $settings `
|
||||
-Description 'One-shot Mission Core E46E stock NVIDIA RT-DETR plus NvDCF recorded RIGHT replay.' `
|
||||
-Force | Out-Null
|
||||
Start-ScheduledTask -TaskName $taskName
|
||||
Write-Host "E46E_TASK_STARTED task=$taskName log=$logPath"
|
||||
@@ -0,0 +1,329 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PackageRoot,
|
||||
|
||||
[string]$SourceJobRoot = 'D:\NDC_MISSIONCORE\runtime\jobs\recorded-camera-602ac89026ed12978619801d',
|
||||
|
||||
[string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46f',
|
||||
|
||||
[string]$LogPath = ''
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
function Get-Sha256([string]$Path) {
|
||||
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
}
|
||||
|
||||
function Assert-Sha256([string]$Path, [string]$Expected, [string]$Label) {
|
||||
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
||||
throw "$Label is missing: $Path"
|
||||
}
|
||||
$actual = Get-Sha256 $Path
|
||||
if ($actual -ne $Expected) {
|
||||
throw "$Label SHA-256 changed: expected $Expected, got $actual"
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Docker([string[]]$Arguments, [string]$Label) {
|
||||
& docker @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "$Label failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
}
|
||||
|
||||
$package = (Resolve-Path -LiteralPath $PackageRoot).Path
|
||||
$sourceJob = (Resolve-Path -LiteralPath $SourceJobRoot).Path
|
||||
$profilePath = Join-Path $package 'profile.json'
|
||||
$manifestPath = Join-Path $package 'manifest.json'
|
||||
if (-not (Test-Path -LiteralPath $profilePath -PathType Leaf) -or
|
||||
-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) {
|
||||
throw 'E46F package is incomplete.'
|
||||
}
|
||||
$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
|
||||
if ($manifest.schema_version -ne 'missioncore.e46f-worker-package/v1' -or
|
||||
$manifest.package_id -ne (Split-Path -Leaf $package)) {
|
||||
throw 'E46F package identity is invalid.'
|
||||
}
|
||||
$expectedPaths = @($manifest.identity.artifact_paths)
|
||||
foreach ($artifact in @($manifest.artifacts)) {
|
||||
if ($expectedPaths -notcontains [string]$artifact.path) {
|
||||
throw "Unexpected E46F package artifact: $($artifact.path)"
|
||||
}
|
||||
$artifactPath = Join-Path $package ([string]$artifact.path)
|
||||
Assert-Sha256 $artifactPath ([string]$artifact.sha256) "package artifact $($artifact.path)"
|
||||
if ((Get-Item -LiteralPath $artifactPath).Length -ne [int64]$artifact.byte_length) {
|
||||
throw "Package artifact length changed: $($artifact.path)"
|
||||
}
|
||||
}
|
||||
$actualPaths = @(Get-ChildItem -LiteralPath $package -Recurse -File | ForEach-Object {
|
||||
$_.FullName.Substring($package.Length + 1).Replace('\', '/')
|
||||
})
|
||||
if (@($actualPaths | Where-Object { $_ -ne 'manifest.json' -and $expectedPaths -notcontains $_ }).Count -ne 0 -or
|
||||
@($expectedPaths | Where-Object { $actualPaths -notcontains $_ }).Count -ne 0) {
|
||||
throw 'E46F package file set changed.'
|
||||
}
|
||||
|
||||
$profile = Get-Content -LiteralPath $profilePath -Raw | ConvertFrom-Json
|
||||
if ($profile.schema_version -ne 'missioncore.e46f-dashcam-bakeoff-profile/v1' -or
|
||||
$profile.comparison_contract.controlled_change -ne 'detector-only') {
|
||||
throw 'E46F profile is incompatible.'
|
||||
}
|
||||
$image = [string]$profile.runtime.container_image
|
||||
$imageDigestMatch = [regex]::Match($image, '@sha256:([0-9a-f]{64})$')
|
||||
if (-not $imageDigestMatch.Success) {
|
||||
throw 'E46F runtime image must be pinned by a full SHA-256 digest.'
|
||||
}
|
||||
$imageDigest = $imageDigestMatch.Groups[1].Value
|
||||
$modelSha = [string]$profile.detector.model_sha256
|
||||
$streamSha = [string]$profile.source.stream_sha256
|
||||
$modelFile = [string]$profile.detector.model_file
|
||||
$modelUrl = [string]$profile.detector.model_url
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $RuntimeRoot | Out-Null
|
||||
$logsRoot = Join-Path $RuntimeRoot 'logs'
|
||||
$modelsRoot = Join-Path $RuntimeRoot "models\dashcamnet\$([string]$profile.detector.version)"
|
||||
$inputsRoot = Join-Path $RuntimeRoot 'inputs'
|
||||
$runsRoot = Join-Path $RuntimeRoot 'runs'
|
||||
$resultsRoot = Join-Path $RuntimeRoot 'dashcam-bakeoff-results'
|
||||
foreach ($path in @($logsRoot, $modelsRoot, $inputsRoot, $runsRoot, $resultsRoot)) {
|
||||
New-Item -ItemType Directory -Force -Path $path | Out-Null
|
||||
}
|
||||
if ($LogPath) {
|
||||
$logParent = Split-Path -Parent $LogPath
|
||||
if ($logParent) { New-Item -ItemType Directory -Force -Path $logParent | Out-Null }
|
||||
Start-Transcript -LiteralPath $LogPath -Append | Out-Null
|
||||
}
|
||||
|
||||
try {
|
||||
Write-Host "E46F package: $($manifest.package_id)"
|
||||
Write-Host "E46F source: $sourceJob"
|
||||
Write-Host "E46F image: $image"
|
||||
|
||||
$modelPath = Join-Path $modelsRoot $modelFile
|
||||
if (Test-Path -LiteralPath $modelPath -PathType Leaf) {
|
||||
Assert-Sha256 $modelPath $modelSha 'DashCamNet model'
|
||||
}
|
||||
else {
|
||||
$modelTemp = "$modelPath.$([Guid]::NewGuid().ToString('N')).download"
|
||||
Write-Host 'Downloading exact NVIDIA DashCamNet model...'
|
||||
& curl.exe --fail --location --retry 3 --output $modelTemp $modelUrl
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "NVIDIA model download failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
Assert-Sha256 $modelTemp $modelSha 'downloaded DashCamNet model'
|
||||
Move-Item -LiteralPath $modelTemp -Destination $modelPath
|
||||
}
|
||||
|
||||
$jobPath = Join-Path $sourceJob 'job.json'
|
||||
$job = Get-Content -LiteralPath $jobPath -Raw | ConvertFrom-Json
|
||||
if ($job.job_id -ne $profile.source.job_id -or
|
||||
$job.input.archive_index_sha256 -ne $profile.source.archive_index_sha256 -or
|
||||
$job.input.archive_summary_sha256 -ne $profile.source.archive_summary_sha256) {
|
||||
throw 'Exact E46F source job binding changed.'
|
||||
}
|
||||
$cameraRoot = Join-Path $sourceJob 'input\camera\sensor.camera.right\epoch-1'
|
||||
$indexPath = Join-Path $cameraRoot 'index.jsonl'
|
||||
$summaryPath = Join-Path $cameraRoot 'summary.json'
|
||||
Assert-Sha256 $indexPath ([string]$profile.source.archive_index_sha256) 'source index'
|
||||
Assert-Sha256 $summaryPath ([string]$profile.source.archive_summary_sha256) 'source summary'
|
||||
$summary = Get-Content -LiteralPath $summaryPath -Raw | ConvertFrom-Json
|
||||
if ($summary.stream_sha256 -ne $streamSha -or
|
||||
[int]$summary.segment_count -ne [int]$profile.source.segment_count) {
|
||||
throw 'Source stream identity changed.'
|
||||
}
|
||||
|
||||
$inputPath = Join-Path $inputsRoot "right-$streamSha.mp4"
|
||||
$e46eInputPath = "D:\NDC_MISSIONCORE\runtime\experiments\e46e\inputs\right-$streamSha.mp4"
|
||||
if (Test-Path -LiteralPath $inputPath -PathType Leaf) {
|
||||
Assert-Sha256 $inputPath $streamSha 'E46F reconstructed RIGHT stream'
|
||||
}
|
||||
elseif (Test-Path -LiteralPath $e46eInputPath -PathType Leaf) {
|
||||
Assert-Sha256 $e46eInputPath $streamSha 'E46E controlled RIGHT stream'
|
||||
Copy-Item -LiteralPath $e46eInputPath -Destination $inputPath
|
||||
Assert-Sha256 $inputPath $streamSha 'E46F copied RIGHT stream'
|
||||
}
|
||||
else {
|
||||
$inputTemp = "$inputPath.$([Guid]::NewGuid().ToString('N')).tmp"
|
||||
$destinationStream = [System.IO.File]::Open(
|
||||
$inputTemp,
|
||||
[System.IO.FileMode]::CreateNew,
|
||||
[System.IO.FileAccess]::Write,
|
||||
[System.IO.FileShare]::None
|
||||
)
|
||||
$incremental = [System.Security.Cryptography.IncrementalHash]::CreateHash(
|
||||
[System.Security.Cryptography.HashAlgorithmName]::SHA256
|
||||
)
|
||||
try {
|
||||
$sourceParts = [System.Collections.Generic.List[string]]::new()
|
||||
$sourceParts.Add((Join-Path $cameraRoot 'init.mp4'))
|
||||
foreach ($line in [System.IO.File]::ReadLines($indexPath)) {
|
||||
$row = $line | ConvertFrom-Json
|
||||
$sourceParts.Add((Join-Path $cameraRoot ([string]$row.path)))
|
||||
}
|
||||
if ($sourceParts.Count -ne ([int]$profile.source.segment_count + 1)) {
|
||||
throw 'Source stream part count changed.'
|
||||
}
|
||||
$buffer = New-Object byte[] (4MB)
|
||||
foreach ($part in $sourceParts) {
|
||||
$inputStream = [System.IO.File]::OpenRead($part)
|
||||
try {
|
||||
while (($read = $inputStream.Read($buffer, 0, $buffer.Length)) -gt 0) {
|
||||
$destinationStream.Write($buffer, 0, $read)
|
||||
$incremental.AppendData($buffer, 0, $read)
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$inputStream.Dispose()
|
||||
}
|
||||
}
|
||||
$destinationStream.Flush($true)
|
||||
$actualStreamSha = ([BitConverter]::ToString(
|
||||
$incremental.GetHashAndReset()
|
||||
)).Replace('-', '').ToLowerInvariant()
|
||||
}
|
||||
finally {
|
||||
$incremental.Dispose()
|
||||
$destinationStream.Dispose()
|
||||
}
|
||||
if ($actualStreamSha -ne $streamSha) {
|
||||
throw "Reconstructed stream SHA-256 changed: $actualStreamSha"
|
||||
}
|
||||
Move-Item -LiteralPath $inputTemp -Destination $inputPath
|
||||
}
|
||||
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& docker image inspect $image *> $null
|
||||
$imageCached = $LASTEXITCODE -eq 0
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if ($imageCached) {
|
||||
Write-Host 'Using the exact cached NVIDIA DeepStream image.'
|
||||
}
|
||||
else {
|
||||
Write-Host 'Pulling exact NVIDIA DeepStream image in the interactive user session...'
|
||||
Invoke-Docker @('pull', $image) 'DeepStream image pull'
|
||||
}
|
||||
|
||||
$runId = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ')
|
||||
$runRoot = Join-Path $runsRoot $runId
|
||||
$rawRoot = Join-Path $runRoot 'raw'
|
||||
$inputMount = Join-Path $runRoot 'input'
|
||||
New-Item -ItemType Directory -Force -Path $rawRoot | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $rawRoot 'detections') | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $rawRoot 'tracks') | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $inputMount | Out-Null
|
||||
Copy-Item -LiteralPath $inputPath -Destination (Join-Path $inputMount 'right.mp4')
|
||||
$deepstreamLog = Join-Path $rawRoot 'deepstream.log'
|
||||
$trackerCopy = Join-Path $rawRoot 'tracker-config.yml'
|
||||
$startedAt = (Get-Date).ToUniversalTime().ToString('o')
|
||||
|
||||
$containerCommand = @"
|
||||
set -euo pipefail
|
||||
cp /opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml /workspace/output/tracker-config.yml
|
||||
deepstream-app -c /workspace/package/runtime/e46f_deepstream_app.txt
|
||||
"@
|
||||
$dockerArguments = @(
|
||||
'run', '--rm', '--name', "ndc-mission-core-deepstream-e46f-$runId",
|
||||
'--gpus', 'all', '--network', 'none', '--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges', '--shm-size', '4g',
|
||||
'--label', 'com.nodedc.product=mission-core',
|
||||
'--label', 'com.nodedc.stack=perception',
|
||||
'--label', 'com.nodedc.role=deepstream-dashcam-bakeoff-e46f',
|
||||
'--label', 'com.nodedc.managed-by=mission-core-worker',
|
||||
'--mount', "type=bind,src=$inputMount,dst=/workspace/input,readonly",
|
||||
'--mount', "type=bind,src=$package,dst=/workspace/package,readonly",
|
||||
'--mount', "type=bind,src=$modelsRoot,dst=/workspace/model",
|
||||
'--mount', "type=bind,src=$rawRoot,dst=/workspace/output",
|
||||
'--entrypoint', '/bin/bash', $image, '-lc', $containerCommand
|
||||
)
|
||||
Write-Host 'Running full 4489-frame NVIDIA DashCamNet + NvDCF bake-off...'
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& docker @dockerArguments 2>&1 | Tee-Object -LiteralPath $deepstreamLog
|
||||
$deepstreamExit = $LASTEXITCODE
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if ($deepstreamExit -ne 0) {
|
||||
throw "DeepStream replay failed with exit code $deepstreamExit"
|
||||
}
|
||||
$invalidOutputBinding = Select-String `
|
||||
-LiteralPath $deepstreamLog `
|
||||
-Pattern 'Could not find output layer','Given invalid tensor name' `
|
||||
-SimpleMatch `
|
||||
-Quiet
|
||||
if ($invalidOutputBinding) {
|
||||
throw 'DeepStream accepted the process but rejected the configured detector output bindings.'
|
||||
}
|
||||
|
||||
$overlayPath = Join-Path $rawRoot 'overlay.mp4'
|
||||
$enginePath = Join-Path $modelsRoot "$modelFile`_b1_gpu0_fp16.engine"
|
||||
if (-not (Test-Path -LiteralPath $overlayPath -PathType Leaf) -or
|
||||
(Get-Item -LiteralPath $overlayPath).Length -eq 0) {
|
||||
throw 'DeepStream did not produce an overlay video.'
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $enginePath -PathType Leaf)) {
|
||||
throw 'DeepStream did not produce the exact TensorRT engine.'
|
||||
}
|
||||
$detectionFiles = @(Get-ChildItem -LiteralPath (Join-Path $rawRoot 'detections') -File)
|
||||
$trackFiles = @(Get-ChildItem -LiteralPath (Join-Path $rawRoot 'tracks') -File)
|
||||
if ($detectionFiles.Count -ne [int]$profile.source.segment_count -or
|
||||
$trackFiles.Count -ne [int]$profile.source.segment_count) {
|
||||
throw "DeepStream frame coverage changed: detections=$($detectionFiles.Count), tracks=$($trackFiles.Count)"
|
||||
}
|
||||
$runtime = [ordered]@{
|
||||
schema_version = 'missioncore.e46f-dashcam-deepstream-runtime/v1'
|
||||
status = 'completed'
|
||||
worker_host = $env:COMPUTERNAME
|
||||
gpu_name = ((& nvidia-smi --query-gpu=name --format=csv,noheader | Select-Object -First 1).Trim())
|
||||
started_at_utc = $startedAt
|
||||
completed_at_utc = (Get-Date).ToUniversalTime().ToString('o')
|
||||
container_image = $image
|
||||
container_image_digest = $imageDigest
|
||||
model_sha256 = Get-Sha256 $modelPath
|
||||
model_engine_sha256 = Get-Sha256 $enginePath
|
||||
deepstream_config_sha256 = Get-Sha256 (Join-Path $package 'runtime\e46f_deepstream_app.txt')
|
||||
detector_config_sha256 = Get-Sha256 (Join-Path $package 'runtime\e46f_dashcamnet_detectnet.txt')
|
||||
tracker_config_sha256 = Get-Sha256 $trackerCopy
|
||||
input_stream_sha256 = Get-Sha256 $inputPath
|
||||
overlay_sha256 = Get-Sha256 $overlayPath
|
||||
frame_count = [int]$profile.source.segment_count
|
||||
deepstream_exit_code = $deepstreamExit
|
||||
}
|
||||
$runtimePath = Join-Path $rawRoot 'runtime.json'
|
||||
$runtime | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $runtimePath -Encoding UTF8
|
||||
|
||||
$consolidatorImage = 'nvcr.io/nvidia/tritonserver:26.06-py3@sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794'
|
||||
$consolidatorArguments = @(
|
||||
'run', '--rm', '--name', "ndc-mission-core-e46f-consolidator-$runId",
|
||||
'--network', 'none', '--read-only', '--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges', '--tmpfs', '/tmp:rw,noexec,nosuid,size=64m',
|
||||
'--mount', "type=bind,src=$package,dst=/workspace/package,readonly",
|
||||
'--mount', "type=bind,src=$sourceJob,dst=/workspace/source-job,readonly",
|
||||
'--mount', "type=bind,src=$rawRoot,dst=/workspace/raw,readonly",
|
||||
'--mount', "type=bind,src=$resultsRoot,dst=/workspace/results",
|
||||
'-e', 'PYTHONPATH=/workspace/package/runtime',
|
||||
'-e', 'PYTHONDONTWRITEBYTECODE=1',
|
||||
$consolidatorImage,
|
||||
'python3', '/workspace/package/runtime/run_e46f_dashcam_bakeoff.py',
|
||||
'--source-job', '/workspace/source-job',
|
||||
'--raw-root', '/workspace/raw',
|
||||
'--profile', '/workspace/package/profile.json',
|
||||
'--output-root', '/workspace/results'
|
||||
)
|
||||
Write-Host 'Freezing immutable E46F evidence...'
|
||||
Invoke-Docker $consolidatorArguments 'E46F consolidation'
|
||||
Write-Host "E46F_DASHCAM_BAKEOFF_COMPLETED run=$runId results=$resultsRoot"
|
||||
}
|
||||
finally {
|
||||
if ($LogPath) { Stop-Transcript | Out-Null }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PackageRoot,
|
||||
|
||||
[string]$SourceJobRoot = 'D:\NDC_MISSIONCORE\runtime\jobs\recorded-camera-602ac89026ed12978619801d',
|
||||
|
||||
[string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46f'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$taskName = 'MissionCore-E46FDashCamBakeoff'
|
||||
$package = (Resolve-Path -LiteralPath $PackageRoot).Path
|
||||
$script = Join-Path $package 'runtime\Invoke-E46FDashCamBakeoff.ps1'
|
||||
if (-not (Test-Path -LiteralPath $script -PathType Leaf)) {
|
||||
throw "E46F runner is missing: $script"
|
||||
}
|
||||
$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||
if ($existing -and $existing.State -eq 'Running') {
|
||||
throw "$taskName is already running."
|
||||
}
|
||||
$logsRoot = Join-Path $RuntimeRoot 'logs'
|
||||
New-Item -ItemType Directory -Force -Path $logsRoot | Out-Null
|
||||
$stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ')
|
||||
$logPath = Join-Path $logsRoot "e46f-dashcam-bakeoff-$stamp.log"
|
||||
$powerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||
$arguments = @(
|
||||
'-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
|
||||
'-File', "`"$script`"",
|
||||
'-PackageRoot', "`"$package`"",
|
||||
'-SourceJobRoot', "`"$SourceJobRoot`"",
|
||||
'-RuntimeRoot', "`"$RuntimeRoot`"",
|
||||
'-LogPath', "`"$logPath`""
|
||||
) -join ' '
|
||||
$userId = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
$action = New-ScheduledTaskAction -Execute $powerShell -Argument $arguments -WorkingDirectory $package
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited
|
||||
$trigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).AddMinutes(30))
|
||||
$settings = New-ScheduledTaskSettingsSet `
|
||||
-AllowStartIfOnBatteries `
|
||||
-DontStopIfGoingOnBatteries `
|
||||
-StartWhenAvailable `
|
||||
-ExecutionTimeLimit ([TimeSpan]::FromHours(6))
|
||||
Register-ScheduledTask `
|
||||
-TaskName $taskName `
|
||||
-Action $action `
|
||||
-Principal $principal `
|
||||
-Trigger $trigger `
|
||||
-Settings $settings `
|
||||
-Description 'One-shot Mission Core E46F stock NVIDIA DashCamNet plus NvDCF detector-only bake-off.' `
|
||||
-Force | Out-Null
|
||||
Start-ScheduledTask -TaskName $taskName
|
||||
Write-Host "E46F_TASK_STARTED task=$taskName log=$logPath"
|
||||
@@ -0,0 +1,440 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PackageRoot,
|
||||
|
||||
[string]$SourceJobRoot = 'D:\NDC_MISSIONCORE\runtime\jobs\recorded-camera-602ac89026ed12978619801d',
|
||||
|
||||
[string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46g',
|
||||
|
||||
[string]$LogPath = ''
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
function Get-Sha256([string]$Path) {
|
||||
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
}
|
||||
|
||||
function Assert-Sha256([string]$Path, [string]$Expected, [string]$Label) {
|
||||
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
||||
throw "$Label is missing: $Path"
|
||||
}
|
||||
$actual = Get-Sha256 $Path
|
||||
if ($actual -ne $Expected) {
|
||||
throw "$Label SHA-256 changed: expected $Expected, got $actual"
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Docker([string[]]$Arguments, [string]$Label) {
|
||||
& docker @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "$Label failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-VideoFrameCount([string]$Path) {
|
||||
$probe = & ffprobe -v error -select_streams v:0 -count_frames `
|
||||
-show_entries stream=nb_read_frames -of json $Path | ConvertFrom-Json
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "ffprobe failed: $Path"
|
||||
}
|
||||
return [int]@($probe.streams)[0].nb_read_frames
|
||||
}
|
||||
|
||||
function Ensure-Model(
|
||||
[pscustomobject]$Candidate,
|
||||
[string]$ModelRoot,
|
||||
[string]$Label
|
||||
) {
|
||||
New-Item -ItemType Directory -Force -Path $ModelRoot | Out-Null
|
||||
$modelPath = Join-Path $ModelRoot ([string]$Candidate.model_file)
|
||||
if (Test-Path -LiteralPath $modelPath -PathType Leaf) {
|
||||
Assert-Sha256 $modelPath ([string]$Candidate.model_sha256) $Label
|
||||
return $modelPath
|
||||
}
|
||||
$temporary = "$modelPath.$([Guid]::NewGuid().ToString('N')).download"
|
||||
& curl.exe --fail --location --retry 3 --output $temporary ([string]$Candidate.model_url)
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "$Label download failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
Assert-Sha256 $temporary ([string]$Candidate.model_sha256) "downloaded $Label"
|
||||
Move-Item -LiteralPath $temporary -Destination $modelPath
|
||||
return $modelPath
|
||||
}
|
||||
|
||||
$package = (Resolve-Path -LiteralPath $PackageRoot).Path
|
||||
$sourceJob = (Resolve-Path -LiteralPath $SourceJobRoot).Path
|
||||
$profilePath = Join-Path $package 'profile.json'
|
||||
$manifestPath = Join-Path $package 'manifest.json'
|
||||
if (-not (Test-Path -LiteralPath $profilePath -PathType Leaf) -or
|
||||
-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) {
|
||||
throw 'E46G package is incomplete.'
|
||||
}
|
||||
$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
|
||||
if ($manifest.schema_version -ne 'missioncore.e46g-worker-package/v1' -or
|
||||
$manifest.package_id -ne (Split-Path -Leaf $package)) {
|
||||
throw 'E46G package identity is invalid.'
|
||||
}
|
||||
$expectedPaths = @($manifest.identity.artifact_paths)
|
||||
foreach ($artifact in @($manifest.artifacts)) {
|
||||
if ($expectedPaths -notcontains [string]$artifact.path) {
|
||||
throw "Unexpected E46G package artifact: $($artifact.path)"
|
||||
}
|
||||
$artifactPath = Join-Path $package ([string]$artifact.path)
|
||||
Assert-Sha256 $artifactPath ([string]$artifact.sha256) "package artifact $($artifact.path)"
|
||||
if ((Get-Item -LiteralPath $artifactPath).Length -ne [int64]$artifact.byte_length) {
|
||||
throw "Package artifact length changed: $($artifact.path)"
|
||||
}
|
||||
}
|
||||
$actualPaths = @(Get-ChildItem -LiteralPath $package -Recurse -File | ForEach-Object {
|
||||
$_.FullName.Substring($package.Length + 1).Replace('\', '/')
|
||||
})
|
||||
if (@($actualPaths | Where-Object { $_ -ne 'manifest.json' -and $expectedPaths -notcontains $_ }).Count -ne 0 -or
|
||||
@($expectedPaths | Where-Object { $actualPaths -notcontains $_ }).Count -ne 0) {
|
||||
throw 'E46G package file set changed.'
|
||||
}
|
||||
|
||||
$profile = Get-Content -LiteralPath $profilePath -Raw | ConvertFrom-Json
|
||||
if ($profile.schema_version -ne 'missioncore.e46g-rectified-detector-bakeoff-profile/v1' -or
|
||||
$profile.source.camera_source_id -ne 'sensor.camera.right' -or
|
||||
$profile.rectification.provider -ne 'NVIDIA Gst-nvdewarper') {
|
||||
throw 'E46G profile is incompatible.'
|
||||
}
|
||||
$image = [string]$profile.runtime.container_image
|
||||
$imageDigestMatch = [regex]::Match($image, '@sha256:([0-9a-f]{64})$')
|
||||
if (-not $imageDigestMatch.Success) {
|
||||
throw 'E46G runtime image must be pinned by a full SHA-256 digest.'
|
||||
}
|
||||
$imageDigest = $imageDigestMatch.Groups[1].Value
|
||||
$streamSha = [string]$profile.source.stream_sha256
|
||||
$sampleFrameCount = [int]$profile.selection.frame_count
|
||||
$firstSourceFrame = [int]$profile.selection.first_source_frame_index
|
||||
$lastSourceFrame = [int]$profile.selection.last_source_frame_index
|
||||
$expectedFullFrameCount = [int]$profile.rectification.expected_full_frame_count
|
||||
$retainedSourceRange = @($profile.rectification.retained_source_frame_index_range)
|
||||
if ($retainedSourceRange.Count -ne 2 -or
|
||||
$firstSourceFrame -lt [int]$retainedSourceRange[0] -or
|
||||
$lastSourceFrame -gt [int]$retainedSourceRange[1]) {
|
||||
throw 'E46G selection is outside the admitted NVIDIA-decoded source prefix.'
|
||||
}
|
||||
$parserPath = Join-Path $package "runtime\$([string]$profile.trafficcamnet_parser.library_file)"
|
||||
Assert-Sha256 $parserPath ([string]$profile.trafficcamnet_parser.library_sha256) `
|
||||
'official NVIDIA DeepStream TAO parser'
|
||||
|
||||
if (-not (Get-Command ffmpeg -ErrorAction SilentlyContinue) -or
|
||||
-not (Get-Command ffprobe -ErrorAction SilentlyContinue)) {
|
||||
throw 'E46G requires the existing Worker ffmpeg/ffprobe installation.'
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $RuntimeRoot | Out-Null
|
||||
$inputsRoot = Join-Path $RuntimeRoot 'inputs'
|
||||
$runsRoot = Join-Path $RuntimeRoot 'runs'
|
||||
$resultsRoot = Join-Path $RuntimeRoot 'rectified-detector-bakeoff-results'
|
||||
foreach ($path in @($inputsRoot, $runsRoot, $resultsRoot)) {
|
||||
New-Item -ItemType Directory -Force -Path $path | Out-Null
|
||||
}
|
||||
if ($LogPath) {
|
||||
$logParent = Split-Path -Parent $LogPath
|
||||
if ($logParent) { New-Item -ItemType Directory -Force -Path $logParent | Out-Null }
|
||||
Start-Transcript -LiteralPath $LogPath -Append | Out-Null
|
||||
}
|
||||
|
||||
try {
|
||||
$jobPath = Join-Path $sourceJob 'job.json'
|
||||
$job = Get-Content -LiteralPath $jobPath -Raw | ConvertFrom-Json
|
||||
if ($job.job_id -ne $profile.source.job_id -or
|
||||
$job.input.archive_index_sha256 -ne $profile.source.archive_index_sha256 -or
|
||||
$job.input.archive_summary_sha256 -ne $profile.source.archive_summary_sha256) {
|
||||
throw 'Exact E46G source job binding changed.'
|
||||
}
|
||||
$inputPath = Join-Path $inputsRoot "right-$streamSha.mp4"
|
||||
$e46eInput = "D:\NDC_MISSIONCORE\runtime\experiments\e46e\inputs\right-$streamSha.mp4"
|
||||
$e46fInput = "D:\NDC_MISSIONCORE\runtime\experiments\e46f\inputs\right-$streamSha.mp4"
|
||||
if (Test-Path -LiteralPath $inputPath -PathType Leaf) {
|
||||
Assert-Sha256 $inputPath $streamSha 'E46G controlled RIGHT stream'
|
||||
}
|
||||
elseif (Test-Path -LiteralPath $e46eInput -PathType Leaf) {
|
||||
Assert-Sha256 $e46eInput $streamSha 'E46E controlled RIGHT stream'
|
||||
Copy-Item -LiteralPath $e46eInput -Destination $inputPath
|
||||
}
|
||||
elseif (Test-Path -LiteralPath $e46fInput -PathType Leaf) {
|
||||
Assert-Sha256 $e46fInput $streamSha 'E46F controlled RIGHT stream'
|
||||
Copy-Item -LiteralPath $e46fInput -Destination $inputPath
|
||||
}
|
||||
else {
|
||||
& (Join-Path $package 'runtime\Prepare-RectifiedCameraReplay.ps1') `
|
||||
-JobRoot $sourceJob -OutputPath $inputPath
|
||||
}
|
||||
Assert-Sha256 $inputPath $streamSha 'E46G reconstructed RIGHT stream'
|
||||
|
||||
$trafficModelRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46e\models\trafficcamnet_transformer_lite\deployable_resnet50_v2.0'
|
||||
$dashModelRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46f\models\dashcamnet\pruned_onnx_v1.0.4'
|
||||
$trafficModel = Ensure-Model $profile.candidates.trafficcamnet $trafficModelRoot 'TrafficCamNet model'
|
||||
$dashModel = Ensure-Model $profile.candidates.dashcamnet $dashModelRoot 'DashCamNet model'
|
||||
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& docker image inspect $image *> $null
|
||||
$imageCached = $LASTEXITCODE -eq 0
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if (-not $imageCached) {
|
||||
Invoke-Docker @('pull', $image) 'DeepStream image pull'
|
||||
}
|
||||
|
||||
$runId = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ')
|
||||
$runRoot = Join-Path $runsRoot $runId
|
||||
$rawRoot = Join-Path $runRoot 'raw'
|
||||
$sourceInputMount = Join-Path $runRoot 'source-input'
|
||||
$geometryRoot = Join-Path $rawRoot 'geometry'
|
||||
$samplesRoot = Join-Path $rawRoot 'samples'
|
||||
$comparisonRoot = Join-Path $rawRoot 'comparison'
|
||||
foreach ($path in @(
|
||||
$rawRoot,
|
||||
$sourceInputMount,
|
||||
$geometryRoot,
|
||||
$samplesRoot,
|
||||
$comparisonRoot
|
||||
)) {
|
||||
New-Item -ItemType Directory -Force -Path $path | Out-Null
|
||||
}
|
||||
Copy-Item -LiteralPath $inputPath -Destination (Join-Path $sourceInputMount 'right.mp4')
|
||||
$workerLog = Join-Path $rawRoot 'worker.log'
|
||||
"E46G run $runId`nsource=$streamSha`nselection=$firstSourceFrame..$lastSourceFrame" |
|
||||
Set-Content -LiteralPath $workerLog -Encoding UTF8
|
||||
$startedAt = (Get-Date).ToUniversalTime().ToString('o')
|
||||
|
||||
$geometryRuntime = [ordered]@{}
|
||||
foreach ($view in @('left', 'front', 'right')) {
|
||||
$viewProfile = $profile.rectification.views.$view
|
||||
$configName = [string]$viewProfile.config_file
|
||||
$configPath = Join-Path $package "runtime\$configName"
|
||||
Assert-Sha256 $configPath ([string]$viewProfile.config_sha256) "$view dewarper config"
|
||||
$outputPath = Join-Path $geometryRoot "$view.mp4"
|
||||
$containerCommand = @"
|
||||
set -euo pipefail
|
||||
gst-launch-1.0 -e filesrc location=/workspace/input/right.mp4 ! qtdemux ! h264parse ! nvv4l2decoder ! nvvideoconvert ! 'video/x-raw(memory:NVMM),format=RGBA' ! nvdewarper config-file=/workspace/package/runtime/$configName source-id=0 num-batch-buffers=1 ! nvvideoconvert ! 'video/x-raw(memory:NVMM),format=NV12' ! nvv4l2h264enc bitrate=6000000 ! h264parse ! qtmux ! filesink location=/workspace/output/$view.mp4
|
||||
"@
|
||||
$dewarperLog = Join-Path $geometryRoot "$view.log"
|
||||
$dockerArguments = @(
|
||||
'run', '--rm', '--name', "ndc-mission-core-e46g-dewarper-$view-$runId",
|
||||
'--gpus', 'all', '--network', 'none', '--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges', '--shm-size', '4g',
|
||||
'--label', 'com.nodedc.product=mission-core',
|
||||
'--label', 'com.nodedc.stack=perception',
|
||||
'--label', 'com.nodedc.role=nvdewarper-e46g',
|
||||
'--mount', "type=bind,src=$sourceInputMount,dst=/workspace/input,readonly",
|
||||
'--mount', "type=bind,src=$package,dst=/workspace/package,readonly",
|
||||
'--mount', "type=bind,src=$geometryRoot,dst=/workspace/output",
|
||||
'--entrypoint', '/bin/bash', $image, '-lc', $containerCommand
|
||||
)
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& docker @dockerArguments 2>&1 | Tee-Object -LiteralPath $dewarperLog
|
||||
$dewarperExit = $LASTEXITCODE
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if ($dewarperExit -ne 0 -or -not (Test-Path -LiteralPath $outputPath -PathType Leaf)) {
|
||||
throw "NVIDIA nvdewarper failed for $view with exit code $dewarperExit"
|
||||
}
|
||||
if ((Get-VideoFrameCount $outputPath) -ne $expectedFullFrameCount) {
|
||||
throw "Full rectified $view frame coverage changed."
|
||||
}
|
||||
|
||||
$samplePath = Join-Path $samplesRoot "$view.mp4"
|
||||
$filter = "select='between(n\,$firstSourceFrame\,$lastSourceFrame)',setpts=N/(10*TB)"
|
||||
& ffmpeg -hide_banner -loglevel error -y -i $outputPath -vf $filter -an `
|
||||
-c:v libx264 -preset fast -crf 18 -pix_fmt yuv420p -r 10 $samplePath
|
||||
if ($LASTEXITCODE -ne 0 -or (Get-VideoFrameCount $samplePath) -ne $sampleFrameCount) {
|
||||
throw "Exact E46G sample extraction failed for $view."
|
||||
}
|
||||
$geometryRuntime[$view] = [ordered]@{
|
||||
dewarper_config_sha256 = Get-Sha256 $configPath
|
||||
dewarper_log_sha256 = Get-Sha256 $dewarperLog
|
||||
full_rectified_video_path = "geometry/$view.mp4"
|
||||
full_rectified_video_sha256 = Get-Sha256 $outputPath
|
||||
sample_video_path = "samples/$view.mp4"
|
||||
sample_video_sha256 = Get-Sha256 $samplePath
|
||||
full_frame_count = $expectedFullFrameCount
|
||||
retained_source_frame_index_range = @(
|
||||
[int]$retainedSourceRange[0],
|
||||
[int]$retainedSourceRange[1]
|
||||
)
|
||||
excluded_source_tail_frame_count = [int]$profile.rectification.excluded_source_tail_frame_count
|
||||
sample_frame_count = $sampleFrameCount
|
||||
}
|
||||
}
|
||||
|
||||
$candidateRuntime = [ordered]@{}
|
||||
$candidateDefinitions = @(
|
||||
[pscustomobject]@{
|
||||
Name = 'trafficcamnet'
|
||||
ModelRoot = $trafficModelRoot
|
||||
ModelPath = $trafficModel
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'dashcamnet'
|
||||
ModelRoot = $dashModelRoot
|
||||
ModelPath = $dashModel
|
||||
}
|
||||
)
|
||||
foreach ($definition in $candidateDefinitions) {
|
||||
$candidate = [string]$definition.Name
|
||||
$candidateProfile = $profile.candidates.$candidate
|
||||
$appConfigPath = Join-Path $package "runtime\$([string]$candidateProfile.deepstream_app_config)"
|
||||
$detectorConfigPath = Join-Path $package "runtime\$([string]$candidateProfile.detector_config)"
|
||||
Assert-Sha256 $appConfigPath ([string]$candidateProfile.deepstream_app_config_sha256) `
|
||||
"$candidate DeepStream app config"
|
||||
Assert-Sha256 $detectorConfigPath ([string]$candidateProfile.detector_config_sha256) `
|
||||
"$candidate detector config"
|
||||
$runs = [ordered]@{}
|
||||
foreach ($view in @('left', 'front', 'right')) {
|
||||
$viewRoot = Join-Path $rawRoot "runs\$candidate\$view"
|
||||
$inputMount = Join-Path $viewRoot 'input'
|
||||
foreach ($path in @(
|
||||
$viewRoot,
|
||||
$inputMount,
|
||||
(Join-Path $viewRoot 'detections'),
|
||||
(Join-Path $viewRoot 'tracks')
|
||||
)) {
|
||||
New-Item -ItemType Directory -Force -Path $path | Out-Null
|
||||
}
|
||||
Copy-Item -LiteralPath (Join-Path $samplesRoot "$view.mp4") `
|
||||
-Destination (Join-Path $inputMount 'view.mp4')
|
||||
$deepstreamLog = Join-Path $viewRoot 'deepstream.log'
|
||||
$containerCommand = @"
|
||||
set -euo pipefail
|
||||
cp /opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml /workspace/output/tracker-config.yml
|
||||
deepstream-app -c /workspace/package/runtime/$([string]$candidateProfile.deepstream_app_config)
|
||||
"@
|
||||
$dockerArguments = @(
|
||||
'run', '--rm', '--name', "ndc-mission-core-e46g-$candidate-$view-$runId",
|
||||
'--gpus', 'all', '--network', 'none', '--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges', '--shm-size', '4g',
|
||||
'--label', 'com.nodedc.product=mission-core',
|
||||
'--label', 'com.nodedc.stack=perception',
|
||||
'--label', "com.nodedc.role=e46g-$candidate-$view",
|
||||
'--mount', "type=bind,src=$inputMount,dst=/workspace/input,readonly",
|
||||
'--mount', "type=bind,src=$package,dst=/workspace/package,readonly",
|
||||
'--mount', "type=bind,src=$($definition.ModelRoot),dst=/workspace/model",
|
||||
'--mount', "type=bind,src=$viewRoot,dst=/workspace/output",
|
||||
'--entrypoint', '/bin/bash', $image, '-lc', $containerCommand
|
||||
)
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& docker @dockerArguments 2>&1 | Tee-Object -LiteralPath $deepstreamLog
|
||||
$deepstreamExit = $LASTEXITCODE
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if ($deepstreamExit -ne 0) {
|
||||
throw "DeepStream failed for $candidate/$view with exit code $deepstreamExit"
|
||||
}
|
||||
$overlayPath = Join-Path $viewRoot 'overlay.mp4'
|
||||
$trackerPath = Join-Path $viewRoot 'tracker-config.yml'
|
||||
$detections = @(Get-ChildItem -LiteralPath (Join-Path $viewRoot 'detections') -File)
|
||||
$tracks = @(Get-ChildItem -LiteralPath (Join-Path $viewRoot 'tracks') -File)
|
||||
if (-not (Test-Path -LiteralPath $overlayPath -PathType Leaf) -or
|
||||
$detections.Count -ne $sampleFrameCount -or $tracks.Count -ne $sampleFrameCount) {
|
||||
throw "DeepStream output coverage changed for $candidate/$view."
|
||||
}
|
||||
$enginePath = "$($definition.ModelPath)_b1_gpu0_fp16.engine"
|
||||
if (-not (Test-Path -LiteralPath $enginePath -PathType Leaf)) {
|
||||
throw "TensorRT engine is missing for $candidate."
|
||||
}
|
||||
$runs[$view] = [ordered]@{
|
||||
overlay_path = "runs/$candidate/$view/overlay.mp4"
|
||||
overlay_sha256 = Get-Sha256 $overlayPath
|
||||
deepstream_log_path = "runs/$candidate/$view/deepstream.log"
|
||||
deepstream_log_sha256 = Get-Sha256 $deepstreamLog
|
||||
tracker_config_sha256 = Get-Sha256 $trackerPath
|
||||
model_engine_sha256 = Get-Sha256 $enginePath
|
||||
frame_count = $sampleFrameCount
|
||||
deepstream_exit_code = $deepstreamExit
|
||||
}
|
||||
}
|
||||
$candidateRuntime[$candidate] = [ordered]@{
|
||||
model_sha256 = Get-Sha256 $definition.ModelPath
|
||||
deepstream_app_config_sha256 = Get-Sha256 $appConfigPath
|
||||
detector_config_sha256 = Get-Sha256 $detectorConfigPath
|
||||
parser_library_sha256 = $(if ($candidate -eq 'trafficcamnet') {
|
||||
Get-Sha256 $parserPath
|
||||
} else { $null })
|
||||
runs = $runs
|
||||
}
|
||||
}
|
||||
|
||||
$comparisonRuntime = [ordered]@{}
|
||||
foreach ($candidate in @('trafficcamnet', 'dashcamnet')) {
|
||||
$left = Join-Path $rawRoot "runs\$candidate\left\overlay.mp4"
|
||||
$front = Join-Path $rawRoot "runs\$candidate\front\overlay.mp4"
|
||||
$right = Join-Path $rawRoot "runs\$candidate\right\overlay.mp4"
|
||||
$output = Join-Path $comparisonRoot "$candidate.mp4"
|
||||
& ffmpeg -hide_banner -loglevel error -y -i $left -i $front -i $right `
|
||||
-filter_complex '[0:v][1:v][2:v]hstack=inputs=3[v]' -map '[v]' -an `
|
||||
-c:v libx264 -preset fast -crf 20 -pix_fmt yuv420p -movflags +faststart $output
|
||||
if ($LASTEXITCODE -ne 0 -or (Get-VideoFrameCount $output) -ne $sampleFrameCount) {
|
||||
throw "E46G synchronized comparison video failed for $candidate."
|
||||
}
|
||||
$comparisonRuntime[$candidate] = [ordered]@{
|
||||
video_path = "comparison/$candidate.mp4"
|
||||
video_sha256 = Get-Sha256 $output
|
||||
frame_count = $sampleFrameCount
|
||||
view_order = @('left', 'front', 'right')
|
||||
}
|
||||
}
|
||||
|
||||
$runtime = [ordered]@{
|
||||
schema_version = 'missioncore.e46g-rectified-detector-runtime/v1'
|
||||
status = 'completed'
|
||||
worker_host = $env:COMPUTERNAME
|
||||
gpu_name = ((& nvidia-smi --query-gpu=name --format=csv,noheader | Select-Object -First 1).Trim())
|
||||
started_at_utc = $startedAt
|
||||
completed_at_utc = (Get-Date).ToUniversalTime().ToString('o')
|
||||
container_image = $image
|
||||
container_image_digest = $imageDigest
|
||||
source_stream_sha256 = Get-Sha256 $inputPath
|
||||
first_source_frame_index = $firstSourceFrame
|
||||
sample_frame_count = $sampleFrameCount
|
||||
geometry = $geometryRuntime
|
||||
candidates = $candidateRuntime
|
||||
comparison = $comparisonRuntime
|
||||
}
|
||||
$runtime | ConvertTo-Json -Depth 16 |
|
||||
Set-Content -LiteralPath (Join-Path $rawRoot 'runtime.json') -Encoding UTF8
|
||||
Add-Content -LiteralPath $workerLog -Value "completed=$(Get-Date -Format o)"
|
||||
|
||||
$consolidatorImage = 'nvcr.io/nvidia/tritonserver:26.06-py3@sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794'
|
||||
$consolidatorArguments = @(
|
||||
'run', '--rm', '--name', "ndc-mission-core-e46g-consolidator-$runId",
|
||||
'--network', 'none', '--read-only', '--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges', '--tmpfs', '/tmp:rw,noexec,nosuid,size=64m',
|
||||
'--mount', "type=bind,src=$package,dst=/workspace/package,readonly",
|
||||
'--mount', "type=bind,src=$sourceJob,dst=/workspace/source-job,readonly",
|
||||
'--mount', "type=bind,src=$rawRoot,dst=/workspace/raw,readonly",
|
||||
'--mount', "type=bind,src=$resultsRoot,dst=/workspace/results",
|
||||
'-e', 'PYTHONPATH=/workspace/package/runtime',
|
||||
'-e', 'PYTHONDONTWRITEBYTECODE=1',
|
||||
$consolidatorImage,
|
||||
'python3', '/workspace/package/runtime/run_e46g_rectified_detector_bakeoff.py',
|
||||
'--source-job', '/workspace/source-job',
|
||||
'--raw-root', '/workspace/raw',
|
||||
'--profile', '/workspace/package/profile.json',
|
||||
'--output-root', '/workspace/results'
|
||||
)
|
||||
Invoke-Docker $consolidatorArguments 'E46G consolidation'
|
||||
Write-Host "E46G_RECTIFIED_DETECTOR_BAKEOFF_COMPLETED run=$runId results=$resultsRoot"
|
||||
}
|
||||
finally {
|
||||
if ($LogPath) { Stop-Transcript | Out-Null }
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PackageRoot,
|
||||
|
||||
[string]$SourceJobRoot = 'D:\NDC_MISSIONCORE\runtime\jobs\recorded-camera-602ac89026ed12978619801d',
|
||||
|
||||
[string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46g'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$taskName = 'MissionCore-E46GRectifiedDetectorBakeoff'
|
||||
$package = (Resolve-Path -LiteralPath $PackageRoot).Path
|
||||
$script = Join-Path $package 'runtime\Invoke-E46GRectifiedDetectorBakeoff.ps1'
|
||||
if (-not (Test-Path -LiteralPath $script -PathType Leaf)) {
|
||||
throw "E46G runner is missing: $script"
|
||||
}
|
||||
$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||
if ($existing -and $existing.State -eq 'Running') {
|
||||
throw "$taskName is already running."
|
||||
}
|
||||
$logsRoot = Join-Path $RuntimeRoot 'logs'
|
||||
New-Item -ItemType Directory -Force -Path $logsRoot | Out-Null
|
||||
$stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ')
|
||||
$logPath = Join-Path $logsRoot "e46g-rectified-detector-bakeoff-$stamp.log"
|
||||
$powerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||
$arguments = @(
|
||||
'-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
|
||||
'-File', "`"$script`"",
|
||||
'-PackageRoot', "`"$package`"",
|
||||
'-SourceJobRoot', "`"$SourceJobRoot`"",
|
||||
'-RuntimeRoot', "`"$RuntimeRoot`"",
|
||||
'-LogPath', "`"$logPath`""
|
||||
) -join ' '
|
||||
$userId = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
$action = New-ScheduledTaskAction -Execute $powerShell -Argument $arguments -WorkingDirectory $package
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited
|
||||
$trigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).AddMinutes(30))
|
||||
$settings = New-ScheduledTaskSettingsSet `
|
||||
-AllowStartIfOnBatteries `
|
||||
-DontStopIfGoingOnBatteries `
|
||||
-StartWhenAvailable `
|
||||
-ExecutionTimeLimit ([TimeSpan]::FromHours(6))
|
||||
Register-ScheduledTask `
|
||||
-TaskName $taskName `
|
||||
-Action $action `
|
||||
-Principal $principal `
|
||||
-Trigger $trigger `
|
||||
-Settings $settings `
|
||||
-Description 'One-shot E46G factory-KB4 NVIDIA nvdewarper detector A/B.' `
|
||||
-Force | Out-Null
|
||||
Start-ScheduledTask -TaskName $taskName
|
||||
Write-Host "E46G_TASK_STARTED task=$taskName log=$logPath"
|
||||
@@ -0,0 +1,369 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PackageRoot,
|
||||
|
||||
[string]$SourceJobRoot = 'D:\NDC_MISSIONCORE\runtime\jobs\recorded-camera-602ac89026ed12978619801d',
|
||||
|
||||
[string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46h',
|
||||
|
||||
[string]$LogPath = ''
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
function Get-Sha256([string]$Path) {
|
||||
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
}
|
||||
|
||||
function Assert-Sha256([string]$Path, [string]$Expected, [string]$Label) {
|
||||
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
||||
throw "$Label is missing: $Path"
|
||||
}
|
||||
$actual = Get-Sha256 $Path
|
||||
if ($actual -ne $Expected) {
|
||||
throw "$Label SHA-256 changed: expected $Expected, got $actual"
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Docker([string[]]$Arguments, [string]$Label) {
|
||||
& docker @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "$Label failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-VideoFrameCount([string]$Path) {
|
||||
$probe = & ffprobe -v error -select_streams v:0 -count_frames `
|
||||
-show_entries stream=nb_read_frames -of json $Path | ConvertFrom-Json
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "ffprobe failed: $Path"
|
||||
}
|
||||
return [int]@($probe.streams)[0].nb_read_frames
|
||||
}
|
||||
|
||||
function Ensure-Model([pscustomobject]$Detector, [string]$ModelRoot) {
|
||||
New-Item -ItemType Directory -Force -Path $ModelRoot | Out-Null
|
||||
$modelPath = Join-Path $ModelRoot ([string]$Detector.model_file)
|
||||
if (Test-Path -LiteralPath $modelPath -PathType Leaf) {
|
||||
Assert-Sha256 $modelPath ([string]$Detector.model_sha256) 'TrafficCamNet model'
|
||||
return $modelPath
|
||||
}
|
||||
$temporary = "$modelPath.$([Guid]::NewGuid().ToString('N')).download"
|
||||
& curl.exe --fail --location --retry 3 --output $temporary ([string]$Detector.model_url)
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "TrafficCamNet download failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
Assert-Sha256 $temporary ([string]$Detector.model_sha256) 'downloaded TrafficCamNet model'
|
||||
Move-Item -LiteralPath $temporary -Destination $modelPath
|
||||
return $modelPath
|
||||
}
|
||||
|
||||
$package = (Resolve-Path -LiteralPath $PackageRoot).Path
|
||||
$sourceJob = (Resolve-Path -LiteralPath $SourceJobRoot).Path
|
||||
$profilePath = Join-Path $package 'profile.json'
|
||||
$manifestPath = Join-Path $package 'manifest.json'
|
||||
if (-not (Test-Path -LiteralPath $profilePath -PathType Leaf) -or
|
||||
-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) {
|
||||
throw 'E46H package is incomplete.'
|
||||
}
|
||||
$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
|
||||
if ($manifest.schema_version -ne 'missioncore.e46h-worker-package/v1' -or
|
||||
$manifest.package_id -ne (Split-Path -Leaf $package)) {
|
||||
throw 'E46H package identity is invalid.'
|
||||
}
|
||||
$expectedPaths = @($manifest.identity.artifact_paths)
|
||||
foreach ($artifact in @($manifest.artifacts)) {
|
||||
if ($expectedPaths -notcontains [string]$artifact.path) {
|
||||
throw "Unexpected E46H package artifact: $($artifact.path)"
|
||||
}
|
||||
$artifactPath = Join-Path $package ([string]$artifact.path)
|
||||
Assert-Sha256 $artifactPath ([string]$artifact.sha256) "package artifact $($artifact.path)"
|
||||
if ((Get-Item -LiteralPath $artifactPath).Length -ne [int64]$artifact.byte_length) {
|
||||
throw "Package artifact length changed: $($artifact.path)"
|
||||
}
|
||||
}
|
||||
$actualPaths = @(Get-ChildItem -LiteralPath $package -Recurse -File | ForEach-Object {
|
||||
$_.FullName.Substring($package.Length + 1).Replace('\', '/')
|
||||
})
|
||||
if (@($actualPaths | Where-Object { $_ -ne 'manifest.json' -and $expectedPaths -notcontains $_ }).Count -ne 0 -or
|
||||
@($expectedPaths | Where-Object { $actualPaths -notcontains $_ }).Count -ne 0) {
|
||||
throw 'E46H package file set changed.'
|
||||
}
|
||||
|
||||
$profile = Get-Content -LiteralPath $profilePath -Raw | ConvertFrom-Json
|
||||
if ($profile.schema_version -ne 'missioncore.e46h-full-rectified-front-replay-profile/v1' -or
|
||||
$profile.source.camera_source_id -ne 'sensor.camera.right' -or
|
||||
$profile.rectification.view -ne 'front' -or
|
||||
$profile.detector.name -ne 'NVIDIA TrafficCamNet Transformer Lite') {
|
||||
throw 'E46H profile is incompatible.'
|
||||
}
|
||||
$image = [string]$profile.runtime.container_image
|
||||
$imageDigestMatch = [regex]::Match($image, '@sha256:([0-9a-f]{64})$')
|
||||
if (-not $imageDigestMatch.Success) {
|
||||
throw 'E46H runtime image must be pinned by a full SHA-256 digest.'
|
||||
}
|
||||
$imageDigest = $imageDigestMatch.Groups[1].Value
|
||||
$streamSha = [string]$profile.source.stream_sha256
|
||||
$frameCount = [int]$profile.selection.frame_count
|
||||
if ($frameCount -ne 4488 -or
|
||||
[int]$profile.selection.first_source_frame_index -ne 0 -or
|
||||
[int]$profile.selection.last_source_frame_index -ne 4487) {
|
||||
throw 'E46H retained route contract changed.'
|
||||
}
|
||||
$parserPath = Join-Path $package "runtime\$([string]$profile.parser.library_file)"
|
||||
Assert-Sha256 $parserPath ([string]$profile.parser.library_sha256) `
|
||||
'official NVIDIA DeepStream TAO parser'
|
||||
|
||||
if (-not (Get-Command ffmpeg -ErrorAction SilentlyContinue) -or
|
||||
-not (Get-Command ffprobe -ErrorAction SilentlyContinue)) {
|
||||
throw 'E46H requires the existing Worker ffmpeg/ffprobe installation.'
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $RuntimeRoot | Out-Null
|
||||
$inputsRoot = Join-Path $RuntimeRoot 'inputs'
|
||||
$runsRoot = Join-Path $RuntimeRoot 'runs'
|
||||
$resultsRoot = Join-Path $RuntimeRoot 'full-rectified-front-results'
|
||||
foreach ($path in @($inputsRoot, $runsRoot, $resultsRoot)) {
|
||||
New-Item -ItemType Directory -Force -Path $path | Out-Null
|
||||
}
|
||||
if ($LogPath) {
|
||||
$logParent = Split-Path -Parent $LogPath
|
||||
if ($logParent) { New-Item -ItemType Directory -Force -Path $logParent | Out-Null }
|
||||
Start-Transcript -LiteralPath $LogPath -Append | Out-Null
|
||||
}
|
||||
|
||||
try {
|
||||
$jobPath = Join-Path $sourceJob 'job.json'
|
||||
$job = Get-Content -LiteralPath $jobPath -Raw | ConvertFrom-Json
|
||||
if ($job.job_id -ne $profile.source.job_id -or
|
||||
$job.input.archive_index_sha256 -ne $profile.source.archive_index_sha256 -or
|
||||
$job.input.archive_summary_sha256 -ne $profile.source.archive_summary_sha256) {
|
||||
throw 'Exact E46H source job binding changed.'
|
||||
}
|
||||
$inputPath = Join-Path $inputsRoot "right-$streamSha.mp4"
|
||||
$e46gInput = "D:\NDC_MISSIONCORE\runtime\experiments\e46g\inputs\right-$streamSha.mp4"
|
||||
$e46eInput = "D:\NDC_MISSIONCORE\runtime\experiments\e46e\inputs\right-$streamSha.mp4"
|
||||
if (Test-Path -LiteralPath $inputPath -PathType Leaf) {
|
||||
Assert-Sha256 $inputPath $streamSha 'E46H controlled RIGHT stream'
|
||||
}
|
||||
elseif (Test-Path -LiteralPath $e46gInput -PathType Leaf) {
|
||||
Assert-Sha256 $e46gInput $streamSha 'E46G controlled RIGHT stream'
|
||||
Copy-Item -LiteralPath $e46gInput -Destination $inputPath
|
||||
}
|
||||
elseif (Test-Path -LiteralPath $e46eInput -PathType Leaf) {
|
||||
Assert-Sha256 $e46eInput $streamSha 'E46E controlled RIGHT stream'
|
||||
Copy-Item -LiteralPath $e46eInput -Destination $inputPath
|
||||
}
|
||||
else {
|
||||
& (Join-Path $package 'runtime\Prepare-RectifiedCameraReplay.ps1') `
|
||||
-JobRoot $sourceJob -OutputPath $inputPath
|
||||
}
|
||||
Assert-Sha256 $inputPath $streamSha 'E46H reconstructed RIGHT stream'
|
||||
|
||||
$modelRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46e\models\trafficcamnet_transformer_lite\deployable_resnet50_v2.0'
|
||||
$modelPath = Ensure-Model $profile.detector $modelRoot
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& docker image inspect $image *> $null
|
||||
$imageCached = $LASTEXITCODE -eq 0
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if (-not $imageCached) {
|
||||
Invoke-Docker @('pull', $image) 'DeepStream image pull'
|
||||
}
|
||||
|
||||
$runId = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ')
|
||||
$runRoot = Join-Path $runsRoot $runId
|
||||
$rawRoot = Join-Path $runRoot 'raw'
|
||||
$sourceInputMount = Join-Path $runRoot 'source-input'
|
||||
$geometryRoot = Join-Path $rawRoot 'geometry'
|
||||
$runOutput = Join-Path $rawRoot 'run'
|
||||
$frontInputMount = Join-Path $runRoot 'front-input'
|
||||
foreach ($path in @(
|
||||
$rawRoot,
|
||||
$sourceInputMount,
|
||||
$geometryRoot,
|
||||
$runOutput,
|
||||
$frontInputMount,
|
||||
(Join-Path $runOutput 'detections'),
|
||||
(Join-Path $runOutput 'tracks')
|
||||
)) {
|
||||
New-Item -ItemType Directory -Force -Path $path | Out-Null
|
||||
}
|
||||
Copy-Item -LiteralPath $inputPath -Destination (Join-Path $sourceInputMount 'right.mp4')
|
||||
$workerLog = Join-Path $rawRoot 'worker.log'
|
||||
"E46H run $runId`nsource=$streamSha`nselection=0..4487`nview=front" |
|
||||
Set-Content -LiteralPath $workerLog -Encoding UTF8
|
||||
$startedAt = (Get-Date).ToUniversalTime().ToString('o')
|
||||
|
||||
$dewarperConfig = Join-Path $package "runtime\$([string]$profile.rectification.config_file)"
|
||||
Assert-Sha256 $dewarperConfig ([string]$profile.rectification.config_sha256) `
|
||||
'FRONT dewarper config'
|
||||
$frontPath = Join-Path $geometryRoot 'front.mp4'
|
||||
$dewarperLog = Join-Path $geometryRoot 'front.log'
|
||||
$dewarperCommand = @"
|
||||
set -euo pipefail
|
||||
gst-launch-1.0 -e filesrc location=/workspace/input/right.mp4 ! qtdemux ! h264parse ! nvv4l2decoder ! nvvideoconvert ! 'video/x-raw(memory:NVMM),format=RGBA' ! nvdewarper config-file=/workspace/package/runtime/$([string]$profile.rectification.config_file) source-id=0 num-batch-buffers=1 ! nvvideoconvert ! 'video/x-raw(memory:NVMM),format=NV12' ! nvv4l2h264enc bitrate=6000000 ! h264parse ! qtmux ! filesink location=/workspace/output/front.mp4
|
||||
"@
|
||||
$dewarperArguments = @(
|
||||
'run', '--rm', '--name', "ndc-mission-core-e46h-dewarper-$runId",
|
||||
'--gpus', 'all', '--network', 'none', '--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges', '--shm-size', '4g',
|
||||
'--label', 'com.nodedc.product=mission-core',
|
||||
'--label', 'com.nodedc.stack=perception',
|
||||
'--label', 'com.nodedc.role=nvdewarper-e46h-front',
|
||||
'--mount', "type=bind,src=$sourceInputMount,dst=/workspace/input,readonly",
|
||||
'--mount', "type=bind,src=$package,dst=/workspace/package,readonly",
|
||||
'--mount', "type=bind,src=$geometryRoot,dst=/workspace/output",
|
||||
'--entrypoint', '/bin/bash', $image, '-lc', $dewarperCommand
|
||||
)
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& docker @dewarperArguments 2>&1 | Tee-Object -LiteralPath $dewarperLog
|
||||
$dewarperExit = $LASTEXITCODE
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if ($dewarperExit -ne 0 -or -not (Test-Path -LiteralPath $frontPath -PathType Leaf)) {
|
||||
throw "NVIDIA nvdewarper failed with exit code $dewarperExit"
|
||||
}
|
||||
if ((Get-VideoFrameCount $frontPath) -ne $frameCount) {
|
||||
throw 'E46H FRONT frame coverage changed.'
|
||||
}
|
||||
$normalizedFront = Join-Path $geometryRoot 'front-normalized.mp4'
|
||||
& ffmpeg -hide_banner -loglevel error -y -fflags +genpts -i $frontPath `
|
||||
-vf 'setpts=N/(10*TB)' -an -c:v libx264 -preset fast -crf 18 `
|
||||
-pix_fmt yuv420p -r 10 -movflags +faststart $normalizedFront
|
||||
if ($LASTEXITCODE -ne 0 -or (Get-VideoFrameCount $normalizedFront) -ne $frameCount) {
|
||||
throw 'E46H FRONT timestamp normalization failed.'
|
||||
}
|
||||
Move-Item -LiteralPath $normalizedFront -Destination $frontPath -Force
|
||||
Copy-Item -LiteralPath $frontPath -Destination (Join-Path $frontInputMount 'view.mp4')
|
||||
|
||||
$appConfig = Join-Path $package "runtime\$([string]$profile.detector.deepstream_app_config)"
|
||||
$detectorConfig = Join-Path $package "runtime\$([string]$profile.detector.detector_config)"
|
||||
Assert-Sha256 $appConfig ([string]$profile.detector.deepstream_app_config_sha256) `
|
||||
'TrafficCamNet DeepStream app config'
|
||||
Assert-Sha256 $detectorConfig ([string]$profile.detector.detector_config_sha256) `
|
||||
'TrafficCamNet detector config'
|
||||
$deepstreamLog = Join-Path $runOutput 'deepstream.log'
|
||||
$deepstreamCommand = @"
|
||||
set -euo pipefail
|
||||
cp /opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml /workspace/output/tracker-config.yml
|
||||
deepstream-app -c /workspace/package/runtime/$([string]$profile.detector.deepstream_app_config)
|
||||
"@
|
||||
$deepstreamArguments = @(
|
||||
'run', '--rm', '--name', "ndc-mission-core-e46h-front-$runId",
|
||||
'--gpus', 'all', '--network', 'none', '--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges', '--shm-size', '4g',
|
||||
'--label', 'com.nodedc.product=mission-core',
|
||||
'--label', 'com.nodedc.stack=perception',
|
||||
'--label', 'com.nodedc.role=e46h-front-trafficcamnet',
|
||||
'--mount', "type=bind,src=$frontInputMount,dst=/workspace/input,readonly",
|
||||
'--mount', "type=bind,src=$package,dst=/workspace/package,readonly",
|
||||
'--mount', "type=bind,src=$modelRoot,dst=/workspace/model",
|
||||
'--mount', "type=bind,src=$runOutput,dst=/workspace/output",
|
||||
'--entrypoint', '/bin/bash', $image, '-lc', $deepstreamCommand
|
||||
)
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
& docker @deepstreamArguments 2>&1 | Tee-Object -LiteralPath $deepstreamLog
|
||||
$deepstreamExit = $LASTEXITCODE
|
||||
}
|
||||
finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
if ($deepstreamExit -ne 0) {
|
||||
throw "DeepStream failed with exit code $deepstreamExit"
|
||||
}
|
||||
$overlayPath = Join-Path $runOutput 'overlay.mp4'
|
||||
$trackerPath = Join-Path $runOutput 'tracker-config.yml'
|
||||
$detections = @(Get-ChildItem -LiteralPath (Join-Path $runOutput 'detections') -File)
|
||||
$tracks = @(Get-ChildItem -LiteralPath (Join-Path $runOutput 'tracks') -File)
|
||||
if (-not (Test-Path -LiteralPath $overlayPath -PathType Leaf) -or
|
||||
$detections.Count -ne $frameCount -or $tracks.Count -ne $frameCount) {
|
||||
throw "DeepStream output coverage changed: detections=$($detections.Count), tracks=$($tracks.Count)"
|
||||
}
|
||||
$fastOverlay = Join-Path $runOutput 'overlay-faststart.mp4'
|
||||
& ffmpeg -hide_banner -loglevel error -y -i $overlayPath -c copy -movflags +faststart $fastOverlay
|
||||
if ($LASTEXITCODE -ne 0 -or (Get-VideoFrameCount $fastOverlay) -ne $frameCount) {
|
||||
throw 'E46H fast-start overlay normalization failed.'
|
||||
}
|
||||
Move-Item -LiteralPath $fastOverlay -Destination $overlayPath -Force
|
||||
$enginePath = "$modelPath`_b1_gpu0_fp16.engine"
|
||||
if (-not (Test-Path -LiteralPath $enginePath -PathType Leaf)) {
|
||||
throw 'TrafficCamNet TensorRT engine is missing.'
|
||||
}
|
||||
|
||||
$runtime = [ordered]@{
|
||||
schema_version = 'missioncore.e46h-full-rectified-front-runtime/v1'
|
||||
status = 'completed'
|
||||
worker_host = $env:COMPUTERNAME
|
||||
gpu_name = ((& nvidia-smi --query-gpu=name --format=csv,noheader | Select-Object -First 1).Trim())
|
||||
started_at_utc = $startedAt
|
||||
completed_at_utc = (Get-Date).ToUniversalTime().ToString('o')
|
||||
container_image = $image
|
||||
container_image_digest = $imageDigest
|
||||
source_stream_sha256 = Get-Sha256 $inputPath
|
||||
frame_count = $frameCount
|
||||
retained_source_frame_index_range = @(0, 4487)
|
||||
geometry = [ordered]@{
|
||||
video_path = 'geometry/front.mp4'
|
||||
video_sha256 = Get-Sha256 $frontPath
|
||||
log_path = 'geometry/front.log'
|
||||
log_sha256 = Get-Sha256 $dewarperLog
|
||||
config_sha256 = Get-Sha256 $dewarperConfig
|
||||
frame_count = $frameCount
|
||||
dewarper_exit_code = $dewarperExit
|
||||
}
|
||||
run = [ordered]@{
|
||||
overlay_path = 'run/overlay.mp4'
|
||||
overlay_sha256 = Get-Sha256 $overlayPath
|
||||
deepstream_log_path = 'run/deepstream.log'
|
||||
deepstream_log_sha256 = Get-Sha256 $deepstreamLog
|
||||
model_sha256 = Get-Sha256 $modelPath
|
||||
model_engine_sha256 = Get-Sha256 $enginePath
|
||||
parser_library_sha256 = Get-Sha256 $parserPath
|
||||
deepstream_app_config_sha256 = Get-Sha256 $appConfig
|
||||
detector_config_sha256 = Get-Sha256 $detectorConfig
|
||||
tracker_config_sha256 = Get-Sha256 $trackerPath
|
||||
frame_count = $frameCount
|
||||
deepstream_exit_code = $deepstreamExit
|
||||
}
|
||||
}
|
||||
$runtime | ConvertTo-Json -Depth 12 |
|
||||
Set-Content -LiteralPath (Join-Path $rawRoot 'runtime.json') -Encoding UTF8
|
||||
Add-Content -LiteralPath $workerLog -Value "completed=$(Get-Date -Format o)"
|
||||
|
||||
$consolidatorImage = 'nvcr.io/nvidia/tritonserver:26.06-py3@sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794'
|
||||
$consolidatorArguments = @(
|
||||
'run', '--rm', '--name', "ndc-mission-core-e46h-consolidator-$runId",
|
||||
'--network', 'none', '--read-only', '--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges', '--tmpfs', '/tmp:rw,noexec,nosuid,size=64m',
|
||||
'--mount', "type=bind,src=$package,dst=/workspace/package,readonly",
|
||||
'--mount', "type=bind,src=$sourceJob,dst=/workspace/source-job,readonly",
|
||||
'--mount', "type=bind,src=$rawRoot,dst=/workspace/raw,readonly",
|
||||
'--mount', "type=bind,src=$resultsRoot,dst=/workspace/results",
|
||||
'-e', 'PYTHONPATH=/workspace/package/runtime',
|
||||
'-e', 'PYTHONDONTWRITEBYTECODE=1',
|
||||
$consolidatorImage,
|
||||
'python3', '/workspace/package/runtime/run_e46h_full_rectified_front_replay.py',
|
||||
'--source-job', '/workspace/source-job',
|
||||
'--raw-root', '/workspace/raw',
|
||||
'--profile', '/workspace/package/profile.json',
|
||||
'--output-root', '/workspace/results'
|
||||
)
|
||||
Invoke-Docker $consolidatorArguments 'E46H consolidation'
|
||||
Write-Host "E46H_FULL_RECTIFIED_FRONT_COMPLETED run=$runId results=$resultsRoot"
|
||||
}
|
||||
finally {
|
||||
if ($LogPath) { Stop-Transcript | Out-Null }
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PackageRoot,
|
||||
|
||||
[string]$SourceJobRoot = 'D:\NDC_MISSIONCORE\runtime\jobs\recorded-camera-602ac89026ed12978619801d',
|
||||
|
||||
[string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46h'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$taskName = 'MissionCore-E46HFullRectifiedFrontReplay'
|
||||
$package = (Resolve-Path -LiteralPath $PackageRoot).Path
|
||||
$script = Join-Path $package 'runtime\Invoke-E46HFullRectifiedFrontReplay.ps1'
|
||||
if (-not (Test-Path -LiteralPath $script -PathType Leaf)) {
|
||||
throw "E46H runner is missing: $script"
|
||||
}
|
||||
$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||
if ($existing -and $existing.State -eq 'Running') {
|
||||
throw "$taskName is already running."
|
||||
}
|
||||
$logsRoot = Join-Path $RuntimeRoot 'logs'
|
||||
New-Item -ItemType Directory -Force -Path $logsRoot | Out-Null
|
||||
$stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ')
|
||||
$logPath = Join-Path $logsRoot "e46h-full-rectified-front-$stamp.log"
|
||||
$powerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||
$arguments = @(
|
||||
'-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
|
||||
'-File', "`"$script`"",
|
||||
'-PackageRoot', "`"$package`"",
|
||||
'-SourceJobRoot', "`"$SourceJobRoot`"",
|
||||
'-RuntimeRoot', "`"$RuntimeRoot`"",
|
||||
'-LogPath', "`"$logPath`""
|
||||
) -join ' '
|
||||
$userId = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
$action = New-ScheduledTaskAction -Execute $powerShell -Argument $arguments -WorkingDirectory $package
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited
|
||||
$trigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).AddMinutes(30))
|
||||
$settings = New-ScheduledTaskSettingsSet `
|
||||
-AllowStartIfOnBatteries `
|
||||
-DontStopIfGoingOnBatteries `
|
||||
-StartWhenAvailable `
|
||||
-ExecutionTimeLimit ([TimeSpan]::FromHours(6))
|
||||
Register-ScheduledTask `
|
||||
-TaskName $taskName `
|
||||
-Action $action `
|
||||
-Principal $principal `
|
||||
-Trigger $trigger `
|
||||
-Settings $settings `
|
||||
-Description 'One-shot E46H full retained FRONT TrafficCamNet + NvDCF replay.' `
|
||||
-Force | Out-Null
|
||||
Start-ScheduledTask -TaskName $taskName
|
||||
Write-Host "E46H_TASK_STARTED task=$taskName log=$logPath"
|
||||
@@ -0,0 +1,118 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PackageRoot,
|
||||
|
||||
[string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46j',
|
||||
|
||||
[string]$SourceVideo = 'D:\NDC_MISSIONCORE\runtime\experiments\e46e\inputs\right-cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8.mp4',
|
||||
|
||||
[string]$ValidFovMask = 'D:\NDC_MISSIONCORE\runtime\inputs\e2\valid-fov-mask-b4dd8ddf2b87c1d520ee8a0868c4fea062d7c14d1bae73ccabd3abe1f3acbac2\mask.png',
|
||||
|
||||
[int]$MaxFrames = 0,
|
||||
|
||||
[switch]$NoOverlay,
|
||||
|
||||
[string]$RunPrefix = 'full'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
function Get-Sha256([string]$Path) {
|
||||
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
}
|
||||
|
||||
$package = (Resolve-Path -LiteralPath $PackageRoot).Path
|
||||
$source = (Resolve-Path -LiteralPath $SourceVideo).Path
|
||||
$mask = (Resolve-Path -LiteralPath $ValidFovMask).Path
|
||||
$profile = Join-Path $package 'e46j_raw_fisheye_yolox_profile.json'
|
||||
$runner = Join-Path $package 'run_e46j_raw_fisheye_yolox.py'
|
||||
if (-not (Test-Path -LiteralPath $profile -PathType Leaf) -or
|
||||
-not (Test-Path -LiteralPath $runner -PathType Leaf)) {
|
||||
throw 'E46J package is incomplete.'
|
||||
}
|
||||
if ((Get-Sha256 $source) -ne 'cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8') {
|
||||
throw 'E46J source stream identity changed.'
|
||||
}
|
||||
|
||||
$runsRoot = Join-Path $RuntimeRoot 'runs'
|
||||
New-Item -ItemType Directory -Force -Path $runsRoot | Out-Null
|
||||
$runId = "$RunPrefix-$((Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ'))"
|
||||
$image = 'nvcr.io/nvidia/tritonserver:26.06-py3@sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794'
|
||||
$command = "python3 /workspace/package/run_e46j_raw_fisheye_yolox.py --input /workspace/input/right.mp4 --mask /workspace/valid-fov/mask.png --profile /workspace/package/e46j_raw_fisheye_yolox_profile.json --triton-url http://127.0.0.1:8000 --output /workspace/runs/$runId"
|
||||
if ($MaxFrames -gt 0) {
|
||||
$command += " --max-frames $MaxFrames"
|
||||
}
|
||||
if (-not $NoOverlay) {
|
||||
$command += ' --overlay'
|
||||
}
|
||||
|
||||
$dockerArguments = @(
|
||||
'run', '--rm', '--name', "ndc-mission-core-e46j-$runId",
|
||||
'--network', 'container:ndc-mission-core-triton',
|
||||
'--gpus', 'all',
|
||||
'--cap-drop', 'ALL',
|
||||
'--security-opt', 'no-new-privileges',
|
||||
'--shm-size', '1g',
|
||||
'--label', 'com.nodedc.product=mission-core',
|
||||
'--label', 'com.nodedc.stack=perception',
|
||||
'--label', 'com.nodedc.role=e46j-raw-fisheye-realtime-gate',
|
||||
'--label', 'com.nodedc.managed-by=mission-core-worker',
|
||||
'--mount', "type=bind,src=$source,dst=/workspace/input/right.mp4,readonly",
|
||||
'--mount', "type=bind,src=$mask,dst=/workspace/valid-fov/mask.png,readonly",
|
||||
'--mount', "type=bind,src=$package,dst=/workspace/package,readonly",
|
||||
'--mount', "type=bind,src=$runsRoot,dst=/workspace/runs",
|
||||
'--mount', 'type=bind,src=D:\NDC_MISSIONCORE\runtime\derived\perception-p0-env-v1,dst=/opt/env,readonly',
|
||||
'--mount', 'type=bind,src=D:\NDC_MISSIONCORE\runtime\derived\perception-e15-media-pyav180-lz445-v1,dst=/opt/media,readonly',
|
||||
'--mount', 'type=bind,src=D:\NDC_MISSIONCORE\runtime\derived\perception-e3-opencv413092-v1,dst=/environment,readonly',
|
||||
'-e', 'PYTHONPATH=/opt/env:/opt/media:/environment/packages',
|
||||
'--entrypoint', '/bin/bash',
|
||||
$image, '-lc', $command
|
||||
)
|
||||
|
||||
& docker @dockerArguments
|
||||
$runnerExitCode = $LASTEXITCODE
|
||||
if ($runnerExitCode -ne 0 -and $runnerExitCode -ne 2) {
|
||||
throw "E46J runner crashed with exit code $runnerExitCode"
|
||||
}
|
||||
$result = Join-Path $runsRoot $runId
|
||||
$runtimePath = Join-Path $result 'runtime.json'
|
||||
if (-not $NoOverlay) {
|
||||
$intermediate = Join-Path $result 'raw-fisheye-yolox-overlay-intermediate.mp4'
|
||||
$overlay = Join-Path $result 'raw-fisheye-yolox-overlay.mp4'
|
||||
if (-not (Test-Path -LiteralPath $intermediate -PathType Leaf)) {
|
||||
throw 'E46J intermediate overlay is missing.'
|
||||
}
|
||||
& ffmpeg.exe -hide_banner -loglevel error -y -i $intermediate `
|
||||
-c:v libx264 -preset veryfast -crf 20 -r 4489000/448723 `
|
||||
-movflags +faststart -an $overlay
|
||||
if ($LASTEXITCODE -ne 0 -or
|
||||
-not (Test-Path -LiteralPath $overlay -PathType Leaf) -or
|
||||
(Get-Item -LiteralPath $overlay).Length -eq 0) {
|
||||
throw 'E46J final H.264 overlay transcode failed.'
|
||||
}
|
||||
$runtime = Get-Content -LiteralPath $runtimePath -Raw | ConvertFrom-Json
|
||||
$runtime.artifacts.PSObject.Properties.Remove('overlay_intermediate')
|
||||
$runtime.artifacts | Add-Member -NotePropertyName overlay -NotePropertyValue ([pscustomobject]@{
|
||||
file = 'raw-fisheye-yolox-overlay.mp4'
|
||||
byte_length = (Get-Item -LiteralPath $overlay).Length
|
||||
sha256 = Get-Sha256 $overlay
|
||||
codec = 'H.264'
|
||||
frame_rate = 10.003944527024467
|
||||
})
|
||||
$runtime | Add-Member -NotePropertyName visual_export -NotePropertyValue ([pscustomobject]@{
|
||||
provider = ((& ffmpeg.exe -version | Select-Object -First 1).Trim())
|
||||
source = 'MPEG-4 Part 2 intermediate produced by OpenCV VideoWriter'
|
||||
output = 'H.264 MP4 with faststart'
|
||||
excluded_from_core_latency = $true
|
||||
})
|
||||
$runtime | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $runtimePath -Encoding UTF8
|
||||
Remove-Item -LiteralPath $intermediate -Force
|
||||
}
|
||||
Write-Output "E46J_RUN_ID=$runId"
|
||||
Write-Output "E46J_RESULT_ROOT=$result"
|
||||
Get-Content -LiteralPath $runtimePath -Raw
|
||||
if ($runnerExitCode -eq 2) {
|
||||
exit 2
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$JobRoot,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$OutputPath,
|
||||
|
||||
[ValidateRange(1, 1000)]
|
||||
[int]$FreeGiBFloor = 360
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
function Assert-DDrivePath {
|
||||
param([string]$Path, [string]$Label)
|
||||
$fullPath = [IO.Path]::GetFullPath($Path)
|
||||
if ([IO.Path]::GetPathRoot($fullPath).TrimEnd("\") -ine "D:") {
|
||||
throw "$Label must be stored on D:"
|
||||
}
|
||||
return $fullPath
|
||||
}
|
||||
|
||||
function Assert-FreeSpace {
|
||||
param([string]$Phase, [int64]$RequiredAdditionalBytes = 0)
|
||||
$freeBytes = [int64](Get-PSDrive -Name D).Free
|
||||
$floorBytes = [int64]$FreeGiBFloor * 1GB
|
||||
Write-Output (
|
||||
"DISK_GUARD PHASE={0} FREE_GIB={1} FLOOR_GIB={2}" -f
|
||||
$Phase,
|
||||
[math]::Round($freeBytes / 1GB, 3),
|
||||
$FreeGiBFloor
|
||||
)
|
||||
if ($freeBytes -lt ($floorBytes + $RequiredAdditionalBytes)) {
|
||||
throw "D: does not have the guarded replay reserve during $Phase"
|
||||
}
|
||||
}
|
||||
|
||||
$jobDirectory = Assert-DDrivePath (
|
||||
(Resolve-Path -LiteralPath $JobRoot).Path
|
||||
) "Job root"
|
||||
$output = Assert-DDrivePath $OutputPath "Output path"
|
||||
if (Test-Path -LiteralPath $output) {
|
||||
Write-Output "REPLAY_ALREADY_PRESENT=$output"
|
||||
exit 0
|
||||
}
|
||||
|
||||
$jobPath = Join-Path $jobDirectory "job.json"
|
||||
$job = Get-Content -LiteralPath $jobPath -Raw | ConvertFrom-Json
|
||||
if (
|
||||
$job.schema_version -ne "missioncore.compute-job/v1" -or
|
||||
$job.job_id -ne "recorded-camera-602ac89026ed12978619801d" -or
|
||||
$job.input.session_id -ne "20260720T065719Z_viewer_live" -or
|
||||
$job.input.source_id -ne "sensor.camera.right" -or
|
||||
[int]$job.input.segment_count -ne 4489
|
||||
) {
|
||||
throw "The requested job is not the immutable RAVNOVES00 right-camera source"
|
||||
}
|
||||
|
||||
$epochRoot = Join-Path $jobDirectory "input\camera\sensor.camera.right\epoch-1"
|
||||
$initPath = Join-Path $epochRoot "init.mp4"
|
||||
$segmentsRoot = Join-Path $epochRoot "segments"
|
||||
if (-not (Test-Path -LiteralPath $initPath -PathType Leaf)) {
|
||||
throw "Camera initialization segment is absent"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $segmentsRoot -PathType Container)) {
|
||||
throw "Camera segment directory is absent"
|
||||
}
|
||||
|
||||
$parent = Split-Path $output -Parent
|
||||
$null = New-Item -ItemType Directory -Path $parent -Force
|
||||
$temporary = Join-Path $parent (".{0}.{1}.partial" -f (Split-Path $output -Leaf), [Guid]::NewGuid().ToString("N"))
|
||||
$requiredBytes = [int64]$job.input.byte_length + 1GB
|
||||
Assert-FreeSpace "preflight" $requiredBytes
|
||||
|
||||
try {
|
||||
$destination = [IO.File]::Open(
|
||||
$temporary,
|
||||
[IO.FileMode]::CreateNew,
|
||||
[IO.FileAccess]::Write,
|
||||
[IO.FileShare]::None
|
||||
)
|
||||
try {
|
||||
$source = [IO.File]::OpenRead($initPath)
|
||||
try { $source.CopyTo($destination) } finally { $source.Dispose() }
|
||||
for ($sequence = 1; $sequence -le 4489; $sequence++) {
|
||||
$segment = Join-Path $segmentsRoot ("{0}.m4s" -f $sequence)
|
||||
if (-not (Test-Path -LiteralPath $segment -PathType Leaf)) {
|
||||
throw "Camera segment is absent: $sequence"
|
||||
}
|
||||
$source = [IO.File]::OpenRead($segment)
|
||||
try { $source.CopyTo($destination) } finally { $source.Dispose() }
|
||||
}
|
||||
$destination.Flush($true)
|
||||
}
|
||||
finally {
|
||||
$destination.Dispose()
|
||||
}
|
||||
|
||||
$probe = & ffprobe -v error -select_streams v:0 -count_frames `
|
||||
-show_entries stream=width,height,nb_read_frames `
|
||||
-of json $temporary | ConvertFrom-Json
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "ffprobe failed for the reconstructed camera source"
|
||||
}
|
||||
$stream = @($probe.streams)[0]
|
||||
if (
|
||||
[int]$stream.width -ne 800 -or
|
||||
[int]$stream.height -ne 600 -or
|
||||
[int]$stream.nb_read_frames -ne 4489
|
||||
) {
|
||||
throw "Reconstructed camera stream violates the immutable frame contract"
|
||||
}
|
||||
Move-Item -LiteralPath $temporary -Destination $output
|
||||
Assert-FreeSpace "published"
|
||||
Write-Output "REPLAY_PATH=$output"
|
||||
Write-Output "FRAME_COUNT=4489"
|
||||
}
|
||||
finally {
|
||||
if (Test-Path -LiteralPath $temporary) {
|
||||
Remove-Item -LiteralPath $temporary -Force
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
[application]
|
||||
enable-perf-measurement=1
|
||||
perf-measurement-interval-sec=5
|
||||
gie-kitti-output-dir=/workspace/output/detections
|
||||
kitti-track-output-dir=/workspace/output/tracks
|
||||
|
||||
[tiled-display]
|
||||
enable=0
|
||||
rows=1
|
||||
columns=1
|
||||
width=800
|
||||
height=600
|
||||
gpu-id=0
|
||||
|
||||
[source0]
|
||||
enable=1
|
||||
type=3
|
||||
num-sources=1
|
||||
uri=file:///workspace/input/right.mp4
|
||||
gpu-id=0
|
||||
|
||||
[streammux]
|
||||
gpu-id=0
|
||||
batch-size=1
|
||||
batched-push-timeout=40000
|
||||
width=800
|
||||
height=600
|
||||
live-source=0
|
||||
|
||||
[primary-gie]
|
||||
enable=1
|
||||
gpu-id=0
|
||||
plugin-type=0
|
||||
batch-size=1
|
||||
gie-unique-id=1
|
||||
config-file=/workspace/package/runtime/e46e_trafficcamnet_rtdetr.txt
|
||||
bbox-border-color1=0.267;0.831;1.0;1.0
|
||||
bbox-border-color2=0.243;0.973;0.553;1.0
|
||||
bbox-border-color3=1.0;0.306;0.765;1.0
|
||||
bbox-border-color4=1.0;0.741;0.153;1.0
|
||||
|
||||
[tracker]
|
||||
enable=1
|
||||
tracker-width=960
|
||||
tracker-height=544
|
||||
ll-lib-file=/opt/nvidia/deepstream/deepstream/lib/libnvds_nvmultiobjecttracker.so
|
||||
ll-config-file=/opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml
|
||||
gpu-id=0
|
||||
display-tracking-id=1
|
||||
compute-hw=1
|
||||
|
||||
[osd]
|
||||
enable=1
|
||||
gpu-id=0
|
||||
border-width=3
|
||||
text-size=16
|
||||
text-color=1;1;1;1
|
||||
text-bg-color=0.08;0.08;0.08;0.9
|
||||
font=Arial
|
||||
display-bbox=1
|
||||
display-text=1
|
||||
|
||||
[sink0]
|
||||
enable=1
|
||||
type=3
|
||||
container=1
|
||||
codec=1
|
||||
enc-type=0
|
||||
sync=0
|
||||
qos=0
|
||||
bitrate=8000000
|
||||
profile=4
|
||||
output-file=/workspace/output/overlay.mp4
|
||||
source-id=0
|
||||
gpu-id=0
|
||||
|
||||
[tests]
|
||||
file-loop=0
|
||||
@@ -0,0 +1,5 @@
|
||||
background
|
||||
bicycle
|
||||
car
|
||||
person
|
||||
road_sign
|
||||
@@ -0,0 +1,26 @@
|
||||
[property]
|
||||
gpu-id=0
|
||||
onnx-file=/workspace/model/resnet50_trafficcamnet_rtdetr.fp16.onnx
|
||||
model-engine-file=/workspace/model/resnet50_trafficcamnet_rtdetr.fp16.onnx_b1_gpu0_fp16.engine
|
||||
labelfile-path=/workspace/package/runtime/e46e_trafficcamnet_labels.txt
|
||||
custom-lib-path=/workspace/package/runtime/libnvds_infercustomparser_tao.so
|
||||
parse-bbox-func-name=NvDsInferParseCustomDDETRTAO
|
||||
output-blob-names=pred_logits;pred_boxes
|
||||
infer-dims=3;544;960
|
||||
maintain-aspect-ratio=1
|
||||
net-scale-factor=0.00392156862745098
|
||||
offsets=0;0;0
|
||||
model-color-format=0
|
||||
network-mode=2
|
||||
network-type=0
|
||||
num-detected-classes=5
|
||||
cluster-mode=4
|
||||
output-tensor-meta=1
|
||||
workspace-size=1048576
|
||||
batch-size=1
|
||||
interval=0
|
||||
gie-unique-id=1
|
||||
|
||||
[class-attrs-all]
|
||||
pre-cluster-threshold=0.5
|
||||
topk=20
|
||||
@@ -0,0 +1,32 @@
|
||||
[property]
|
||||
gpu-id=0
|
||||
onnx-file=/workspace/model/resnet18_dashcamnet_pruned.onnx
|
||||
model-engine-file=/workspace/model/resnet18_dashcamnet_pruned.onnx_b1_gpu0_fp16.engine
|
||||
labelfile-path=/workspace/package/runtime/e46f_dashcamnet_labels.txt
|
||||
output-blob-names=output_bbox/BiasAdd:0;output_cov/Sigmoid:0
|
||||
infer-dims=3;544;960
|
||||
maintain-aspect-ratio=0
|
||||
net-scale-factor=0.00392156862745098
|
||||
offsets=0;0;0
|
||||
model-color-format=0
|
||||
network-mode=2
|
||||
network-type=0
|
||||
num-detected-classes=4
|
||||
cluster-mode=2
|
||||
output-tensor-meta=0
|
||||
workspace-size=1048576
|
||||
batch-size=1
|
||||
interval=0
|
||||
gie-unique-id=1
|
||||
|
||||
[class-attrs-all]
|
||||
topk=20
|
||||
nms-iou-threshold=0.5
|
||||
pre-cluster-threshold=0.2
|
||||
roi-top-offset=0
|
||||
roi-bottom-offset=0
|
||||
|
||||
[class-attrs-0]
|
||||
topk=20
|
||||
nms-iou-threshold=0.5
|
||||
pre-cluster-threshold=0.4
|
||||
@@ -0,0 +1,4 @@
|
||||
car
|
||||
bicycle
|
||||
person
|
||||
road_sign
|
||||
@@ -0,0 +1,78 @@
|
||||
[application]
|
||||
enable-perf-measurement=1
|
||||
perf-measurement-interval-sec=5
|
||||
gie-kitti-output-dir=/workspace/output/detections
|
||||
kitti-track-output-dir=/workspace/output/tracks
|
||||
|
||||
[tiled-display]
|
||||
enable=0
|
||||
rows=1
|
||||
columns=1
|
||||
width=800
|
||||
height=600
|
||||
gpu-id=0
|
||||
|
||||
[source0]
|
||||
enable=1
|
||||
type=3
|
||||
num-sources=1
|
||||
uri=file:///workspace/input/right.mp4
|
||||
gpu-id=0
|
||||
|
||||
[streammux]
|
||||
gpu-id=0
|
||||
batch-size=1
|
||||
batched-push-timeout=40000
|
||||
width=800
|
||||
height=600
|
||||
live-source=0
|
||||
|
||||
[primary-gie]
|
||||
enable=1
|
||||
gpu-id=0
|
||||
plugin-type=0
|
||||
batch-size=1
|
||||
gie-unique-id=1
|
||||
config-file=/workspace/package/runtime/e46f_dashcamnet_detectnet.txt
|
||||
bbox-border-color0=0.243;0.973;0.553;1.0
|
||||
bbox-border-color1=0.267;0.831;1.0;1.0
|
||||
bbox-border-color2=1.0;0.306;0.765;1.0
|
||||
bbox-border-color3=1.0;0.741;0.153;1.0
|
||||
|
||||
[tracker]
|
||||
enable=1
|
||||
tracker-width=960
|
||||
tracker-height=544
|
||||
ll-lib-file=/opt/nvidia/deepstream/deepstream/lib/libnvds_nvmultiobjecttracker.so
|
||||
ll-config-file=/opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml
|
||||
gpu-id=0
|
||||
display-tracking-id=1
|
||||
compute-hw=1
|
||||
|
||||
[osd]
|
||||
enable=1
|
||||
gpu-id=0
|
||||
border-width=3
|
||||
text-size=16
|
||||
text-color=1;1;1;1
|
||||
text-bg-color=0.08;0.08;0.08;0.9
|
||||
font=Arial
|
||||
display-bbox=1
|
||||
display-text=1
|
||||
|
||||
[sink0]
|
||||
enable=1
|
||||
type=3
|
||||
container=1
|
||||
codec=1
|
||||
enc-type=0
|
||||
sync=0
|
||||
qos=0
|
||||
bitrate=8000000
|
||||
profile=4
|
||||
output-file=/workspace/output/overlay.mp4
|
||||
source-id=0
|
||||
gpu-id=0
|
||||
|
||||
[tests]
|
||||
file-loop=0
|
||||
@@ -0,0 +1,78 @@
|
||||
[application]
|
||||
enable-perf-measurement=1
|
||||
perf-measurement-interval-sec=5
|
||||
gie-kitti-output-dir=/workspace/output/detections
|
||||
kitti-track-output-dir=/workspace/output/tracks
|
||||
|
||||
[tiled-display]
|
||||
enable=0
|
||||
rows=1
|
||||
columns=1
|
||||
width=960
|
||||
height=544
|
||||
gpu-id=0
|
||||
|
||||
[source0]
|
||||
enable=1
|
||||
type=3
|
||||
num-sources=1
|
||||
uri=file:///workspace/input/view.mp4
|
||||
gpu-id=0
|
||||
|
||||
[streammux]
|
||||
gpu-id=0
|
||||
batch-size=1
|
||||
batched-push-timeout=40000
|
||||
width=960
|
||||
height=544
|
||||
live-source=0
|
||||
|
||||
[primary-gie]
|
||||
enable=1
|
||||
gpu-id=0
|
||||
plugin-type=0
|
||||
batch-size=1
|
||||
gie-unique-id=1
|
||||
config-file=/workspace/package/runtime/e46f_dashcamnet_detectnet.txt
|
||||
bbox-border-color0=0.243;0.973;0.553;1.0
|
||||
bbox-border-color1=0.267;0.831;1.0;1.0
|
||||
bbox-border-color2=1.0;0.306;0.765;1.0
|
||||
bbox-border-color3=1.0;0.741;0.153;1.0
|
||||
|
||||
[tracker]
|
||||
enable=1
|
||||
tracker-width=960
|
||||
tracker-height=544
|
||||
ll-lib-file=/opt/nvidia/deepstream/deepstream/lib/libnvds_nvmultiobjecttracker.so
|
||||
ll-config-file=/opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml
|
||||
gpu-id=0
|
||||
display-tracking-id=1
|
||||
compute-hw=1
|
||||
|
||||
[osd]
|
||||
enable=1
|
||||
gpu-id=0
|
||||
border-width=3
|
||||
text-size=16
|
||||
text-color=1;1;1;1
|
||||
text-bg-color=0.08;0.08;0.08;0.9
|
||||
font=Arial
|
||||
display-bbox=1
|
||||
display-text=1
|
||||
|
||||
[sink0]
|
||||
enable=1
|
||||
type=3
|
||||
container=1
|
||||
codec=1
|
||||
enc-type=0
|
||||
sync=0
|
||||
qos=0
|
||||
bitrate=6000000
|
||||
profile=4
|
||||
output-file=/workspace/output/overlay.mp4
|
||||
source-id=0
|
||||
gpu-id=0
|
||||
|
||||
[tests]
|
||||
file-loop=0
|
||||
@@ -0,0 +1,22 @@
|
||||
[property]
|
||||
output-width=960
|
||||
output-height=544
|
||||
num-batch-buffers=1
|
||||
cuda-memory-type=2
|
||||
|
||||
[surface0]
|
||||
projection-type=4
|
||||
surface-index=0
|
||||
width=960
|
||||
height=544
|
||||
yaw=0.0
|
||||
pitch=0.0
|
||||
roll=0.0
|
||||
rot-axes=YXZ
|
||||
focal-length=194.59817287616025;194.57531427932872
|
||||
distortion=-0.023164451386679667;-0.0014974198594105452;-0.001039213149441563;-0.000035237331915978814
|
||||
src-x0=396.31861150187996
|
||||
src-y0=301.49644357408005
|
||||
dst-focal-length=402.76782296509447;402.76782296509447
|
||||
dst-principal-point=479.5;271.5
|
||||
cuda-address-mode=1
|
||||
@@ -0,0 +1,22 @@
|
||||
[property]
|
||||
output-width=960
|
||||
output-height=544
|
||||
num-batch-buffers=1
|
||||
cuda-memory-type=2
|
||||
|
||||
[surface0]
|
||||
projection-type=4
|
||||
surface-index=0
|
||||
width=960
|
||||
height=544
|
||||
yaw=270.0
|
||||
pitch=0.0
|
||||
roll=0.0
|
||||
rot-axes=YXZ
|
||||
focal-length=194.59817287616025;194.57531427932872
|
||||
distortion=-0.023164451386679667;-0.0014974198594105452;-0.001039213149441563;-0.000035237331915978814
|
||||
src-x0=396.31861150187996
|
||||
src-y0=301.49644357408005
|
||||
dst-focal-length=402.76782296509447;402.76782296509447
|
||||
dst-principal-point=479.5;271.5
|
||||
cuda-address-mode=1
|
||||
@@ -0,0 +1,22 @@
|
||||
[property]
|
||||
output-width=960
|
||||
output-height=544
|
||||
num-batch-buffers=1
|
||||
cuda-memory-type=2
|
||||
|
||||
[surface0]
|
||||
projection-type=4
|
||||
surface-index=0
|
||||
width=960
|
||||
height=544
|
||||
yaw=90.0
|
||||
pitch=0.0
|
||||
roll=0.0
|
||||
rot-axes=YXZ
|
||||
focal-length=194.59817287616025;194.57531427932872
|
||||
distortion=-0.023164451386679667;-0.0014974198594105452;-0.001039213149441563;-0.000035237331915978814
|
||||
src-x0=396.31861150187996
|
||||
src-y0=301.49644357408005
|
||||
dst-focal-length=402.76782296509447;402.76782296509447
|
||||
dst-principal-point=479.5;271.5
|
||||
cuda-address-mode=1
|
||||
@@ -0,0 +1,78 @@
|
||||
[application]
|
||||
enable-perf-measurement=1
|
||||
perf-measurement-interval-sec=5
|
||||
gie-kitti-output-dir=/workspace/output/detections
|
||||
kitti-track-output-dir=/workspace/output/tracks
|
||||
|
||||
[tiled-display]
|
||||
enable=0
|
||||
rows=1
|
||||
columns=1
|
||||
width=960
|
||||
height=544
|
||||
gpu-id=0
|
||||
|
||||
[source0]
|
||||
enable=1
|
||||
type=3
|
||||
num-sources=1
|
||||
uri=file:///workspace/input/view.mp4
|
||||
gpu-id=0
|
||||
|
||||
[streammux]
|
||||
gpu-id=0
|
||||
batch-size=1
|
||||
batched-push-timeout=40000
|
||||
width=960
|
||||
height=544
|
||||
live-source=0
|
||||
|
||||
[primary-gie]
|
||||
enable=1
|
||||
gpu-id=0
|
||||
plugin-type=0
|
||||
batch-size=1
|
||||
gie-unique-id=1
|
||||
config-file=/workspace/package/runtime/e46e_trafficcamnet_rtdetr.txt
|
||||
bbox-border-color1=0.267;0.831;1.0;1.0
|
||||
bbox-border-color2=0.243;0.973;0.553;1.0
|
||||
bbox-border-color3=1.0;0.306;0.765;1.0
|
||||
bbox-border-color4=1.0;0.741;0.153;1.0
|
||||
|
||||
[tracker]
|
||||
enable=1
|
||||
tracker-width=960
|
||||
tracker-height=544
|
||||
ll-lib-file=/opt/nvidia/deepstream/deepstream/lib/libnvds_nvmultiobjecttracker.so
|
||||
ll-config-file=/opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml
|
||||
gpu-id=0
|
||||
display-tracking-id=1
|
||||
compute-hw=1
|
||||
|
||||
[osd]
|
||||
enable=1
|
||||
gpu-id=0
|
||||
border-width=3
|
||||
text-size=16
|
||||
text-color=1;1;1;1
|
||||
text-bg-color=0.08;0.08;0.08;0.9
|
||||
font=Arial
|
||||
display-bbox=1
|
||||
display-text=1
|
||||
|
||||
[sink0]
|
||||
enable=1
|
||||
type=3
|
||||
container=1
|
||||
codec=1
|
||||
enc-type=0
|
||||
sync=0
|
||||
qos=0
|
||||
bitrate=6000000
|
||||
profile=4
|
||||
output-file=/workspace/output/overlay.mp4
|
||||
source-id=0
|
||||
gpu-id=0
|
||||
|
||||
[tests]
|
||||
file-loop=0
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Consolidate raw DeepStream/NvDCF output into immutable E46E evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.e46e_ready_stack import build_e46e_ready_stack
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--source-job", type=Path, required=True)
|
||||
parser.add_argument("--raw-root", type=Path, required=True)
|
||||
parser.add_argument("--profile", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
result = build_e46e_ready_stack(
|
||||
source_job_root=args.source_job,
|
||||
raw_root=args.raw_root,
|
||||
profile_path=args.profile,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"result_id": result["result_id"],
|
||||
"result_root": str(result["result_root"]),
|
||||
"metrics": result["report"]["metrics"],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Consolidate raw DashCamNet/NvDCF output into immutable E46F evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.e46f_dashcam_bakeoff import build_e46f_dashcam_bakeoff
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--source-job", type=Path, required=True)
|
||||
parser.add_argument("--raw-root", type=Path, required=True)
|
||||
parser.add_argument("--profile", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
result = build_e46f_dashcam_bakeoff(
|
||||
source_job_root=args.source_job,
|
||||
raw_root=args.raw_root,
|
||||
profile_path=args.profile,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"result_id": result["result_id"],
|
||||
"result_root": str(result["result_root"]),
|
||||
"metrics": result["report"]["metrics"],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze raw NVIDIA E46G output as immutable Mission Core evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.e46g_rectified_detector_bakeoff import (
|
||||
build_e46g_rectified_detector_bakeoff,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--source-job", type=Path, required=True)
|
||||
parser.add_argument("--raw-root", type=Path, required=True)
|
||||
parser.add_argument("--profile", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
result = build_e46g_rectified_detector_bakeoff(
|
||||
source_job_root=args.source_job,
|
||||
raw_root=args.raw_root,
|
||||
profile_path=args.profile,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
print(result["result_root"])
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze raw NVIDIA E46H output as immutable Mission Core evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.e46h_full_rectified_front_replay import (
|
||||
build_e46h_full_rectified_front_replay,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--source-job", type=Path, required=True)
|
||||
parser.add_argument("--raw-root", type=Path, required=True)
|
||||
parser.add_argument("--profile", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
result = build_e46h_full_rectified_front_replay(
|
||||
source_job_root=args.source_job,
|
||||
raw_root=args.raw_root,
|
||||
profile_path=args.profile,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
print(result["result_root"])
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,706 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the frozen one-pass YOLOX-S realtime gate on the full K1 RIGHT fisheye.
|
||||
|
||||
The runner deliberately performs exactly one detector request per decoded source
|
||||
frame. It does not rectify, crop, tile, track, hold, stitch or use route-specific
|
||||
filters. H.264 overlay encoding is measured separately from the detector path.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import http.client
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import statistics
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
import urllib.parse
|
||||
from collections import Counter
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
PROFILE_SCHEMA = "missioncore.e46j-raw-fisheye-realtime-profile/v1"
|
||||
RUNTIME_SCHEMA = "missioncore.e46j-raw-fisheye-realtime-runtime/v1"
|
||||
FRAME_SCHEMA = "missioncore.e46j-raw-fisheye-realtime-frame/v1"
|
||||
|
||||
COCO_CLASSES = (
|
||||
"person", "bicycle", "car", "motorcycle", "airplane", "bus", "train",
|
||||
"truck", "boat", "traffic light", "fire hydrant", "stop sign",
|
||||
"parking meter", "bench", "bird", "cat", "dog", "horse", "sheep",
|
||||
"cow", "elephant", "bear", "zebra", "giraffe", "backpack", "umbrella",
|
||||
"handbag", "tie", "suitcase", "frisbee", "skis", "snowboard",
|
||||
"sports ball", "kite", "baseball bat", "baseball glove", "skateboard",
|
||||
"surfboard", "tennis racket", "bottle", "wine glass", "cup", "fork",
|
||||
"knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange",
|
||||
"broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair",
|
||||
"couch", "potted plant", "bed", "dining table", "toilet", "tv",
|
||||
"laptop", "mouse", "remote", "keyboard", "cell phone", "microwave",
|
||||
"oven", "toaster", "sink", "refrigerator", "book", "clock", "vase",
|
||||
"scissors", "teddy bear", "hair drier", "toothbrush",
|
||||
)
|
||||
|
||||
|
||||
def arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--input", type=Path, required=True)
|
||||
parser.add_argument("--mask", type=Path, required=True)
|
||||
parser.add_argument("--profile", type=Path, required=True)
|
||||
parser.add_argument("--triton-url", required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--max-frames", type=int, default=0)
|
||||
parser.add_argument("--overlay", action="store_true")
|
||||
parser.add_argument("--telemetry-interval", type=float, default=0.5)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def read_object(path: Path) -> dict[str, Any]:
|
||||
value = json.loads(path.resolve(strict=True).read_text(encoding="utf-8-sig"))
|
||||
if not isinstance(value, dict):
|
||||
raise RuntimeError(f"JSON object expected: {path}")
|
||||
return value
|
||||
|
||||
|
||||
def canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value, sort_keys=True, separators=(",", ":"), allow_nan=False
|
||||
).encode()
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def distribution(values: list[float]) -> dict[str, float]:
|
||||
if not values:
|
||||
return {"mean": 0.0, "p50": 0.0, "p95": 0.0, "maximum": 0.0}
|
||||
ordered = sorted(values)
|
||||
|
||||
def percentile(fraction: float) -> float:
|
||||
index = (len(ordered) - 1) * fraction
|
||||
lower = math.floor(index)
|
||||
upper = math.ceil(index)
|
||||
if lower == upper:
|
||||
return ordered[lower]
|
||||
ratio = index - lower
|
||||
return ordered[lower] * (1.0 - ratio) + ordered[upper] * ratio
|
||||
|
||||
return {
|
||||
"mean": round(statistics.fmean(ordered), 6),
|
||||
"p50": round(percentile(0.5), 6),
|
||||
"p95": round(percentile(0.95), 6),
|
||||
"maximum": round(max(ordered), 6),
|
||||
}
|
||||
|
||||
|
||||
def validate_profile(profile: dict[str, Any], source_sha256: str) -> None:
|
||||
source = profile.get("source")
|
||||
detector = profile.get("detector")
|
||||
detection = profile.get("detection")
|
||||
acceptance = profile.get("acceptance")
|
||||
if (
|
||||
profile.get("schema_version") != PROFILE_SCHEMA
|
||||
or not isinstance(source, dict)
|
||||
or source.get("camera_source_id") != "sensor.camera.right"
|
||||
or source.get("stream_sha256") != source_sha256
|
||||
or source.get("resolution") != [800, 600]
|
||||
or source.get("calibration_model") != "KB4"
|
||||
or not isinstance(detector, dict)
|
||||
or detector.get("id") != "yolox_s"
|
||||
or detector.get("input_shape") != [1, 3, 640, 640]
|
||||
or detector.get("single_inference_per_source_frame") is not True
|
||||
or not isinstance(detection, dict)
|
||||
or detection.get("custom_detector_logic") is not False
|
||||
or detection.get("route_specific_filtering") is not False
|
||||
or not isinstance(acceptance, dict)
|
||||
or acceptance.get("require_full_raw_fov") is not True
|
||||
):
|
||||
raise RuntimeError("E46J profile contract changed")
|
||||
|
||||
|
||||
def load_mask(path: Path) -> np.ndarray:
|
||||
from PIL import Image
|
||||
|
||||
mask = np.asarray(Image.open(path.resolve(strict=True)).convert("L")) > 0
|
||||
if mask.shape != (600, 800) or not np.any(mask):
|
||||
raise RuntimeError("E46J valid-FOV mask changed")
|
||||
return mask
|
||||
|
||||
|
||||
def preprocess(
|
||||
image_bgr: np.ndarray, mask: np.ndarray, profile: dict[str, Any]
|
||||
) -> np.ndarray:
|
||||
import cv2
|
||||
|
||||
target_height = int(profile["detector"]["input_shape"][2])
|
||||
target_width = int(profile["detector"]["input_shape"][3])
|
||||
height, width = image_bgr.shape[:2]
|
||||
ratio = min(target_height / height, target_width / width)
|
||||
resized_width = int(width * ratio)
|
||||
resized_height = int(height * ratio)
|
||||
fill = int(profile["preprocessing"]["valid_fov_fill_value"])
|
||||
masked = np.where(mask[..., None], image_bgr, fill).astype(np.uint8)
|
||||
resized = cv2.resize(
|
||||
masked, (resized_width, resized_height), interpolation=cv2.INTER_LINEAR
|
||||
)
|
||||
canvas = np.full((target_height, target_width, 3), fill, dtype=np.uint8)
|
||||
canvas[:resized_height, :resized_width] = resized
|
||||
return np.ascontiguousarray(canvas.transpose(2, 0, 1), dtype=np.float32)[None]
|
||||
|
||||
|
||||
class TritonHttpClient:
|
||||
"""Minimal persistent Triton HTTP client for one sequential camera stream."""
|
||||
|
||||
def __init__(self, url: str, model: dict[str, Any]) -> None:
|
||||
parsed = urllib.parse.urlsplit(url)
|
||||
if parsed.scheme != "http" or not parsed.hostname:
|
||||
raise RuntimeError("E46J requires an explicit HTTP Triton endpoint")
|
||||
self.model = model
|
||||
self.path = f"{parsed.path.rstrip('/')}/v2/models/{model['id']}/infer"
|
||||
self.connection = http.client.HTTPConnection(
|
||||
parsed.hostname,
|
||||
parsed.port or 80,
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
def close(self) -> None:
|
||||
self.connection.close()
|
||||
|
||||
def infer(self, tensor: np.ndarray) -> np.ndarray:
|
||||
contiguous = np.ascontiguousarray(tensor, dtype=np.float32)
|
||||
binary = contiguous.tobytes()
|
||||
header = {
|
||||
"inputs": [
|
||||
{
|
||||
"name": self.model["input_name"],
|
||||
"shape": list(contiguous.shape),
|
||||
"datatype": "FP32",
|
||||
"parameters": {"binary_data_size": len(binary)},
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": self.model["output_name"],
|
||||
"parameters": {"binary_data": True},
|
||||
}
|
||||
],
|
||||
}
|
||||
encoded = canonical_json(header)
|
||||
self.connection.request(
|
||||
"POST",
|
||||
self.path,
|
||||
body=encoded + binary,
|
||||
headers={
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Inference-Header-Content-Length": str(len(encoded)),
|
||||
},
|
||||
)
|
||||
response = self.connection.getresponse()
|
||||
payload = response.read()
|
||||
if response.status != 200:
|
||||
raise RuntimeError(
|
||||
f"E46J Triton inference failed: HTTP {response.status}: "
|
||||
f"{payload[:512]!r}"
|
||||
)
|
||||
header_length_value = response.getheader("Inference-Header-Content-Length")
|
||||
if not header_length_value:
|
||||
raise RuntimeError("E46J Triton output header length is missing")
|
||||
header_length = int(header_length_value)
|
||||
descriptor = json.loads(payload[:header_length])["outputs"][0]
|
||||
if (
|
||||
descriptor["name"] != self.model["output_name"]
|
||||
or descriptor["datatype"] != "FP32"
|
||||
):
|
||||
raise RuntimeError("E46J Triton output descriptor changed")
|
||||
shape = tuple(int(value) for value in descriptor["shape"])
|
||||
array = np.frombuffer(payload[header_length:], dtype="<f4")
|
||||
if array.size != math.prod(shape):
|
||||
raise RuntimeError("E46J Triton output byte length changed")
|
||||
return array.reshape(shape)
|
||||
|
||||
|
||||
def decode_yolox(output: np.ndarray) -> np.ndarray:
|
||||
predictions = output.copy()
|
||||
grids: list[np.ndarray] = []
|
||||
strides: list[np.ndarray] = []
|
||||
for stride in (8, 16, 32):
|
||||
height = 640 // stride
|
||||
width = 640 // stride
|
||||
yv, xv = np.meshgrid(np.arange(height), np.arange(width), indexing="ij")
|
||||
grids.append(np.stack((xv, yv), axis=2).reshape(1, -1, 2))
|
||||
strides.append(np.full((1, height * width, 1), stride))
|
||||
grid = np.concatenate(grids, axis=1)
|
||||
expanded_strides = np.concatenate(strides, axis=1)
|
||||
predictions[..., :2] = (predictions[..., :2] + grid) * expanded_strides
|
||||
predictions[..., 2:4] = np.exp(predictions[..., 2:4]) * expanded_strides
|
||||
return predictions
|
||||
|
||||
|
||||
def box_iou(one: np.ndarray, many: np.ndarray) -> np.ndarray:
|
||||
if many.size == 0:
|
||||
return np.zeros((0,), dtype=np.float32)
|
||||
top_left = np.maximum(one[:2], many[:, :2])
|
||||
bottom_right = np.minimum(one[2:], many[:, 2:])
|
||||
intersection = np.prod(np.maximum(0.0, bottom_right - top_left), axis=1)
|
||||
one_area = max(0.0, float(one[2] - one[0])) * max(
|
||||
0.0, float(one[3] - one[1])
|
||||
)
|
||||
many_area = np.maximum(0.0, many[:, 2] - many[:, 0]) * np.maximum(
|
||||
0.0, many[:, 3] - many[:, 1]
|
||||
)
|
||||
union = one_area + many_area - intersection
|
||||
return np.divide(
|
||||
intersection, union, out=np.zeros_like(intersection), where=union > 0
|
||||
)
|
||||
|
||||
|
||||
def nms(boxes: np.ndarray, scores: np.ndarray, threshold: float) -> list[int]:
|
||||
order = scores.argsort()[::-1]
|
||||
keep: list[int] = []
|
||||
while order.size:
|
||||
index = int(order[0])
|
||||
keep.append(index)
|
||||
overlaps = box_iou(boxes[index], boxes[order[1:]])
|
||||
order = order[np.where(overlaps <= threshold)[0] + 1]
|
||||
return keep
|
||||
|
||||
|
||||
def valid_fraction(
|
||||
box: np.ndarray, integral: np.ndarray
|
||||
) -> tuple[float, bool, float]:
|
||||
height = integral.shape[0] - 1
|
||||
width = integral.shape[1] - 1
|
||||
x1 = int(np.clip(math.floor(float(box[0])), 0, width))
|
||||
y1 = int(np.clip(math.floor(float(box[1])), 0, height))
|
||||
x2 = int(np.clip(math.ceil(float(box[2])), 0, width))
|
||||
y2 = int(np.clip(math.ceil(float(box[3])), 0, height))
|
||||
area = float(max(0, x2 - x1) * max(0, y2 - y1))
|
||||
if area <= 0:
|
||||
return 0.0, False, 0.0
|
||||
inside = integral[y2, x2] - integral[y1, x2] - integral[y2, x1] + integral[
|
||||
y1, x1
|
||||
]
|
||||
center_x = int(
|
||||
np.clip(round((float(box[0]) + float(box[2])) / 2.0), 0, width - 1)
|
||||
)
|
||||
center_y = int(
|
||||
np.clip(round((float(box[1]) + float(box[3])) / 2.0), 0, height - 1)
|
||||
)
|
||||
center_inside = bool(
|
||||
integral[center_y + 1, center_x + 1]
|
||||
- integral[center_y, center_x + 1]
|
||||
- integral[center_y + 1, center_x]
|
||||
+ integral[center_y, center_x]
|
||||
)
|
||||
return float(inside) / area, center_inside, area
|
||||
|
||||
|
||||
def detections(
|
||||
output: np.ndarray, profile: dict[str, Any], mask: np.ndarray
|
||||
) -> tuple[list[dict[str, Any]], dict[str, int]]:
|
||||
prediction = decode_yolox(output)[0]
|
||||
boxes = prediction[:, :4]
|
||||
boxes_xyxy = np.empty_like(boxes)
|
||||
boxes_xyxy[:, 0] = boxes[:, 0] - boxes[:, 2] / 2.0
|
||||
boxes_xyxy[:, 1] = boxes[:, 1] - boxes[:, 3] / 2.0
|
||||
boxes_xyxy[:, 2] = boxes[:, 0] + boxes[:, 2] / 2.0
|
||||
boxes_xyxy[:, 3] = boxes[:, 1] + boxes[:, 3] / 2.0
|
||||
source_height, source_width = mask.shape
|
||||
ratio = min(640 / source_height, 640 / source_width)
|
||||
boxes_xyxy /= ratio
|
||||
class_scores = prediction[:, 4:5] * prediction[:, 5:]
|
||||
class_ids = class_scores.argmax(axis=1)
|
||||
scores = class_scores[np.arange(class_scores.shape[0]), class_ids]
|
||||
detection = profile["detection"]
|
||||
target_ids = set(int(value) for value in detection["target_class_ids"])
|
||||
candidate_mask = np.logical_and(
|
||||
scores >= float(detection["minimum_score"]),
|
||||
np.isin(class_ids, list(target_ids)),
|
||||
)
|
||||
candidate_boxes = boxes_xyxy[candidate_mask]
|
||||
candidate_scores = scores[candidate_mask]
|
||||
candidate_classes = class_ids[candidate_mask]
|
||||
integral = np.pad(mask.astype(np.int64), ((1, 0), (1, 0))).cumsum(0).cumsum(1)
|
||||
result: list[dict[str, Any]] = []
|
||||
rejected: Counter[str] = Counter()
|
||||
for class_id in sorted(target_ids):
|
||||
indices = np.where(candidate_classes == class_id)[0]
|
||||
if not indices.size:
|
||||
continue
|
||||
keep = nms(
|
||||
candidate_boxes[indices],
|
||||
candidate_scores[indices],
|
||||
float(detection["nms_iou_threshold"]),
|
||||
)
|
||||
for selected in indices[keep]:
|
||||
box = candidate_boxes[selected].copy()
|
||||
box[[0, 2]] = np.clip(box[[0, 2]], 0, source_width)
|
||||
box[[1, 3]] = np.clip(box[[1, 3]], 0, source_height)
|
||||
fraction, center_inside, area = valid_fraction(box, integral)
|
||||
if area < float(detection["minimum_box_area_pixels"]):
|
||||
rejected["small_box"] += 1
|
||||
continue
|
||||
if area / float(source_width * source_height) > float(
|
||||
detection["maximum_box_area_fraction"]
|
||||
):
|
||||
rejected["large_box"] += 1
|
||||
continue
|
||||
if fraction < float(detection["minimum_valid_fov_fraction"]):
|
||||
rejected["outside_valid_fov"] += 1
|
||||
continue
|
||||
if detection["require_center_inside_valid_fov"] and not center_inside:
|
||||
rejected["center_outside_valid_fov"] += 1
|
||||
continue
|
||||
result.append(
|
||||
{
|
||||
"class_id": int(class_id),
|
||||
"label": COCO_CLASSES[int(class_id)],
|
||||
"score": round(float(candidate_scores[selected]), 9),
|
||||
"bbox_xyxy": [round(float(value), 6) for value in box],
|
||||
"valid_fov_fraction": round(fraction, 6),
|
||||
}
|
||||
)
|
||||
result.sort(key=lambda item: (-float(item["score"]), int(item["class_id"])))
|
||||
return result, dict(rejected)
|
||||
|
||||
|
||||
def draw_overlay(image: np.ndarray, rows: list[dict[str, Any]]) -> np.ndarray:
|
||||
import cv2
|
||||
|
||||
for row in rows:
|
||||
x1, y1, x2, y2 = (int(round(value)) for value in row["bbox_xyxy"])
|
||||
label = f"{row['label']} {float(row['score']):.2f}"
|
||||
cv2.rectangle(image, (x1, y1), (x2, y2), (248, 248, 248), 2)
|
||||
(text_width, text_height), baseline = cv2.getTextSize(
|
||||
label, cv2.FONT_HERSHEY_SIMPLEX, 0.45, 1
|
||||
)
|
||||
top = max(0, y1 - text_height - baseline - 6)
|
||||
cv2.rectangle(
|
||||
image,
|
||||
(x1, top),
|
||||
(min(image.shape[1] - 1, x1 + text_width + 8), y1),
|
||||
(18, 18, 18),
|
||||
-1,
|
||||
)
|
||||
cv2.putText(
|
||||
image,
|
||||
label,
|
||||
(x1 + 4, max(text_height + 1, y1 - baseline - 3)),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.45,
|
||||
(248, 248, 248),
|
||||
1,
|
||||
cv2.LINE_AA,
|
||||
)
|
||||
return image
|
||||
|
||||
|
||||
class Telemetry:
|
||||
def __init__(self, interval: float) -> None:
|
||||
self.interval = interval
|
||||
self.samples: list[dict[str, float]] = []
|
||||
self.stop_event = threading.Event()
|
||||
self.thread = threading.Thread(target=self._run, daemon=True)
|
||||
|
||||
def __enter__(self) -> Telemetry:
|
||||
self.thread.start()
|
||||
return self
|
||||
|
||||
def __exit__(self, *_args: object) -> None:
|
||||
self.stop_event.set()
|
||||
self.thread.join(timeout=5)
|
||||
|
||||
def _run(self) -> None:
|
||||
while not self.stop_event.is_set():
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
[
|
||||
"nvidia-smi",
|
||||
"--query-gpu=utilization.gpu,memory.used,power.draw,temperature.gpu",
|
||||
"--format=csv,noheader,nounits",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
values = [float(value.strip()) for value in completed.stdout.split(",")]
|
||||
self.samples.append(
|
||||
{
|
||||
"gpu_utilization_percent": values[0],
|
||||
"gpu_memory_used_mib": values[1],
|
||||
"gpu_power_watts": values[2],
|
||||
"gpu_temperature_celsius": values[3],
|
||||
}
|
||||
)
|
||||
except (OSError, ValueError, subprocess.SubprocessError):
|
||||
pass
|
||||
self.stop_event.wait(self.interval)
|
||||
|
||||
|
||||
def telemetry_summary(samples: list[dict[str, float]]) -> dict[str, object]:
|
||||
result: dict[str, object] = {"sample_count": len(samples)}
|
||||
for key in (
|
||||
"gpu_utilization_percent",
|
||||
"gpu_memory_used_mib",
|
||||
"gpu_power_watts",
|
||||
"gpu_temperature_celsius",
|
||||
):
|
||||
result[key] = distribution([row[key] for row in samples])
|
||||
return result
|
||||
|
||||
|
||||
def run(args: argparse.Namespace) -> dict[str, Any]:
|
||||
import av
|
||||
|
||||
source = args.input.resolve(strict=True)
|
||||
profile_path = args.profile.resolve(strict=True)
|
||||
mask_path = args.mask.resolve(strict=True)
|
||||
output = args.output.expanduser().absolute()
|
||||
output.mkdir(parents=True, exist_ok=False)
|
||||
profile = read_object(profile_path)
|
||||
source_sha = sha256(source)
|
||||
validate_profile(profile, source_sha)
|
||||
mask = load_mask(mask_path)
|
||||
mask_sha = sha256(mask_path)
|
||||
|
||||
container = av.open(str(source))
|
||||
video_stream = container.streams.video[0]
|
||||
frame_rate = float(video_stream.average_rate)
|
||||
if not math.isclose(frame_rate, float(profile["source"]["frame_rate"]), abs_tol=1e-6):
|
||||
raise RuntimeError(
|
||||
"E46J source frame rate changed: "
|
||||
f"profile={profile['source']['frame_rate']}, actual={frame_rate}, "
|
||||
f"average_rate={video_stream.average_rate}, time_base={video_stream.time_base}"
|
||||
)
|
||||
|
||||
overlay_path = output / "raw-fisheye-yolox-overlay-intermediate.mp4"
|
||||
overlay_writer = None
|
||||
if args.overlay:
|
||||
import cv2
|
||||
|
||||
overlay_writer = cv2.VideoWriter(
|
||||
str(overlay_path),
|
||||
cv2.VideoWriter_fourcc(*"mp4v"),
|
||||
frame_rate,
|
||||
(800, 600),
|
||||
)
|
||||
if not overlay_writer.isOpened():
|
||||
raise RuntimeError("E46J intermediate overlay encoder is unavailable")
|
||||
|
||||
frame_path = output / "frames.jsonl"
|
||||
frame_stream = frame_path.open("x", encoding="utf-8")
|
||||
class_counts: Counter[str] = Counter()
|
||||
rejected_counts: Counter[str] = Counter()
|
||||
latencies: dict[str, list[float]] = {
|
||||
"preprocess_ms": [],
|
||||
"inference_request_ms": [],
|
||||
"postprocess_ms": [],
|
||||
"core_path_ms": [],
|
||||
"overlay_encode_ms": [],
|
||||
}
|
||||
zero_detection_frames = 0
|
||||
maximum_detections = 0
|
||||
failed_frames = 0
|
||||
processed_frames = 0
|
||||
run_started_utc = datetime.now(UTC)
|
||||
wall_started = time.perf_counter()
|
||||
warm_tensor = np.full((1, 3, 640, 640), 114.0, dtype=np.float32)
|
||||
triton = TritonHttpClient(args.triton_url, profile["detector"])
|
||||
triton.infer(warm_tensor)
|
||||
|
||||
with Telemetry(args.telemetry_interval) as telemetry:
|
||||
try:
|
||||
for decoded in container.decode(video_stream):
|
||||
if args.max_frames and processed_frames >= args.max_frames:
|
||||
break
|
||||
frame_index = processed_frames
|
||||
image = decoded.to_ndarray(format="bgr24")
|
||||
if image.shape != (600, 800, 3):
|
||||
raise RuntimeError(f"E46J source raster changed at frame {frame_index}")
|
||||
core_started = time.perf_counter()
|
||||
preprocess_started = core_started
|
||||
tensor = preprocess(image, mask, profile)
|
||||
preprocess_ms = (time.perf_counter() - preprocess_started) * 1000.0
|
||||
inference_started = time.perf_counter()
|
||||
raw_output = triton.infer(tensor)
|
||||
inference_ms = (time.perf_counter() - inference_started) * 1000.0
|
||||
postprocess_started = time.perf_counter()
|
||||
rows, rejected = detections(raw_output, profile, mask)
|
||||
postprocess_ms = (time.perf_counter() - postprocess_started) * 1000.0
|
||||
core_path_ms = (time.perf_counter() - core_started) * 1000.0
|
||||
latencies["preprocess_ms"].append(preprocess_ms)
|
||||
latencies["inference_request_ms"].append(inference_ms)
|
||||
latencies["postprocess_ms"].append(postprocess_ms)
|
||||
latencies["core_path_ms"].append(core_path_ms)
|
||||
class_counts.update(str(row["label"]) for row in rows)
|
||||
rejected_counts.update(rejected)
|
||||
maximum_detections = max(maximum_detections, len(rows))
|
||||
if not rows:
|
||||
zero_detection_frames += 1
|
||||
|
||||
overlay_ms = 0.0
|
||||
if overlay_writer is not None:
|
||||
overlay_started = time.perf_counter()
|
||||
overlay = draw_overlay(image.copy(), rows)
|
||||
overlay_writer.write(overlay)
|
||||
overlay_ms = (time.perf_counter() - overlay_started) * 1000.0
|
||||
latencies["overlay_encode_ms"].append(overlay_ms)
|
||||
frame_stream.write(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": FRAME_SCHEMA,
|
||||
"frame_index": frame_index,
|
||||
"session_seconds": round(frame_index / frame_rate, 6),
|
||||
"detections": rows,
|
||||
"rejected": rejected,
|
||||
"latency_ms": {
|
||||
"preprocess": round(preprocess_ms, 6),
|
||||
"inference_request": round(inference_ms, 6),
|
||||
"postprocess": round(postprocess_ms, 6),
|
||||
"core_path": round(core_path_ms, 6),
|
||||
"overlay_encode": round(overlay_ms, 6),
|
||||
},
|
||||
},
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
processed_frames += 1
|
||||
except BaseException:
|
||||
failed_frames += 1
|
||||
raise
|
||||
finally:
|
||||
frame_stream.flush()
|
||||
os.fsync(frame_stream.fileno())
|
||||
frame_stream.close()
|
||||
container.close()
|
||||
triton.close()
|
||||
if overlay_writer is not None:
|
||||
overlay_writer.release()
|
||||
|
||||
wall_seconds = time.perf_counter() - wall_started
|
||||
run_completed_utc = datetime.now(UTC)
|
||||
core = distribution(latencies["core_path_ms"])
|
||||
inference = distribution(latencies["inference_request_ms"])
|
||||
acceptance = profile["acceptance"]
|
||||
expected_frames = (
|
||||
min(int(args.max_frames), int(profile["source"]["frame_count"]))
|
||||
if args.max_frames
|
||||
else int(profile["source"]["frame_count"])
|
||||
)
|
||||
checks = {
|
||||
"frame_coverage": processed_frames == expected_frames,
|
||||
"zero_failed_frames": failed_frames == 0,
|
||||
"core_capacity_fps": (1000.0 / core["mean"])
|
||||
>= float(acceptance["minimum_core_capacity_fps"]),
|
||||
"core_path_p95_ms": core["p95"]
|
||||
<= float(acceptance["maximum_core_path_p95_ms"]),
|
||||
"inference_request_p95_ms": inference["p95"]
|
||||
<= float(acceptance["maximum_inference_request_p95_ms"]),
|
||||
"single_pass_full_raw_fov": True,
|
||||
}
|
||||
runtime: dict[str, Any] = {
|
||||
"schema_version": RUNTIME_SCHEMA,
|
||||
"status": "completed" if all(checks.values()) else "completed-gate-failed",
|
||||
"worker_host": platform.node(),
|
||||
"gpu_name": subprocess.run(
|
||||
["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
).stdout.strip().splitlines()[0],
|
||||
"started_at_utc": run_started_utc.isoformat(timespec="milliseconds").replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"completed_at_utc": run_completed_utc.isoformat(
|
||||
timespec="milliseconds"
|
||||
).replace("+00:00", "Z"),
|
||||
"source": {
|
||||
"video_sha256": source_sha,
|
||||
"frame_rate": frame_rate,
|
||||
"resolution": [800, 600],
|
||||
"decoded_frame_count": processed_frames,
|
||||
"valid_fov_mask_sha256": mask_sha,
|
||||
"preprocessing": "raw KB4 valid-FOV fill plus top-left letterbox; no crop/dewarp/tile",
|
||||
},
|
||||
"model": {
|
||||
"id": profile["detector"]["id"],
|
||||
"model_sha256": profile["detector"]["model_sha256"],
|
||||
"config_sha256": profile["detector"]["config_sha256"],
|
||||
"runtime": profile["detector"]["runtime"],
|
||||
"inference_requests": processed_frames,
|
||||
},
|
||||
"profile_sha256": sha256(profile_path),
|
||||
"metrics": {
|
||||
"processed_frame_count": processed_frames,
|
||||
"failed_frame_count": failed_frames,
|
||||
"wall_seconds_including_overlay_export": round(wall_seconds, 6),
|
||||
"export_throughput_fps": round(processed_frames / wall_seconds, 6),
|
||||
"core_capacity_fps": round(1000.0 / core["mean"], 6),
|
||||
"latency_ms": {key: distribution(values) for key, values in latencies.items()},
|
||||
"detection_observation_count": sum(class_counts.values()),
|
||||
"class_observation_counts": dict(sorted(class_counts.items())),
|
||||
"mean_detections_per_frame": round(
|
||||
sum(class_counts.values()) / max(processed_frames, 1), 6
|
||||
),
|
||||
"maximum_detections_per_frame": maximum_detections,
|
||||
"zero_detection_frame_count": zero_detection_frames,
|
||||
"zero_detection_frame_fraction": round(
|
||||
zero_detection_frames / max(processed_frames, 1), 9
|
||||
),
|
||||
"rejected_counts": dict(sorted(rejected_counts.items())),
|
||||
"gpu": telemetry_summary(telemetry.samples),
|
||||
},
|
||||
"acceptance": {
|
||||
"thresholds": acceptance,
|
||||
"checks": checks,
|
||||
"passed": all(checks.values()),
|
||||
},
|
||||
"artifacts": {
|
||||
"frames": {
|
||||
"file": frame_path.name,
|
||||
"byte_length": frame_path.stat().st_size,
|
||||
"sha256": sha256(frame_path),
|
||||
},
|
||||
},
|
||||
"authority": profile["authority"],
|
||||
}
|
||||
if overlay_path.is_file():
|
||||
runtime["artifacts"]["overlay_intermediate"] = {
|
||||
"file": overlay_path.name,
|
||||
"byte_length": overlay_path.stat().st_size,
|
||||
"sha256": sha256(overlay_path),
|
||||
"codec": "MPEG-4 Part 2",
|
||||
"frame_rate": frame_rate,
|
||||
}
|
||||
runtime_path = output / "runtime.json"
|
||||
runtime_path.write_text(
|
||||
json.dumps(runtime, ensure_ascii=False, indent=2, allow_nan=False) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return runtime
|
||||
|
||||
|
||||
def main() -> int:
|
||||
runtime = run(arguments())
|
||||
print(json.dumps(runtime, ensure_ascii=False, indent=2, allow_nan=False))
|
||||
return 0 if runtime["acceptance"]["passed"] else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,457 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the canonical YOLOX detector on calibration-derived K1 perspective views.
|
||||
|
||||
This module is an adapter between the already qualified K1 KB4 rectification
|
||||
from LAB E3 and the already qualified YOLOX/ByteTrack detector from LAB E8.
|
||||
It deliberately does not introduce another detector, tracker or coordinate
|
||||
system. Detections are projected back into the immutable right-camera frame
|
||||
before class-aware NMS and tracking.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import concurrent.futures
|
||||
import json
|
||||
import math
|
||||
import statistics
|
||||
import time
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
from run_e3_rectified_segmentation import _clahe, _rectification_maps
|
||||
from run_e5_instance_tracking import (
|
||||
_detections,
|
||||
_infer,
|
||||
_load_valid_fov,
|
||||
_nms,
|
||||
_preprocess,
|
||||
_valid_fraction,
|
||||
)
|
||||
|
||||
|
||||
def _arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--job", type=Path, required=True)
|
||||
parser.add_argument("--frames", type=Path, required=True)
|
||||
parser.add_argument("--rectification-profile", type=Path, required=True)
|
||||
parser.add_argument("--detector-profile", type=Path, required=True)
|
||||
parser.add_argument("--valid-fov-root", type=Path, required=True)
|
||||
parser.add_argument("--triton-url", required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--contrast", choices=("none", "clahe"), default="none")
|
||||
parser.add_argument(
|
||||
"--active-tiles",
|
||||
default="front,left,right,up,down",
|
||||
help="Comma-separated E3 rectification tile names evaluated this pass",
|
||||
)
|
||||
parser.add_argument("--limit", type=int, default=0)
|
||||
parser.add_argument(
|
||||
"--tile-size",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Detector raster size; 0 preserves the E3 profile raster",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _read_object(path: Path) -> dict[str, Any]:
|
||||
value = json.loads(path.resolve(strict=True).read_text(encoding="utf-8-sig"))
|
||||
if not isinstance(value, dict):
|
||||
raise RuntimeError(f"JSON root is not an object: {path}")
|
||||
return value
|
||||
|
||||
|
||||
def _percentile(values: list[float], percentile: float) -> float:
|
||||
if not values:
|
||||
return 0.0
|
||||
ordered = sorted(values)
|
||||
index = (len(ordered) - 1) * percentile
|
||||
lower = math.floor(index)
|
||||
upper = math.ceil(index)
|
||||
if lower == upper:
|
||||
return ordered[lower]
|
||||
fraction = index - lower
|
||||
return ordered[lower] * (1.0 - fraction) + ordered[upper] * fraction
|
||||
|
||||
|
||||
def _project_tile_box(
|
||||
box: list[float],
|
||||
tile: dict[str, Any],
|
||||
selected_tile: np.ndarray,
|
||||
tile_index: int,
|
||||
) -> tuple[np.ndarray, tuple[float, float]] | None:
|
||||
"""Project a perspective bbox perimeter into the raw fisheye frame."""
|
||||
|
||||
map_x = tile["raw_map_x"]
|
||||
map_y = tile["raw_map_y"]
|
||||
height, width = map_x.shape
|
||||
x1, y1, x2, y2 = (float(value) for value in box)
|
||||
x1 = float(np.clip(x1, 0.0, width - 1.0))
|
||||
x2 = float(np.clip(x2, 0.0, width - 1.0))
|
||||
y1 = float(np.clip(y1, 0.0, height - 1.0))
|
||||
y2 = float(np.clip(y2, 0.0, height - 1.0))
|
||||
if x2 <= x1 or y2 <= y1:
|
||||
return None
|
||||
|
||||
samples = max(16, int(max(x2 - x1, y2 - y1) / 4.0))
|
||||
horizontal = np.linspace(x1, x2, samples)
|
||||
vertical = np.linspace(y1, y2, samples)
|
||||
sample_x = np.concatenate(
|
||||
(horizontal, horizontal, np.full(samples, x1), np.full(samples, x2))
|
||||
)
|
||||
sample_y = np.concatenate(
|
||||
(np.full(samples, y1), np.full(samples, y2), vertical, vertical)
|
||||
)
|
||||
integer_x = np.clip(np.rint(sample_x).astype(np.int64), 0, width - 1)
|
||||
integer_y = np.clip(np.rint(sample_y).astype(np.int64), 0, height - 1)
|
||||
raw_x = map_x[integer_y, integer_x]
|
||||
raw_y = map_y[integer_y, integer_x]
|
||||
finite = np.isfinite(raw_x) & np.isfinite(raw_y)
|
||||
if not np.any(finite):
|
||||
return None
|
||||
|
||||
center_x = int(np.clip(round((x1 + x2) / 2.0), 0, width - 1))
|
||||
center_y = int(np.clip(round((y1 + y2) / 2.0), 0, height - 1))
|
||||
raw_center_x = float(map_x[center_y, center_x])
|
||||
raw_center_y = float(map_y[center_y, center_x])
|
||||
raw_height, raw_width = selected_tile.shape
|
||||
if not (
|
||||
math.isfinite(raw_center_x)
|
||||
and math.isfinite(raw_center_y)
|
||||
and 0.0 <= raw_center_x < raw_width
|
||||
and 0.0 <= raw_center_y < raw_height
|
||||
):
|
||||
return None
|
||||
owner_x = int(np.clip(round(raw_center_x), 0, raw_width - 1))
|
||||
owner_y = int(np.clip(round(raw_center_y), 0, raw_height - 1))
|
||||
if int(selected_tile[owner_y, owner_x]) != tile_index:
|
||||
return None
|
||||
|
||||
projected = np.asarray(
|
||||
[
|
||||
float(np.min(raw_x[finite])),
|
||||
float(np.min(raw_y[finite])),
|
||||
float(np.max(raw_x[finite])),
|
||||
float(np.max(raw_y[finite])),
|
||||
],
|
||||
dtype=np.float64,
|
||||
)
|
||||
projected[[0, 2]] = np.clip(projected[[0, 2]], 0.0, raw_width)
|
||||
projected[[1, 3]] = np.clip(projected[[1, 3]], 0.0, raw_height)
|
||||
return projected, (raw_center_x, raw_center_y)
|
||||
|
||||
|
||||
def _merge_raw_detections(
|
||||
candidates: list[dict[str, Any]],
|
||||
detector_profile: dict[str, Any],
|
||||
valid_mask: np.ndarray,
|
||||
) -> tuple[list[dict[str, Any]], dict[str, int]]:
|
||||
detection = detector_profile["detection"]
|
||||
integral = np.pad(valid_mask.astype(np.int64), ((1, 0), (1, 0))).cumsum(0).cumsum(1)
|
||||
raw_height, raw_width = valid_mask.shape
|
||||
admitted: list[dict[str, Any]] = []
|
||||
rejected = Counter()
|
||||
for candidate in candidates:
|
||||
box = np.asarray(candidate["bbox_xyxy"], dtype=np.float64)
|
||||
valid_fraction, center_inside, area = _valid_fraction(box, integral)
|
||||
if area < float(detection["minimum_box_area_pixels"]):
|
||||
rejected["small_raw_box"] += 1
|
||||
continue
|
||||
if area / float(raw_width * raw_height) > float(
|
||||
detection["maximum_box_area_fraction"]
|
||||
):
|
||||
rejected["large_raw_box"] += 1
|
||||
continue
|
||||
if valid_fraction < float(detection["minimum_valid_fov_fraction"]):
|
||||
rejected["outside_raw_valid_fov"] += 1
|
||||
continue
|
||||
if detection["require_center_inside_valid_fov"] and not center_inside:
|
||||
rejected["raw_center_outside_valid_fov"] += 1
|
||||
continue
|
||||
enriched = dict(candidate)
|
||||
enriched["valid_fov_fraction"] = round(valid_fraction, 6)
|
||||
admitted.append(enriched)
|
||||
|
||||
result: list[dict[str, Any]] = []
|
||||
class_ids = sorted({int(item["class_id"]) for item in admitted})
|
||||
for class_id in class_ids:
|
||||
group = [item for item in admitted if int(item["class_id"]) == class_id]
|
||||
boxes = np.asarray([item["bbox_xyxy"] for item in group], dtype=np.float64)
|
||||
scores = np.asarray([item["score"] for item in group], dtype=np.float64)
|
||||
keep = _nms(
|
||||
boxes,
|
||||
scores,
|
||||
float(detection["nms_iou_threshold"]),
|
||||
float(detection["nms_containment_threshold"]),
|
||||
)
|
||||
result.extend(group[index] for index in keep)
|
||||
rejected["cross_tile_duplicate"] += len(group) - len(keep)
|
||||
result.sort(key=lambda item: (-float(item["score"]), int(item["class_id"])))
|
||||
return result, dict(rejected)
|
||||
|
||||
|
||||
def detect_rectified(
|
||||
image: np.ndarray,
|
||||
*,
|
||||
maps: dict[str, Any],
|
||||
rectification_profile: dict[str, Any],
|
||||
detector_profile: dict[str, Any],
|
||||
valid_mask: np.ndarray,
|
||||
triton_url: str,
|
||||
contrast: str,
|
||||
active_tiles: set[str],
|
||||
cv2: Any,
|
||||
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
||||
candidates: list[dict[str, Any]] = []
|
||||
tile_metrics: list[dict[str, Any]] = []
|
||||
tile_valid_mask = np.ones(
|
||||
(
|
||||
int(rectification_profile["rectification"]["tile_size"]),
|
||||
int(rectification_profile["rectification"]["tile_size"]),
|
||||
),
|
||||
dtype=bool,
|
||||
)
|
||||
|
||||
active_entries = [
|
||||
(tile_index, tile)
|
||||
for tile_index, tile in enumerate(maps["tiles"])
|
||||
if tile["name"] in active_tiles
|
||||
]
|
||||
|
||||
def prepare(entry: tuple[int, dict[str, Any]]) -> dict[str, Any]:
|
||||
tile_index, tile = entry
|
||||
started = time.perf_counter()
|
||||
tile_image = cv2.remap(
|
||||
image,
|
||||
tile["raw_map_x"],
|
||||
tile["raw_map_y"],
|
||||
interpolation=cv2.INTER_LINEAR,
|
||||
borderMode=cv2.BORDER_CONSTANT,
|
||||
borderValue=(114, 114, 114),
|
||||
)
|
||||
if contrast == "clahe":
|
||||
tile_image = _clahe(tile_image, rectification_profile, cv2)
|
||||
preprocess_started = time.perf_counter()
|
||||
tensor = _preprocess(tile_image, tile_valid_mask, detector_profile)
|
||||
preprocess_ms = (time.perf_counter() - preprocess_started) * 1000.0
|
||||
return {
|
||||
"tile_index": tile_index,
|
||||
"tile": tile,
|
||||
"tensor": tensor,
|
||||
"preprocess_ms": preprocess_ms,
|
||||
"preparation_ms": (time.perf_counter() - started) * 1000.0,
|
||||
}
|
||||
|
||||
# Rectification/remap is CPU work and independent for each calibrated view.
|
||||
# Keep the pool bounded to the three operational core views; Triton requests
|
||||
# below remain sequential against the single canonical model instance.
|
||||
with concurrent.futures.ThreadPoolExecutor(
|
||||
max_workers=len(active_entries), thread_name_prefix="k1-kb4-view"
|
||||
) as executor:
|
||||
prepared = list(executor.map(prepare, active_entries))
|
||||
|
||||
for item in prepared:
|
||||
tile_index = int(item["tile_index"])
|
||||
tile = item["tile"]
|
||||
tile_started = time.perf_counter()
|
||||
output, inference_ms = _infer(
|
||||
triton_url, detector_profile["model"], item["tensor"]
|
||||
)
|
||||
tile_detections, tile_rejected = _detections(
|
||||
output, detector_profile, tile_valid_mask
|
||||
)
|
||||
owned = 0
|
||||
for detection in tile_detections:
|
||||
projected = _project_tile_box(
|
||||
detection["bbox_xyxy"], tile, maps["selected_tile"], tile_index
|
||||
)
|
||||
if projected is None:
|
||||
continue
|
||||
raw_box, raw_center = projected
|
||||
candidate = dict(detection)
|
||||
candidate["bbox_xyxy"] = [round(float(value), 6) for value in raw_box]
|
||||
candidate["raw_center_xy"] = [round(value, 6) for value in raw_center]
|
||||
candidate["rectification_tile"] = tile["name"]
|
||||
candidates.append(candidate)
|
||||
owned += 1
|
||||
tile_metrics.append(
|
||||
{
|
||||
"tile": tile["name"],
|
||||
"detections": len(tile_detections),
|
||||
"owned_detections": owned,
|
||||
"inference_ms": round(inference_ms, 6),
|
||||
"preprocess_ms": round(float(item["preprocess_ms"]), 6),
|
||||
"preparation_ms": round(float(item["preparation_ms"]), 6),
|
||||
"serial_inference_postprocess_ms": round(
|
||||
(time.perf_counter() - tile_started) * 1000.0, 6
|
||||
),
|
||||
"rejected": tile_rejected,
|
||||
}
|
||||
)
|
||||
merged, rejected = _merge_raw_detections(candidates, detector_profile, valid_mask)
|
||||
return merged, {
|
||||
"tiles": tile_metrics,
|
||||
"candidate_count": len(candidates),
|
||||
"rejected": rejected,
|
||||
}
|
||||
|
||||
|
||||
def _draw_overlay(image: np.ndarray, detections: list[dict[str, Any]]) -> Image.Image:
|
||||
output = Image.fromarray(image)
|
||||
draw = ImageDraw.Draw(output)
|
||||
palette = {
|
||||
"person": "#8cff5d",
|
||||
"bicycle": "#50d7ff",
|
||||
"car": "#ffffff",
|
||||
"motorcycle": "#ffcd57",
|
||||
"bus": "#ff8b5d",
|
||||
"truck": "#ff8b5d",
|
||||
}
|
||||
for detection in detections:
|
||||
box = tuple(float(value) for value in detection["bbox_xyxy"])
|
||||
color = palette.get(str(detection["label"]), "#ffffff")
|
||||
draw.rectangle(box, outline=color, width=3)
|
||||
label = (
|
||||
f"{detection['label']} {float(detection['score']):.0%} "
|
||||
f"[{detection['rectification_tile']}]"
|
||||
)
|
||||
text_box = draw.textbbox((box[0], max(0.0, box[1] - 18.0)), label)
|
||||
draw.rectangle(text_box, fill="#0b0b0d")
|
||||
draw.text((box[0], max(0.0, box[1] - 18.0)), label, fill=color)
|
||||
return output
|
||||
|
||||
|
||||
def main() -> None:
|
||||
arguments = _arguments()
|
||||
if arguments.limit < 0:
|
||||
raise RuntimeError("--limit cannot be negative")
|
||||
if arguments.tile_size and not 320 <= arguments.tile_size <= 1024:
|
||||
raise RuntimeError("--tile-size must be zero or within [320, 1024]")
|
||||
import cv2
|
||||
|
||||
rectification_profile = _read_object(arguments.rectification_profile)
|
||||
if arguments.tile_size:
|
||||
rectification_profile = copy.deepcopy(rectification_profile)
|
||||
rectification_profile["rectification"]["tile_size"] = arguments.tile_size
|
||||
detector_profile = _read_object(arguments.detector_profile)
|
||||
job = _read_object(arguments.job)
|
||||
active_tiles = {
|
||||
value.strip() for value in arguments.active_tiles.split(",") if value.strip()
|
||||
}
|
||||
configured_tiles = {
|
||||
str(tile["name"])
|
||||
for tile in rectification_profile["rectification"]["tiles"]
|
||||
}
|
||||
if not active_tiles or not active_tiles <= configured_tiles:
|
||||
raise RuntimeError("--active-tiles contains an unknown or empty tile set")
|
||||
expected_resolution = tuple(rectification_profile["source"]["resolution"])
|
||||
if expected_resolution != tuple(detector_profile["source"]["resolution"]):
|
||||
raise RuntimeError("Rectification and detector source resolutions differ")
|
||||
if (
|
||||
rectification_profile["source"]["calibration_sha256"]
|
||||
!= detector_profile["source"]["calibration_sha256"]
|
||||
):
|
||||
raise RuntimeError("Rectification and detector calibration identities differ")
|
||||
|
||||
valid_mask, _mask_metadata = _load_valid_fov(
|
||||
arguments.valid_fov_root, job, detector_profile
|
||||
)
|
||||
maps_started = time.perf_counter()
|
||||
maps = _rectification_maps(rectification_profile, valid_mask)
|
||||
maps_ms = (time.perf_counter() - maps_started) * 1000.0
|
||||
frame_paths = sorted(arguments.frames.glob("*.jpg"))
|
||||
if arguments.limit:
|
||||
frame_paths = frame_paths[: arguments.limit]
|
||||
if not frame_paths:
|
||||
raise RuntimeError("No JPEG benchmark frames were found")
|
||||
|
||||
arguments.output.mkdir(parents=True, exist_ok=False)
|
||||
overlays = arguments.output / "overlays"
|
||||
overlays.mkdir()
|
||||
documents: list[dict[str, Any]] = []
|
||||
frame_latencies: list[float] = []
|
||||
inference_latencies: list[float] = []
|
||||
detection_counts: list[int] = []
|
||||
for frame_path in frame_paths:
|
||||
image = np.asarray(Image.open(frame_path).convert("RGB"), dtype=np.uint8)
|
||||
if (image.shape[1], image.shape[0]) != expected_resolution:
|
||||
raise RuntimeError(f"Unexpected camera resolution: {frame_path}")
|
||||
started = time.perf_counter()
|
||||
detections, diagnostics = detect_rectified(
|
||||
image,
|
||||
maps=maps,
|
||||
rectification_profile=rectification_profile,
|
||||
detector_profile=detector_profile,
|
||||
valid_mask=valid_mask,
|
||||
triton_url=arguments.triton_url,
|
||||
contrast=arguments.contrast,
|
||||
active_tiles=active_tiles,
|
||||
cv2=cv2,
|
||||
)
|
||||
elapsed_ms = (time.perf_counter() - started) * 1000.0
|
||||
frame_latencies.append(elapsed_ms)
|
||||
inference_latencies.extend(
|
||||
float(tile["inference_ms"]) for tile in diagnostics["tiles"]
|
||||
)
|
||||
detection_counts.append(len(detections))
|
||||
overlay_name = frame_path.name
|
||||
_draw_overlay(image, detections).save(
|
||||
overlays / overlay_name, format="JPEG", quality=92, optimize=True
|
||||
)
|
||||
documents.append(
|
||||
{
|
||||
"frame": frame_path.name,
|
||||
"elapsed_ms": round(elapsed_ms, 6),
|
||||
"detections": detections,
|
||||
"diagnostics": diagnostics,
|
||||
"overlay": f"overlays/{overlay_name}",
|
||||
}
|
||||
)
|
||||
print(
|
||||
f"FRAME={frame_path.name} DETECTIONS={len(detections)} "
|
||||
f"ELAPSED_MS={elapsed_ms:.3f}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
summary = {
|
||||
"schema_version": "missioncore.rectified-yolox-benchmark/v1",
|
||||
"pipeline": "k1-kb4-cubemap5-yolox-raw-frame/v1",
|
||||
"contrast": arguments.contrast,
|
||||
"active_tiles": sorted(active_tiles),
|
||||
"tile_size": int(rectification_profile["rectification"]["tile_size"]),
|
||||
"frame_count": len(documents),
|
||||
"rectification_map_build_ms": round(maps_ms, 6),
|
||||
"latency_ms": {
|
||||
"mean": round(statistics.fmean(frame_latencies), 6),
|
||||
"p50": round(_percentile(frame_latencies, 0.5), 6),
|
||||
"p95": round(_percentile(frame_latencies, 0.95), 6),
|
||||
"maximum": round(max(frame_latencies), 6),
|
||||
},
|
||||
"tile_inference_ms": {
|
||||
"mean": round(statistics.fmean(inference_latencies), 6),
|
||||
"p95": round(_percentile(inference_latencies, 0.95), 6),
|
||||
"maximum": round(max(inference_latencies), 6),
|
||||
},
|
||||
"detections_per_frame": {
|
||||
"mean": round(statistics.fmean(detection_counts), 6),
|
||||
"minimum": min(detection_counts),
|
||||
"maximum": max(detection_counts),
|
||||
"total": sum(detection_counts),
|
||||
},
|
||||
"coverage": maps["coverage"],
|
||||
"frames": documents,
|
||||
}
|
||||
(arguments.output / "benchmark.json").write_text(
|
||||
json.dumps(summary, ensure_ascii=False, sort_keys=True, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(json.dumps({key: value for key, value in summary.items() if key != "frames"}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,326 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Qualify the rectified YOLOX + existing ByteTrack path on full RAVNOVES00."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import json
|
||||
import platform
|
||||
import statistics
|
||||
import time
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from run_e3_rectified_segmentation import _rectification_maps
|
||||
from run_e5_instance_tracking import (
|
||||
TwoStageTracker,
|
||||
_duplicate_pairs,
|
||||
_load_valid_fov,
|
||||
_track_document,
|
||||
)
|
||||
from run_rectified_yolox_detector import (
|
||||
_draw_overlay,
|
||||
_percentile,
|
||||
_read_object,
|
||||
detect_rectified,
|
||||
)
|
||||
|
||||
|
||||
def _arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--job", type=Path, required=True)
|
||||
parser.add_argument("--video", type=Path, required=True)
|
||||
parser.add_argument("--rectification-profile", type=Path, required=True)
|
||||
parser.add_argument("--detector-profile", type=Path, required=True)
|
||||
parser.add_argument("--valid-fov-root", type=Path, required=True)
|
||||
parser.add_argument("--triton-url", required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--active-tiles", default="front,left,right")
|
||||
parser.add_argument("--tile-size", type=int, default=640)
|
||||
parser.add_argument("--preview-stride", type=int, default=250)
|
||||
parser.add_argument("--limit", type=int, default=0)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _latency(values: list[float]) -> dict[str, float]:
|
||||
return {
|
||||
"mean": round(statistics.fmean(values), 6),
|
||||
"p50": round(_percentile(values, 0.5), 6),
|
||||
"p95": round(_percentile(values, 0.95), 6),
|
||||
"p99": round(_percentile(values, 0.99), 6),
|
||||
"maximum": round(max(values), 6),
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
arguments = _arguments()
|
||||
if arguments.tile_size != 640:
|
||||
raise RuntimeError("Operational qualification is fixed to detector-native 640 tiles")
|
||||
if arguments.preview_stride < 1 or arguments.limit < 0:
|
||||
raise RuntimeError("Preview stride or frame limit is invalid")
|
||||
if not arguments.triton_url.startswith("http://"):
|
||||
raise RuntimeError("Triton URL must use the internal HTTP endpoint")
|
||||
import cv2
|
||||
|
||||
job = _read_object(arguments.job)
|
||||
if (
|
||||
job.get("job_id") != "recorded-camera-602ac89026ed12978619801d"
|
||||
or job.get("input", {}).get("session_id") != "20260720T065719Z_viewer_live"
|
||||
or job.get("input", {}).get("source_id") != "sensor.camera.right"
|
||||
or job.get("input", {}).get("segment_count") != 4489
|
||||
):
|
||||
raise RuntimeError("Qualification is not bound to immutable RAVNOVES00")
|
||||
|
||||
rectification_profile = copy.deepcopy(_read_object(arguments.rectification_profile))
|
||||
detector_profile = _read_object(arguments.detector_profile)
|
||||
rectification_profile["rectification"]["tile_size"] = arguments.tile_size
|
||||
if (
|
||||
rectification_profile["source"]["calibration_sha256"]
|
||||
!= detector_profile["source"]["calibration_sha256"]
|
||||
):
|
||||
raise RuntimeError("Calibration identity differs between detector stages")
|
||||
active_tiles = {
|
||||
value.strip() for value in arguments.active_tiles.split(",") if value.strip()
|
||||
}
|
||||
if active_tiles != {"front", "left", "right"}:
|
||||
raise RuntimeError("Operational qualification requires front,left,right views")
|
||||
|
||||
valid_mask, valid_fov = _load_valid_fov(
|
||||
arguments.valid_fov_root, job, detector_profile
|
||||
)
|
||||
maps_started = time.perf_counter()
|
||||
maps = _rectification_maps(rectification_profile, valid_mask)
|
||||
maps_ms = (time.perf_counter() - maps_started) * 1000.0
|
||||
|
||||
video = arguments.video.resolve(strict=True)
|
||||
capture = cv2.VideoCapture(str(video))
|
||||
if not capture.isOpened():
|
||||
raise RuntimeError("OpenCV could not open the immutable camera replay")
|
||||
width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||
height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
source_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
if (width, height, source_count) != (800, 600, 4489):
|
||||
raise RuntimeError("Camera replay metadata violates the RAVNOVES00 contract")
|
||||
|
||||
output = arguments.output.resolve()
|
||||
if output.exists():
|
||||
raise RuntimeError("Qualification output must be absent")
|
||||
output.mkdir(parents=True, mode=0o700)
|
||||
previews = output / "previews"
|
||||
previews.mkdir()
|
||||
frames_path = output / "frames.jsonl"
|
||||
tracker = TwoStageTracker(detector_profile["tracking"])
|
||||
from scipy.optimize import linear_sum_assignment
|
||||
|
||||
linear_sum_assignment(np.zeros((1, 1), dtype=np.float64))
|
||||
|
||||
latency: list[float] = []
|
||||
decode_latency: list[float] = []
|
||||
detector_latency: list[float] = []
|
||||
tracking_latency: list[float] = []
|
||||
detections_by_label: Counter[str] = Counter()
|
||||
tracks_by_label: Counter[str] = Counter()
|
||||
rejections: Counter[str] = Counter()
|
||||
unique_tracks: set[int] = set()
|
||||
duplicate_pairs = 0
|
||||
processed = 0
|
||||
failures = 0
|
||||
|
||||
# One explicit warmup makes the source-rate metrics independent from model
|
||||
# initialization while preserving the first source frame for the real run.
|
||||
ok, warm_bgr = capture.read()
|
||||
if not ok:
|
||||
raise RuntimeError("Camera replay has no warmup frame")
|
||||
warm_rgb = cv2.cvtColor(warm_bgr, cv2.COLOR_BGR2RGB)
|
||||
detect_rectified(
|
||||
warm_rgb,
|
||||
maps=maps,
|
||||
rectification_profile=rectification_profile,
|
||||
detector_profile=detector_profile,
|
||||
valid_mask=valid_mask,
|
||||
triton_url=arguments.triton_url,
|
||||
contrast="none",
|
||||
active_tiles=active_tiles,
|
||||
cv2=cv2,
|
||||
)
|
||||
capture.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
||||
|
||||
run_started = time.perf_counter()
|
||||
try:
|
||||
with frames_path.open("x", encoding="utf-8", newline="\n") as stream:
|
||||
while True:
|
||||
if arguments.limit and processed >= arguments.limit:
|
||||
break
|
||||
decode_started = time.perf_counter()
|
||||
ok, bgr = capture.read()
|
||||
decoded = time.perf_counter()
|
||||
if not ok:
|
||||
break
|
||||
image = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)
|
||||
frame_started = time.perf_counter()
|
||||
try:
|
||||
detections, diagnostics = detect_rectified(
|
||||
image,
|
||||
maps=maps,
|
||||
rectification_profile=rectification_profile,
|
||||
detector_profile=detector_profile,
|
||||
valid_mask=valid_mask,
|
||||
triton_url=arguments.triton_url,
|
||||
contrast="none",
|
||||
active_tiles=active_tiles,
|
||||
cv2=cv2,
|
||||
)
|
||||
detected = time.perf_counter()
|
||||
tracks = tracker.update(detections, processed)
|
||||
tracked = time.perf_counter()
|
||||
except Exception:
|
||||
failures += 1
|
||||
raise
|
||||
|
||||
frame_ms = (tracked - frame_started) * 1000.0
|
||||
latency.append(frame_ms)
|
||||
decode_latency.append((decoded - decode_started) * 1000.0)
|
||||
detector_latency.append((detected - frame_started) * 1000.0)
|
||||
tracking_latency.append((tracked - detected) * 1000.0)
|
||||
detections_by_label.update(str(item["label"]) for item in detections)
|
||||
tracks_by_label.update(track.label for track in tracks)
|
||||
for track in tracks:
|
||||
unique_tracks.add(track.track_id)
|
||||
duplicate_pairs += _duplicate_pairs(tracks)
|
||||
rejections.update(diagnostics["rejected"])
|
||||
|
||||
document = {
|
||||
"schema_version": "missioncore.rectified-yolox-frame/v1",
|
||||
"frame_index": processed,
|
||||
"sequence": processed + 1,
|
||||
"detections": detections,
|
||||
"tracks": [_track_document(track) for track in tracks],
|
||||
"processing_ms": round(frame_ms, 6),
|
||||
"tile_inference_ms": round(
|
||||
sum(float(tile["inference_ms"]) for tile in diagnostics["tiles"]),
|
||||
6,
|
||||
),
|
||||
}
|
||||
stream.write(
|
||||
json.dumps(
|
||||
document,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
if processed % arguments.preview_stride == 0:
|
||||
_draw_overlay(image, detections).save(
|
||||
previews / f"frame-{processed:06d}.jpg",
|
||||
format="JPEG",
|
||||
quality=92,
|
||||
optimize=True,
|
||||
)
|
||||
processed += 1
|
||||
if processed % 100 == 0:
|
||||
stream.flush()
|
||||
print(
|
||||
f"PHASE=rectified-yolox FRAMES={processed} "
|
||||
f"LAST_MS={frame_ms:.3f}",
|
||||
flush=True,
|
||||
)
|
||||
finally:
|
||||
capture.release()
|
||||
wall_seconds = time.perf_counter() - run_started
|
||||
expected = arguments.limit or source_count
|
||||
steady = latency[1:] if len(latency) > 1 else latency
|
||||
effective_fps = processed / wall_seconds
|
||||
steady_latency = _latency(steady)
|
||||
checks = {
|
||||
"complete_frame_accounting": processed == expected,
|
||||
"zero_failures": failures == 0,
|
||||
"minimum_effective_fps_9_5": effective_fps >= 9.5,
|
||||
"maximum_steady_p95_ms_100": steady_latency["p95"] <= 100.0,
|
||||
"same_class_duplicate_pairs_zero": duplicate_pairs == 0,
|
||||
}
|
||||
accepted = all(checks.values())
|
||||
report: dict[str, Any] = {
|
||||
"schema_version": "missioncore.rectified-yolox-qualification/v1",
|
||||
"state": "accepted" if accepted else "rejected",
|
||||
"source": {
|
||||
"job_id": job["job_id"],
|
||||
"session_id": job["input"]["session_id"],
|
||||
"source_id": job["input"]["source_id"],
|
||||
"frame_count": source_count,
|
||||
"calibration_sha256": detector_profile["source"]["calibration_sha256"],
|
||||
},
|
||||
"pipeline": {
|
||||
"id": "k1-kb4-core3-yolox-bytetrack/v1",
|
||||
"rectification": "five-perspective-gnomonic/v1",
|
||||
"active_tiles": sorted(active_tiles),
|
||||
"tile_size": arguments.tile_size,
|
||||
"detector": detector_profile["model"],
|
||||
"tracker": detector_profile["tracking"],
|
||||
"peripheral_views": "up/down remain on the existing 2 Hz semantic cadence",
|
||||
},
|
||||
"runtime": {
|
||||
"hostname": platform.node(),
|
||||
"python": platform.python_version(),
|
||||
"opencv": cv2.__version__,
|
||||
"rectification_map_build_ms": round(maps_ms, 6),
|
||||
"wall_seconds": round(wall_seconds, 6),
|
||||
"effective_fps": round(effective_fps, 6),
|
||||
},
|
||||
"metrics": {
|
||||
"frames_processed": processed,
|
||||
"failures": failures,
|
||||
"latency_ms": _latency(latency),
|
||||
"steady_latency_ms": steady_latency,
|
||||
"decode_latency_ms": _latency(decode_latency),
|
||||
"detector_latency_ms": _latency(detector_latency),
|
||||
"tracking_latency_ms": _latency(tracking_latency),
|
||||
"detections": int(sum(detections_by_label.values())),
|
||||
"detections_by_label": dict(sorted(detections_by_label.items())),
|
||||
"unique_confirmed_tracks": len(unique_tracks),
|
||||
"track_observations": int(sum(tracks_by_label.values())),
|
||||
"track_observations_by_label": dict(sorted(tracks_by_label.items())),
|
||||
"rejections": dict(sorted(rejections.items())),
|
||||
"same_class_duplicate_pairs_iou_ge_0_8": duplicate_pairs,
|
||||
"tracker_tracks_created": tracker.created,
|
||||
"tracker_tracks_retired": tracker.retired,
|
||||
},
|
||||
"acceptance": {"accepted": accepted, "checks": checks},
|
||||
"valid_fov": valid_fov,
|
||||
"limitations": [
|
||||
"RAVNOVES00 has no independent exhaustive object ground truth.",
|
||||
"This gate qualifies runtime and visual evidence, not safety accuracy.",
|
||||
"LiDAR association, temporal world state and segmentation are existing downstream stages and are not recomputed by this detector-only gate.",
|
||||
],
|
||||
"artifacts": {
|
||||
"frames": "frames.jsonl",
|
||||
"previews": "previews/",
|
||||
},
|
||||
}
|
||||
(output / "qualification.json").write_text(
|
||||
json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"accepted": accepted,
|
||||
"frames_processed": processed,
|
||||
"effective_fps": round(effective_fps, 6),
|
||||
"steady_p95_ms": steady_latency["p95"],
|
||||
"detections": report["metrics"]["detections"],
|
||||
"unique_confirmed_tracks": len(unique_tracks),
|
||||
},
|
||||
sort_keys=True,
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
raise SystemExit(0 if accepted else 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user