草庐IT

Web端CAD图形找不同?一键在Web端找出CAD图不同并对比分析

vjmap 2023-03-28 原文

引言

在实际中,当多专业设计协助时,遇到图纸更新后,要对比图纸找出图纸的不同处,一直是一个比较耗时费力的事情,也是业内的一大痛点。一般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

实现

我们基于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

有关Web端CAD图形找不同?一键在Web端找出CAD图不同并对比分析的更多相关文章

  1. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  2. java - 为什么 ruby​​ modulo 与 java/other lang 不同? - 2

    我基本上来自Java背景并且努力理解Ruby中的模运算。(5%3)(-5%3)(5%-3)(-5%-3)Java中的上述操作产生,2个-22个-2但在Ruby中,相同的表达式会产生21个-1-2.Ruby在逻辑上有多擅长这个?模块操作在Ruby中是如何实现的?如果将同一个操作定义为一个web服务,两个服务如何匹配逻辑。 最佳答案 在Java中,模运算的结果与被除数的符号相同。在Ruby中,它与除数的符号相同。remainder()在Ruby中与被除数的符号相同。您可能还想引用modulooperation.

  3. ruby-on-rails - 在 RSpec 中,如何以任意顺序期望具有不同参数的多条消息? - 2

    RSpec似乎按顺序匹配方法接收的消息。我不确定如何使以下代码工作:allow(a).toreceive(:f)expect(a).toreceive(:f).with(2)a.f(1)a.f(2)a.f(3)我问的原因是a.f的一些调用是由我的代码的上层控制的,所以我不能对这些方法调用添加期望。 最佳答案 RSpecspy是测试这种情况的一种方式。要监视一个方法,用allowstub,除了方法名称之外没有任何约束,调用该方法,然后expect确切的方法调用。例如:allow(a).toreceive(:f)a.f(2)a.f(1)

  4. ruby - 如何配置 Ruby Mechanize 代理以通过 Charles Web 代理工作? - 2

    我正在使用Ruby/Mechanize编写一个“自动填写表格”应用程序。它几乎可以工作。我可以使用精彩CharlesWeb代理以查看服务器和我的Firefox浏览器之间的交换。现在我想使用Charles查看服务器和我的应用程序之间的交换。Charles在端口8888上代理。假设服务器位于https://my.host.com。.一件不起作用的事情是:@agent||=Mechanize.newdo|agent|agent.set_proxy("my.host.com",8888)end这会导致Net::HTTP::Persistent::Error:...lib/net/http/pe

  5. ruby-on-rails - 如何用不同的用户运行nginx主进程 - 2

    A/ctohttp://wiki.nginx.org/CoreModule#usermaster进程曾经以root用户运行,是否可以以不同的用户运行nginxmaster进程? 最佳答案 只需以非root身份运行init脚本(即/etc/init.d/nginxstart),就可以用不同的用户运行nginxmaster进程。如果这真的是你想要做的,你将需要确保日志和pid目录(通常是/var/log/nginx&/var/run/nginx.pid)对该用户是可写的,并且您所有的listen调用都是针对大于1024的端口(因为绑定(

  6. ruby - 从 sinatra 中的 before do block 返回不同的值 - 2

    有没有办法在sinatra的beforedoblock中停止执行并返回不同的值?beforedo#codeishere#Iwouldliketo'return"Message"'#Iwouldlike"/home"tonotgetcalled.end//restofthecodeget'/home'doend 最佳答案 beforedohalt401,{'Content-Type'=>'text/plain'},'Message!'end如果你愿意,你可以只指定状态,这里有状态、标题和正文的例子

  7. ruby-on-rails - Sunspot:如何对具有不同值的多个字段进行全文查询? - 2

    我想用sunspot重现以下原始solr查询q=exact_term_text:fooORterm_textv:foo*ORalternate_text:bar*但我无法通过标准的太阳黑子界面理解这是否可能以及如何实现,因为看起来:fulltext方法似乎不接受多个文本/搜索字段参数我不知道将什么参数作为第一个参数传递给fulltext,就好像我通过了"foo"或"bar"结果不匹配如果我传递一个空参数,我得到一个q=*:*范围过滤器(例如with(:term).starting_with('foo*')(顾名思义)作为过滤器查询应用,因此不参与评分。似乎可以手动编写字符串(或者可能使

  8. ruby-on-rails - 如何找出拦截 'method_missing' 的内容 - 2

    使用Ruby1.8.6/Rails2.3.2我注意到在我的任何ActiveRecord模型类上调用的任何方法都返回nil而不是NoMethodError。除了烦人之外,这还破坏了动态查找器(find_by_name、find_by_id等),因为即使存在记录,它们也总是返回nil。不从ActiveRecord::Base派生的标准类不受影响。有没有办法追踪在ActiveRecord::Base之前拦截method_missing的是什么?更新:切换到1.8.7后,我发现(感谢@MichaelKohl)will_paginate插件首先处理method_missing。但是will_pa

  9. ruby - 拆分字符串并分配给不同的变量 - 2

    我从ui中得到日期范围为-approved_between"=>"2013-03-17-2013-03-18"我需要拆分此approved_start_date="2013-03-17"和approved_end_date="2013-03-18"...我希望使用它在mysql中查询,因为mysql中的日期格式是created_at:2012-07-2810:35:01.我正在做的是:approved=approved_between.split("")approved_start_date=approved[0]approved_end_date=approved[2]很确定这不是处

  10. ruby - 如果散列有 key ,则使用它。否则,使用不同的 key - 2

    response是一个散列,可能看起来像以下两种情况之一:response={'demo'=>'nil','test_01'=>'DemoData'}或response={'test'=>'DemoData','demo'=>'nil'}我想做这样的事情:ifresponse.has_key?'test_01'new_response.update(:nps_score=>response['test_01']elsenew_response.update(:nps_score=>response['test']end是否有更“Ruby”的方法来解决这个问题?也许使用||的东西运算符(

随机推荐