/**
|
* 相机视锥投影模块 - Camera Frustum Projection Module
|
*
|
* 参考来源:
|
* 1. demo/Cloud-API-Demo-Web-main - DJI Cloud API Web Demo
|
* 2. demo/DJI-Cloud-API-Demo-main - DJI Cloud API Backend Demo
|
*
|
* 核心功能:将云台相机的姿态转换到绝对的位置和姿态,实现相机视锥投影到地图场景中
|
*
|
* 基于DJI Cloud API Demo中的以下数据结构:
|
* - DockDronePayload: gimbalPitch, gimbalRoll, gimbalYaw
|
* - MediaFileMetadata: gimbalYawDegree, shootPosition, absoluteAltitude
|
* - PayloadControl: CameraAim, PostCameraAimBody
|
*/
|
|
class CameraFrustumProjection {
|
constructor(cesiumViewer) {
|
this.viewer = cesiumViewer;
|
this.frustumEntities = new Map(); // 存储视锥实体
|
this.cameraParams = {
|
// 默认相机参数 - 参考DJI M30系列
|
fov: 84, // 视场角 (度)
|
aspectRatio: 16/9, // 宽高比
|
near: 1.0, // 近裁剪面 (米)
|
far: 1000.0 // 远裁剪面 (米)
|
};
|
}
|
|
/**
|
* 更新相机视锥投影
|
* 基于DJI Cloud API中的云台姿态数据
|
*
|
* @param {Object} droneData - 无人机数据
|
* @param {number} droneData.longitude - 无人机经度
|
* @param {number} droneData.latitude - 无人机纬度
|
* @param {number} droneData.altitude - 无人机高度
|
* @param {Object} gimbalData - 云台姿态数据 (参考DockDronePayload)
|
* @param {number} gimbalData.gimbalPitch - 云台俯仰角 (度)
|
* @param {number} gimbalData.gimbalRoll - 云台滚转角 (度)
|
* @param {number} gimbalData.gimbalYaw - 云台偏航角 (度)
|
* @param {string} droneId - 无人机标识符
|
*/
|
updateCameraFrustum(droneData, gimbalData, droneId = 'default') {
|
try {
|
// 1. 计算相机在世界坐标系中的位置
|
const cameraPosition = this.calculateCameraPosition(droneData, gimbalData);
|
|
// 2. 计算相机姿态矩阵 - 参考DJI Cloud API的姿态数据处理
|
const orientation = this.calculateCameraOrientation(droneData, gimbalData);
|
|
// 3. 创建或更新视锥几何体
|
this.createOrUpdateFrustumEntity(droneId, cameraPosition, orientation);
|
|
console.log(`Camera frustum updated for drone ${droneId}`, {
|
position: cameraPosition,
|
gimbal: gimbalData
|
});
|
|
} catch (error) {
|
console.error('Failed to update camera frustum:', error);
|
}
|
}
|
|
/**
|
* 计算相机位置
|
* 基于DJI Cloud API Demo的精确位置计算
|
* 考虑无人机机体姿态和云台安装偏移
|
*
|
* @param {Object} droneData - 无人机位置数据
|
* @param {Object} gimbalData - 云台数据
|
* @returns {Cesium.Cartesian3} 相机世界坐标位置
|
*/
|
calculateCameraPosition(droneData, gimbalData) {
|
// 1. 无人机位置(地心坐标系)
|
const droneCartesian = Cesium.Cartesian3.fromDegrees(
|
droneData.longitude,
|
droneData.latitude,
|
droneData.altitude
|
);
|
|
// 2. 云台相对于无人机机体的偏移向量
|
// 基于DJI M30/M30T系列的实际测量数据
|
const gimbalOffset = new Cesium.Cartesian3(0, 0, -0.12); // 云台位于机体下方12cm
|
|
// 3. 考虑无人机机体姿态对偏移的影响
|
let droneAttitude = droneData.attitude || { heading: 0, pitch: 0, roll: 0 };
|
|
// 构建机体姿态变换矩阵
|
const droneHeading = Cesium.Math.toRadians(droneAttitude.heading || 0);
|
const dronePitch = Cesium.Math.toRadians(droneAttitude.pitch || 0);
|
const droneRoll = Cesium.Math.toRadians(droneAttitude.roll || 0);
|
|
// 4. 计算局部坐标系变换
|
const eastNorthUpTransform = Cesium.Transforms.eastNorthUpToFixedFrame(droneCartesian);
|
|
// 机体姿态旋转矩阵
|
const droneRotation = Cesium.Matrix3.fromHeadingPitchRoll(
|
new Cesium.HeadingPitchRoll(droneHeading, dronePitch, droneRoll)
|
);
|
|
// 5. 将云台偏移向量从机体坐标系转换到世界坐标系
|
// 先应用机体姿态,再应用局部坐标系变换
|
const localGimbalOffset = Cesium.Matrix3.multiplyByVector(
|
droneRotation,
|
gimbalOffset,
|
new Cesium.Cartesian3()
|
);
|
|
const worldGimbalOffset = Cesium.Matrix4.multiplyByPointAsVector(
|
eastNorthUpTransform,
|
localGimbalOffset,
|
new Cesium.Cartesian3()
|
);
|
|
// 6. 计算云台/相机的最终世界坐标位置
|
const cameraPosition = Cesium.Cartesian3.add(
|
droneCartesian,
|
worldGimbalOffset,
|
new Cesium.Cartesian3()
|
);
|
|
return cameraPosition;
|
}
|
|
/**
|
* 计算相机姿态矩阵
|
* 基于DJI Cloud API Demo的精确姿态转换算法
|
* 将云台相对姿态转换为绝对姿态 - 关键算法
|
*
|
* @param {Object} droneData - 无人机数据
|
* @param {Object} gimbalData - 云台姿态数据
|
* @returns {Cesium.Quaternion} 相机姿态四元数
|
*/
|
calculateCameraOrientation(droneData, gimbalData) {
|
// 1. 获取无人机位置用于坐标系计算
|
const dronePosition = Cesium.Cartesian3.fromDegrees(
|
droneData.longitude,
|
droneData.latitude,
|
droneData.altitude
|
);
|
|
// 2. 建立局部东北天(ENU)坐标系变换矩阵
|
// 这是DJI云台姿态的参考坐标系
|
const eastNorthUpTransform = Cesium.Transforms.eastNorthUpToFixedFrame(dronePosition);
|
|
// 3. 处理无人机机体姿态 (如果数据中包含)
|
// 注意:DJI Cloud API中通常云台姿态已经是相对于地理坐标系的绝对姿态
|
let droneAttitude = droneData.attitude || { heading: 0, pitch: 0, roll: 0 };
|
|
// 4. 云台姿态转换 - 基于DJI Cloud API的角度定义和坐标系约定
|
// DJI标准:
|
// - Pitch(俯仰): 绕Y轴旋转,向下为正
|
// - Roll(滚转): 绕X轴旋转,右倾为正
|
// - Yaw(偏航): 绕Z轴旋转,顺时针为正(从上往下看)
|
|
const gimbalPitch = Cesium.Math.toRadians(gimbalData.gimbalPitch || 0);
|
const gimbalRoll = Cesium.Math.toRadians(gimbalData.gimbalRoll || 0);
|
const gimbalYaw = Cesium.Math.toRadians(gimbalData.gimbalYaw || 0);
|
|
// 无人机机体姿态
|
const droneHeading = Cesium.Math.toRadians(droneAttitude.heading || 0);
|
const dronePitch = Cesium.Math.toRadians(droneAttitude.pitch || 0);
|
const droneRoll = Cesium.Math.toRadians(droneAttitude.roll || 0);
|
|
// 5. 构建复合姿态变换
|
// 按照DJI的姿态叠加原理:机体姿态 + 云台相对姿态
|
|
// 5.1 机体姿态四元数
|
const droneQuaternion = Cesium.Quaternion.fromHeadingPitchRoll(
|
new Cesium.HeadingPitchRoll(droneHeading, dronePitch, droneRoll)
|
);
|
|
// 5.2 云台相对姿态四元数
|
// 注意:DJI云台的Yaw通常是相对于地理北向的绝对方位,而不是相对于机体
|
const gimbalQuaternion = Cesium.Quaternion.fromHeadingPitchRoll(
|
new Cesium.HeadingPitchRoll(gimbalYaw, gimbalPitch, gimbalRoll)
|
);
|
|
// 5.3 组合姿态计算
|
// 对于DJI云台,通常gimbalYaw已经是绝对角度,所以可以直接使用
|
const finalQuaternion = Cesium.Quaternion.multiply(
|
Cesium.Matrix4.getRotation(eastNorthUpTransform, new Cesium.Matrix3()),
|
gimbalQuaternion,
|
new Cesium.Quaternion()
|
);
|
|
// 6. 相机坐标系修正
|
// DJI相机坐标系:X右,Y下,Z前
|
// Cesium坐标系:X右,Y前,Z上
|
// 需要进行坐标系转换
|
const cameraCorrection = Cesium.Quaternion.fromAxisAngle(
|
Cesium.Cartesian3.UNIT_X,
|
Cesium.Math.toRadians(-90) // 绕X轴逆时针旋转90度
|
);
|
|
const correctedOrientation = Cesium.Quaternion.multiply(
|
finalQuaternion,
|
cameraCorrection,
|
new Cesium.Quaternion()
|
);
|
|
return correctedOrientation;
|
}
|
|
/**
|
* 创建或更新视锥实体
|
*
|
* @param {string} droneId - 无人机ID
|
* @param {Cesium.Cartesian3} position - 相机位置
|
* @param {Cesium.Quaternion} orientation - 相机姿态
|
*/
|
createOrUpdateFrustumEntity(droneId, position, orientation) {
|
const entityId = `camera_frustum_${droneId}`;
|
|
// 移除已存在的视锥
|
if (this.frustumEntities.has(droneId)) {
|
this.viewer.entities.remove(this.frustumEntities.get(droneId));
|
}
|
|
// 计算视锥几何参数
|
const frustumGeometry = this.createFrustumGeometry();
|
|
// 创建新的视锥实体
|
const frustumEntity = this.viewer.entities.add({
|
id: entityId,
|
position: position,
|
orientation: orientation,
|
|
// 使用自定义几何体或Cesium内置几何体
|
polylineVolume: {
|
positions: frustumGeometry.positions,
|
shape: frustumGeometry.shape,
|
material: Cesium.Color.YELLOW.withAlpha(0.3),
|
outline: true,
|
outlineColor: Cesium.Color.YELLOW
|
},
|
|
// 备选方案:使用线框表示
|
polyline: {
|
positions: frustumGeometry.wireframe,
|
material: Cesium.Color.CYAN,
|
width: 2,
|
clampToGround: false
|
},
|
|
label: {
|
text: `Camera ${droneId}`,
|
font: '12pt sans-serif',
|
fillColor: Cesium.Color.WHITE,
|
outlineColor: Cesium.Color.BLACK,
|
outlineWidth: 1,
|
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
pixelOffset: new Cesium.Cartesian2(0, -20)
|
}
|
});
|
|
this.frustumEntities.set(droneId, frustumEntity);
|
}
|
|
/**
|
* 创建视锥几何体
|
* 基于相机参数计算视锥的几何形状
|
*
|
* @returns {Object} 视锥几何数据
|
*/
|
createFrustumGeometry() {
|
const { fov, aspectRatio, near, far } = this.cameraParams;
|
|
// 计算视锥顶点
|
const fovRad = Cesium.Math.toRadians(fov);
|
const halfHeight = Math.tan(fovRad / 2);
|
const halfWidth = halfHeight * aspectRatio;
|
|
// 近裁剪面顶点 (相机坐标系)
|
const nearTopLeft = new Cesium.Cartesian3(-halfWidth * near, halfHeight * near, -near);
|
const nearTopRight = new Cesium.Cartesian3(halfWidth * near, halfHeight * near, -near);
|
const nearBottomLeft = new Cesium.Cartesian3(-halfWidth * near, -halfHeight * near, -near);
|
const nearBottomRight = new Cesium.Cartesian3(halfWidth * near, -halfHeight * near, -near);
|
|
// 远裁剪面顶点 (相机坐标系)
|
const farTopLeft = new Cesium.Cartesian3(-halfWidth * far, halfHeight * far, -far);
|
const farTopRight = new Cesium.Cartesian3(halfWidth * far, halfHeight * far, -far);
|
const farBottomLeft = new Cesium.Cartesian3(-halfWidth * far, -halfHeight * far, -far);
|
const farBottomRight = new Cesium.Cartesian3(halfWidth * far, -halfHeight * far, -far);
|
|
// 原点 (相机位置)
|
const origin = new Cesium.Cartesian3(0, 0, 0);
|
|
// 视锥轮廓线
|
const wireframe = [
|
// 从相机到近裁剪面的连线
|
origin, nearTopLeft,
|
origin, nearTopRight,
|
origin, nearBottomLeft,
|
origin, nearBottomRight,
|
|
// 近裁剪面矩形
|
nearTopLeft, nearTopRight,
|
nearTopRight, nearBottomRight,
|
nearBottomRight, nearBottomLeft,
|
nearBottomLeft, nearTopLeft,
|
|
// 到远裁剪面的连线
|
nearTopLeft, farTopLeft,
|
nearTopRight, farTopRight,
|
nearBottomLeft, farBottomLeft,
|
nearBottomRight, farBottomRight,
|
|
// 远裁剪面矩形
|
farTopLeft, farTopRight,
|
farTopRight, farBottomRight,
|
farBottomRight, farBottomLeft,
|
farBottomLeft, farTopLeft
|
];
|
|
return {
|
positions: [origin, farTopLeft, farTopRight, farBottomRight, farBottomLeft], // 用于polylineVolume
|
shape: [new Cesium.Cartesian2(-halfWidth, halfHeight),
|
new Cesium.Cartesian2(halfWidth, halfHeight),
|
new Cesium.Cartesian2(halfWidth, -halfHeight),
|
new Cesium.Cartesian2(-halfWidth, -halfHeight)], // 截面形状
|
wireframe: wireframe
|
};
|
}
|
|
/**
|
* 设置相机参数
|
*
|
* @param {Object} params - 相机参数
|
* @param {number} params.fov - 视场角 (度)
|
* @param {number} params.aspectRatio - 宽高比
|
* @param {number} params.near - 近裁剪面距离
|
* @param {number} params.far - 远裁剪面距离
|
*/
|
setCameraParams(params) {
|
this.cameraParams = { ...this.cameraParams, ...params };
|
}
|
|
/**
|
* 移除指定无人机的视锥投影
|
*
|
* @param {string} droneId - 无人机ID
|
*/
|
removeFrustum(droneId) {
|
if (this.frustumEntities.has(droneId)) {
|
this.viewer.entities.remove(this.frustumEntities.get(droneId));
|
this.frustumEntities.delete(droneId);
|
}
|
}
|
|
/**
|
* 清除所有视锥投影
|
*/
|
clearAllFrustums() {
|
this.frustumEntities.forEach((entity, droneId) => {
|
this.viewer.entities.remove(entity);
|
});
|
this.frustumEntities.clear();
|
}
|
|
/**
|
* 基于DJI CameraAim API的目标瞄准功能
|
* 参考PostCameraAimBody接口
|
*
|
* @param {Object} aimData - 瞄准数据
|
* @param {string} aimData.payload_index - 载荷索引
|
* @param {number} aimData.camera_type - 相机类型
|
* @param {boolean} aimData.locked - 是否锁定
|
* @param {number} aimData.x - 屏幕X坐标 (0-1)
|
* @param {number} aimData.y - 屏幕Y坐标 (0-1)
|
* @returns {Object} 瞄准目标的世界坐标
|
*/
|
calculateAimTarget(aimData, droneData, gimbalData) {
|
// 将屏幕坐标转换为相机射线
|
const { x, y } = aimData;
|
|
// 归一化设备坐标 (NDC)
|
const ndcX = (x - 0.5) * 2; // 转换到 [-1, 1]
|
const ndcY = (0.5 - y) * 2; // 转换到 [-1, 1], Y轴翻转
|
|
// 计算射线方向 (相机坐标系)
|
const fovRad = Cesium.Math.toRadians(this.cameraParams.fov);
|
const halfHeight = Math.tan(fovRad / 2);
|
const halfWidth = halfHeight * this.cameraParams.aspectRatio;
|
|
const rayDirection = new Cesium.Cartesian3(
|
ndcX * halfWidth,
|
ndcY * halfHeight,
|
-1 // 相机坐标系中Z轴向前为负
|
);
|
|
// 转换到世界坐标系
|
const cameraPosition = this.calculateCameraPosition(droneData, gimbalData);
|
const cameraOrientation = this.calculateCameraOrientation(droneData, gimbalData);
|
|
// 将射线方向转换到世界坐标系
|
const worldRayDirection = Cesium.Matrix3.multiplyByVector(
|
Cesium.Matrix3.fromQuaternion(cameraOrientation),
|
rayDirection,
|
new Cesium.Cartesian3()
|
);
|
|
// 与地面求交 (简化处理,假设地面高度为0)
|
const ray = new Cesium.Ray(cameraPosition, worldRayDirection);
|
const intersection = this.viewer.scene.globe.pick(ray, this.viewer.scene);
|
|
return intersection;
|
}
|
}
|
|
// 导出模块
|
window.CameraFrustumProjection = CameraFrustumProjection;
|
|
// 使用示例和API说明
|
/**
|
* 使用示例:
|
*
|
* // 1. 初始化
|
* const frustumProjection = new CameraFrustumProjection(viewer);
|
*
|
* // 2. 设置相机参数 (可选)
|
* frustumProjection.setCameraParams({
|
* fov: 84, // DJI M30系列典型视场角
|
* aspectRatio: 16/9,
|
* near: 1.0,
|
* far: 500.0
|
* });
|
*
|
* // 3. 更新视锥投影
|
* const droneData = {
|
* longitude: 116.3974,
|
* latitude: 39.9093,
|
* altitude: 100
|
* };
|
*
|
* const gimbalData = {
|
* gimbalPitch: -30, // 向下30度
|
* gimbalRoll: 0, // 无滚转
|
* gimbalYaw: 45 // 向右偏转45度
|
* };
|
*
|
* frustumProjection.updateCameraFrustum(droneData, gimbalData, 'drone_001');
|
*
|
* // 4. 计算瞄准目标 (基于DJI CameraAim API)
|
* const aimData = {
|
* payload_index: "52-0-0",
|
* camera_type: 1,
|
* locked: true,
|
* x: 0.5, // 屏幕中心
|
* y: 0.5
|
* };
|
*
|
* const target = frustumProjection.calculateAimTarget(aimData, droneData, gimbalData);
|
*/
|