tiandytmd

This commit is contained in:
Ankle committed 2026-09-15 09:53:00 +08:00
1 parent f7a782dcef
commit ddea6d0578
6 files changed
+389 -34

No files matched your search

+122 -21
View File
@@ -284,12 +284,14 @@ def candidate_deltas(axis: str) -> list[int]:
return [25, 40, 60, 90, 130, 190, 280, 420, 620, 850, 1150]
def fov_guided_deltas(delta: int, overlap: float, axis: str) -> list[int]:
def fov_guided_deltas(actual_delta: int, overlap: float, axis: str) -> list[int]:
"""Choose probes that retain enough shared image for robust matching."""
shift = 1.0 - overlap
if not 0.04 <= shift <= 0.92:
# Tiny shifts amplify PTZ quantization and image-matching error. Wait for a
# 15%..75% real image shift before replacing the generic probe sequence.
if not 0.15 <= shift <= 0.75:
return []
estimated_fov = delta / shift
estimated_fov = actual_delta / shift
minimum = 20 if axis == "pan" else 12
# Fractions are image displacement/FOV, hence target overlaps are roughly
# 82%, 72%, 60%, 48% and 35%. The middle probes provide the strongest
@@ -300,6 +302,15 @@ def fov_guided_deltas(delta: int, overlap: float, axis: str) -> list[int]:
})
def safe_probe_cap(actual_delta: int, overlap: float) -> int | None:
"""Keep later probes inside the FOV already observed by the camera."""
shift = 1.0 - overlap
if actual_delta <= 0 or not 0.03 <= shift <= 0.85:
return None
estimated_fov = actual_delta / shift
return max(1, round(estimated_fov * 0.85))
def choose_direction(axis: str, origin: dict[str, int], points: dict) -> int:
if axis == "pan":
# Pan is circular. The direction changes only the sign, not the FOV estimate.
@@ -437,12 +448,12 @@ def configured_image_aspect_ratio() -> float:
def failure_detail(result: dict) -> str:
reason = str(result.get("reason") or "unknown")
labels = {
"ptz_motion_shortfall": "云台受限,实际移动量不足",
"not_enough_features": "两张图片可检测特征不足",
"not_enough_matches": "两张图片的公共特征不足",
"homography_failed": "公共特征无法形成可靠几何变换",
"axis_motion_too_small": "匹配到的主轴画面位移过小",
"axis_motion_inconsistent": "匹配位移方向异常或串轴过大",
"ptz_motion_shortfall": "摄像头实际移动小于请求值,本样本自动跳过",
"not_enough_features": "画面纹理太少,无法测量,本样本自动跳过",
"not_enough_matches": "移动前后相同画面太少,本样本自动跳过",
"homography_failed": "移动前后相同画面太少或纹理重复,本样本自动跳过",
"axis_motion_too_small": "画面位移太小,本样本自动跳过",
"axis_motion_inconsistent": "画面同时出现过大横纵位移,本样本自动跳过",
}
fields = []
if result.get("actual_axis_motion") is not None:
@@ -476,6 +487,7 @@ def estimate_axis(axis: str, origin: dict[str, int], points: dict,
pending = candidate_deltas(axis)
tested: set[int] = set()
adaptive_seeded = False
direction_switched = False
while pending:
delta = pending.pop(0)
if delta in tested:
@@ -485,14 +497,20 @@ def estimate_axis(axis: str, origin: dict[str, int], points: dict,
axis, delta, direction, 1.0, baseline=baseline, origin=origin
)
snapshots.add(str(result.get("candidate_file", "")))
actual_motion = int(result.get("actual_axis_motion") or 0)
switch_threshold = 150 if axis == "pan" else 90
if (
axis == "tilt"
and not reliable
not reliable
and not direction_switched
and result.get("reason") == "ptz_motion_shortfall"
and delta >= switch_threshold
and actual_motion < max(2, round(delta * 0.15))
):
direction *= -1
direction_switched = True
say(
f" step={delta}: 原方向受云台限位,自动改用相反方向继续标定"
f" step={delta}: {axis}在原方向基本没有移动,"
"可能接近限位;程序只切换一次反方向再试"
)
result = core.measure_delta(
axis, delta, direction, 1.0, baseline=baseline, origin=origin
@@ -501,7 +519,20 @@ def estimate_axis(axis: str, origin: dict[str, int], points: dict,
if not result.get("valid"):
reason = str(result.get("reason") or "unknown")
failure_reasons[reason] = failure_reasons.get(reason, 0) + 1
say(f" step={delta}: {failure_detail(result)},继续尝试")
stop_expanding = (
bool(reliable)
and reason in {"not_enough_matches", "homography_failed"}
)
say(
f" step={delta}: {failure_detail(result)}"
f",{'停止扩大步长' if stop_expanding else '继续尝试'}"
)
if stop_expanding:
say(
" 移动前后已没有足够相同画面;"
"更大step只会移得更远,现在停止扩大步长。"
)
break
continue
overlap = float(result["overlap"])
inliers = int(result.get("inliers") or 0)
@@ -511,8 +542,12 @@ def estimate_axis(axis: str, origin: dict[str, int], points: dict,
else:
quality = f"有效特征={inliers},内点率={ratio*100:.1f}%"
say(f" step={delta}: 重叠约 {overlap*100:.1f}%,{quality}")
actual_delta = int(result.get("actual_axis_motion") or delta)
cap = safe_probe_cap(actual_delta, overlap)
if cap is not None:
pending = [value for value in pending if value <= max(delta, cap)]
if not adaptive_seeded:
guided = fov_guided_deltas(delta, overlap, axis)
guided = fov_guided_deltas(actual_delta, overlap, axis)
if guided:
# Keep only a small safety tail beyond the guided range. Very
# large fixed probes cannot match once the current optical FOV
@@ -533,11 +568,9 @@ def estimate_axis(axis: str, origin: dict[str, int], points: dict,
and float(result.get("phase_response") or 0) >= core.PHASE_MIN_RESPONSE
)
if (feature_reliable or phase_reliable) and 0.08 <= overlap <= 0.90:
# Hikvision absolute PTZ movement is quantized and can also have
# backlash. At high optical zoom, using the requested delta
# instead of the SDK-reported actual movement creates a large,
# systematic FOV error. Shortfall samples have already been
# rejected above, so the actual movement is authoritative here.
# Absolute PTZ movement may be quantized or affected by backlash on
# either vendor. Use the adapter-reported motion, not the request.
# Shortfall samples have already been rejected above.
actual_delta = int(result.get("actual_axis_motion") or delta)
reliable.append((
actual_delta, overlap,
@@ -564,7 +597,10 @@ def estimate_axis(axis: str, origin: dict[str, int], points: dict,
)
core.fail(
f"{axis}方向只得到{len(reliable)}个可靠样本,至少需要2个不同移动量互相验证。"
f"失败统计:{summary or '无'}。请根据上方每个step的具体原因处理后重试"
f"失败统计:{summary or '无'}。\n"
"现场处理:1. 把镜头移到扫描区域中部;"
"2. 画面中保留岸线、树木、建筑等固定纹理;"
"3. 不要操作Web云台;4. 回复1重试。"
)
# Fixed OSD text and repeated scenery can yield a convincing false
@@ -575,7 +611,9 @@ def estimate_axis(axis: str, origin: dict[str, int], points: dict,
values = ", ".join(f"{sample[2]:.0f}" for sample in reliable)
core.fail(
f"{axis}方向不同移动量反算的视场不一致({values}),疑似重复图案误匹配;"
"本次结果不写入配置,请更换标定画面"
"本次结果不写入配置。\n"
"现场处理:把镜头移到扫描区域中部,对准距离较远、"
"整幅都有不重复固定纹理的场景,然后回复1重试。"
)
estimated_fov, spread = fov_statistics(consistent)
@@ -1003,10 +1041,72 @@ def reusable_step_axes(state: dict, points: dict) -> set[str]:
return reusable
def acquire_calibration_control() -> bool:
"""Pause an active patrol and wait for its in-flight PTZ command to end."""
status, camera = core.get_control_status()
host = str(camera["camera"].get("sdk_host") or "").strip()
detail = f",摄像头={host}" if host else ""
say(
f"已识别摄像头适配层:vendor={camera['vendor']}{detail};"
"PTZ位置读取和绝对移动能力正常。"
)
control = status.get("control", {})
if not isinstance(control, dict):
core.fail("scan HTTP未返回合法的control状态")
if bool(control.get("scan_active")):
core.fail("当前正在执行完整扫图,请等待扫图结束后再运行标定向导")
should_resume = bool(
control.get("patrol_active")
and not control.get("patrol_paused_by_operator")
)
if should_resume:
core.api(
"POST", "/api/v1/patrol/stop",
{"source": "scan_calibrator"}, timeout=15,
)
say("已临时暂停巡航;标定结束或正常中断时会自动恢复。", color=YELLOW)
try:
# This is a synchronization barrier: GET waits behind any PTZ command
# already in flight when patrol was paused, then claims manual control.
core.get_ptz()
except BaseException:
if should_resume:
try:
core.api(
"POST", "/api/v1/patrol/start",
{"source": "scan_calibrator"}, timeout=15,
)
except BaseException as resume_error:
core.warn(f"标定准备失败后未能自动恢复巡航:{resume_error}")
raise
return should_resume
def release_calibration_control(should_resume: bool) -> None:
if not should_resume:
return
try:
core.api(
"POST", "/api/v1/patrol/start",
{"source": "scan_calibrator"}, timeout=15,
)
say("已恢复进入向导前的巡航状态。", color=YELLOW)
except BaseException as exc:
core.warn(
f"未能自动恢复巡航:{exc};请执行 "
"docker exec smart-cam curl -sS -X POST "
"http://127.0.0.1:18080/api/v1/patrol/start"
)
def main() -> None:
snapshots: set[str] = set()
resume_patrol = False
try:
say("您好!我们现在开始配置扫图参数!", color=GREEN)
resume_patrol = acquire_calibration_control()
say(
"标定进度会逐步保存;中途回复 3 或按 Ctrl+C 退出后,"
"下次运行仍可选择本次记录继续。"
@@ -1150,6 +1250,7 @@ def main() -> None:
say(f"\n向导失败:{exc}", color=RED)
finally:
remove_session_snapshots(snapshots)
release_calibration_control(resume_patrol)
if __name__ == "__main__":