原創(chuàng) freegiser Spatial Data 2021-03-18 17:30
收錄于合集
一 背景
WebGIS中非常常見(jiàn)的一個(gè)功能,就是疊加地理相關(guān)的Image到底圖上,眾所周知,底圖是有地理坐標(biāo)系的,Image通常也是基于某個(gè)地理坐標(biāo)系制圖的產(chǎn)物,如果底圖坐標(biāo)系和Image坐標(biāo)系不一致,那么他們疊加肯定就會(huì)發(fā)生位置的變形與偏移:
不同投影疊加形變
上圖,地理底圖一般是墨卡托投影(epsg:3857),假設(shè)疊加的圖片是生產(chǎn)單位按照WGS84經(jīng)緯度(epsg:4326)制圖輸出的,并且已知這個(gè)圖片真實(shí)地理范圍是[84minx,84miny,84maxx,84maxy],常規(guī)思路就是計(jì)算84的范圍對(duì)應(yīng)底圖的墨卡托的范圍[mkt_minx,mkt_miny,mkt_maxx,mkt_maxy],然后工工整整把這個(gè)圖像貼到底圖這個(gè)范圍區(qū)間里,效果如上圖,非常明顯的投影形變。
看到這很多朋友會(huì)問(wèn):有方法讓與底圖坐標(biāo)系不同的Image正確疊加底圖嗎?答案只有一個(gè):讓image和底圖的坐標(biāo)系相同。
那么下個(gè)問(wèn)題:如何能讓image和底圖坐標(biāo)系相同?
一個(gè)是逐像素轉(zhuǎn)換,這不是本篇話(huà)題,計(jì)算量比較大,性能不好,通常不被采用。
另一個(gè)方法是:將圖像分解三角形進(jìn)行投影轉(zhuǎn)換(很熟悉的味道啊,圖像?三角形?對(duì)應(yīng)webgl的紋理和三角片?)。
image投影到與底圖一致
二 技術(shù)資料
openlayers目前原生就已經(jīng)支持image重投影了,demo如下:
https://openlayers.org/en/latest/examples/reprojection-image.html
具體實(shí)現(xiàn)技術(shù)原理參考如下博客:
https://www.jianshu.com/p/98da62d88e83 Openlayers raster reprojection 柵格重投影原理分析
只不過(guò)ol目前的源碼里是用canvas實(shí)現(xiàn)的,在leaflet的里也有大神提供了一個(gè)webgl實(shí)現(xiàn)的圖層,網(wǎng)速慢要等圖像加載完畢:
https://ivansanchez.gitlab.io/Leaflet.ImageOverlay.Arrugator/demo.html
本質(zhì)上ol和leaflet思路都是圖像拆分三角形,三角形整體投影(相比逐像素投影效率高非常多),很像數(shù)學(xué)里的積分概念,拆分三角形越多,那么計(jì)算越精確,可以想象,三角形足夠的的多,是不是等于非常精確,和逐像素一樣了?
下面我們主要基于leaflet的這個(gè)webgl方案做下簡(jiǎn)單說(shuō)明,開(kāi)發(fā)者是基于arrugator庫(kù)實(shí)現(xiàn)三角形切分的:
https://gitlab.com/IvanSanchez/arrugator
這個(gè)庫(kù)使用很簡(jiǎn)單:
第一步:定義圖像坐標(biāo)系
proj4.defs( "EPSG:25833", "+proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs");// 定義投影轉(zhuǎn)換規(guī)則,如從圖像的25833轉(zhuǎn)底圖的3857坐標(biāo)系const projector = proj4("EPSG:25833", "EPSG:3857").forward;
第二步:定義圖像的控制坐標(biāo)點(diǎn)(圖像對(duì)應(yīng)的地理bbox)與對(duì)應(yīng)紋理的uv坐標(biāo)?
//圖像的地理bbox坐標(biāo)const controlPoints = [ [ -183622.3, 7996344.0 ], // top-left [ -183622.3, 6396344.0 ], // bottom-left [ 1416377.7, 7996344.0 ], // top-right [ 1416377.7, 6396344.0 ], // bottom-right];//紋理uv坐標(biāo),注意top left這些方向與地理的一一對(duì)應(yīng)const sourceUV = [ [ 0, 0 ], // top-left [ 0, 1 ], // bottom-left [ 1, 0 ], // top-right [ 1, 1 ], // bottom-right];
第三步:計(jì)算三角形
let arruga = new Arrugator( forward, controlPoints, sourceUV, [ [ 0, 1, 3 ], [ 0, 3, 2 ] ] // 這里寫(xiě)死不變);// 根據(jù)閾值切分三角形const epsilon = 0.0000000001; // 不同坐標(biāo)系這個(gè)閾值是不同的,實(shí)際使用可以自行調(diào)試arruga.lowerEpsilon(epsilon);// 結(jié)果輸出let arrugado = arruga.output();
這里有個(gè)問(wèn)題,閾值設(shè)置多少合適?開(kāi)發(fā)者可以根據(jù)實(shí)際的坐標(biāo)系轉(zhuǎn)換時(shí)候,調(diào)試以下代碼,先保證這個(gè)條件成立,然后在這個(gè)條件成立的基礎(chǔ)上,epsilon越小,三角形切分越多,那么耗時(shí)多點(diǎn),結(jié)果正確點(diǎn)唄。
例如:peek().epsilon是0.1,那么你傳閾值必須要小于0.1,大于的話(huà)就不切分了其實(shí),那我可以設(shè)置0.01,0.001都可以的,自己調(diào)試效果和性能,取個(gè)折衷數(shù)值較好,就是不能設(shè)置太小追求精確度,但犧牲性能,中庸點(diǎn)好。
這個(gè)庫(kù)最終輸出的其實(shí)有三個(gè)東西:
projected:切分的小三角形投影后的頂點(diǎn)。
uv: 切分的小三角形uv紋理頂點(diǎn)。
trigs:切分后的小三角形頂點(diǎn)索引,就是webgl里的那個(gè)indices。
拿到地理頂點(diǎn),uv頂點(diǎn),indices的繪制索引,再加一張image作為紋理,使用webgl渲染簡(jiǎn)直是水到渠成的事。
三 mapboxgl實(shí)踐
由于mapboxgl本身不支持image的投影轉(zhuǎn)換,我們使用arrugator來(lái)幫我們實(shí)現(xiàn)這個(gè)功能,選擇openlayers這個(gè)例子來(lái)改寫(xiě)到mapbox上。
https://openlayers.org/en/latest/examples/reprojection-image.html
首先對(duì)Image三角形切分:
import test_map from '../../images/2000px-British_National_Grid.svg.png';
import proj4 from 'proj4';
import Arrugator from 'arrugator';
// 墨卡托投影的左上角坐標(biāo),對(duì)應(yīng)mapbox左上角起始坐標(biāo)[0,0]
const origin=[
-20037508.342789244,20037508.342789244
];
proj4.defs(
'EPSG:27700',
'+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 ' +
'+x_0=400000 +y_0=-100000 +ellps=airy ' +
'+towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 ' +
'+units=m +no_defs'
);
// 定義轉(zhuǎn)換規(guī)則
const projector = proj4("EPSG:27700", "EPSG:3857").forward;
// 改寫(xiě)坐標(biāo)轉(zhuǎn)換函數(shù)
// 原因是mapbox的墨卡托坐標(biāo)是0-1區(qū)間,并且對(duì)應(yīng)地理范圍與標(biāo)準(zhǔn)3857不同。
function forward(coors) {
// 墨卡托坐標(biāo)
const coor_3857 = projector(coors);
// 墨卡托坐標(biāo)轉(zhuǎn)換到 0-1區(qū)間,origin對(duì)應(yīng)mapbox 0 0點(diǎn),所以這么寫(xiě)
const mapbox_coor1 = Math.abs((coor_3857[0]-origin[0])/(20037508.342789244*2));
const mapbox_coor2 = Math.abs((coor_3857[1]-origin[1])/(20037508.342789244*2));
return [mapbox_coor1,mapbox_coor2];
}
const epsilon = 0.00000000001;
const controlPoints = [
[ 0, 1300000 ], // top-left
[ 0, 0 ], // bottom-left
[ 700000, 1300000 ], // top-right
[ 700000, 0 ], // bottom-right
];
//紋理uv坐標(biāo)
const sourceUV = [
[ 0, 0 ], // top-left
[ 0, 1 ], // bottom-left
[ 1, 0 ], // top-right
[ 1, 1 ], // bottom-right
];
let arruga = new Arrugator(
forward,
controlPoints,
sourceUV,
[ [ 0, 1, 3 ], [ 0, 3, 2 ] ]
);
arruga.lowerEpsilon(epsilon);
// arrugado里存了頂點(diǎn),索引
let arrugado = arruga.output();
然后構(gòu)建一個(gè)自定義圖層,部分webgl自封裝的方法,就不展開(kāi)說(shuō)了:
class ImageLayer {
// 傳入圖像和切分三角形頂點(diǎn)和索引
constructor(img, arrugado) {
this.id = 'test_layer';
this.type = 'custom';
this.renderingMode = '2d';
this.img = img;
this.arrugado = arrugado;
this.map;
this.gl;
}
onAdd(m, gl) {
this.map = m;
this.gl = gl;
// 墨卡托坐標(biāo)轉(zhuǎn)mapbox的墨卡托坐標(biāo)
let pos = this.arrugado.projected.flat();
// uv紋理
let uv = this.arrugado.uv.flat();
// 三角形index
let trigs = arrugado.trigs.flat();
// 著色器代碼
const vs = `#version 300 es
layout(location=0) in vec2 a_position;
layout(location=1) in vec2 a_uv;
uniform mat4 u_worldViewProjection;
out vec2 v_uv;
void main() {
gl_Position = u_worldViewProjection*vec4(a_position, 0.0,1.0);
v_uv = a_uv;
}`;
const fs = `#version 300 es
precision highp int;
precision highp float;
uniform sampler2D u_image;
in vec2 v_uv;
out vec4 outColor;
void main() {
// 根據(jù)紋理坐標(biāo)獲取顏色
vec4 texel= texture(u_image, v_uv);
if (texel.r > 0.95 && texel.g > 0.95 && texel.b > 0.95) {
texel = vec4(0.6, 0.0, 0.0, 1.0);
}
else {
outColor= vec4(texel.r*0.4,texel.g*0.4,texel.b*0.4,0.4);
}
}`;
// 定義渲染model
this.drawModel = createModel(gl, vs, fs);
//定義vao對(duì)象,綁定頂點(diǎn),索引
this.vao = gl.createVertexArray();
gl.bindVertexArray(this.vao);
const positionBuffer = createBuffer(gl, gl.ARRAY_BUFFER, new Float32Array(pos));
bindAttribute(gl, positionBuffer, 0, 2);
const uvBuffer = createBuffer(gl, gl.ARRAY_BUFFER, new Float32Array(uv));
bindAttribute(gl, uvBuffer, 1, 2);
const indexBuffer = createBuffer(gl, gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(trigs));
this.position_count = trigs.length;
// 綁定結(jié)束
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindVertexArray(null);
//構(gòu)造紋理并綁定
const texture = createTexture2D(gl, {
data: this.img,
mipLevel: 0,
internalFormat: gl.RGBA,//webgl中格式
srcFormat: gl.RGBA,//輸入數(shù)據(jù)源格式
type: gl.UNSIGNED_BYTE,
height: 2000,
width: 3696,
parameters: {
[ gl.TEXTURE_MAG_FILTER ]: gl.LINEAR,
[ gl.TEXTURE_MIN_FILTER ]: gl.LINEAR,
[ gl.TEXTURE_WRAP_S ]: gl.CLAMP_TO_EDGE,
[ gl.TEXTURE_WRAP_T ]: gl.CLAMP_TO_EDGE
}
});
//drawProgram綁定紋理,不能綁定單元0
gl.useProgram(this.drawModel.program);
bindTexture2D(gl, texture, 3);
gl.uniform1i(this.drawModel.u_image, 3);
}
render(gl, matrix) {
gl.useProgram(this.drawModel.program);
//設(shè)置unifrom
gl.uniformMatrix4fv(this.drawModel.u_worldViewProjection, false, matrix);
//綁定頂點(diǎn)vao
gl.bindVertexArray(this.vao);
gl.drawElements(gl.TRIANGLES, this.position_count, gl.UNSIGNED_SHORT, 0);
//如果取消綁定,會(huì)報(bào)錯(cuò)GL_INVALID_OPERATION: Insufficient buffer size.
gl.bindVertexArray(null);
}
onRemove(map, gl) {
// 。。。
}
}
第三步:新建圖層,疊加地圖
let img = new Image(); img.src = test_map; img.onload = function () { let layer = new ImageLayer(img, arrugado); var container = document.createElement('div'); const width = 1600, height = 900; container.style.width = `${width}px`; container.style.height = `${height}px`; document.body.appendChild(container); // 兼容webgl2 mapboxglExtension(mapboxgl); var map = new mapboxgl.Map({ container: container, style: 'mapbox://styles/mapbox/dark-v10', center: [ 0, 65 ], zoom: 4, antialias: true }); map.on('load', () => { map.addLayer(layer); }); }
至于怎么樣讓mapboxgl支持webgl2,為什么紋理單元不能寫(xiě)0,為什么這么多手動(dòng)綁定解綁webgl資源對(duì)象,參考下文《使用WebGL2進(jìn)行Mapbox自定義圖層開(kāi)發(fā)》:
https://mp.weixin.qq.com/s?__biz=Mzg2OTUxMzM2MA==&mid=2247483684&idx=1&sn=cbec2c833fa0a2a30e3ee0d6063fbf0c&chksm=ce9aa0dbf9ed29cd1b65bebf5e773eb8b4005c3d347b033426d0c4b65807ab74934268d9df88&token=1580692165&lang=zh_CN#rd
效果如下圖:
四 應(yīng)用延申
這個(gè)image重投影一般在哪用咧?比如氣象領(lǐng)域,很多產(chǎn)品出的柵格圖像,想要疊加到地圖上,坐標(biāo)系可能圖像用4326經(jīng)緯度,底圖是墨卡托,這時(shí)候疊加歪了,就可以使用這個(gè)庫(kù)去轉(zhuǎn)換,例如下圖:
轉(zhuǎn)換前
轉(zhuǎn)換后
轉(zhuǎn)換前,日本列島的溫度都疊加底圖上的日本區(qū)域,都飄上面去了,越往北偏移越大其實(shí),赤道那近似,這個(gè)就是投影變換的原理了,球面轉(zhuǎn)平面的關(guān)系,這里講不清楚,自行研究。轉(zhuǎn)換后,可以看到疊加的基本嚴(yán)絲合縫了。
文章已于2021-03-18修改
聯(lián)系客服