This commit is contained in:
Ankle committed 2026-07-29 18:28:28 +08:00
1 parent 98ed2576c6
commit 136ba0ce89
2 files changed
+112 -10

No files matched your search

+101 -3
View File
@@ -8,6 +8,7 @@ from contextlib import redirect_stdout
from datetime import datetime
import io
import json
import math
from pathlib import Path
import re
import select
@@ -25,7 +26,7 @@ CONFIG_PATH = "/smart-cam/config/config.json"
# the tested PTZ camera retain more real-world overlap than the center-shift
# model predicts, while vertical seams need a small safety margin. Keep these
# axis-specific targets within the generator's accepted 0%..5% range.
TARGET_OVERLAPS = {"pan": 0.04, "tilt": 0.02}
TARGET_OVERLAPS = {"pan": 0.045, "tilt": 0.02}
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
@@ -110,6 +111,95 @@ def fov_statistics(samples: list[tuple[int, float, float, dict]]) -> tuple[float
return estimated_fov, (max(fovs) - min(fovs)) / estimated_fov
def horizontal_fov_from_vertical(vertical_fov: float, aspect_ratio: float) -> float:
"""Convert a rectilinear vertical FOV to horizontal FOV in PTZ units."""
vertical_radians = math.radians(vertical_fov / 100.0)
horizontal_radians = 2.0 * math.atan(
aspect_ratio * math.tan(vertical_radians / 2.0)
)
return math.degrees(horizontal_radians) * 100.0
def constrain_pan_with_tilt(pan_info: dict, tilt_info: dict,
aspect_ratio: float) -> tuple[dict, str]:
measured_pan_fov = float(pan_info["derived_from"]["estimated_fov"])
measured_tilt_fov = float(tilt_info["derived_from"]["estimated_fov"])
optical_pan_fov = horizontal_fov_from_vertical(
measured_tilt_fov, aspect_ratio
)
agreement = measured_pan_fov / optical_pan_fov
diagnostic = {
"image_aspect_ratio": round(aspect_ratio, 6),
"measured_pan_fov": round(measured_pan_fov, 4),
"measured_tilt_fov": round(measured_tilt_fov, 4),
"tilt_predicted_pan_fov": round(optical_pan_fov, 4),
"agreement_ratio": round(agreement, 6),
}
if 0.85 <= agreement <= 1.15:
tilt_target_overlap = TARGET_OVERLAPS["tilt"]
tilt_predicted_step = max(
1, round(optical_pan_fov * (1.0 - tilt_target_overlap))
)
direct_pan_step = int(pan_info["value"])
# Neither estimate is authoritative on a PTZ dome: direct horizontal
# matching is affected by distortion, while the aspect-ratio prediction
# assumes an ideal rectilinear lens. Fuse them instead of replacing one
# with the other. The geometric mean stays between both measurements.
constrained_step = max(
1, round(math.sqrt(direct_pan_step * tilt_predicted_step))
)
effective_overlap = max(
0.0, min(core.MAX_GENERATED_OVERLAP,
1.0 - constrained_step / measured_pan_fov)
)
return {
**pan_info,
"value": constrained_step,
"overlap": effective_overlap,
"target_overlap": effective_overlap,
"overlap_method": "axis_shift_v1_pan_tilt_fused",
"cross_axis_check": {
**diagnostic,
"applied": True,
"direct_pan_step": direct_pan_step,
"tilt_predicted_pan_step": tilt_predicted_step,
"fused_pan_step": constrained_step,
},
}, (
f"pan/tilt视场比例符合普通透视模型(一致率{agreement*100:.1f}%);"
f"融合pan实测={direct_pan_step}与tilt推导={tilt_predicted_step}"
f"后pan_step={constrained_step}"
)
return {
**pan_info,
"cross_axis_check": {
**diagnostic,
"applied": False,
"reason": "non_rectilinear_or_axis_scale_mismatch",
},
}, (
f"pan/tilt视场比例仅为普通透视预测的{agreement*100:.1f}%,"
"疑似镜头畸变或不同tilt行的水平覆盖不一致;"
"不能用tilt比例替代pan实测,固定pan_step应以不缺图为优先"
)
def configured_image_aspect_ratio() -> float:
try:
raw = subprocess.run(
["docker", "exec", "smart-cam", "cat", CONFIG_PATH],
text=True, capture_output=True, check=True,
).stdout
image = json.loads(raw).get("scan", {}).get("image", {})
width, height = int(image.get("width", 1280)), int(image.get("height", 720))
if width > 0 and height > 0:
return width / height
except (OSError, ValueError, KeyError, json.JSONDecodeError,
subprocess.SubprocessError):
pass
return 16.0 / 9.0
def failure_detail(result: dict) -> str:
reason = str(result.get("reason") or "unknown")
labels = {
@@ -360,8 +450,16 @@ def calculate_steps(points: dict, snapshots: set[str], mount: str) -> None:
color=YELLOW,
)
state = core.load_state()
state["steps"]["pan"] = estimate_axis("pan", origin, points, snapshots)
state["steps"]["tilt"] = estimate_axis("tilt", origin, points, snapshots)
pan_info = estimate_axis("pan", origin, points, snapshots)
tilt_info = estimate_axis("tilt", origin, points, snapshots)
pan_info, cross_axis_message = constrain_pan_with_tilt(
pan_info, tilt_info, configured_image_aspect_ratio()
)
say(f" {cross_axis_message}", color=(
GREEN if pan_info["cross_axis_check"]["applied"] else YELLOW
))
state["steps"]["pan"] = pan_info
state["steps"]["tilt"] = tilt_info
core.save_state(state)