Ceisum无人机巡检直播视频投射

news2025/7/18 1:58:31

接上次的视频投影,Leader告诉我这个视频投影要用在两个地方,一个是我原先写的轨迹回放那里,另一个在无人机起飞后的地图回显,要实时播放无人机拍摄的视频,还要能转镜头,让我把这个也接一下。
我的天!告诉我的时候人都傻了,这是一个功能嘛?
一个是拿到了全部的轨迹数据进行回显,播放的视频也是完整的资源,视频要求投射在地面上。另一个是接收实时的轨迹数据进行回显,播放的是实时的直播,视频居然还要求跟着镜头一起转。
这是两个完完全全不一样的功能好吧!!!
我拿着标书仔仔细细看过那行“演示无人机画面投影到地图上”,一时间陷入了沉默。
无语归无语,但特么还是要写,跟Leader据理力争这不是改改就能换上的功能,然后争取来两周的研发时间。
两周时间看起来很长,其实也就10天,有时候我写个计算方法都要两天,还不一定是最终版,所以只能拜托接下来运气很好,别让我遇到太多阻碍。
开工吧!

Step 0:
思路:最初都是想要拿原来的方法改改看,哪怕不成功,也绝了这个念想。
首先要解决的是视频投放的问题,原本这个投影方法只能投放视频,且只能投影到地面,我要优化成能投影直播并且不接触地面也能投放。
在这里插入图片描述
为了解决直播投放问题,我尝试修改了源码,发现困难重重,主要是两点,一个是原方法不依赖dom元素,这意味着我不能暴力篡改成直播通道,另一个是封装层级过多,我想在某几个关键步骤看下效果都不能被满足,只能盲写看最终效果。
卡在第一步我是万万没有想到,经过一下午的尝试,最终让我放弃了继续下去的想法。

Step0.1:当然,我也没有急着完全放弃之前的代码,我还记得我czml的无人机轨迹方法已经很完善了,从静态路径加载改为动态路径回显似乎并不难,如果成功的话只需要给视频材质连四根线,接着解决朝向转动的问题就完成了,大大减轻了工作量。

在这里插入图片描述
我将原有轨迹数据做成坐标发射器,通过动态接收点位组成路径,每次接收到数据就更新czml加载的点位集合,成功改成了动态路径回显。
然后我尝试将我之前做好的视椎体视频替换进去,然后切换朝向,发现在dataSource.then中格外难操作矩阵,需要多考虑很多问题,于是暂时搁置该方法,等待重启机会。

旧路已死,新步骤开始从0开始一点点搭建功能。

Step 1:
思路: 选择实体结合时间轴,更为灵活地构建动画,同时代码将更繁琐。
首先写一段简单动画,从一个点到另一个点。
在这里插入图片描述

部分代码:

var startPosition = Cesium.Cartesian3.fromDegrees(-75.0, 40.0, 1000.0);
var endPosition = Cesium.Cartesian3.fromDegrees(-75.0, 42.0, 1000.0);

// 设置时间范围
var startTime = Cesium.JulianDate.now();
var endTime = Cesium.JulianDate.addSeconds(
  startTime,
  15,
  new Cesium.JulianDate()
);

// 创建一个 SampledPositionProperty 来定义路径
var positionProperty = new Cesium.SampledPositionProperty();

// 添加时间和位置样本
positionProperty.addSample(startTime, startPosition);
positionProperty.addSample(endTime, endPosition);

// 创建一个 entity 并设置其 position 为定义的路径
var entity = viewer.entities.add({
  position: positionProperty,
  point: {
    pixelSize: 10,
    color: Cesium.Color.RED,
  },
});

// 使视图跟踪 entity
viewer.trackedEntity = entity;

// 设置时钟范围
viewer.clock.startTime = startTime.clone();
viewer.clock.stopTime = endTime.clone();
viewer.clock.currentTime = startTime.clone();
viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP; // 当到达结束时间时停止
viewer.clock.multiplier = 1; // 时间加速倍率
viewer.clock.clockRange = Cesium.ClockRange.CLAMPED;
viewer.clock.shouldAnimate = true;

Step 2:
思路:两个点变多个点,写一个点位生成器,模拟实时接收数据,实现多点位连续飞行
在这里插入图片描述

Step 3:
思路:模拟点位飞行,点换成模型,视椎体跟随模型一起,并封装成class,效果很好,但视角连贯性有待提高
在这里插入图片描述
部分代码:

