为了账号安全,请及时绑定邮箱和手机立即绑定

基于开源软件的无人机照片三维建模与地图展示教程

将你的无人机拍摄的照片转化为三维模型,然后将这些照片转换成的三维模型放到地图上显示

照片由 Jonathan Lampel 拍摄,来自 Unsplash.

基于专家建议,翻译如下:
基于上下文,如果这是一个非正式的笔记或非正式交流的开始,可以使用更轻松的语气,例如“先来个自我介绍吧”。如果文档风格较为简练,则可以仅使用“介绍”。
介绍

维基百科上说:

摄影测量是通过记录、测量和解释摄影图像和电磁辐射影像及其他现象的特征,来获得物理对象及其周围环境准确信息的科学及技术。

首先,我要说的是,我不是这个领域的专家,我只是一个试图用我的无人机做一些更有趣事情的开发者而已。说了这些之后,要用摄影测量法创建一个3D模型,需要大量相互重叠的照片。

这将是我们的最终结果。这里就是代码。请参见此处的结果https://joyful-daffodil-16661f.netlify.app/。代码可以从这里访问https://github.com/kauly/church-map

这段代码其实很简单,但要实现这个目标,我们还需要一些非常酷的库和工具。接下来就是这些步骤。

飞行计划安排

每台无人机都自带一个专有的手机应用,在其中包含智能飞行模式的选择。我有一台FIMI X8S2无人机,这里我将以它的应用为例,不过所有无人机的应用选项都大同小异。

首先,你需要选定你的目标。这里,就是我附近的一座老教堂。

那座旧教堂

