项目场景:
遇到了一个需求: 就是用cesium加载TIFF影像,本来主流的方式是后台切片发布服务,前端直接调用服务加载就行,但是 后台直接给了我一个tif文件,就需要前端来进行绘图渲染了
问题描述
做之前先上网查看了一下有没有相关的文档,查到了某大佬写的文https://blog.youkuaiyun.com/lovefengruoqing/article/details/115306876
给了我思路,但是 执行下来就会遇到各种各样的问题,以下详细说一下:
const tiff = await fromBlob(blob);
let image = await tiff.getImage();
let [west, south, east, north] = image.getBoundingBox();
const code = image.geoKeys.ProjectedCSTypeGeoKey || image.geoKeys.GeographicTypeGeoKey;
// 我这里拿到的epsg是32651
所以需要对 方位坐标(west, south, east, north) 的数据进行转换为4326格式的数据,这里用的是proj4 库进行的转换
// 定义 EPSG32651 和 EPSG4326 的投影信息
proj4.defs('EPSG:32651', '+proj=utm +zone=51 +datum=WGS84 +units=m +no_defs');
proj4.defs('EPSG:4326', '+proj=longlat +datum=WGS84 +no_defs');
// 进行坐标转换
const utmCoords = [west, south]; // 组合坐标
const utmCoords1 = [east, north]; // 组合坐标
// 注意这两条数据 后面绘制图片区域时要用!!!
const convertedCoords = proj4('EPSG:32651', 'EPSG:4326', utmCoords);
const convertedCoords1 = proj4('EPSG:32651', 'EPSG:4326', utmCoords1);
接下来就是要用canvas绘制图像了
// 注意这一步,有坑***1.0
const [red = [], green = [], blue = []] = await image.readRasters();
// 将像素信息写入canvas
const canvas = document.createElement('canvas');
let width = image.getWidth();
let height = image.getHeight();
canvas.width = width;
canvas.height = height;
let ctx = canvas.getContext('2d');
let imageData = ctx.createImageData(width, height);
console.time('写入像素');
for (var i = 0; i < imageData.data.length / 4; i++) {
imageData.data[i * 4 + 0] = red[i];
imageData.data[i * 4 + 1] = green[i] || 0;
imageData.data[i * 4 + 2] = blue[i] || 0;
imageData.data[i * 4 + 3] = 255;
}
ctx.putImageData(imageData, 0, 0);
console.log('imageData', imageData);
利用上面的方式,成功渲染出来一张全部像素都为白色的图像。。。 打印出来的imageData.data里的数据都是255
所以绘制canvas图像出现了问题
原因分析:
1.0 readRasters这个API 返回的数组并不一定为四个通道数组rgba, 这个返回值跟图像的dataType有关,
如果是uint8: 8位无符号整型,数值在0-255 之间则返回的是RGBA四个通道数组,可以用上面的方式正常渲染
如果是uint16:16位无符号整型,则返回的并不是4个通道,会多几个通道,我这里是7个。数值范围也远超0-255,因此不能用上面的方式进行渲染。
于是打印出来了 red,green, blue等数组,发现数值都远远大于255,根据rgba渲染原理 数字是不能超过255的,超过了就按照255来进行渲染, 所以RGBA都为255 图像是全白的。
解决方案:
将uint16的数值转为unit8 的格式以便于显然canvas
// const minValue = math.min(...red); 这种方法浏览器会崩溃!!!
// const maxValue = math.max(...red);
const min = 1000; // 手动设置最小值最大值。
const max = 4000;
const redArr = red.map(value => Math.round(((value - min) / (max - min)) * 255));
const greenArr = green.map(value => Math.round(((value - min) / (max - min)) * 255));
const blueArr = blue.map(value => Math.round(((value - min) / (max - min)) * 255));
注意我上见的代码, 由于我数据量太大了,一共一亿多条数据,直接取最大值的话浏览器会崩溃停止运行
所以我这里手动定义了最大最小值,为什么我定义这两个数值呢,因为我在QGIS里的Band里查看了具体的最大最小值,
这里就写了一个大致的区间。并将uint16映射到unit8.组成一个新的数组 替换掉上面的red green blue即可。
这样写图片画出来跟实际图片像素存在一定的偏差,效果没那么完美,只能勉强凑合着用了。效果如图
完整代码如下:
const showTif = async () => {
const blob = files.value[0].originFileObj;
const tiff = await fromBlob(blob);
let image = await tiff.getImage();
let [west, south, east, north] = image.getBoundingBox();
const code = image.geoKeys.ProjectedCSTypeGeoKey || image.geoKeys.GeographicTypeGeoKey;
const pool = new Pool();
// 读取像素信息
const [red = [], green = [], blue = []] = await image.readRasters({ pool });
// const minValue = math.min(...red); 这种方法浏览器会崩溃!!!
// const maxValue = math.max(...red);
const min = 1000; // 手动设置最小值最大值。
const max = 4000;
const redArr = red.map(value => Math.round(((value - min) / (max - min)) * 255));
const greenArr = green.map(value => Math.round(((value - min) / (max - min)) * 255));
const blueArr = blue.map(value => Math.round(((value - min) / (max - min)) * 255));
// 定义 EPSG32651 和 EPSG4326 的投影信息
proj4.defs('EPSG:32651', '+proj=utm +zone=51 +datum=WGS84 +units=m +no_defs');
proj4.defs('EPSG:4326', '+proj=longlat +datum=WGS84 +no_defs');
// 进行坐标转换
const utmCoords = [west, south]; // 组合坐标
const utmCoords1 = [east, north]; // 组合坐标
const convertedCoords = proj4('EPSG:32651', 'EPSG:4326', utmCoords);
const convertedCoords1 = proj4('EPSG:32651', 'EPSG:4326', utmCoords1);
// 将像素信息写入canvas
const canvas = document.createElement('canvas');
let width = image.getWidth();
let height = image.getHeight();
canvas.width = width;
canvas.height = height;
let ctx = canvas.getContext('2d');
let imageData = ctx.createImageData(width, height);
console.time('写入像素');
for (var i = 0; i < imageData.data.length / 4; i++) {
imageData.data[i * 4 + 0] = redArr[i];
imageData.data[i * 4 + 1] = greenArr[i] || 0;
imageData.data[i * 4 + 2] = blueArr[i] || 0;
imageData.data[i * 4 + 3] = 255;
}
ctx.putImageData(imageData, 0, 0);
console.log('imageData', imageData);
console.timeEnd('写入像素');
let rectangle = new Cesium.Rectangle.fromDegrees(...convertedCoords, ...convertedCoords1);
let du = canvas.toDataURL();
console.log('du', du, rectangle, canvas);
viewer.imageryLayers.addImageryProvider(
new Cesium.SingleTileImageryProvider({
url: du,
tileWidth: width,
tileHeight: height,
ellipsoid: Cesium.Ellipsoid.WGS84,
rectangle: rectangle,
})
);
setTimeout(() => {
const cameraView = {
destination: rectangle,
duration: 2.0,
maximumHeight: 2000,
pitchAdjustHeight: 2000,
endTransform: Cesium.Matrix4.IDENTITY,
};
viewer.camera.flyTo(cameraView);
}, 2000);
};
总结:如果是uint16数据类型的TIFF文件,用这种方式能转为在rgba范围内的uint8格式,实际图像跟原图像存在颜色差别,但是这已经是我目前已知的将uint16格式绘制在cesium上的仅有的方式,不知道有咩有其他方法,欢迎留言。