class PointMover {
  constructor(viewer) {
    this.viewer = viewer;
    this.pointQueue = [];
    this.isAnimating = false;
    this.videoEntity = null;

    // 添加模型
    this.entity = ...

    // 添加视椎体
    this.frustumPrimitive = viewer.scene.primitives.add(
      new Cesium.Primitive({
        geometryInstances: new Cesium.GeometryInstance({
          geometry: geo,
          attributes: {
            color: Cesium.ColorGeometryInstanceAttribute.fromColor(
              Cesium.Color.RED.withAlpha(0.5)
            ),
          },
        }),
        appearance: new Cesium.PerInstanceColorAppearance({
          translucent: true,
          flat: true,
        }),
        asynchronous: false,
      })
    );
    // 初始化视频材质
    this.videoEntity = viewer.entities.add({
      name: "uav-tmp-fly-wxsimple",
      polygon: {
        hierarchy: new Cesium.CallbackProperty((time) => {
          // 添加安全检查
          if (!this.entity || !this.entity.position) {
            return null;
          }

          try {
            const position = this.entity.position.getValue(time);
            if (!Cesium.defined(position)) {
              return null;
            }

            // 计算视锥体的远截面四个角点

            // 返回新的多边形层次结构
            return new Cesium.PolygonHierarchy([
              upRightPt,
              upLeftPt,
              downLeftPt,
              downRightPt,
            ]);
          } catch (error) {
            console.warn("Error calculating polygon positions:", error);
            return null;
          }
        }, false),
        perPositionHeight: true,
        material: videoElement,
        // 添加其他属性以提高渲染性能
        shadows: Cesium.ShadowMode.DISABLED,
        classificationType: Cesium.ClassificationType.BOTH,
        zIndex: 999,
      },
    });
    // 内部方法:启动动画
    startAnimation() {
      if (this.pointQueue.length > 2 && !this.isAnimating) {
        // 在 onTick 事件处理函数中更新视锥体位置
        const onTick = (clock) => {
          // 移除旧的视锥体
          // 添加新的视锥体

          // ...
        }
        // 添加 onTick 事件监听器
        this.viewer.clock.onTick.addEventListener(onTick);
      }
    }
    // 外部调用方法:更新点位并启动动画
    updatePositionsAndAnimate(newPosition) {
      this.pointQueue.push(newPosition);
      if (!this.isAnimating) {
        this.startAnimation();
      }
    }
  }
}
          

Step 4:
思路:加入虚线路径
在这里插入图片描述
部分代码:

class PointMover {
  constructor(viewer) {
    this.viewer = viewer;
    this.pointQueue = [];
    this.isAnimating = false;
    this.videoEntity = null;
    this.pathPoints = []; // 用于记录路径点
    this.pathPolyline = null; // 用于绘制路径

    // ...
  }
  // 绘制路径的方法
  createPathPolyline() {
    this.pathPolyline = this.viewer.entities.add({
      polyline: {
        positions: new Cesium.CallbackProperty((time) => {
          // 取当前记录的路径点
          return this.pathPoints;
        }, false),
        material: new Cesium.PolylineDashMaterialProperty({
          dashLength: 16.0, // 虚线的长度
        }),
        width: 3,
      },
    });
  }

  // 更新路径并添加新的位置
  updatePath(newPosition) {
    this.pathPoints.push(newPosition); // 将新位置加入到路径点

    // 更新虚线的路径
    if (this.pathPolyline) {
      this.pathPolyline.polyline.positions = new Cesium.CallbackProperty(
        (time) => {
          return this.pathPoints;
        },
        false
      );
    }
  }
  // 内部方法:启动动画
  startAnimation() {
    if (this.pointQueue.length > 2 && !this.isAnimating) {
      // 在 onTick 事件处理函数中更新视锥体位置
      const onTick = (clock) => {
       // 将当前位置添加到路径数组
       this.updatePath(currentPosition);

       //...
      }
    }
  }
}

Step 4延伸:
思路:在Step 4基础上尝试扩展,显隐,销毁 show的配合
在这里插入图片描述
部分代码:

