canshu
This commit is contained in:
1 parent
98ed2576c6
commit
136ba0ce89
2 files changed
+112
-10
No files matched your search
+101
-3
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user