get step
This commit is contained in:
1 parent
7e9ecfe01e
commit
a773166c91
2 files changed
+135
-36
No files matched your search
@@ -60,8 +60,10 @@ cd /home/zhangw/scan-calibrator
|
||||
> 1
|
||||
已读取: pan=13420 (134.20°), tilt=-340 (-3.40°), zoom=810 (8.10×)
|
||||
|
||||
④ 请保持zoom不变,把摄像头移到扫描区域中央、距离较远且纹理丰富的位置。
|
||||
请避开纯天空、纯墙面、水面、玻璃和重复图案;确认后程序会自动移动、拍照并计算step。
|
||||
④ 这一步用于测量当前zoom下的水平/垂直视场。
|
||||
请保持zoom不变,优先把摄像头移到扫描区域内部、距离较远且纹理分布在整幅画面的位置。
|
||||
请避开纯天空、纯墙面、水面、玻璃、近距离物体和重复图案;
|
||||
回复1后不要再操作Web云台,程序会自动移动、拍照并返回起点。
|
||||
到位后回复 1;回复 3 随时退出。
|
||||
> 1
|
||||
|
||||
@@ -81,8 +83,8 @@ cd /home/zhangw/scan-calibrator
|
||||
⑤ 本次得到的扫图参数:
|
||||
{
|
||||
"pan_start": 13420,
|
||||
"pan_end": 19672,
|
||||
"pan_step": 1042,
|
||||
"pan_end": 19870,
|
||||
"pan_step": 1075,
|
||||
"tilt_start": -340,
|
||||
"tilt_end": 1511,
|
||||
"tilt_step": 617,
|
||||
@@ -90,7 +92,7 @@ cd /home/zhangw/scan-calibrator
|
||||
}
|
||||
安装方式:desktop
|
||||
范围检查:视觉左→右pan=decreasing,指定弧段=57.00°
|
||||
整步扩边:pan_step=1042,向终点外扩=552(5.52°)
|
||||
整步扩边:pan_step=1075,向终点外扩=750(7.50°)
|
||||
整步扩边:tilt_step=617,向终点外扩=521(5.21°)
|
||||
预计网格:7列 × 4行 = 28张
|
||||
|
||||
@@ -164,20 +166,25 @@ smart-cam的HTTP接口读取PTZ并记录,等价于:
|
||||
|
||||
### ④ 自动计算pan_step和tilt_step
|
||||
|
||||
现场人员把镜头移到扫描区域中央、距离较远且纹理丰富的位置。回复`1`后,向导会:
|
||||
现场人员保持zoom不变,把镜头移到扫描区域内部、距离较远且纹理丰富的位置。
|
||||
回复`1`后,向导会:
|
||||
|
||||
1. 抓取基准图;
|
||||
2. 沿pan方向尝试若干候选角度,每次抓图后回到起点;
|
||||
3. 用ORB特征、RANSAC和单轴画面位移求水平视场;
|
||||
4. 沿tilt方向重复测量;
|
||||
5. 按`step = 视场 × (1 - 0.04)`计算目标重叠4%的step;
|
||||
6. 要求至少50个几何内点且内点率至少45%,否则拒绝结果;
|
||||
7. 删除本次自动标定产生的临时截图。
|
||||
5. 每个方向用不同移动量反算视场:两组结果只有在特征质量高且视场差不超过8%时才接受,
|
||||
否则要求至少三组一致结果;
|
||||
6. 水平方向按1%目标重叠计算step,垂直方向按4%目标重叠计算step;
|
||||
7. 要求至少50个几何内点且内点率至少45%,否则拒绝结果;
|
||||
8. 删除本次自动标定产生的临时截图。
|
||||
|
||||
这一步不能省略。两个角点只能确定扫描范围,无法确定当前摄像头型号和zoom下的一幅画面
|
||||
覆盖多少角度。
|
||||
|
||||
不要把画面对准纯天空、纯墙、水面、玻璃、近距离物体或重复图案,否则无法可靠匹配。
|
||||
这一步不能改变zoom;自动对焦不影响zoom。标定失败时可以只更换第四步的位置重试,
|
||||
不需要重新记录左上角和右下角。
|
||||
|
||||
### ⑤ 显示最终参数
|
||||
|
||||
@@ -261,5 +268,7 @@ docker exec smart-cam curl -fsS \
|
||||
- 每一个摄像头点位都要重新记录角点;
|
||||
- 摄像头型号或zoom变化时必须重新自动计算step;
|
||||
- 标定期间不要同时从Web界面控制PTZ;
|
||||
- 向导目标是4%重叠,为5%上限保留1%误差;
|
||||
- 水平目标重叠为1%,垂直目标重叠为4%,都不超过5%上限;
|
||||
- 自动标定不会使用单个样本;两组样本必须同时满足高特征质量和视场差不超过8%,
|
||||
普通质量的结果必须由至少三组样本互相验证;
|
||||
- 机械回差、镜头畸变和近景视差可能导致不同位置略有差异,最终必须完整试扫并拼图复核;
|
||||
+116
-26
@@ -20,7 +20,7 @@ import scan_calibrator as core
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
CONFIG_PATH = "/smart-cam/config/config.json"
|
||||
TARGET_OVERLAP = 0.04
|
||||
TARGET_OVERLAPS = {"pan": 0.01, "tilt": 0.04}
|
||||
RED = "\033[31m"
|
||||
GREEN = "\033[32m"
|
||||
YELLOW = "\033[33m"
|
||||
@@ -67,8 +67,8 @@ def remove_session_snapshots(paths: set[str]) -> None:
|
||||
|
||||
def candidate_deltas(axis: str) -> list[int]:
|
||||
if axis == "pan":
|
||||
return [250, 450, 700, 1000, 1400, 1900, 2600]
|
||||
return [120, 220, 350, 500, 700, 950, 1300]
|
||||
return [150, 250, 350, 450, 600, 800, 1100, 1500, 2100]
|
||||
return [80, 120, 170, 220, 280, 350, 450, 600, 850, 1150]
|
||||
|
||||
|
||||
def choose_direction(axis: str, origin: dict[str, int], points: dict) -> int:
|
||||
@@ -85,7 +85,7 @@ def estimate_axis(axis: str, origin: dict[str, int], points: dict,
|
||||
direction = choose_direction(axis, origin, points)
|
||||
baseline = core.snapshot()
|
||||
snapshots.add(baseline)
|
||||
reliable: list[tuple[float, int, dict]] = []
|
||||
reliable: list[tuple[int, float, float, dict]] = []
|
||||
say(f"\n正在标定 {axis}_step,摄像头会自动移动并回到起点……")
|
||||
|
||||
for delta in candidate_deltas(axis):
|
||||
@@ -102,28 +102,79 @@ def estimate_axis(axis: str, origin: dict[str, int], points: dict,
|
||||
say(f" step={delta}: 重叠约 {overlap*100:.1f}%,"
|
||||
f"有效特征={inliers},内点率={ratio*100:.1f}%")
|
||||
if inliers >= 50 and ratio >= 0.45 and 0.08 <= overlap <= 0.80:
|
||||
# Prefer a mid-overlap sample: enough common features without tiny motion.
|
||||
reliable.append((abs(overlap - 0.40), delta, result))
|
||||
if 0.18 <= overlap <= 0.55 and reliable:
|
||||
break
|
||||
reliable.append((delta, overlap, delta / (1.0 - overlap), result))
|
||||
|
||||
if not reliable:
|
||||
if len(reliable) < 2:
|
||||
core.move(origin)
|
||||
core.fail(
|
||||
f"{axis}方向没有得到可靠视场测量。请把摄像头移到远处、纹理丰富、"
|
||||
"非重复图案的位置后重新运行向导"
|
||||
f"{axis}方向只得到{len(reliable)}个可靠样本,至少需要2个不同移动量互相验证。"
|
||||
"请把摄像头移到远处、纹理丰富、非重复图案的位置后重新运行向导"
|
||||
)
|
||||
|
||||
_, measured_step, detail = min(reliable, key=lambda item: item[0])
|
||||
measured_overlap = float(detail["overlap"])
|
||||
estimated_fov = measured_step / (1.0 - measured_overlap)
|
||||
step = max(1, round(estimated_fov * (1.0 - TARGET_OVERLAP)))
|
||||
# Fixed OSD text and repeated scenery can yield a convincing false
|
||||
# homography. A real optical FOV must agree across different PTZ deltas.
|
||||
clusters = [
|
||||
[
|
||||
sample for sample in reliable
|
||||
if abs(sample[2] - seed[2]) / max(seed[2], 1.0) <= 0.12
|
||||
]
|
||||
for seed in reliable
|
||||
]
|
||||
consistent = max(
|
||||
clusters, key=lambda cluster: (len(cluster), -max(x[2] for x in cluster))
|
||||
)
|
||||
if len(consistent) < 2:
|
||||
core.move(origin)
|
||||
values = ", ".join(f"{sample[2]:.0f}" for sample in reliable)
|
||||
core.fail(
|
||||
f"{axis}方向不同移动量反算的视场不一致({values}),疑似重复图案误匹配;"
|
||||
"本次结果不写入配置,请更换标定画面"
|
||||
)
|
||||
|
||||
fovs = sorted(sample[2] for sample in consistent)
|
||||
estimated_fov = (
|
||||
fovs[len(fovs) // 2] if len(fovs) % 2
|
||||
else (fovs[len(fovs) // 2 - 1] + fovs[len(fovs) // 2]) / 2
|
||||
)
|
||||
spread = (max(fovs) - min(fovs)) / estimated_fov
|
||||
if len(consistent) == 2:
|
||||
strong_pair = all(
|
||||
int(sample[3].get("inliers") or 0) >= 100
|
||||
and float(sample[3].get("inlier_ratio") or 0) >= 0.65
|
||||
for sample in consistent
|
||||
)
|
||||
allowed_spread = 0.08
|
||||
if not strong_pair:
|
||||
core.move(origin)
|
||||
core.fail(
|
||||
f"{axis}方向目前只有2个一致样本,但特征质量不足以使用双样本规则;"
|
||||
"需要第3个可靠样本,请更换标定画面"
|
||||
)
|
||||
else:
|
||||
allowed_spread = 0.15
|
||||
if spread > allowed_spread:
|
||||
core.move(origin)
|
||||
core.fail(
|
||||
f"{axis}方向视场样本离散度为{spread*100:.1f}%,"
|
||||
f"超过{allowed_spread*100:.0f}%;"
|
||||
"本次结果不写入配置,请更换标定画面"
|
||||
)
|
||||
|
||||
target_overlap = TARGET_OVERLAPS[axis]
|
||||
step = max(1, round(estimated_fov * (1.0 - target_overlap)))
|
||||
measured_step, measured_overlap, _, detail = max(
|
||||
consistent, key=lambda sample: int(sample[3].get("inliers") or 0)
|
||||
)
|
||||
core.move(origin)
|
||||
say(
|
||||
f" {axis}视场一致性通过:{len(consistent)}个样本,"
|
||||
f"估算视场={estimated_fov:.0f},离散度={spread*100:.1f}%"
|
||||
)
|
||||
return {
|
||||
"value": step,
|
||||
"direction": direction,
|
||||
"overlap": TARGET_OVERLAP,
|
||||
"target_overlap": TARGET_OVERLAP,
|
||||
"overlap": target_overlap,
|
||||
"target_overlap": target_overlap,
|
||||
"overlap_method": "axis_shift_v1_retargeted",
|
||||
"zoom": origin["zoom"],
|
||||
"measured_at": origin,
|
||||
@@ -134,15 +185,43 @@ def estimate_axis(axis: str, origin: dict[str, int], points: dict,
|
||||
"value": measured_step,
|
||||
"overlap": measured_overlap,
|
||||
"estimated_fov": round(estimated_fov, 4),
|
||||
"samples": [
|
||||
{
|
||||
"value": sample[0],
|
||||
"overlap": round(sample[1], 6),
|
||||
"estimated_fov": round(sample[2], 4),
|
||||
"inliers": sample[3].get("inliers"),
|
||||
"inlier_ratio": sample[3].get("inlier_ratio"),
|
||||
}
|
||||
for sample in consistent
|
||||
],
|
||||
"spread": round(spread, 6),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def calculate_steps(points: dict, snapshots: set[str]) -> None:
|
||||
def calculate_steps(points: dict, snapshots: set[str], mount: str) -> None:
|
||||
origin = camera_ptz()
|
||||
zooms = [int(points[name]["zoom"]) for name in ("top-left", "bottom-right")]
|
||||
if max(zooms + [origin["zoom"]]) - min(zooms + [origin["zoom"]]) > 10:
|
||||
core.fail("三个位置的zoom不一致;请固定焦段后重新开始")
|
||||
top_left = points["top-left"]
|
||||
bottom_right = points["bottom-right"]
|
||||
pan_start, _, pan_span, _ = core.scan_pan_range(
|
||||
int(top_left["pan"]), int(bottom_right["pan"]), mount
|
||||
)
|
||||
pan_inside = (origin["pan"] - pan_start) % 36000 <= pan_span
|
||||
tilt_low = min(int(top_left["tilt"]), int(bottom_right["tilt"]))
|
||||
tilt_high = max(int(top_left["tilt"]), int(bottom_right["tilt"]))
|
||||
tilt_inside = tilt_low <= origin["tilt"] <= tilt_high
|
||||
if not pan_inside or not tilt_inside:
|
||||
say(
|
||||
"警告:当前标定位置不在刚才选择的扫描区域内。"
|
||||
f"当前pan={origin['pan']}、tilt={origin['tilt']};"
|
||||
f"扫描区域tilt={tilt_low}~{tilt_high}。"
|
||||
"同zoom下仍可尝试,但优先选择扫描区域内部的远处纹理。",
|
||||
color=YELLOW,
|
||||
)
|
||||
state = core.load_state()
|
||||
state["steps"]["pan"] = estimate_axis("pan", origin, points, snapshots)
|
||||
state["steps"]["tilt"] = estimate_axis("tilt", origin, points, snapshots)
|
||||
@@ -310,15 +389,26 @@ def main() -> None:
|
||||
state["points"]["bottom-right"] = bottom_right
|
||||
core.save_state(state)
|
||||
|
||||
confirm_position(
|
||||
"④ 请保持zoom不变,把摄像头移到扫描区域中央、"
|
||||
"距离较远且纹理丰富的位置。\n"
|
||||
"请避开纯天空、纯墙面、水面、玻璃和重复图案;"
|
||||
"确认后程序会自动移动、拍照并计算step。"
|
||||
)
|
||||
say("\n正在计算,请不要操作摄像头或Web控制页面……")
|
||||
points = core.load_state()["points"]
|
||||
calculate_steps(points, snapshots)
|
||||
while True:
|
||||
confirm_position(
|
||||
"④ 这一步用于测量当前zoom下的水平/垂直视场。\n"
|
||||
"请保持zoom不变,优先把摄像头移到扫描区域内部、"
|
||||
"距离较远且纹理分布在整幅画面的位置。\n"
|
||||
"请避开纯天空、纯墙面、水面、玻璃、近距离物体和重复图案;"
|
||||
"回复1后不要再操作Web云台,程序会自动移动、拍照并返回起点。"
|
||||
)
|
||||
say("\n正在计算,请不要操作摄像头或Web控制页面……")
|
||||
try:
|
||||
calculate_steps(points, snapshots, mount)
|
||||
break
|
||||
except SystemExit as exc:
|
||||
say(f"\n本次视场标定没有通过:{exc}", color=RED)
|
||||
choose(
|
||||
"请换一个纹理更丰富的位置重新尝试第四步,两个角点无需重录。\n"
|
||||
"重新尝试回复 1;结束对话回复 3。",
|
||||
{"1"},
|
||||
)
|
||||
scan, generated, detail = generate(mount)
|
||||
say("\n⑤ 本次得到的扫图参数:", color=GREEN)
|
||||
say(json.dumps(scan, ensure_ascii=False, indent=2))
|
||||
|
||||
Reference in new issue
Block a user