class PointMover {
  constructor(viewer, show) {
    this.viewer = viewer;
    this.pointQueue = [];
    this.isAnimating = false;
    this.videoEntity = null;
    this.pathPoints = [];
    this.pathPolyline = null; 
    this.isFrustumShow = show; //用于显隐控制

    // ...

    if (this.frustumPrimitive) {
      if (!this.isFrustumShow) {
        this.frustumPrimitive.show = false;
      } else {
        this.frustumPrimitive.show = true;
      }
    }
    if (this.videoEntity) {
      if (!this.isFrustumShow) {
        this.videoEntity.show = false;
      } else {
        this.videoEntity.show = true;
      }
    }
    
  }
  // 内部方法:启动动画
  startAnimation() {
    if (this.pointQueue.length > 2 && !this.isAnimating) {
      // 在 onTick 事件处理函数中更新视锥体位置
      const onTick = (clock) => {
       // 将当前位置添加到路径数组

       //...
       this.frustumPrimitive = this.viewer.scene.primitives.add(
          new Cesium.Primitive({
            geometryInstances: new Cesium.GeometryInstance({
              geometry: newGeometry,
              attributes: {
                color: Cesium.ColorGeometryInstanceAttribute.fromColor(
                  Cesium.Color.RED.withAlpha(0.5)
                ),
              },
            }),
            appearance: new Cesium.PerInstanceColorAppearance({
              translucent: true,
              flat: true,
            }),
            asynchronous: false,
          })
        );
        if (this.frustumPrimitive) {
          if (!this.isFrustumShow) {
            this.frustumPrimitive.show = false;
          } else {
            this.frustumPrimitive.show = true;
          }
        }
        if (this.videoEntity) {
          if (!this.isFrustumShow) {
            this.videoEntity.show = false;
          } else {
            this.videoEntity.show = true;
          }
        }
      }
    }
  }
  // ...
  // 外部调用方法,更新显隐开关
  updateVisibleAndHidden(val) {
    this.isFrustumShow = val;
  }
  dispose() {
    if (this.frustumPrimitive) {
      viewer.scene.primitives.remove(this.frustumPrimitive);
      this.frustumPrimitive = null;
    }
    if (this.videoEntity) {
      viewer.entities.remove(this.videoEntity);
      this.videoEntity = null;
    }
    if (this.entity) {
      viewer.entities.remove(this.entity);
      this.entity = null;
    }
    if (this.pathPolyline) {
      viewer.entities.remove(this.pathPolyline);
      this.pathPolyline = null;
    }
  }
}
if (this.pointMover) {
  clearInterval(this.intervalId);
  this.intervalId = null;
  this.pointMover.dispose();
  this.pointMover = null;
}
this.pointMover = new PointMover(viewer, this.isFrustumShow);

Step 5:
思路:尝试改变视椎体朝向
在这里插入图片描述
部分代码:

class PointMover {
  constructor(viewer) {
    // ...
    this.currentHeading = 0;
    this.currentPitch = 0;
    this.currentRoll = 0;

    // ...
  }
 
  // 内部方法:启动动画
  startAnimation() {
    if (this.pointQueue.length > 2 && !this.isAnimating) {
      // 在 onTick 事件处理函数中更新视锥体位置
      const onTick = (clock) => {
        const heading = Cesium.Math.toRadians(this.currentHeading); // 偏航角
        const pitch = Cesium.Math.toRadians(this.currentPitch); // 俯仰角
        const roll = Cesium.Math.toRadians(this.currentRoll); // 翻滚角

        // 创建一个HeadingPitchRoll对象
        const headingPitchRoll = new Cesium.HeadingPitchRoll(
          heading,
          pitch,
          roll
        );

        // 创建一个旋转矩阵
        const rotationMatrix =
          Cesium.Transforms.headingPitchRollToFixedFrame(
            currentPosition,
            headingPitchRoll,
            Cesium.Ellipsoid.WGS84
          );

        // 从旋转矩阵计算四元数
        const orientation =
          Cesium.Quaternion.fromRotationMatrix(rotationMatrix);

        //...
      }
    }
  }
  // ...
  updateHeadingPitchRoll(heading, patch, roll) {
    this.currentHeading = heading;
    this.currentPitch = patch;
    this.currentRoll = roll;
  }
}

Step 5延伸:
思路:在Step 5基础上尝试,同样矩阵转动视频材质
在这里插入图片描述
发现视频材质无法贴合视椎体,方案终止

Step 6:
思路:换用另一种视椎体构建方法,采用相机视角重置视椎体视角的方法,消除地理位置的影响
在这里插入图片描述
部分代码:

