在 Cesium 中结合 OpenCV.js 对影像图层进行分割,并将结果转为 GeoJSON 加载到地图,需要以下步骤:
1. 获取 Cesium 影像数据
首先,需要从 Cesium 的 ImageryLayer
中提取当前视图的影像像素数据(RGB 或 RGBA)。
const viewer = new Cesium.Viewer('cesiumContainer');
// 获取当前激活的影像图层(如 Bing 地图或自定义 WMS)
const imageryLayer = viewer.imageryLayers.get(0);
// 获取当前视图的 Canvas
const 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 影像绘制到临时 Canvas
tempCtx.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);
// 加载 GeoJSON
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);
});
// 释放 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>
关键点总结
- OpenCV.js 处理:使用颜色阈值(HSV)或深度学习模型(需额外训练)分割水体。
- 坐标转换:将 OpenCV 检测的像素坐标映射回 Cesium 的经纬度坐标。
- GeoJSON 生成:确保多边形闭合(首尾点相同),并加载到 Cesium。
- 性能优化:
- 限制处理区域(ROI)以提高速度。
- 使用
WebWorker
避免界面卡顿。
适用于 水库、湖泊、河流等水体边界提取,也可扩展至其他地物分类(如建筑、森林)。