浅析一个低成本开源动捕项目
项目链接:https://github.com/jyjblrd/Mocap-Drones
作者搞这个项目的原因:https://joshuabird.com/blog/post/mocap-drones
关于npm和yarn:npm(Node Package Manager)和Yarn都是 JavaScript 的软件包管理工具,用于在项目中安装、管理和共享代码包。它们的主要功能包括通过npm或Yarn,你可以轻松地在项目中安装所需的软件包。只需简单的命令,这些工具就会下载并安装所需的软件包及其依赖项等。
关于环境部署:前端环境部署如图片所示,后端环境部署主要是opencv的源码编译勾选sfm选项。

一、项目框架分析:

该项目主要分为电脑服务器端和四旋翼运动端(光学动捕的标记点分为主动标记点和被动标记点,该项目使用的是主动标记点即四旋翼端搭载IR LED发射红外光,通过将SONY PS3摄像头中的IR cut filter换成floppy disk IR filter就能使SONY PS3摄像头变成动捕摄像头了,之所以使用SONY PS3摄像头是因为其软硬件支持多个相机时间同步,方便后续进行帧对齐处理。)。
其中电脑服务端分为前端上位机操作界面和后端数据处理节点,前端采用TypeScript(升级版JavaScript)语言进行编写。后端采用python实现目标物体位置解算(姿态依靠飞控本身传感器测量)、四旋翼运动规划等功能,前后端的通信基于 WebSocket 协议,允许在客户端和服务器之间建立持久的双向通信通道,从而实现实时数据传输。
二、选型
| SONY PS3 camera | 动捕相机 |
| ESP32 | 通信芯片(优秀的WIFI特性) |
| Flight controller | 烧录开源BetaFlight早期版本程序 |
三、算法分析
computer_code/api/helpers.py中进行了类的封装, 位置解算算法主要集中在computer_code/api/index.py程序中:
from helpers import camera_pose_to_serializable, calculate_reprojection_errors, bundle_adjustment, Cameras, triangulate_points
from KalmanFilter import KalmanFilter
from flask import Flask, Response, request #Flask 是一个 Python 的微框架,用于构建 Web 应用程序
import cv2 as cv #包括cv.sfm相关库
import numpy as np
import json
from scipy import linalg
from flask_socketio import SocketIO
import copy
import time
import serial
import threading
from ruckig import InputParameter, OutputParameter, Result, Ruckig
from flask_cors import CORS
import json #导入json数据格式
serialLock = threading.Lock()
ser = serial.Serial("/dev/cu.usbserial-02X2K2GE", 1000000, write_timeout=1, )
app = Flask(__name__)
CORS(app, supports_credentials=True)
socketio = SocketIO(app, cors_allowed_origins='*')
cameras_init = False
num_objects = 2
@app.route("/api/camera-stream") #路由1
def camera_stream():
cameras = Cameras.instance()
cameras.set_socketio(socketio)
cameras.set_ser(ser)
cameras.set_serialLock(serialLock)
cameras.set_num_objects(num_objects)
def gen(cameras):
frequency = 150
loop_interval = 1.0 / frequency
last_run_time = 0
i = 0
while True:
time_now = time.time()
i = (i+1)%10
if i == 0:
socketio.emit("fps", {"fps": round(1/(time_now - last_run_time))})
if time_now - last_run_time < loop_interval:
time.sleep(last_run_time - time_now + loop_interval)
last_run_time = time.time()
frames = cameras.get_frames()
jpeg_frame = cv.imencode('.jpg', frames)[1].tostring()
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + jpeg_frame + b'\r\n')
return Response(gen(cameras), mimetype='multipart/x-mixed-replace; boundary=frame')
@app.route("/api/trajectory-planning", methods=["POST"])#路由2
def trajectory_planning_api():
data = json.loads(request.data)
waypoint_groups = [] # grouped by continuious movement (no stopping)
for waypoint in data["waypoints"]:
stop_at_waypoint = waypoint[-1]
if stop_at_waypoint:
waypoint_groups.append([waypoint[:3*num_objects]])
else:
waypoint_groups[-1].append(waypoint[:3*num_objects])
setpoints = []
for i in range(0, len(waypoint_groups)-1):
start_pos = waypoint_groups[i][0]
end_pos = waypoint_groups[i+1][0]
waypoints = waypoint_groups[i][1:]
setpoints += plan_trajectory(start_pos, end_pos, waypoints, data["maxVel"], data["maxAccel"], data["maxJerk"], data["timestep"])
return json.dumps({
"setpoints": setpoints
})
def plan_trajectory(start_pos, end_pos, waypoints, max_vel, max_accel, max_jerk, timestep):
otg = Ruckig(3*num_objects, timestep, len(waypoints)) # DoFs, timestep, number of waypoints
inp = InputParameter(3*num_objects)
out = OutputParameter(3*num_objects, len(waypoints))
inp.current_position = start_pos
inp.current_velocity = [0,0,0]*num_objects
inp.current_acceleration = [0,0,0]*num_objects
inp.target_position = end_pos
inp.target_velocity = [0,0,0]*num_objects
inp.target_acceleration = [0,0,0]*num_objects
inp.intermediate_positions = waypoints
inp.max_velocity = max_vel*num_objects
inp.max_acceleration = max_accel*num_objects
inp.max_jerk = max_jerk*num_objects
setpoints = []
res = Result.Working
while res == Result.Working:
res = otg.update(inp, out)
setpoints.append(copy.copy(out.new_position))
out.pass_to_input(inp)
return setpoints
@socketio.on("arm-drone")
def arm_drone(data):
global cameras_init
if not cameras_init:
return
Cameras.instance().drone_armed = data["droneArmed"]
for droneIndex in range(0, num_objects):
serial_data = {
"armed": data["droneArmed"][droneIndex],
}
with serialLock:
ser.write(f"{str(droneIndex)}{json.dumps(serial_data)}".encode('utf-8'))
time.sleep(0.01)
@socketio.on("set-drone-pid")
def arm_drone(data):
serial_data = {
"pid": [float(x) for x in data["dronePID"]],
}
with serialLock:
ser.write(f"{str(data['droneIndex'])}{json.dumps(serial_data)}".encode('utf-8'))
time.sleep(0.01)
@socketio.on("set-drone-setpoint")
def arm_drone(data):
serial_data = {
"setpoint": [float(x) for x in data["droneSetpoint"]],
}
with serialLock:
ser.write(f"{str(data['droneIndex'])}{json.dumps(serial_data)}".encode('utf-8'))
time.sleep(0.01)
@socketio.on("set-drone-trim")
def arm_drone(data):
serial_data = {
"trim": [int(x) for x in data["droneTrim"]],
}
with serialLock:
ser.write(f"{str(data['droneIndex'])}{json.dumps(serial_data)}".encode('utf-8'))
time.sleep(0.01)
@socketio.on("acquire-floor")
def acquire_floor(data):#计算相机坐标系到地面坐标系的转换矩阵
cameras = Cameras.instance()
object_points = data["objectPoints"]
object_points = np.array([item for sublist in object_points for item in sublist])
tmp_A = []
tmp_b = []
for i in range(len(object_points)):
tmp_A.append([object_points[i,0], object_points[i,1], 1])
tmp_b.append(object_points[i,2])
b = np.matrix(tmp_b).T
A = np.matrix(tmp_A)
fit, residual, rnk, s = linalg.lstsq(A, b)
fit = fit.T[0]
plane_normal = np.array([[fit[0]], [fit[1]], [-1]])
plane_normal = plane_normal / linalg.norm(plane_normal)
up_normal = np.array([[0],[0],[1]], dtype=np.float32)
plane = np.array([fit[0], fit[1], -1, fit[2]])
# https://math.stackexchange.com/a/897677/1012327
G = np.array([
[np.dot(plane_normal.T,up_normal)[0][0], -linalg.norm(np.cross(plane_normal.T[0],up_normal.T[0])), 0],
[linalg.norm(np.cross(plane_normal.T[0],up_normal.T[0])), np.dot(plane_normal.T,up_normal)[0][0], 0],
[0, 0, 1]
])
F = np.array([plane_normal.T[0], ((up_normal-np.dot(plane_normal.T,up_normal)[0][0]*plane_normal)/linalg.norm((up_normal-np.dot(plane_normal.T,up_normal)[0][0]*plane_normal))).T[0], np.cross(up_normal.T[0],plane_normal.T[0])]).T
R = F @ G @ linalg.inv(F)
R = R @ [[1,0,0],[0,-1,0],[0,0,1]] # i dont fucking know why
cameras.to_world_coords_matrix = np.array(np.vstack((np.c_[R, [0,0,0]], [[0,0,0,1]])))
socketio.emit("to-world-coords-matrix", {"to_world_coords_matrix": cameras.to_world_coords_matrix.tolist()})
@socketio.on("set-origin")
def set_origin(data):#设置坐标系原点
cameras = Cameras.instance()
object_point = np.array(data["objectPoint"])
to_world_coords_matrix = np.array(data["toWorldCoordsMatrix"])
transform_matrix = np.eye(4)
object_point[1], object_point[2] = object_point[2], object_point[1] # i dont fucking know why
transform_matrix[:3, 3] = -object_point
to_world_coords_matrix = transform_matrix @ to_world_coords_matrix
cameras.to_world_coords_matrix = to_world_coords_matrix
socketio.emit("to-world-coords-matrix", {"to_world_coords_matrix": cameras.to_world_coords_matrix.tolist()})
@socketio.on("update-camera-settings")
def change_camera_settings(data):
cameras = Cameras.instance()
cameras.edit_settings(data["exposure"], data["gain"])
@socketio.on("capture-points")
def capture_points(data):
start_or_stop = data["startOrStop"]
cameras = Cameras.instance()
if (start_or_stop == "start"):
cameras.start_capturing_points()
return
elif (start_or_stop == "stop"):
cameras.stop_capturing_points()
@socketio.on("calculate-camera-pose")
def calculate_camera_pose(data):#计算目标物体相对于相机坐标系的位置
cameras = Cameras.instance()
image_points = np.array(data["cameraPoints"])
image_points_t = image_points.transpose((1, 0, 2))
camera_poses = [{
"R": np.eye(3),
"t": np.array([[0],[0],[0]], dtype=np.float32)
}]
for camera_i in range(0, cameras.num_cameras-1):
camera1_image_points = image_points_t[camera_i]
camera2_image_points = image_points_t[camera_i+1]
not_none_indicies = np.where(np.all(camera1_image_points != None, axis=1) & np.all(camera2_image_points != None, axis=1))[0]
camera1_image_points = np.take(camera1_image_points, not_none_indicies, axis=0).astype(np.float32)
camera2_image_points = np.take(camera2_image_points, not_none_indicies, axis=0).astype(np.float32)
F, _ = cv.findFundamentalMat(camera1_image_points, camera2_image_points, cv.FM_RANSAC, 1, 0.99999)
E = cv.sfm.essentialFromFundamental(F, cameras.get_camera_params(0)["intrinsic_matrix"], cameras.get_camera_params(1)["intrinsic_matrix"])
possible_Rs, possible_ts = cv.sfm.motionFromEssential(E)
R = None
t = None
max_points_infront_of_camera = 0
for i in range(0, 4):
object_points = triangulate_points(np.hstack([np.expand_dims(camera1_image_points, axis=1), np.expand_dims(camera2_image_points, axis=1)]), np.concatenate([[camera_poses[-1]], [{"R": possible_Rs[i], "t": possible_ts[i]}]]))
object_points_camera_coordinate_frame = np.array([possible_Rs[i].T @ object_point for object_point in object_points])
points_infront_of_camera = np.sum(object_points[:,2] > 0) + np.sum(object_points_camera_coordinate_frame[:,2] > 0)
if points_infront_of_camera > max_points_infront_of_camera:
max_points_infront_of_camera = points_infront_of_camera
R = possible_Rs[i]
t = possible_ts[i]
R = R @ camera_poses[-1]["R"]
t = camera_poses[-1]["t"] + (camera_poses[-1]["R"] @ t)
camera_poses.append({
"R": R,
"t": t
})
camera_poses = bundle_adjustment(image_points, camera_poses, socketio)
object_points = triangulate_points(image_points, camera_poses)
error = np.mean(calculate_reprojection_errors(image_points, object_points, camera_poses))
socketio.emit("camera-pose", {"camera_poses": camera_pose_to_serializable(camera_poses)})
@socketio.on("locate-objects")
def start_or_stop_locating_objects(data):
cameras = Cameras.instance()
start_or_stop = data["startOrStop"]
if (start_or_stop == "start"):
cameras.start_locating_objects()
return
elif (start_or_stop == "stop"):
cameras.stop_locating_objects()
@socketio.on("determine-scale")
def determine_scale(data):
object_points = data["objectPoints"]
camera_poses = data["cameraPoses"]
actual_distance = 0.15
observed_distances = []
for object_points_i in object_points:
if len(object_points_i) != 2:
continue
object_points_i = np.array(object_points_i)
observed_distances.append(np.sqrt(np.sum((object_points_i[0] - object_points_i[1])**2)))
scale_factor = actual_distance/np.mean(observed_distances)
for i in range(0, len(camera_poses)):
camera_poses[i]["t"] = (np.array(camera_poses[i]["t"]) * scale_factor).tolist()
socketio.emit("camera-pose", {"error": None, "camera_poses": camera_poses})
@socketio.on("triangulate-points")
def live_mocap(data):#解算出目标物体的位置
cameras = Cameras.instance()
start_or_stop = data["startOrStop"]
camera_poses = data["cameraPoses"]
cameras.to_world_coords_matrix = data["toWorldCoordsMatrix"]
if (start_or_stop == "start"):
cameras.start_trangulating_points(camera_poses)
return
elif (start_or_stop == "stop"):
cameras.stop_trangulating_points()
if __name__ == '__main__':
socketio.run(app, port=3001, debug=True)
对于其中用到的OpenCV中的相关函数,Chatgpt的答复如下:
这些函数通常在计算机视觉和三维重建领域中使用,用于处理相机的投影、三角测量和姿态估计等任务。它们之间的关系如下:
1. **triangulate_points(三角测量)**:
- 这个函数用于从多个相机的投影点估计三维空间中的点的位置。
- 输入参数通常是一系列的相机投影点,以及相应的相机姿态信息(包括内外参数)。
- 输出结果是估计的三维点的位置。
2. **fundamentalFromProjections(从投影中计算基础矩阵)**:
- 这个函数用于根据相机的投影信息计算基础矩阵。
- 基础矩阵描述了两个相机之间的几何关系,可以用于计算视差、进行立体视觉匹配等任务。
- 输入参数是两个相机的投影信息。
- 输出结果是计算得到的基础矩阵。
3. **essentialFromFundamental(从基础矩阵计算本质矩阵)**:
- 这个函数用于根据基础矩阵计算本质矩阵。
- 本质矩阵描述了两个相机之间的运动关系,可以用于计算相机的相对姿态。
- 输入参数是基础矩阵和相机的内参数。
- 输出结果是计算得到的本质矩阵。
4. **motionFromEssential(从本质矩阵计算相机姿态)**:
- 这个函数用于根据本质矩阵计算相机的相对姿态。
- 输入参数是本质矩阵。
- 输出结果是计算得到的相机相对姿态。
这些函数通常在三维重建、立体视觉和多视图几何等领域中密切相关。在这些领域中,需要对相机的投影信息进行处理和分析,以从图像中恢复出三维场景的结构和相机的运动信息。这些函数提供了实现这些任务所需的数学工具和算法。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)