class PointMover {
  constructor(viewer) {
    // ...
    this.videoEntity = viewer.entities.add({
      name: "uav-tmp-fly-wxsimple",
      polygon: {
        hierarchy: new Cesium.CallbackProperty((time) => {
          if (!this.entity || !this.entity.position) {
            return null;
          }

          try {
            const position = this.entity.position.getValue(time);
            if (!Cesium.defined(position)) {
              return null;
            }

            let frustum = this.camera.frustum;
            let Cartesian3 = Cesium.Cartesian3;
            let camera = this.camera;

            // ...

            return new Cesium.PolygonHierarchy([
              upRightPt,
              upLeftPt,
              downLeftPt,
              downRightPt,
            ]);
          } catch (error) {
            console.warn("Error calculating polygon positions:", error);
            return null;
          }
        }, false),
        perPositionHeight: true,
        material: new Cesium.ImageMaterialProperty({
          image: videoElement, // 这里传入视频元素
          transparent: true, // 设置透明
          repeat: new Cesium.Cartesian2(1.0, 1.0), // 控制重复
        }),
        shadows: Cesium.ShadowMode.DISABLED,
        classificationType: Cesium.ClassificationType.BOTH,
        zIndex: 999,
      },
    });
  }
 
  // 内部方法:启动动画
  startAnimation() {
    if (this.pointQueue.length > 2 && !this.isAnimating) {
      // 在 onTick 事件处理函数中更新视锥体位置
      const onTick = (clock) => {
        var scene = this.viewer.scene;  
        this.camera = new Cesium.Camera(scene)

        //...
      }
    }
  }
  // ...
}

最终
接入实际项目,无人机航拍镜头实时同步反显,完结撒花°˖✧◝(⁰▿⁰)◜✧˖°
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2281328.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一句话,我让 AI 帮我做了个 P 图网站!

每到过节,不少小伙伴都会给自己的头像 P 个图,加点儿装饰。 比如圣诞节给自己头上 P 个圣诞帽,国庆节 P 个小红旗等等。这是一类比较简单、需求量却很大的 P 图场景,也有很多现成的网站和小程序,能帮你快速完成这件事…

【深度学习】 自动微分

自动微分 正如上节所说,求导是几乎所有深度学习优化算法的关键步骤。 虽然求导的计算很简单,只需要一些基本的微积分。 但对于复杂的模型,手工进行更新是一件很痛苦的事情(而且经常容易出错)。 深度学习框架通过自动…

Go语言中的值类型和引用类型特点

一、值类型 值类型的数据直接包含值,当它们被赋值给一个新的变量或者作为参数传递给函数时,实际上是创建了原值的一个副本。这意味着对新变量的修改不会影响原始变量的值。 Go中的值类型包括: 基础类型:int,float64…

Vue2 项目二次封装Axios

引言 在现代前端开发中,HTTP请求管理是构建健壮应用的核心能力之一。Axios作为目前最流行的HTTP客户端库,其灵活性和可扩展性为开发者提供了强大的基础能力。 1. 为什么要二次封装Axios? 1.1 统一项目管理需求 API路径标准化:…

使用Layout三行布局(SemiDesign)

tips:Semi Design网址 :Semi Design 1、安装Semi # 使用 npm npm i douyinfe/semi-ui# 使用 yarn yarn add douyinfe/semi-ui# 使用 pnpm pnpm add douyinfe/semi-ui 2、引入Layout布局 import { Layout } from douyinfe/semi-ui;3、开始实现三行布局…

css命名规范——BEM

目录 引言 BEM是什么? 块Block 元素Element 修饰语Modifier BEM解决了哪些问题? 在流行框架的组件中使用 BEM 格式 实战 认识设计图 如何使用当前的css规范正确命名? 引言 css样式类命名难、太难了,难于上青天,这个和js变量命名还不一样。看看项目中五花八门的样…

mysql 学习2 MYSQL数据模型,mysql内部可以创建多个数据库,一个数据库中有多个表;表是真正放数据的地方,关系型数据库 。

在第一章中安装 ,启动mysql80 服务后,连接上了mysql,那么就要 使用 SQL语句来 操作mysql数据库了。那么在学习 SQL语言操作 mysql 数据库 之前,要对于 mysql数据模型有一个了解。 MYSQL数据模型 在下图中 客户端 将 SQL语言&…

(十四)WebGL纹理坐标初识

纹理坐标是 WebGL 中将 2D 图像(纹理)应用到 3D 物体表面的重要概念。在 WebGL 中,纹理坐标通常使用一个二维坐标系,称为 uv 坐标,它们决定了纹理图像如何映射到几何体上。理解纹理坐标的核心就是明白它们如何将二维纹…

数据统计–图形报表(day11)

