> 技术文档 > Cesium 中结合 OpenCV.js 对影像图层进行分割,并将结果转为 GeoJSON 加载到地图

Cesium 中结合 OpenCV.js 对影像图层进行分割,并将结果转为 GeoJSON 加载到地图

在 Cesium 中结合 OpenCV.js 对影像图层进行分割,并将结果转为 GeoJSON 加载到地图,需要以下步骤:


1. 获取 Cesium 影像数据

首先,需要从 Cesium 的 ImageryLayer 中提取当前视图的影像像素数据(RGB 或 RGBA)。

const viewer = new Cesium.Viewer(\'cesiumContainer\');// 获取当前激活的影像图层(如 Bing 地图或自定义 WMS)const imageryLayer = viewer.imageryLayers.get(0);// 获取当前视图的 Canvasconst canvas = viewer.scene.canvas;const context = canvas.getContext(\'2d\');// 创建一个临时 Canvas 用于提取影像数据const tempCanvas = document.createElement(\'canvas\');tempCanvas.width = canvas.width;tempCanvas.height = canvas.height;const tempCtx = tempCanvas.getContext(\'2d\');// 将 Cesium 影像绘制到临时 CanvastempCtx.drawImage(canvas, 0, 0);const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);

2. 使用 OpenCV.js 进行图像分割

加载 OpenCV.js 并应用图像分割算法(如 阈值分割、边缘检测、语义分割)。

(1) 加载 OpenCV.js

在 HTML 中引入 OpenCV.js:

<script async src=\"https://docs.opencv.org/4.5.5/opencv.js\" onload=\"onOpenCvReady();\"></script><script> function onOpenCvReady() { console.log(\"OpenCV.js is ready!\"); }</script>

(2) 图像分割(示例:基于颜色阈值的水体提取)

function segmentWater(imageData) { // 将 ImageData 转为 OpenCV Mat const src = cv.matFromImageData(imageData); const dst = new cv.Mat(); // 转换到 HSV 颜色空间(更容易提取水体) const hsv = new cv.Mat(); cv.cvtColor(src, hsv, cv.COLOR_RGBA2RGB); cv.cvtColor(hsv, hsv, cv.COLOR_RGB2HSV); // 定义水体颜色范围(HSV 格式) const lower = new cv.Mat(hsv.rows, hsv.cols, hsv.type(), [90, 50, 50]); // 浅蓝色下限 const upper = new cv.Mat(hsv.rows, hsv.cols, hsv.type(), [130, 255, 255]); // 浅蓝色上限 // 应用颜色阈值 const mask = new cv.Mat(); cv.inRange(hsv, lower, upper, mask); // 查找轮廓 const contours = new cv.MatVector(); const hierarchy = new cv.Mat(); cv.findContours(mask, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE); // 返回轮廓数据 return { contours, hierarchy };}

3. 提取轮廓并转为 GeoJSON

将 OpenCV 检测到的轮廓(contours)转换为 GeoJSON 格式。

(1) 轮廓坐标转换

由于 OpenCV 返回的是 像素坐标,需要将其映射回 经纬度坐标

function contoursToGeoJSON(contours, viewer) { const features = []; for (let i = 0; i < contours.size(); i++) { const contour = contours.get(i); const coordinates = []; // 遍历轮廓点 for (let j = 0; j < contour.rows; j++) { const point = contour.data32S.slice(j * 2, j * 2 + 2); // [x, y] 像素坐标 // 将像素坐标转为经纬度 const cartesian = viewer.scene.camera.pickEllipsoid( new Cesium.Cartesian2(point[0], point[1]), viewer.scene.globe.ellipsoid ); if (cartesian) { const cartographic = Cesium.Cartographic.fromCartesian(cartesian); const lon = Cesium.Math.toDegrees(cartographic.longitude); const lat = Cesium.Math.toDegrees(cartographic.latitude); coordinates.push([lon, lat]); } } // 生成 GeoJSON Feature if (coordinates.length > 2) { // 至少 3 个点才能形成多边形 features.push({ type: \"Feature\", geometry: { type: \"Polygon\", coordinates: [coordinates] // GeoJSON 要求闭合环 } }); } } return { type: \"FeatureCollection\", features };}

4. 加载 GeoJSON 到 Cesium

将生成的 GeoJSON 数据加载到 Cesium 进行可视化:

const { contours, hierarchy } = segmentWater(imageData);const geoJSON = contoursToGeoJSON(contours, viewer);// 加载 GeoJSONCesium.GeoJsonDataSource.load(geoJSON, { stroke: Cesium.Color.RED, fill: Cesium.Color.BLUE.withAlpha(0.3), strokeWidth: 2}).then(dataSource => { viewer.dataSources.add(dataSource); viewer.zoomTo(dataSource);});// 释放 OpenCV 内存src.delete(); hsv.delete(); mask.delete(); contours.delete(); hierarchy.delete();

5. 完整代码示例

<!DOCTYPE html><html><head> <meta charset=\"UTF-8\"> <title>Cesium + OpenCV.js 水库分割</title> <script src=\"https://cesium.com/downloads/cesiumjs/releases/1.95/Build/Cesium/Cesium.js\"></script> <script async src=\"https://docs.opencv.org/4.5.5/opencv.js\" onload=\"onOpenCvReady();\"></script> <style> #cesiumContainer { width: 100%; height: 100%; } </style></head><body> <div id=\"cesiumContainer\"></div> <button id=\"extractButton\">提取水库边界</button> <script> const viewer = new Cesium.Viewer(\'cesiumContainer\', { imageryProvider: new Cesium.BingMapsImageryProvider({ url: \'https://dev.virtualearth.net\', key: \'Your_Bing_Maps_Key\', mapStyle: Cesium.BingMapsStyle.AERIAL }), baseLayerPicker: false }); let isOpenCvReady = false; function onOpenCvReady() { isOpenCvReady = true; console.log(\"OpenCV.js loaded!\"); } document.getElementById(\'extractButton\').addEventListener(\'click\', () => { if (!isOpenCvReady) { alert(\"OpenCV.js 尚未加载完成!\"); return; } extractReservoirBoundary(); }); function extractReservoirBoundary() { const canvas = viewer.scene.canvas; const tempCanvas = document.createElement(\'canvas\'); tempCanvas.width = canvas.width; tempCanvas.height = canvas.height; const tempCtx = tempCanvas.getContext(\'2d\'); tempCtx.drawImage(canvas, 0, 0); const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height); // OpenCV 处理 const src = cv.matFromImageData(imageData); const hsv = new cv.Mat(); cv.cvtColor(src, hsv, cv.COLOR_RGBA2RGB); cv.cvtColor(hsv, hsv, cv.COLOR_RGB2HSV); const lower = new cv.Mat(hsv.rows, hsv.cols, hsv.type(), [90, 50, 50]); const upper = new cv.Mat(hsv.rows, hsv.cols, hsv.type(), [130, 255, 255]); const mask = new cv.Mat(); cv.inRange(hsv, lower, upper, mask); const contours = new cv.MatVector(); const hierarchy = new cv.Mat(); cv.findContours(mask, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE); // 转为 GeoJSON const geoJSON = { type: \"FeatureCollection\", features: [] }; for (let i = 0; i < contours.size(); i++) { const contour = contours.get(i); const coordinates = []; for (let j = 0; j < contour.rows; j++) { const point = contour.data32S.slice(j * 2, j * 2 + 2); const cartesian = viewer.scene.camera.pickEllipsoid( new Cesium.Cartesian2(point[0], point[1]), viewer.scene.globe.ellipsoid ); if (cartesian) { const cartographic = Cesium.Cartographic.fromCartesian(cartesian); coordinates.push([  Cesium.Math.toDegrees(cartographic.longitude),  Cesium.Math.toDegrees(cartographic.latitude) ]); } } if (coordinates.length > 2) { geoJSON.features.push({ type: \"Feature\", geometry: {  type: \"Polygon\",  coordinates: [coordinates] } }); } } // 加载到 Cesium Cesium.GeoJsonDataSource.load(geoJSON, { stroke: Cesium.Color.RED, fill: Cesium.Color.BLUE.withAlpha(0.3), strokeWidth: 2 }).then(dataSource => { viewer.dataSources.add(dataSource); viewer.zoomTo(dataSource); }); // 释放内存 src.delete(); hsv.delete(); mask.delete(); contours.delete(); hierarchy.delete(); } </script></body></html>

关键点总结

  1. OpenCV.js 处理:使用颜色阈值(HSV)或深度学习模型(需额外训练)分割水体。
  2. 坐标转换:将 OpenCV 检测的像素坐标映射回 Cesium 的经纬度坐标。
  3. GeoJSON 生成:确保多边形闭合(首尾点相同),并加载到 Cesium。
  4. 性能优化
    • 限制处理区域(ROI)以提高速度。
    • 使用 WebWorker 避免界面卡顿。

适用于 水库、湖泊、河流等水体边界提取,也可扩展至其他地物分类(如建筑、森林)。