这是一个很好的YouTube视频(https://www.youtube.com/watch?v=fE_VWv1Mvas&t=427s) ,讲解了飞行计划和捕获的过程。顺便提一句,视频是葡萄牙语的,那也是我的语言,但你可以选择开启任何语言的字幕。

要点是拍摄目标的不同角度的照片,这些照片需要有重叠的部分。你可以使用轨道模式(Orbit)或航点模式(Waypoints)来完成这个任务。航点模式是最常用的一种,但我选择了轨道模式,因为它更简单。在轨道模式下,需要将无人机置于目标中心,并设定一个半径。相机始终需要对准目标。

配置好飞行计划之后,进入相机选项并选择“间隔2秒”。另一个重要的配置项是飞行过程中的无人机速度,我选择了3米每秒。速度和间隔的结合会让照片产生重叠效果。虽然这些参数涉及一些复杂的计算,但这些默认设置已经足够好。

好的,让你的无人机起飞,拍张照片,并注意相机的角度。无人机应该能很好地看到目标。还有一件事,多拍点照片总是好的。我拍了241张。

使用OpenDroneMap (ODM) 处理图像

ODM 是一个出色的开源工具;它会帮你生成多种有用的输出

只需Docker就足够运行ODM了。ODM还有一份非常详细的文档,非常值得一读。我在这里提供GitHub链接。这上面你可以找到更详细的说明。

那么,把SD卡插入电脑,仔细看看这些图片。把那些不符合要求的图片删掉。需要在这个项目中创建两个文件夹。

    ├── 教堂/
    │   ├── 图片/

church 文件夹里,请运行以下命令:

docker run -ti --rm -v .:/datasets/code opendronemap/odm --project-path /datasets

这个命令会花费很长时间来完成。在我的情况下,它花了超过一个小时。你应该能在你的 church 文件夹里找到提到的相关文件。在 odm_report 文件夹里,有一个质量报告,里面不仅包含了大量的信息,还有数据的预览。

从下面这张图片中,你可以看到我的Orbit飞行模式(或Orbit模式)和拍摄位置的情况。

打开无人机地图的质量报告图片。

3D模型位于odm_texturing文件夹里。你可以用像Blender这样的软件来渲染它,但在下一节里,我们会看到CesiumIon生成的模型。

将数据上传到CesiumIon:上传数据到CesiumIon

一个开放的平台,用于以3D瓦片形式托管和提供地理空间数据。

我们将用CesiumIon来托管并提供模型。模型将以3D Tile的形式进行提供。你可以先去创建一个Cesium账户,对于开发者来说,这完全是免费的。将模型上传到CesiumIon的过程非常简单,具体步骤可以参考这个教程:教程

不要忘记更新 odm-texturing 文件夹中的所有文件。但我遇到了一个麻烦。Cesium 无法在地球模型上找到该位置,所以我得手动设置它。Cesium 还有关于这个的教程,可以参考这里

在这里,我从一架无人机拍的照片里找到了坐标。这些坐标就在图片的元数据里,你可以找找看。

写代码

好的,让我们终于开始写点代码吧。我们将使用各种库在地图上渲染模型,但最终的代码会很简单。这些库我们将会用到。

我也在用Tailwind,但这只是习惯问题。这个项目的CSS部分很简单。我现在用pnpm,但用npm或yarn也可以。我们先创建一个项目。

你可以启动一个新的 React 和 TypeScript 项目:

    pnpm create vite your-project-name - template react-ts

使用 pnpm 创建一个使用 React 和 TypeScript 的 Vite 项目:

    pnpm create vite your-project-name - template react-ts

去项目文件夹,你可以安装依赖包:

cd 项目文件夹
npm install

注意:已将原句调整为更口语化的表达,并在代码段前后添加了适当的指示语句。

请在你的项目名称下切换目录并运行以下命令来安装这些依赖包:

cd your-project-name  
pnpm add @deck.gl/core @deck.gl/layers @deck.gl/react @deck.gl/mesh-layers @deck.gl/geo-layers @deck.gl/mapbox @loaders.gl/3d-tiles react-map-gl maplibre-gl

现在,我们来创建一个文件夹放我们的组件。

切换到src目录,创建一个名为components的文件夹,然后在components文件夹内创建两个文件Loading.tsx和ChurchMap.tsx。

你可以删除 app.css 文件,然后在 App.tsx 文件中删掉所有样板代码。接着,进入 main.tsx 文件,删除 app.css 的导入,并添加 maplibre 的 CSS 文件导入。这样,这个 CSS 就能让基础地图正确显示了。

    import React from "react";  
    import ReactDOM from "react-dom/client";  
    import App from "./App.tsx";  
    import "./index.css";  
    import "maplibre-gl/dist/maplibre-gl.css";  

    // 创建根节点并挂载应用
    ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(  
      <React.StrictMode>  
        <App />  
      </React.StrictMode>  
    );

为了避免以后出错,我们先只渲染基础地图。这里我使用的是栅格底图;虽然矢量地图更好,但需要付费。react-map-gl 需要一个 Mapbox-style 配置,因此我们需要创建一个文件来保存它。

touch src/mapHelpers.tsx
# 触发 src/mapHelpers.tsx 文件的创建或更新

这是一个按照Mapbox样式定义的对象。我们可以指定地图来源并进行样式设计。

    // src/mapHelpers.tsx  
    import { MapboxStyle } from "react-map-gl";  

    export const mapStyle: MapboxStyle = {  
      version: 8,  
      源: {  
        osm: {  
          type: "raster",  
          tiles: ["https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"],  
          tileSize: 256,  
          引用: "&copy; OpenStreetMap 贡献者团队",  
          maxzoom: 19,  
        },  
      },  
      图层: [  
        {  
          id: "osm",  
          type: "raster",  
          source: "osm",  
        },  
      ],  
    };

到了,我们绘制一张地图。首先,我们只绘制基本的地图。

    // src/components/ChurchMap.tsx
    import Map, { NavigationControl, useControl, MapRef } from "react-map-gl";
    import maplibregl from "maplibre-gl";
    import { mapStyle } from "../mapHelpers";

    const INITIAL_VIEW_STATE = {
      longitude: -48.5495,
      latitude: -27.5969,
      zoom: 9,
    };

    export default function ChurchMap() {
      return (
        <Map
          mapLib={maplibregl}
          mapStyle={mapStyle}
          initialViewState={INITIAL_VIEW_STATE}
          style={{ width: "100vw", height: "100vh" }}
        >
          <NavigationControl />
        </Map>
      );
    }

有了工作底图,我们可以在其基础上添加 Deck.glDeck.gl 有良好的文档,教我们如何将其与其他库集成使用。在我们的情况下,这指的是 react-map-gl,但这里有个问题。他们提供的很多示例都使用了 react-map-gl 的旧版本。要正确地把这两个库集成起来,我们应该参考以下示例:

MapboxOverlay | deck.glMapboxOverlay 是 Mapbox GL JS 的 IControl 接口的实现。当添加 MapboxOverlay 到 mapbox…deck.gl 地图时

在我的示例中,你需要从你的CesiumIon账户获取asset-idaccess-token。然后创建一个.env.local文件来存储你的accessToken

    touch .env.local

运行此命令来创建或更新.env.local文件: touch .env.local

    # .env.local  
    VITE_CESIUM = yourAccessToken  # 你的访问令牌

Deck.gl 和 Cesium Ion 数据来更新 ChurchMap.tsx 组件。

    import { Tile3DLayer } from "@deck.gl/geo-layers/typed";  
    import { CesiumIonLoader } from "@loaders.gl/3d-tiles";  
    import { MapboxOverlay, MapboxOverlayProps } from "@deck.gl/mapbox/typed";  
    import Map, { NavigationControl, useControl, MapRef } from "react-map-gl";  
    import maplibregl from "maplibre-gl";  

    import { mapStyle } from "../mapHelpers";  
    import { useRef, useState } from "react";  

    // 用您的CESIUM数据替换以下内容  
    const CESIUM_CONFIG = {  
      assetId: 1691493,  
      tilesetUrl: "https://assets.ion.cesium.com/1691493/tileset.json",  
      token: import.meta.env.VITE_CESIUM,  
    };  

    const INITIAL_VIEW_STATE = {  
      longitude: -48.5495,  
      latitude: -27.5969,  
      zoom: 9,  
    };  

    function DeckGLOverlay(  
      props: MapboxOverlayProps & {  
        interleaved?: boolean;  
      }  
    ) {  
      const overlay = useControl<MapboxOverlay>(() => new MapboxOverlay(props));  
      overlay.setProps(props);  
      return null;  
    }  

    export default function ChurchMap() {  
      const mapRef = useRef<MapRef>(null);  

      const layer3D = new Tile3DLayer({  
        id: "layer-3d",  
        pointSize: 2,  
        data: CESIUM_CONFIG.tilesetUrl,  
        loader: CesiumIonLoader,  
        loadOptions: {  
          "cesium-ion": {  
            accessToken: CESIUM_CONFIG.token,  
          },  
        },  
        onTilesetLoad(tile) {  
          const { cartographicCenter } = tile;  
          if (cartographicCenter) {  
            mapRef.current?.flyTo({  
              center: [cartographicCenter[0], cartographicCenter[1]],  
              zoom: 19,  
              bearing: -80,  
              pitch: 80,  
            });  
          }  
        },  
      });  
      return (  
        <Map  
          mapLib={maplibregl}  
          mapStyle={mapStyle}  
          initialViewState={INITIAL_VIEW_STATE}  
          style={{ width: "100vw", height: "100vh" }}  
          ref={mapRef}  
        >  
          <DeckGLOverlay layers={[layer3D]} />  
          <NavigationControl />  
        </Map>  
      );  
    }

我那里的模型加载需要几秒钟,我加了个加载指示器。

最后的结果。

就是这样,大家。感谢大家的阅读。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消