航拍视频与公路桩号的双向定位脚本
- IT
- 8天前
- 26热度
- 0评论
当我们航拍一条公路时,如果里程较长,最终拼接而成的视频也是比较长的。设计工作中可能需要经常查询某个特定桩号的航拍,但多数路段缺乏足够的特征,如何快速定位就成为一个现实的问题。
一种可行的解决思路是内插:根据视频长度和起终点桩号,直接内插得到对应关系。但这种方式并不精确,一方面,无人机的飞行速度未必均匀,里程较长时误差较大;另一方面,由于禁飞区(或隧道等无法航拍的路段)的存在,视频未必连续,那么这种简单内插就更不可行了。
那么其实可以设置多个“锚点”,将视频“切分”为多个段落,然后就目标桩号所在的锚点区间进行“局部插值”。很显然,只需要“锚点”足够密,则精度是完全可控的。还可以用这种方式将禁飞区段“隔离”出去。
一、配置文件
首先编辑配置文件:KPointerSet.ini,保存为UTF-8 BOM。内容格式如下:
# 航拍视频时间与桩号双向定位脚本 - 配置文件 (v1.1)
[Parameters]
# 是否允许在锚点范围之外进行线性外推估算。
# 接受的值: true 或 false
AllowExtrapolation = false
# 当前正在处理的视频名称,会显示在脚本界面上。
# 不同的视频需要自己的ini设置,因此该变量用来作基本的识别与区分。
VideoName = XX项目XX段
# ===================================================
# 禁飞区定义(或无视频的片段对应的桩号)
# 通过定义多个 [Zone.X] 段来添加禁飞区。
# ===================================================
[Zone.1]
startStake = K10+100
endStake = K15+300
message = => 注意:桩号处于禁飞区,没有对应的视频片段。
# ===================================================
# 锚点定义
# 在下方使用 "时间 = 桩号" 的格式直接添加锚点。
# 在每个禁飞区前后桩号设置锚点。
# 脚本会自动按时间顺序进行排序,无需手动排序。
# ===================================================
[Anchors]
00:00:00 = K0+000
00:05:12 = K4+950
00:10:39 = K10+101 #这里隔出了禁飞段
00:10:50 = K15+299 #这里隔出了禁飞段
00:15:02 = K19+520
00:20:47 = K24+090
二、Powershell脚本
然后是脚本主体:KPointer.ps1。用powershell编写,无需额外安装环境。注意保存为UTF-8 BOM。
# ===== 航拍视频时间与桩号双向定位脚本v1.1 =====
# ===== INI文件解析器 =====
function Parse-IniFile {
param([string]$filePath)
$ini = @{}
$currentSection = ""
(Get-Content -Path $filePath -Raw) -split '(\r?\n)' | ForEach-Object {
$line = $_.Trim()
if ($line -match '^\[(.+)\]$') {
$currentSection = $matches[1].Trim()
if (-not $ini.ContainsKey($currentSection)) {
$ini[$currentSection] = @{}
}
} elseif ($line -match '^([^#;].+?)\s*=\s*(.*)') {
if ($currentSection) {
$key = $matches[1].Trim()
$value = $matches[2].Trim()
$ini[$currentSection][$key] = $value
}
}
}
return $ini
}
# ===== 加载配置 =====
try {
$scriptPath = $MyInvocation.MyCommand.Definition
$scriptDir = Split-Path -Parent $scriptPath
$iniPath = Join-Path $scriptDir "KPointerSet.ini"
if (-not (Test-Path $iniPath)) {
throw "配置文件 KPointerSet.ini 未在脚本目录中找到!"
}
$Config = Parse-IniFile -filePath $iniPath
$AllowExtrapolation = ($Config.Parameters.AllowExtrapolation -eq 'true')
$VideoName = $Config.Parameters.VideoName
$NoFlyZones = @()
foreach ($section in $Config.Keys) {
if ($section -like 'Zone.*') {
$NoFlyZones += [pscustomobject]$Config[$section]
}
}
$Anchors = @()
if ($Config.ContainsKey('Anchors')) {
foreach ($entry in $Config.Anchors.GetEnumerator()) {
$Anchors += [pscustomobject]@{ time = $entry.Name; stake = $entry.Value }
}
$Anchors = $Anchors | Sort-Object { [System.TimeSpan]::Parse($_.time) }
}
if ($Anchors.Count -lt 2) {
throw "错误:锚点数量少于2个。请检查 KPointerSet.ini 中的 [Anchors] 段落。"
}
} catch {
Write-Host "[初始化错误] $($_.Exception.Message)" -ForegroundColor Red
Read-Host "按 Enter 键退出..."
return
}
# ===== 基本工具 =====
function TimeToSec([string]$timeString){ return [long]([System.TimeSpan]::Parse($timeString).TotalSeconds) }
function SecToTime([long]$totalSeconds){ return [System.TimeSpan]::FromSeconds($totalSeconds).ToString('hh\:mm\:ss') }
function StakeToMeters([string]$stake){
$cleanInput = $stake.Trim()
try { return [long]::Parse($cleanInput) } catch {}
try {
$upperStake = $cleanInput.ToUpper() -replace '\s',''
if (-not $upperStake.StartsWith('K')) { throw }
$upperStake = $upperStake.Substring(1)
$parts = $upperStake.Split('+')
$km = [long]$parts[0]
$plus = if ($parts.Length -gt 1) { [long]$parts[1] } else { 0 }
if ($plus -ge 1000) { throw "桩号米段应<1000:$stake" }
return (($km * 1000) + $plus)
} catch {
throw "桩号格式无效:'$stake' (可接受 'K20+200' 或 '20200' 格式)"
}
}
function MetersToStake([long]$m){
$neg = $m -lt 0; $v = [Math]::Abs($m)
$km = [long]([Math]::Truncate([double]$v / 1000)); $plus = $v % 1000
"{0}{1}+{2:D3}" -f ($(if($neg){'-K'}else{'K'}),$km,$plus)
}
# ===== 构建并排序映射列表 =====
$Ks_Base = foreach($a in $Anchors){ [pscustomobject]@{ d = (StakeToMeters $a.stake); t = (TimeToSec $a.time); tstr = $a.time } }
$KsByDist = $Ks_Base | Sort-Object d, t
$KsByTime = $Ks_Base | Sort-Object t, d
$ExactTime = @{}; $Ks_Base.ForEach({ $ExactTime[$_.d] = $_.tstr })
$ExactDist = @{}; $Ks_Base.ForEach({ $ExactDist[$_.t] = (MetersToStake $_.d) })
# ===== 核心功能函数 =====
function Check-NoFlyZone([long]$d_target) {
foreach ($zone in $NoFlyZones) {
$d_start = StakeToMeters $zone.startStake; $d_end = StakeToMeters $zone.endStake
$zone_min = [Math]::Min($d_start, $d_end); $zone_max = [Math]::Max($d_start, $d_end)
if ($d_target -ge $zone_min -and $d_target -le $zone_max) {
Write-Host $zone.message -ForegroundColor Yellow
return $true
}
}
return $false
}
function TimeAtDistance([long]$d){
if($ExactTime.ContainsKey($d)){ return $ExactTime[$d] }
for ($i = 1; $i -lt $KsByDist.Length; $i++) {
$p0 = $KsByDist[$i-1]; $p1 = $KsByDist[$i]
if ($p0.d -le $d -and $d -le $p1.d) {
$dx = $p1.d - $p0.d; if ($dx -eq 0) { throw "Data error: Duplicate distance points." }
$dt = $p1.t - $p0.t; $delta = $d - $p0.d
$add = [long][Math]::Round([double]$delta * $dt / $dx); return (SecToTime ($p0.t + $add))
}
}
if ($AllowExtrapolation) {
if ($d -lt $KsByDist[0].d) { $p0 = $KsByDist[0]; $p1 = $KsByDist[1] }
else { $p0 = $KsByDist[-2]; $p1 = $KsByDist[-1] }
$dx = $p1.d - $p0.d; if ($dx -eq 0) { throw "Data error" }
$dt = $p1.t - $p0.t; $delta = $d - $p0.d
$add = [long][Math]::Round([double]$delta * $dt / $dx); return (SecToTime ($p0.t + $add))
}
throw "桩号 `$d` 超出定义的锚点范围。"
}
function DistanceAtTime([long]$t){
if($ExactDist.ContainsKey($t)){ return $ExactDist[$t] }
for ($i = 1; $i -lt $KsByTime.Length; $i++) {
$p0 = $KsByTime[$i-1]; $p1 = $KsByTime[$i]
if ($p0.t -le $t -and $t -le $p1.t) {
$dt = $p1.t - $p0.t; if ($dt -eq 0) { throw "Data error: Duplicate time points." }
$dx = $p1.d - $p0.d; $delta = $t - $p0.t
$add = [long][Math]::Round([double]$delta * $dx / $dt); return MetersToStake($p0.d + $add)
}
}
if ($AllowExtrapolation) {
if ($t -lt $KsByTime[0].t) { $p0 = $KsByTime[0]; $p1 = $KsByTime[1] }
else { $p0 = $KsByTime[-2]; $p1 = $KsByTime[-1] }
$dt = $p1.t - $p0.t; if ($dt -eq 0) { throw "Data error" }
$dx = $p1.d - $p0.d; $delta = $t - $p0.t
$add = [long][Math]::Round([double]$delta * $dx / $dt); return MetersToStake($p0.d + $add)
}
throw "时间 `$t` 超出定义的锚点范围。"
}
# ===== 交互 =====
function ModeLoop(){
while($true){
Write-Host "`n=== 航拍视频时间与桩号双向定位脚本v1.1 ===" -ForegroundColor Cyan
Write-Host "Tips:建议安装Potplayer(完美解码),按快捷键G可以读取当前视频时间,或输入要定位的时间。使用更加方便。"
Write-Host "视频名称:"$VideoName -ForegroundColor Green
Write-Host "`n模式1:桩号 -> 时间"; Write-Host "模式2:时间 -> 桩号"
Write-Host "输入 'q' 退出;输入 'mode' 返回模式选择。"
$mode = Read-Host '选择模式 [1/2]'; if($mode -eq 'q'){ break }
if($mode -eq '1'){
while($true){
$s = Read-Host '桩号'; if($s -eq 'q'){ return }; if($s -eq 'mode'){ break }
try{
$d_target = StakeToMeters $s
if (Check-NoFlyZone $d_target) { continue }
$calculatedTime = TimeAtDistance $d_target
$calculatedTime | Set-Clipboard
Write-Host ("=> 时间: {0} (已复制到剪贴板)" -f $calculatedTime) -ForegroundColor Green
} catch { Write-Host "[错误] $($_.Exception.Message)" -ForegroundColor Red }
}
} elseif($mode -eq '2'){
while($true){
$tstr = Read-Host '时间 (HH:MM:SS)'; if($tstr -eq 'q'){ return }; if($tstr -eq 'mode'){ break }
try{
$tsec = TimeToSec $tstr
$calculatedStake = DistanceAtTime $tsec
$calculatedStake | Set-Clipboard
Write-Host ("=> 桩号: {0} (已复制到剪贴板)" -f $calculatedStake) -ForegroundColor Green
} catch { Write-Host "[错误] $($_.Exception.Message)" -ForegroundColor Red }
}
}
}
}
ModeLoop
这里有几个注意事项:
1、TimeToSec()和SecToTime()使用System.TimeSpan方法来处理时间格式与秒数的换算,以避免潜在的双精度造成的“跳进”问题。
2、MetersToStake()用Truncate()来处理桩号与米数的换算,防止四舍五入(看似简单但很容易踩坑)。
3、对锚点的读取支持自动排序,防止用户在输入锚点时没有严格遵循先后顺序。
4、StakeToMeters()考虑用户习惯,既能兼容“K20+200”这样的格式,也能兼容“20200”这样的格式。
5、采用循环设计,以便用户连续定位。给出“q”和“mode”两个关键字用来退出循环或切换模式。
6、将输出的时间点或桩号复制到剪贴板,便于用户直接粘贴到播放器中定位,或在其他专业软件中使用。可以显著提升用户体验。
三、vbs用来执行Powershell脚本
Windows下直接执行powershell有一定门槛,为了方便普通用户,用vbs脚本来执行ps1。创建RunKPointer.vbs(注意保存为ANSI格式)。
Option Explicit
Dim fso, shell, scriptDir, scriptName, ps1Path, pwsh, cmd
Set fso = CreateObject("Scripting.FileSystemObject")
Set shell = CreateObject("WScript.Shell")
' 1. 获取脚本的完整目录和文件名
scriptDir = fso.GetParentFolderName(WScript.ScriptFullName)
scriptName = "KPointer.ps1" ' PowerShell脚本的文件名
ps1Path = fso.BuildPath(scriptDir, scriptName)
pwsh = "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe"
If Not fso.FileExists(ps1Path) Then
MsgBox "未找到 " & scriptName & "(应与本VBS同目录)", 16, "KPointer"
WScript.Quit 1
End If
cmd = pwsh & " -NoLogo -NoProfile -ExecutionPolicy Bypass -NoExit -Command ""& {Set-Location -Path '" & scriptDir & "'; & '.\" & scriptName & "' }"""
' 运行命令
' 1=正常窗口;第三个参数 False=不阻塞VBS
shell.Run cmd, 1, False
对于每一个单独的长视频文件,将上述3个文件复制到视频同目录下,设置好ini文件,即可使用。