Fits small cameras

This commit is contained in:
Ankle committed 2026-07-29 16:25:16 +08:00
1 parent a773166c91
commit 98ed2576c6
7 files changed
+526 -96

No files matched your search

+61 -11
View File
@@ -20,6 +20,7 @@ HELPER_CONTAINER_SOURCE = "/tmp/scan-calibrator-measure.cpp"
HELPER_CONTAINER_BIN = "/tmp/scan-calibrator-measure"
DEFAULT_TARGET_OVERLAP = 0.04
MAX_GENERATED_OVERLAP = 0.05
PHASE_MIN_RESPONSE = 0.20
def fail(message: str) -> "NoReturn":
@@ -120,6 +121,40 @@ def measure_delta(axis: str, delta: int, direction: int, wait_seconds: float,
time.sleep(wait_seconds)
candidate = snapshot()
result = measure_images(baseline, candidate)
if axis == "pan":
actual_motion = abs(
((actual["pan"] - origin["pan"] + 18000) % 36000) - 18000
)
else:
actual_motion = abs(actual["tilt"] - origin["tilt"])
result["actual_axis_motion"] = actual_motion
result["requested_axis_motion"] = delta
phase_primary_key = "phase_dx" if axis == "pan" else "phase_dy"
phase_cross_key = "phase_dy" if axis == "pan" else "phase_dx"
phase_primary = abs(float(result.get(phase_primary_key, 0.0)))
phase_cross = abs(float(result.get(phase_cross_key, 0.0)))
phase_response = float(result.get("phase_response", 0.0))
minimum_plausible_shift = delta / 18000.0
if (
not result.get("valid")
and phase_response >= PHASE_MIN_RESPONSE
and minimum_plausible_shift <= phase_primary <= 0.50
and phase_cross <= 0.05
):
result.update({
"valid": True,
"reason": "phase_correlation_fallback",
"match_method": "phase_correlation",
"center_dx": float(result.get("phase_dx", 0.0)),
"center_dy": float(result.get("phase_dy", 0.0)),
})
# Absolute PTZ commands can be clamped by a camera's mechanical/firmware
# limit. In that case identical images are not a feature-texture failure.
if actual_motion < max(2, round(delta * 0.5)):
result["valid"] = False
result["reason"] = "ptz_motion_shortfall"
if result.get("valid"):
primary_key = "center_dx" if axis == "pan" else "center_dy"
cross_key = "center_dy" if axis == "pan" else "center_dx"
@@ -134,7 +169,6 @@ def measure_delta(axis: str, delta: int, direction: int, wait_seconds: float,
# wall patterns. Use displacement along the commanded axis instead.
result["overlap"] = max(0.0, min(1.0, 1.0 - primary_shift))
minimum_plausible_shift = delta / 18000.0
result["minimum_plausible_shift"] = minimum_plausible_shift
if primary_shift < minimum_plausible_shift:
result["valid"] = False
@@ -303,23 +337,34 @@ def command_calibrate(args: argparse.Namespace) -> None:
"good_matches": detail.get("good_matches"), "inliers": detail.get("inliers"),
"inlier_ratio": detail.get("inlier_ratio"),
}
if args.axis == "pan":
state["steps"][args.axis]["visual_direction"] = (
"increasing" if args.direction == 1 else "decreasing"
)
save_state(state)
print(f"推荐 {args.axis}_step={step}({step/100:.2f}°),实测重叠={overlap*100:.1f}%")
print("推荐值已写入 calibration.json")
def scan_pan_range(top_left: int, bottom_right: int, mount: str) -> tuple[int, int, int, str]:
def scan_pan_range(
top_left: int,
bottom_right: int,
mount: str,
visual_direction: str | None = None,
) -> tuple[int, int, int, str]:
"""Encode the user-selected visual left-to-right arc for the current scanner.
With the tested desktop setup, firmware rotates the image by 180 degrees, so
visual left-to-right is decreasing PTZ pan. The current scanner only executes
increasing pan, therefore reversing both endpoints traverses the exact same
selected arc in the opposite order. A normally hanging camera uses the native
increasing direction.
visual_direction should be measured from signed image displacement on this
exact camera model. mount remains only as a backward-compatible fallback for
older manual calibration state.
"""
top_left %= 36000
bottom_right %= 36000
if mount == "desktop":
if visual_direction is None:
visual_direction = "decreasing" if mount == "desktop" else "increasing"
if visual_direction not in {"increasing", "decreasing"}:
fail(f"未知pan视觉方向:{visual_direction}")
if visual_direction == "decreasing":
span = (top_left - bottom_right) % 36000
return bottom_right, top_left, span, "decreasing"
span = (bottom_right - top_left) % 36000
@@ -370,16 +415,21 @@ def command_generate(args: argparse.Namespace) -> None:
f"请使用--target-overlap {DEFAULT_TARGET_OVERLAP:.2f}重新标定"
)
inliers = int(info.get("inliers") or 0)
if inliers < 50:
phase_response = float(info.get("phase_response") or 0.0)
if inliers < 50 and phase_response < PHASE_MIN_RESPONSE:
fail(
f"{axis}_step标定只有{inliers}个几何内点,低于可靠阈值50;"
f"{axis}_step标定质量不足(几何内点={inliers},"
f"相位响应={phase_response:.2f});"
f"请在纹理丰富且尽量远离摄像头的位置重新执行calibrate {axis}"
)
if abs(int(top_left["zoom"]) - int(bottom_right["zoom"])) > 10:
fail("左上角和右下角zoom不一致,请固定zoom后重新记录两个角点")
zoom = round((int(top_left["zoom"]) + int(bottom_right["zoom"])) / 2)
pan_start, pan_end, pan_span, visual_pan_direction = scan_pan_range(
int(top_left["pan"]), int(bottom_right["pan"]), args.mount
int(top_left["pan"]),
int(bottom_right["pan"]),
args.mount,
pan_info.get("visual_direction"),
)
tilt_start = min(int(top_left["tilt"]), int(bottom_right["tilt"]))
tilt_end = max(int(top_left["tilt"]), int(bottom_right["tilt"]))