文章目录
- 3D定位
- 简述
- 3D定位原理分析
- 海康SDK实现
- ONVIF实现
- 球机的PTZ与ONVIF的PTZ的对应关系
- 球机的视场角与ONVIF的Zoom的对应关系
- 3D 定位
- 参考链接
海康部分球机有3D定位功能,最近研究了一下海康球机SDK实现和任意球机ONVIF实现。
3D定位
简述
- 3D定位是指通过设置图像目标坐标,旋转球机PTZ,使目标在球机视野中心;
- 球机坐标系有三个轴分别控制pan(水平) tilt(垂直) zoom(倍率);
3D定位原理分析
- 3D定位原理见下图(C是镜头中心,O是图像中心点,P是目标点在图上位置,A是图像边界点)
- 由上图可得
- 可得图像上的目标点
P
到图像视野中心的水平和垂直角度差 - 通过水平和垂直视野角度差,转动云台指定角度即可。
海康SDK实现
- 目前在海康球机看到有相关功能实现,点击3D,然后在图上选择想定位的目标点。
- 海康球机有相关SDK实现,SDK下载
- 相关实现函数
-
pStruPointFrame
图像上的点归一化到255x255的坐标系(具体原因可以参考海康专利《一种利用快球跟踪系统实现监控的方法及装置》) -
NET_DVR_PTZSelZoomIn
和NET_DVR_PTZSelZoomIn_EX
功能一致,实现略微不同 - SDK调用可以查看《设备网络SDK使用手册》
NET_DVR_PTZSelZoomIn(LONG lRealHandle, LPNET_DVR_POINT_FRAME pStruPointFrame);
NET_DVR_PTZSelZoomIn_EX(LONG lUserID, LONG lChannel, LPNET_DVR_POINT_FRAME pStruPointFrame);
strcpy(cam_ip, "192.168.1.x");
cam_port = 8000;
strcpy(user_name, "admin");
strcpy(pwd, "xxxxx");
NET_DVR_DEVICEINFO_V30 DeviceInfoTmp;
memset(&DeviceInfoTmp, 0, sizeof(NET_DVR_DEVICEINFO_V30));
LONG lLoginID = NET_DVR_Login_V30(cam_ip, cam_port, user_name, pwd, &DeviceInfoTmp); // 注册
int std_cols = 2560; // 图像长宽
int std_rows = 1440;
NET_DVR_POINT_FRAME posdata;
posdata.xTop = (int)(target_x * 255 / std_cols); // 坐标归一化到(255, 255)
posdata.xBottom = posdata.xTop;
posdata.yTop = (int)(target_y * 255 / std_rows);
posdata.yBottom = posdata.yTop;
posdata.bCounter = 3; // 没用
if (!NET_DVR_PTZSelZoomIn_EX(0, 1, &posdata)){
printf("3D定位失败, %d\n", NET_DVR_GetLastError());
return -1;
}
ONVIF实现
- 既然原理已知,考虑以下,研究一下3D定位通用方法,
- 海康SDK有Linux和Windows的实现方式,但是在arm上没找到相关SDK?
- 如果有别的品牌球机,没有海康SDK,需要如何实现?
- 如果不是球机,只有一个云台和一个摄像头又要如何实现?
- ONVIF协议作为标准通用的公有协议 ,可以实现摄像头PTZ控制
- 需要获取球机的PTZ和视场角与ONVIF的PTZ对应关系,参考《球机的PTZ和视场角与ONVIF的PTZ对应关系》
- 以海康球机
DS-2DC22041W-D3/W
为例
球机的PTZ与ONVIF的PTZ的对应关系
- 球机的PTZ与onvif的ptz是线性关系
- ONVIF与SDK的PTZ对应关系
ONVIF_P | ONVIF_T | ONVIF_Z | SDK_P | SDK_T | SDK_Z |
-1 | -1 | 0 | 0 | 0 | 1 |
0 | 0 | 0.5 | 180 | 45 | 2.5 |
1 | 1 | 1 | 350 | 90 | 4 |
- 拟合线性关系
球机的视场角与ONVIF的Zoom的对应关系
- 此版本不支持SDK直接读取视场角值
- 参照《视场角相关计算》,通过相机标定,获取像素焦距
- 计算出水平和垂直视场角
3D 定位
- 主要分为ONVIF控制、3D控制和UI交互三部分
app.py
负责UI交互
def onMouse(event, x, y, flags, param):
if event == cv2.EVENT_LBUTTONDBLCLK:
# print(x, y, flags, param)
coor = (x,y)
param.put(coor)
if param.qsize() > 1:
param.get()
else:
time.sleep(0.01)
def show_image(wnd_name, img, delay=0):
cv2.namedWindow(wnd_name, cv2.WINDOW_NORMAL)
cv2.resizeWindow(wnd_name, 1280, 720)
cv2.imshow(wnd_name, img)
ret = cv2.waitKey(delay)
return ret
def img_put(queue, status):
wnd_name = "frame"
cap = cv2.VideoCapture(rtsp)
if not cap.isOpened():
print('error open cam')
return
num = 0
while True:
num += 1
frame = cv2.flip(cap.read()[1], 0)
status.value = show_image(wnd_name, frame, 1)
if status.value == ord('q'):
print("image put exit!")
return
cv2.setMouseCallback(wnd_name, onMouse, param=queue)
def img_get(queue, status):
my_onvif = onvif_control(ip, usr, pwd)
my_locate = locate(Size[0], Size[1], FOV[0], FOV[1])
num = 0
while True:
num += 1
if status.value == ord('q'):
print("image get exit!")
return
if queue.qsize() > 0:
coor = queue.get()
pos_cur = my_onvif.get_status()
pos_target = my_locate.locate(coor, pos_cur)
my_onvif.abs_move(pos_target)
def app():
multiprocessing.set_start_method(method='spawn')
queue = multiprocessing.Queue(maxsize=5)
processes = []
status = Value('i', -1)
processes.append(multiprocessing.Process(target=img_put, args=(queue, status)))
processes.append(multiprocessing.Process(target=img_get, args=(queue, status)))
for process in processes:
process.daemon = True
process.start()
for process in processes:
process.join()
print("Done")
-
onvif_control.py
负责ONVIF控制
class onvif_control:
def __init__(self, ip, usr, pwd, hom_pos=None):
self.usr = usr
self.pwd = pwd
self.hom_pos = hom_pos
self.cam = ONVIFCamera(ip, 80, usr, pwd)
self.ptz = self.cam.create_ptz_service()
self.media = self.cam.create_media_service()
self.imaging = self.cam.create_imaging_service()
self.profile = self.cam.media.GetProfiles()[0]
def get_status(self):
request = self.cam.ptz.create_type('GetStatus')
request.ProfileToken = self.profile.token
status = self.cam.ptz.GetStatus(request)
return {'pan': status.Position.PanTilt.x, 'tilt': status.Position.PanTilt.y, 'zoom': status.Position.Zoom.x}
def move_home(self, speed={'pan':1, 'tilt':1, 'zoom':1}):
request = self.ptz.create_type('AbsoluteMove')
request.ProfileToken = self.profile.token
request.Position = {'PanTilt':{'x':self.hom_pos['pan'], 'y':self.hom_pos['tilt']}, 'Zoom':self.hom_pos['zoom']}
request.Speed = {'PanTilt':{'x':speed['pan'], 'y':speed['tilt']}, 'Zoom':speed['zoom']}
self.ptz.AbsoluteMove(request)
def abs_move(self, pos, speed={'pan':1, 'tilt':1, 'zoom':1}):
request = self.ptz.create_type('AbsoluteMove')
request.ProfileToken = self.profile.token
request.Position = {'PanTilt':{'x':pos['pan'], 'y':pos['tilt']}, 'Zoom':pos['zoom']}
request.Speed = {'PanTilt':{'x':speed['pan'], 'y':speed['tilt']}, 'Zoom':speed['zoom']}
self.ptz.AbsoluteMove(request)
def continuous_move(self, speed):
request = self.ptz.create_type("ContinuousMove")
request.ProfileToken = self.profile.token
request.Velocity = {'PanTilt':{'x':speed['pan'], 'y':speed['tilt']}, 'Zoom':speed['zoom']}
self.ptz.ContinuousMove(request)
def relative_move(self, pos, speed={'pan':1, 'tilt':1, 'zoom':1}):
request = self.cam.ptz.create_type('RelativeMove')
request.ProfileToken = self.profile.token
request.Translation = {'PanTilt':{'x':pos['pan'], 'y':pos['tilt']}, 'Zoom':pos['zoom']}
request.Speed = {'PanTilt':{'x':speed['pan'], 'y':speed['tilt']}, 'Zoom':speed['zoom']}
self.ptz.RelativeMove(request)
def stop(self):
self.ptz.Stop({'ProfileToken': self.profile.token})
def snap_image(self, path):
import requests, os
res = self.media.GetSnapshotUri({'ProfileToken': self.profile.token})
auth = requests.auth.HTTPDigestAuth(self.usr, self.pwd)
response = requests.get(url=res.Uri, auth=auth)
path_out = os.path.join(path, 'tmp.jpg')
with open(path_out, 'wb') as fp:
fp.write(response.content)
def get_rtsp(self):
obj = self.media.create_type('GetStreamUri')
obj.StreamSetup = {'Stream': 'RTP-Unicast', 'Transport': {'Protocol': 'RTSP'}}
obj.ProfileToken = self.profile.token
res_uri = self.media.GetStreamUri(obj)['Uri']
return res_uri
-
locate_3d.py
负责3D定位计算
class locate:
def __init__(self, width, height, fov_h, fov_v):
self.width = width
self.height = height
self.fov_h = fov_h
self.fov_v = fov_v
def ptz_to_sdk(self, pos_onvif):
p_ = min(180 * pos_onvif['pan'] + 180, 360)
t_ = 45 * pos_onvif['tilt'] + 45
z_ = 3 * pos_onvif['zoom'] + 1
return {'pan' :p_, 'tilt' :t_, 'zoom' :z_}
def ptz_to_onvif(self, pos_sdk):
p_ = (pos_sdk['pan'] - 180) / 180
t_ = (pos_sdk['tilt'] - 45) / 45
z_ = (pos_sdk['zoom'] - 1) / 3
return {'pan' :p_, 'tilt' :t_, 'zoom' :z_}
def locate(self, target, pos_onvif):
# 图像目标点,移动到视野中心
# pos转到sdk
pos_sdk = self.ptz_to_sdk(pos_onvif)
print("[pos_sdk]:", pos_sdk)
x, y = target
# 水平方向
if x > (self.width / 2):
delt_x = np.rad2deg( np.arctan((x - self.width / 2) / (self.width / 2) * np.tan( np.deg2rad(self.fov_h /2))) )
else:
delt_x = -np.rad2deg( np.arctan((self.width / 2 - x) / (self.width / 2) * np.tan( np.deg2rad(self.fov_h /2))) )
# 垂直方向
if y > (self.height / 2):
delt_y = np.rad2deg( np.arctan((y - self.height / 2) / (self.height / 2) * np.tan( np.deg2rad(self.fov_v / 2))) )
else:
delt_y = -np.rad2deg( np.arctan((self.height / 2 - y) / (self.height / 2) * np.tan( np.deg2rad(self.fov_v / 2))) )
# print("delt:[%.3f, %.3f]" % (delt_x, delt_y))
pos_sdk['pan'] += delt_x # 得到转换后的pt
pos_sdk['tilt'] -= delt_y # 相机是倒置放置的。。。
pos_onvif = self.ptz_to_onvif(pos_sdk) # pos转到onvif
return pos_onvif