引言 在實際中,當多專業設計協助時,遇到圖紙更新後,要對比圖紙找出圖紙的不同處,一直是一個比較耗時費力的事情,也是業內的一大痛點。一般CAD新舊圖紙的內容對比,包括增加新的圖形元素、減少原有的圖形元素以及對原有的圖形進行修改。傳統的方式一般是在PC端CAD環境中實現對圖紙比較的功能,然後隨著互聯網移 ...
引言
在實際中,當多專業設計協助時,遇到圖紙更新後,要對比圖紙找出圖紙的不同處,一直是一個比較耗時費力的事情,也是業內的一大痛點。一般CAD新舊圖紙的內容對比,包括增加新的圖形元素、減少原有的圖形元素以及對原有的圖形進行修改。傳統的方式一般是在PC端CAD環境中實現對圖紙比較的功能,然後隨著互聯網移動端技術的不斷發展,如何擺脫CAD環境,在Web端輕鬆實現圖紙對比功能呢?
實現思路
通常對比圖紙不同有兩種思路:
數據比較法
此方法是對圖紙的原始數據進行比較分析。思路是通過遍歷圖紙中的所有實體元素,根據屬性數據逐一比較差異性比較,找出不同處。
優點:演算法準確。能定位出不同的實體對象。
缺點:圖紙大時運算量大;同時,如果同一個實體刪除了重新繪製會導致ObjectID發生變化,導致不好判斷是否是同一個實體,演算法實現難度大。
像素比較法
此方法是根據渲染後的圖片進行比較。對圖片的像素進行分析對比,找出不同的區域。
優點:速度快,演算法實現相對容易。
缺點:只能定位出不同的區域,不能定位出具體是哪些實體。
在實際需求中,要求快速定位不同處,而無需定位到是哪些具體的實體對象。所以我們選用像素比較法來進行對比分析實現。
先上最終效果圖如下:
同步對比分析效果:
地圖捲簾效果效果:
演算法分析
大家看到圖片像素對比分析,肯定第一反應是這演算法太簡單了。一個個像素判斷是否相等,然後就知道差異性了。如果這麼想,那就是把問題想的太簡單了。實際中,由於渲染時反鋸齒的功能,會導致相同的繪製內容也會導致像素值細微的區別。而演算法的核心就是把這些干擾因素給排除,找到真正差異的部分。
圖片相似度計算方法總結
- 餘弦相似度
把圖片表示成一個向量,通過計算向量之間的餘弦距離來表徵兩張圖片的相似度
具體演算法可參考 https://zhuanlan.zhihu.com/p/93893211
- 直方圖
按照某種距離度量的標準對兩幅圖像的直方圖進行相似度的測量
具體演算法可參考 https://zhuanlan.zhihu.com/p/274429582
- 哈希演算法
感知哈希可以用來判斷兩個圖片的相似度,通常可以用來進行圖像檢索。感知哈希演算法對每一張圖片生成一個“指紋”,通過比較兩張圖片的指紋,來判斷他們的相似度,是否屬於同一張圖片。常用的有三種:平均哈希(aHash),感知哈希(pHash),差異值哈希(dHash).
-
像素匹配pixelmatch
利用像素之間的匹配來計算相似度。
實現
我們基於BS模式對圖片進行對比分析找出不同處。在服務端實現解析CAD圖紙,生成像素圖片;利用pixelmatch演算法找出不同處。在瀏覽器端載入CAD圖並顯示出不同的地方。
(1) Web端線上打開CAD圖
如何在Web網頁端展示CAD圖形(唯傑地圖雲端圖紙管理平臺 https://vjmap.com/app/cloud),這個在前面的博文中已講過,這裡不再重覆,有需要的朋友可下載工程源代碼研究下。
(2) 把CAD圖轉成圖片
因為唯傑地圖採用的把CAD圖轉成GIS數據渲染的思路,所以可以通過提供的WMS服務,渲染成指定像素大小的圖片。這裡為了對比結果準確,可以把渲染的級別設置大點,得到的圖片像素大小也變大,更加清晰,對比結果更準確。
介面如下:
/**
* wms服務url地址介面
*/
export interface IWmsTileUrl {
/** 地圖ID(為空時採用當前打開的mapid), 為數組時表時同時請求多個. */
mapid?: string | string[];
/** 地圖版本(為空時採用當前打開的地圖版本). */
version?: string | string[];
/** 圖層名稱(為空時採用當前打開的地圖圖層名稱). */
layers?: string | string[];
/** 範圍,預設{bbox-epsg-3857}. (如果要獲取地圖cad一個範圍的wms數據無需任何坐標轉換,將此範圍填cad範圍,srs,crs,mapbounds填為空).*/
bbox?: string;
/** 當前坐標系,預設(EPSG:3857). */
srs?: string;
/** cad圖的坐標系,為空的時候由元數據坐標系決定. */
crs?: string | string[];
/** 地理真實範圍,如有值時,srs將不起作用 */
mapbounds?: string;
/** 寬. */
width?: number;
/** 高. */
height?: number;
/** 是否透明. */
transparent?: boolean;
/** 四參數(x偏移,y偏移,縮放,旋轉弧度),可選,對坐標最後進行修正*/
fourParameter?: string | string[];
/** 是否是矢量瓦片. */
mvt?: boolean;
/** 是否考慮旋轉,在不同坐標系中轉換是需要考慮。預設自動考慮是否需要旋轉. */
useImageRotate?: boolean;
}
(3) 像素對比分析演算法
其反鋸齒像素對比核心演算法代碼如下
uint8_t blend(uint8_t c, double a) {
return 255 + (c - 255) * a;
}
double rgb2y(uint8_t r, uint8_t g, uint8_t b) { return r * 0.29889531 + g * 0.58662247 + b * 0.11448223; }
double rgb2i(uint8_t r, uint8_t g, uint8_t b) { return r * 0.59597799 - g * 0.27417610 - b * 0.32180189; }
double rgb2q(uint8_t r, uint8_t g, uint8_t b) { return r * 0.21147017 - g * 0.52261711 + b * 0.31114694; }
// 使用YIQ NTSC傳輸顏色空間測量感知色差”計算色差
double colorDelta(const uint8_t* img1, const uint8_t* img2, std::size_t k, std::size_t m, bool yOnly = false) {
double a1 = double(img1[k + 3]) / 255;
double a2 = double(img2[m + 3]) / 255;
uint8_t r1 = blend(img1[k + 0], a1);
uint8_t g1 = blend(img1[k + 1], a1);
uint8_t b1 = blend(img1[k + 2], a1);
uint8_t r2 = blend(img2[m + 0], a2);
uint8_t g2 = blend(img2[m + 1], a2);
uint8_t b2 = blend(img2[m + 2], a2);
double y = rgb2y(r1, g1, b1) - rgb2y(r2, g2, b2);
if (yOnly) return y; // 僅亮度差
double i = rgb2i(r1, g1, b1) - rgb2i(r2, g2, b2);
double q = rgb2q(r1, g1, b1) - rgb2q(r2, g2, b2);
return 0.5053 * y * y + 0.299 * i * i + 0.1957 * q * q;
}
void drawPixel(uint8_t* output, std::size_t pos, uint8_t r, uint8_t g, uint8_t b) {
output[pos + 0] = r;
output[pos + 1] = g;
output[pos + 2] = b;
output[pos + 3] = 255;
}
double grayPixel(const uint8_t* img, std::size_t i) {
double a = double(img[i + 3]) / 255;
uint8_t r = blend(img[i + 0], a);
uint8_t g = blend(img[i + 1], a);
uint8_t b = blend(img[i + 2], a);
return rgb2y(r, g, b);
}
// 檢查像素是否可能是抗鋸齒的一部分
bool antialiased(const uint8_t* img, std::size_t x1, std::size_t y1, std::size_t width, std::size_t height, const uint8_t* img2 = nullptr) {
std::size_t x0 = x1 > 0 ? x1 - 1 : 0;
std::size_t y0 = y1 > 0 ? y1 - 1 : 0;
std::size_t x2 = std::min(x1 + 1, width - 1);
std::size_t y2 = std::min(y1 + 1, height - 1);
std::size_t pos = (y1 * width + x1) * 4;
uint64_t zeroes = 0;
uint64_t positives = 0;
uint64_t negatives = 0;
double min = 0;
double max = 0;
std::size_t minX = 0, minY = 0, maxX = 0, maxY = 0;
// 穿過8個相鄰像素
for (std::size_t x = x0; x <= x2; x++) {
for (std::size_t y = y0; y <= y2; y++) {
if (x == x1 && y == y1) continue;
// 中心像素和相鄰像素之間的亮度增量
double delta = colorDelta(img, img, pos, (y * width + x) * 4, true);
// 計算相等、較暗和較亮相鄰像素的數量
if (delta == 0) zeroes++;
else if (delta < 0) negatives++;
else if (delta > 0) positives++;
// 如果找到兩個以上相同的同級,則絕對不是抗鋸齒
if (zeroes > 2) return false;
if (!img2) continue;
// 記得最暗的像素
if (delta < min) {
min = delta;
minX = x;
minY = y;
}
// 記住最亮的像素
if (delta > max) {
max = delta;
maxX = x;
maxY = y;
}
}
}
if (!img2) return true;
// 如果同級之間沒有較暗和較亮的像素,則不是抗鋸齒
if (negatives == 0 || positives == 0) return false;
// 如果最暗或最亮的像素在兩幅圖像中都有兩個以上相同的同級
//(絕對不是反走樣),該像素是反走樣的
return (!antialiased(img, minX, minY, width, height) && !antialiased(img2, minX, minY, width, height)) ||
(!antialiased(img, maxX, maxY, width, height) && !antialiased(img2, maxX, maxY, width, height));
}
}
(4) 前端調用演算法並展示
相關代碼如下
// 地圖比較不同
let diff = await service.cmdMapDiff({
// 要比較圖1的圖名稱
mapid1: mapId1,
// 要比較圖1的圖版本,如為空,表示是最新版本
version1: "",
// 要比較圖1的圖層樣式名稱,可為空。為空的用預設的
layer1: map1.getService().currentMapParam().layer,
// 要比較圖2的圖名稱,圖名稱可以和mapid1不一樣
mapid2: mapId2,
// 要比較圖2的圖版本,如為空,表示是最新版本
version2: "",
// 要比較圖2的圖層樣式名稱,可為空。為空的用預設的
layer2: map2.getService().currentMapParam().layer
})
if (diff.error) {
message.error(diff.error);
return;
}
const drawPolygons = (map, points, color) => {
if (points.length === 0) return;
points.forEach(p => p.push(p[0])) ;// 閉合
let polygons = points.map(p => {
return {
points: map.toLngLat(p),
properties: {
color: color
}
}
})
vjmap.createAntPathAnimateLineLayer(map, polygons, {
fillColor1: color,
fillColor2: "#0ffb",
canvasWidth: 128,
canvasHeight: 32,
frameCount: 4,
lineWidth: 4,
lineOpacity: 0.8
});
}
if (diff.modify.length === 0) {
message.info("完全相同,沒有找到不同處");
return;
}
// 修改的部分
drawPolygons(map2, diff.modify, "#f00");
// 新增部分
drawPolygons(map2, diff.new, "#0f0");
// 刪除部分
drawPolygons(map1, diff.del, "#00f");
以上前端的實現代碼已開源至github。 地址:https://github.com/vjmap/vjmap-playground/blob/main/src/02service_%E5%9C%B0%E5%9B%BE%E6%9C%8D%E5%8A%A1/17zmapDiff.js
線上體驗地址為:https://vjmap.com/demo/#/demo/map/service/17zmapDiff