OrangePi3588Media/transform/calibrate_camera.py

295 lines
11 KiB
Python
Raw 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.

#!/usr/bin/env python3
"""
相机标定工具 - 为车间人脸识别系统生成ROI和三分区参数
Usage:
python calibrate_camera.py --height 5.0 --pitch 45 --report
python calibrate_camera.py --height 4.5 --pitch 40 --focal-estimate 2000 -o cam.json
Output:
生成可直接复制到配置文件的params参数
"""
import json
import argparse
import numpy as np
from dataclasses import dataclass
from typing import List, Tuple, Dict
import sys
@dataclass
class CameraParams:
"""相机参数"""
height: float # 安装高度(m)
pitch_deg: float # 俯仰角(°)
focal_px: float # 像素焦距(px)
img_w: int # 图像宽度
img_h: int # 图像高度
class CameraCalibrator:
"""相机标定器 - 计算ROI和三分区参数"""
def __init__(self, p: CameraParams):
self.p = p
self.cy = p.img_h // 2
self.cx = p.img_w // 2
self.theta = np.radians(p.pitch_deg)
self._build_lut()
def _build_lut(self):
"""预计算距离查找表 LUT[y] = distance(m)"""
self.lut = np.zeros(self.p.img_h, dtype=np.float32)
for y in range(self.p.img_h):
dy = y - self.cy
angle = self.theta + np.arctan2(dy, self.p.focal_px)
if abs(angle) > 1e-6 and np.tan(angle) > 0:
self.lut[y] = self.p.height / np.tan(angle)
else:
self.lut[y] = np.inf
def pixel_to_distance(self, y: int) -> float:
"""像素y坐标 -> 距离(m)"""
if 0 <= y < self.p.img_h:
return float(self.lut[y])
return np.inf
def distance_to_pixel(self, d: float) -> int:
"""距离(m) -> 像素y坐标"""
if d <= 0:
return self.cy
angle = np.arctan2(self.p.height, d)
offset = self.p.focal_px * np.tan(angle - self.theta)
return int(np.clip(self.cy + offset, 0, self.p.img_h - 1))
def calculate_roi(self, min_d: float = 3.0, max_d: float = 9.0,
margin: int = 20) -> Dict:
"""计算ROI裁剪区域
Args:
min_d: 最近检测距离(米)
max_d: 最远检测距离(米)
margin: 边界余量(像素)
"""
# 注意距离越远y坐标越大画面下方
y_min = self.distance_to_pixel(max_d) # 远距在下
y_max = self.distance_to_pixel(min_d) # 近距在上
# 添加边界余量
y_min = max(0, y_min - margin)
y_max = min(self.p.img_h, y_max + margin)
saving = 1 - (y_max - y_min) / self.p.img_h
return {
"crop": {
"x": 0,
"y": int(y_min),
"w": self.p.img_w,
"h": int(y_max - y_min)
},
"saving_percent": round(saving * 100, 1),
"y_min": int(y_min),
"y_max": int(y_max)
}
def get_zone_boundaries(self, boundaries_m: List[float]) -> List[int]:
"""获取分区边界像素坐标
Args:
boundaries_m: 分界线距离列表,如[5.0, 7.0]
"""
return [self.distance_to_pixel(d) for d in boundaries_m]
def estimate_face_size(self, distance: float, real_width: float = 0.16) -> float:
"""估算给定距离的人脸像素大小"""
if distance <= 0:
return 0
return self.p.focal_px * real_width / distance
def generate_config(self, min_d: float = 3.0, max_d: float = 9.0,
boundaries_m: List[float] = None) -> Dict:
"""生成配置文件
Returns:
包含params和meta的配置字典
"""
if boundaries_m is None:
boundaries_m = [5.0, 7.0] # 默认5米和7米分界线
roi = self.calculate_roi(min_d, max_d)
boundaries_y = self.get_zone_boundaries(boundaries_m)
# 生成可直接复制到instances params的配置
config = {
"params": {
"camera_pitch": self.p.pitch_deg,
"roi_enabled": True,
"roi_y": roi["crop"]["y"],
"roi_h": roi["crop"]["h"],
"zones_enabled": True,
"zone_boundary_5m": boundaries_y[0],
"zone_boundary_7m": boundaries_y[1],
# 320模型缩放因子
"zone_scale_near": 1.0,
"zone_scale_mid": 1.3,
"zone_scale_far": 1.8
},
"meta": {
"height": self.p.height,
"pitch": self.p.pitch_deg,
"focal_px": self.p.focal_px,
"image_size": [self.p.img_w, self.p.img_h],
"roi_saving": roi["saving_percent"],
"zone_boundaries_m": boundaries_m,
"zone_boundaries_y": boundaries_y,
"detection_range": [min_d, max_d]
}
}
return config
def print_report(self, min_d: float = 3.0, max_d: float = 9.0,
boundaries_m: List[float] = None):
"""打印标定报告"""
if boundaries_m is None:
boundaries_m = [5.0, 7.0]
roi = self.calculate_roi(min_d, max_d)
boundaries_y = self.get_zone_boundaries(boundaries_m)
print("=" * 70)
print("相机标定报告")
print("=" * 70)
print(f"\n【相机参数】")
print(f" 安装高度 H: {self.p.height}m")
print(f" 俯仰角 θ: {self.p.pitch_deg}°")
print(f" 像素焦距 f: {self.p.focal_px}px")
print(f" 图像尺寸: {self.p.img_w}x{self.p.img_h}")
print(f"\n【ROI配置】(检测范围 {min_d}-{max_d}m)")
print(f" 裁剪区域: y={roi['crop']['y']}, h={roi['crop']['h']}")
print(f" 算力节省: {roi['saving_percent']}%")
print(f"\n【三分区配置】(RetinaFace_320)")
print(f" 分界线y坐标: 5m={boundaries_y[0]}px, 7m={boundaries_y[1]}px")
# y坐标画面上方y小= 远距离画面下方y大= 近距离
by5, by7 = boundaries_y[0], boundaries_y[1]
if by5 > by7:
# 5米分界线在下方y大7米在上方y小
print(f" 画面分布: 上方(y小)=远距离, 下方(y大)=近距离")
print(f" 近区(3-5m): y>{by5}, scale=1.0x (画面下方)")
print(f" 中区(5-7m): {by7}<y≤{by5}, scale=1.3x (画面中部)")
print(f" 远区(7-9m): y≤{by7}, scale=1.8x (画面上方)")
else:
print(f" 近区(3-5m): y<{by5}, scale=1.0x")
print(f" 中区(5-7m): {by5}≤y<{by7}, scale=1.3x")
print(f" 远区(7-9m): y≥{by7}, scale=1.8x")
print(f"\n【距离-像素-人脸大小映射】")
print(f" {'距离':>6} | {'像素y':>6} | {'原始人脸':>10} | {'320输入':>9} | {'区域':>6}")
print(f" {'-'*60}")
for d in [3, 4, 5, 6, 7, 8, 9]:
y = self.distance_to_pixel(d)
face_orig = self.estimate_face_size(d)
# 确定区域和320输入大小
zone_idx = 0
if d >= boundaries_m[1]:
zone_idx = 2
elif d >= boundaries_m[0]:
zone_idx = 1
scales = [1.0, 1.3, 1.8]
face_320 = face_orig * scales[zone_idx]
zone_name = ["", "", ""][zone_idx]
print(f" {d:>6.0f}m | {y:>6} | {face_orig:>9.0f}px | {face_320:>9.0f}px | {zone_name:>6}")
print(f"\n【320模型说明】")
print(f" - 320最佳检测范围25-35px占320输入的8-11%")
print(f" - 8-9米检测率可能降至70%如不满足可升级到640模型")
print(f" - 升级只需改model_path + scales=[0.7,1.0,1.4]")
print("\n" + "=" * 70)
def main():
parser = argparse.ArgumentParser(
description='相机标定工具 - 生成ROI和三分区参数',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
# 基础标定(推荐)
python calibrate_camera.py --height 5.0 --pitch 45 --report
# 自定义参数并保存
python calibrate_camera.py --height 4.5 --pitch 40 --focal-estimate 2000 -o cam.json --report
# 调整分区边界
python calibrate_camera.py --height 5.0 --zones 4.5 6.5 --report
"""
)
parser.add_argument('--height', type=float, required=True,
help='相机安装高度(米),如 5.0')
parser.add_argument('--pitch', type=float, default=45,
help='俯仰角默认45')
parser.add_argument('--focal-estimate', type=float, default=2200,
help='像素焦距估算值默认22002.5K相机4-6mm镜头')
parser.add_argument('--image-size', type=int, nargs=2,
default=[2560, 1440],
help='图像尺寸 宽 高,默认 2560 1440')
parser.add_argument('--range', type=float, nargs=2,
default=[3.0, 9.0],
help='检测范围 最小距离 最大距离,默认 3.0 9.0')
parser.add_argument('--zones', type=float, nargs=2,
default=[5.0, 7.0],
help='分区边界距离(米),默认 5.0 7.0形成3-5,5-7,7-9三区')
parser.add_argument('-o', '--output', default='camera_calib.json',
help='输出文件路径,默认 camera_calib.json')
parser.add_argument('--report', action='store_true',
help='打印详细报告')
args = parser.parse_args()
# 创建标定器
params = CameraParams(
height=args.height,
pitch_deg=args.pitch,
focal_px=args.focal_estimate,
img_w=args.image_size[0],
img_h=args.image_size[1]
)
calib = CameraCalibrator(params)
config = calib.generate_config(
args.range[0],
args.range[1],
list(args.zones)
)
# 保存配置
with open(args.output, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
print(f"[OK] 配置已保存: {args.output}")
# 打印报告
if args.report:
calib.print_report(args.range[0], args.range[1], list(args.zones))
print(f"\n【可直接复制到配置文件的 params】")
print(json.dumps(config["params"], indent=2, ensure_ascii=False))
else:
# 简洁输出关键参数
print(f"\n关键参数:")
print(f" roi_y: {config['params']['roi_y']}")
print(f" roi_h: {config['params']['roi_h']}")
print(f" zone_boundary_5m: {config['params']['zone_boundary_5m']}")
print(f" zone_boundary_7m: {config['params']['zone_boundary_7m']}")
if __name__ == '__main__':
main()