video_monitor/workshop_monitor/calibration.py
2026-09-06 22:26:35 +08:00

127 lines
4.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""平面控制点标定和像素/世界坐标转换。"""
import math
class CalibrationError(ValueError):
pass
def _cv():
try:
import cv2
import numpy as np
return cv2, np
except Exception as exc:
raise CalibrationError("OpenCV/Numpy 不可用,无法执行标定") from exc
def map_point(homography, u, v):
"""使用 3x3 Homography 将像素点映射到世界平面。"""
_cv2, np = _cv()
matrix = np.asarray(homography, dtype=np.float64)
if matrix.shape != (3, 3) or not np.isfinite(matrix).all():
raise CalibrationError("Homography 格式无效")
p = matrix.dot(np.asarray([float(u), float(v), 1.0], dtype=np.float64))
if abs(float(p[2])) < 1e-10:
raise CalibrationError("像素点无法投影到地面")
return float(p[0] / p[2]), float(p[1] / p[2])
def _area_ratio(points, frame_width, frame_height):
cv2, np = _cv()
hull = cv2.convexHull(np.asarray(points, dtype=np.float32))
denom = max(1.0, float(frame_width) * float(frame_height))
return float(cv2.contourArea(hull)) / denom
def solve_planar_calibration(observations, frame_width, frame_height,
max_validation_mean_m=1.0):
"""根据拟合点求 H并使用完全独立的验证点评价世界坐标误差。"""
cv2, np = _cv()
try:
fw, fh = int(frame_width), int(frame_height)
except Exception as exc:
raise CalibrationError("图像尺寸无效") from exc
if fw <= 0 or fh <= 0:
raise CalibrationError("图像尺寸必须大于 0")
fit = [p for p in observations if p.get("role", "fit") == "fit"]
verify = [p for p in observations if p.get("role") == "verify"]
if len(fit) < 4:
raise CalibrationError("至少需要 4 个拟合点")
if len(verify) < 3:
raise CalibrationError("至少需要 3 个独立验证点")
def pixels(rows):
return np.asarray([[float(p["u"]), float(p["v"])] for p in rows], dtype=np.float64)
def worlds(rows):
return np.asarray([[float(p["x"]), float(p["y"])] for p in rows], dtype=np.float64)
src, dst = pixels(fit), worlds(fit)
if not np.isfinite(src).all() or not np.isfinite(dst).all():
raise CalibrationError("控制点包含非有限数值")
if _area_ratio(src, fw, fh) < 0.01:
raise CalibrationError("拟合点共线或过度集中,请扩大点位分布")
if float(cv2.contourArea(cv2.convexHull(dst.astype(np.float32)))) < 0.01:
raise CalibrationError("世界坐标点共线或过度集中")
matrix, mask = cv2.findHomography(src, dst, cv2.RANSAC, 0.75)
if matrix is None or not np.isfinite(matrix).all() or abs(float(np.linalg.det(matrix))) < 1e-12:
raise CalibrationError("无法计算稳定的 Homography")
fit_projection = cv2.perspectiveTransform(src.reshape(-1, 1, 2), matrix).reshape(-1, 2)
fit_errors = np.linalg.norm(fit_projection - dst, axis=1)
verify_src, verify_dst = pixels(verify), worlds(verify)
verify_projection = cv2.perspectiveTransform(verify_src.reshape(-1, 1, 2), matrix).reshape(-1, 2)
verify_errors = np.linalg.norm(verify_projection - verify_dst, axis=1)
fit_rmse = math.sqrt(float(np.mean(np.square(fit_errors))))
validation_mean = float(np.mean(verify_errors))
validation_max = float(np.max(verify_errors))
warnings = []
if len(fit) < 8:
warnings.append("拟合点少于建议的 8 个")
coverage = _area_ratio(src, fw, fh)
if coverage < 0.15:
warnings.append("拟合点仅覆盖画面 %.1f%%,建议增加边缘和远端点" % (coverage * 100.0))
inliers = int(mask.sum()) if mask is not None else len(fit)
if inliers < len(fit):
warnings.append("RANSAC 排除了 %d 个异常拟合点" % (len(fit) - inliers))
detail = []
fit_index = verify_index = 0
for p in observations:
row = dict(p)
if p.get("role", "fit") == "fit":
row["error_m"] = float(fit_errors[fit_index])
fit_index += 1
else:
row["error_m"] = float(verify_errors[verify_index])
verify_index += 1
detail.append(row)
return {
"homography": matrix.tolist(),
"fit_rmse_m": fit_rmse,
"validation_mean_m": validation_mean,
"validation_max_m": validation_max,
"coverage_ratio": coverage,
"inlier_count": inliers,
"is_valid": validation_mean <= float(max_validation_mean_m),
"warnings": warnings,
"observations": detail,
}
def foot_point_world(box, homography, width_m, height_m, boundary_margin_m=1.0):
if not box or len(box) != 4:
return None
u = (float(box[0]) + float(box[2])) * 0.5
v = float(box[3])
x, y = map_point(homography, u, v)
margin = max(0.0, float(boundary_margin_m))
if x < -margin or y < -margin or x > float(width_m) + margin or y > float(height_m) + margin:
return None
return max(0.0, min(float(width_m), x)), max(0.0, min(float(height_m), y))