引言
在實際中,當多專業設計協助時,遇到圖紙更新后,要對比圖紙找出圖紙的不同處,一直是一個比較耗時費力的事情,也是業內的一大痛點,一般CAD新舊圖紙的內容對比,包括增加新的圖形元素、減少原有的圖形元素以及對原有的圖形進行修改,傳統的方式一般是在PC端CAD環境中實作對圖紙比較的功能,然后隨著互聯網移動端技術的不斷發展,如何擺脫CAD環境,在Web端輕松實作圖紙對比功能呢?
實作思路
通常對比圖紙不同有兩種思路:
資料比較法
此方法是對圖紙的原始資料進行比較分析,思路是通過遍歷圖紙中的所有物體元素,根據屬性資料逐一比較差異性比較,找出不同處,
優點:演算法準確,能定位出不同的物體物件,
缺點:圖紙大時運算量大;同時,如果同一個物體洗掉了重新繪制會導致ObjectID發生變化,導致不好判斷是否是同一個物體,演算法實作難度大,
像素比較法
此方法是根據渲染后的圖片進行比較,對圖片的像素進行分析對比,找出不同的區域,
優點:速度快,演算法實作相對容易,
缺點:只能定位出不同的區域,不能定位出具體是哪些物體,
在實際需求中,要求快速定位不同處,而無需定位到是哪些具體的物體物件,所以我們選用像素比較法來進行對比分析實作,
先上最終效果圖如下:
同步對比分析效果:

地圖卷簾效果效果:

演算法分析
大家看到圖片像素對比分析,肯定第一反應是這演算法太簡單了,一個個像素判斷是否相等,然后就知道差異性了,如果這么想,那就是把問題想的太簡單了,實際中,由于渲染時反鋸齒的功能,會導致相同的繪制內容也會導致像素值細微的區別,而演算法的核心就是把這些干擾因素給排除,找到真正差異的部分,
圖片相似度計算方法總結
- 余弦相似度
? 把圖片表示成一個向量,通過計算向量之間的余弦距離來表征兩張圖片的相似度
? 具體演算法可參考 https://zhuanlan.zhihu.com/p/93893211
- 直方圖
? 按照某種距離度量的標準對兩幅影像的直方圖進行相似度的測量
? 具體演算法可參考 https://zhuanlan.zhihu.com/p/274429582
- 哈希演算法
? 感知哈希可以用來判斷兩個圖片的相似度,通常可以用來進行影像檢索,感知哈希演算法對每一張圖片生成一個“指紋”,通過比較兩張圖片的指紋,來判斷他們的相似度,是否屬于同一張圖片,常用的有三種:平均哈希(aHash),感知哈希(pHash),差異值哈希(dHash).
具體演算法可參考 https://blog.csdn.net/qq_32799915/article/details/81000437?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~Rate-1-81000437-blog-83271885.pc_relevant_aa&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~Rate-1-81000437-blog-83271885.pc_relevant_aa&utm_relevant_index=2
-
像素匹配pixelmatch
利用像素之間的匹配來計算相似度,
https://github.com/whtsky/pixelmatch-py
實作
我們基于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
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/503307.html
標籤:其他