Apache ECharts 介绍 Apache ECharts 介绍 Apache ECharts 是一款基于 Javascript 的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表。 官网地址:Apache ECharts 入门案例 Apache Echarts官方…

小游戏源码开发搭建技术栈和服务器配置流程

近些年各种场景小游戏开发搭建版本层出不穷,山东布谷科技拥有多年海内外小游戏源码开发经验,现为从事小游戏源码开发或游戏运营的朋友们详细介绍小游戏开发及服务器配置流程。 一、可以对接到app的小游戏是如何开发的 1、小游戏源码开发的需求分析: 明…

Android Studio安装配置

一、注意事项 想做安卓app和开发板通信,踩了大坑,Android 开发不是下载了就能直接开发的,对于新手需要注意的如下: 1、Android Studio版本,根据自己的Android Studio版本对应决定了你所兼容的AGP(Android…

三分钟简单了解一些HTML的标签和语法_02

1.a标签演示 点击然后跳转 代码加入title 2.图片链接 3.锚点链接 点击就会跳转的当前位置 4.a标签小知识补充 该实例会跳转到顶,锚点链接则会跳转到相应的锚点 5. 结果:直接跳转到该页面的锚点处 6. 在 HTML 中&#xff0c;<tr>标签表示表格中的行&#xff08;TableRow&…

【CES2025】超越界限:ThinkAR推出8小时满电可用的超轻AR眼镜AiLens

在2025年国际消费类电子产品展览会(CES 2025)上,日本AR技术开发商ThinkAR携手超低功耗半导体和边缘AI解决方案提供商Ambiq,共同推出了名为AiLens的最新AR眼镜产品。这款设备不仅具备轻便的设计,而且拥有长达8小时的连续使用时间,为用户带来了前所未有的便捷体验。 AiLen…

Vue入门(Vue基本语法、axios、组件、事件分发)

Vue入门 Vue概述 Vue (读音/vju/&#xff0c;类似于view)是一套用于构建用户界面的渐进式框架&#xff0c;发布于2014年2月。与其它大型框架不同的是&#xff0c;Vue被设计为可以自底向上逐层应用。Vue的核心库只关注视图层&#xff0c;不仅易于上手&#xff0c;还便于与第三…

vue项目的创建

运行第一个vue-cli应用程序 创建一个基于webpack模板的vue应用程序 vue init webpack 项目名根据自己需求选择 创建好之后如下 运行 cd vue01npm run dev运行之后如下 复制访问地址 &#xff1a; http://localhost:8080 停止服务 两次ctrlC 或者 一次ctrlc然后y idea中使用…

【深度学习】神经网络实战分类与回归任务

第一步 读取数据 ①导入torch import torch ②使用魔法命令&#xff0c;使它使得生成的图形直接嵌入到 Notebook 的单元格输出中&#xff0c;而不是弹出新的窗口来显示图形 %matplotlib inline③读取文件 from pathlib import Path import requestsDATA_PATHPath("dat…

翻译:How do I reset my FPGA?

文章目录 背景翻译&#xff1a;How do I reset my FPGA?1、Understanding the flip-flop reset behavior2、Reset methodology3、Use appropriate resets to maximize utilization4、Many options5、About the author 背景 在写博客《复位信号的同步与释放&#xff08;同步复…

Linux调试器-gdb的使用简介

1、背景 程序的发布方式有两种&#xff0c;debug模式(给程序员用的)和release模式(给用户用的)Linux gcc/g出来的二进制程序&#xff0c;默认是release模式要使用gdb调试&#xff0c;必须在源代码生成二进制程序的时候&#xff0c;加上 -g 选项 注&#xff1a;debug模式产生的…

通过 Visual Studio Code 启动 IPython

在Visual Studio Code 中&#xff0c;你可以使用内置的终端来启动 ipython&#xff0c;当然首先要安装好ipython。 安装ipython的方法是在cmd里面输入以下命令安装&#xff1a; pip install ipython 启动ipython的步骤如下&#xff1a; 打开 VSCode 终端&#xff1a; 在 VSCo…

019:什么是 Resnet50 神经网络

本文为合集收录&#xff0c;欢迎查看合集/专栏链接进行全部合集的系统学习。 合集完整版请查看这里。 在上一节中&#xff0c;使用了一个简单的神经网络进行识别数字。 这个网络结构非常简单&#xff0c;一是因为层数少&#xff0c;二是因为结构是顺序的&#xff0c;没有其他…