照片由 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) 处理图像只需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 还有关于这个的教程,可以参考这里。
在这里,我从一架无人机拍的照片里找到了坐标。这些坐标就在图片的元数据里,你可以找找看。
写代码好的,让我们终于开始写点代码吧。我们将使用各种库在地图上渲染模型,但最终的代码会很简单。这些库我们将会用到。
- Vite — 启动项目
- react-map-gl — 用 React 渲染基础地图
- maplibre — react-map-gl 使用的,是 mapbox-gl 的一个替代品
- deck.gl — 渲染 3D 模型
- loaders.gl — 加载 3D 模型,
我也在用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,
引用: "© 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.gl
。Deck.gl
有良好的文档,教我们如何将其与其他库集成使用。在我们的情况下,这指的是 react-map-gl,但这里有个问题。他们提供的很多示例都使用了 react-map-gl 的旧版本。要正确地把这两个库集成起来,我们应该参考以下示例:
在我的示例中,你需要从你的CesiumIon账户获取asset-id
和access-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>
);
}
我那里的模型加载需要几秒钟,我加了个加载指示器。
最后的结果。
就是这样,大家。感谢大家的阅读。