Fits small cameras
This commit is contained in:
1 parent
a773166c91
commit
98ed2576c6
7 files changed
+526
-96
No files matched your search
+61
-11
@@ -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"]))
|
||||
|
||||
Reference in new issue
Block a user