FreeCAD中几何元素类型的适配误差

FreeCAD是基于opencascade的。对于一些几何元素的提取时,偶尔会产生一些误差。例如,把线段判断成更广义的BSplineCurve, 把圆锥面判断成更广义的旋转面等等。本文将对这些可能存在的偏差做一些记录。

  1. 线段识别为BSplineCurve, 此时虽然是BSplineCurve,但是该曲线所属边只有2个顶点。这种情况下,如果对线段类型要求严格,则需要对其进行纠正。

需要擅长于使用pyke中的in规则

前面在pyke引擎的总结文章中多次提到pyke是为了寻找一个集合中的组合方式而构建的推理引擎。在一种方式里,我们可以通过kfb文件或者_assert,add_universal_fact等方法去声明很多事实。但在构建一些规则的时候,我们需要一个模式去在一些其他方式构建的集合中去遍历。例如我们需要某一个对象遍历从0到10之间所有的数字,来检验一些规则。在这种情况下,我们就需要使用$pattern in python_iterable这样的方式来创建遍历集合了。使用规则示例如下:

test
        use test($pair)
        when
                $first in [0,1,2,3,4]
                $second in ['a', 'b', 'c']
                $pair = str($first) + '_' + $second


测试结果:(with engine.prove_goal(<rule_base>.test($pair)) as gen: ....)

result: {'pair': '0_a'}
result: {'pair': '0_b'}
result: {'pair': '0_c'}
result: {'pair': '1_a'}
result: {'pair': '1_b'}
result: {'pair': '1_c'}
result: {'pair': '2_a'}
result: {'pair': '2_b'}
result: {'pair': '2_c'}
result: {'pair': '3_a'}
result: {'pair': '3_b'}
result: {'pair': '3_c'}
result: {'pair': '4_a'}
result: {'pair': '4_b'}
result: {'pair': '4_c'}

pyke推理引擎关于*$pattern的注意事项

定义

*$pattern在pyke中用来匹配不能确定长度的tuple模式。

必须出现在tuple模式匹配中

根据这个定义,我们首先得出第一条规则,*$pattern必须出现在tuple模式匹配之中。例如:

use someRule($value0, $value1, ($inValue0, *$theRest))

并且这样的方式不能出现在非tuple的匹配中。例如下面这样:

use someRule($value0, $value1, *$theRest)  #不能这样使用

必须出现在tuple模式匹配的后面

pyke解释器在很大程度上是参照python的,*$pattern只能出现tuple的最后。像下面这样是不行的:

use someRule($value0, $value1, ($inValue0, *$theRest, $theLast))

在规则内部对*$pattern赋值时只能用tuple赋值

非tuple类型的数据给pattern赋值是会报错的。看下面示例

test
        use test(($theOne, *$theRest))
        when
                $theOne = 10
                $theRest = (1,2,3,4)  #正确
                $theRest = (1,)     #正确
                $theRest = (1)       #错误
                $theRest = 1         #错误

使用pyke推理引擎的一些原则

推理引擎解决的基本问题是对一些命题的排列组合方式做试探的问题。它可以对很多事实分层次,分逻辑层面做组合问题。虽然说在pyke系统用的krb里面我们可以利用goal来实现很多非排列组合的问题,例如构建一些运算函数等。但是,随着使用的深入体验,我发现并不适合做。很多该是由程序代码来完成的任务还是交给程序代码来做更合适。我是基于以下理由给出此结论:

python对静态代码块的嵌套有限制

python或者这很多编程语言是对静态代码块的嵌套做过限定的。而每一次bc的使用都会引入一个新的嵌套代码块。如果在一个bc里面调用多个bc很容易就会让pyke生成的python代码超出python静态嵌套限制。要知道python只允许嵌套20个代码块的深度。而每一个bc规则的嵌套,会引入2个以上的静态代码块。一个bc规则中,嵌套8个以上bc规则,就会开始危险起来了。

直接使用python代码可以应用很多的python资源

bc规则的设定是用来回答这样的问题的:在某某集合中,是否能找到满足某某条件的组合? 例如,在一些列路径的选择组合中,能否找到一种组合走出迷宫?在一系列数据库的检索语言中,能否找到合适的组合来检索我们需要的内容?如果我们需要证明的问题是,a和b两个对象是否满足某个条件?那么这个问题就不适合用bc规则来写了。因为在krb文件中直接使用python可以调用大量的python资源,尤其是krb支持bc_extra的模式。我们甚至可以专门写python库,然后再在bc_extra中import这些python库。要知道在bc中,我们甚至都没有直接使用if, else。

结论

推理引擎是个很好的东西,在应用它的时候,我们要发挥它的长处来使用,而不是滥用它。基本使用逻辑是,首先构建推理事实库,然后再构建用于寻找这些事实库中满足某些规则的排列组合的问题。并且我们要用python代码来丰富bc规则的能力,然bc代码块能够对一些非排列组合验证性的问题来做出判断。

计算多边形的最小包围矩形

在opencv中可以根据多边形的顶点计算一个最小包围矩形。这个矩形可以是任意位置和方向的,不拘泥与必须是长宽都要水平和竖直。

请看下图:

其中蓝色的矩形和红色的矩形都是中间那块像素块的包围矩形,显然红色的矩形面积更小。有时候我们就需要找到一个像素块的最小包围矩形来进行测量,对齐或者其他的一些应用。opencv中提供了minAreaRect函数来处理此问题。

函数原型

以下是python版本的函数原型:
(center(x, y), (width, height), angle of rotation) = cv2.minAreaRect(points)

从这里可以看出,minAreaRect函数接收的是一串像素点坐标,然后再给出一个矩形的参数。这里的points一般指的是一个像素块的包围多边形,而不是该像素块所有像素点的集合。一般此函数会和cv2.findContours函数配合使用。在检测出矩形后,如果我们想把该矩形的四个点坐标都提取出来,可以采取如下方案:

rect = cv2.minAreaRect(cnt)
box = cv2.boxPoints(rect)
box = np.int0(box)

检索图形周多边形

这个功能一般用在检索mask图形区域的周边多边形。findContours函数是opencv中很基本的一个函数。此外将对其应用做简要介绍。

检索高亮区块

findContours函数一般都检索高亮像素块的边界。例如下图所示:

此图中,从左到右依次是:原图,灰度图,阈值取反分割图,周边多边形图。图中数字部分高亮的时候,就会被检测成边界。这相当于在对图做threshold运算时采用了TRESH_BINARY_INV运算。

这个示例是为了提示我们,在应用cv2.findContours函数时,要让感兴趣的部分高亮显示。如果反过来,让背景高亮,内容黑暗(白板黑子)则会发生错误。如下图所示:

函数原型

findContours函数的原型如下:


void cv::findContours	(	
  InputArray 	image,
  OutputArrayOfArrays 	contours,
  OutputArray 	hierarchy,
  int 	mode,
  int 	method,
  Point 	offset = Point() 
)		

其中输入参数image是一般的图形对象,contours是检索出来的周边多边形。结构一般为这样的一个数组:[[[x00,y00], [x01,y01], ...],[[x10,y10],[x11,y11],...], ...],数组中0轴上,每一个对象都是一个封闭的多边形。

hierarchy指的是contours中多边形的层次关系。其数据结构为这样一个数组[[next0, previous0, child0, parent0], [next1, previous1, child1, parent1], ...]。这里的next, previous, child, parent都是整数类型,用来指示contours对应的多边形序号。next和previous指的的多边形的顺序关系,child, parent指的是包含与被包含的关系。

函数中mode的值,影响了识别多边形的结构组成方式。mode的取值有如下几种情况:

cv.RETR_TREE,这种情况会检索出所有的区块边界多边形,并对区块的包含与被包含形成树形结构。

cv.RETR_EXTERNAL, 这种情况只检索各区块的最外侧边界多边形。因此在hierarchy中不会形成child, parent的结构,并且在cv.RETR_TREE中能找出的所有parnet不为-1的多边形在cv.RETR_EXTERNAL的情况下都不会被检测出来

cv.RETR_CCOMP, 这种情况下检索算法也会像cv.RETR_TREE一样检索出所有的边界多边形。不同的是,它不会形成树形结构,而只会形成两层结构。在第一层里面只会有最外层多边形,第二层都是第一层中出现多边形的第一个内层多边形。如果在第二层中还有嵌套多边形,则此多边形会归入第一层中。

method参数用来指示检索边界时采用的算法,method可能取cv.CHAIN_APPROX_NONE, cv.CHAIN_APPROX_SIMPLE。前者不会对检测出来的多边形做任何的简化或者说近似的计算。而后者会对检测出来的多边形做简化运算,例如检测出来的一个像素区块时正方形时,前者的多边形可能会包含这个正方形周长上面经过的所有像素点,而后者的多边形可能就只有多边形4个角的顶点。

基于阀值的图形分割

基于阈值可以给图形做分隔,提取出我们需要的像素点。用来制作mask或者分离图片的背景等都有很多实际的应用。

cv2.threshold

最基本的定义一个全局阈值,然后对整体图片应用这个阈值。可以用来生成mask然后再用这个mask来做其他的操作。更进一步的,此算法还可以使用Otsu优化过程,让算法自己来决定阈值。要注意的是,Otsh算法它是假设这个图片的前景与背景区分得非常分明。它根据对整体图片的统计,然后找到两个最大的像素频率,并假设此图的背景与前景就对应这两个最大频率。然后用这两个最大频率出现的像素数值来计算阈值。因此Otsh算法在前景和背景有好几个像素集中区域时就会出现问题。

cv2.adaptiveThreshold

与常规的全局应用阈值不同,这里的(adaptiveThreshold)是调整式的应用阈值。它会应用局部的阈值来针对局部的像素来进行运算和判断。

此算法实现的基本思路,是对每一个像素的一定范围内的领域找一种平均值出来。然后用这个平均值减去一个常数,最后得到一个判据数值。然后在对比该像素与此判据数值的大小,并以此来判断该点是背景还是前景。对图像中所有点都应用此算法,最后得到一个新的图。如下图所示,计算图中红色所标识像素点是前景还是背景时,则需要综合计算黄色包围圈内所有像素的一种平均值来考虑。一般情况下,我们可以使用算数平均值或者高斯平均值,采用算数平均值时,该黄色包围圈内所有像素点对平均值的贡献一样,而使用高斯平均值时,在黄色包围圈内,距离该点越近,则对该点平均值的贡献越大。

我们可以看看opencv中adaptiveThreshold函数的原型:


void cv::adaptiveThreshold	(	
  InputArray 	src,
  OutputArray 	dst,
  double 	maxValue,
  int 	adaptiveMethod,
  int 	thresholdType,
  int 	blockSize,
  double 	C 
)	

其中,src, dst是输入图和输出图片,maxValue是判断为前景时填充的像素值,adaptiveMethod值的是前面提到的求相邻点平均值的算法。主要值的就是算数平均值和高斯平均值。thresholdType指的是如何填充0和maxValue的方法。或者是说,如何区分前背景的方法。至于后面的blockSize是前面提到算法中要采用的领域的大小。图示示例中,领域大小为7。C为计算出来平均值后再减去的那个常数。

更多基于神经网络的图形分割

对于一些更难以区分前背景的图片的应用场景,我们还可以使用人工智能的方法,把独立对象都识别出来,并给这些独立对象用于图形分段。此类方法已经超出此文要讨论问题(基于阈值)的范畴。要了解更多,请检索关键词:Mask R-CNN , U-Net segmentation network

pyke推理引擎的一些注意事项

pyke是一个与prolog类似的纯python版本的推理引擎。这一类推理引擎比较适合用在一些需要处理大量基于规则来做判断或者决策的应用。规则成为了这类系统里面最重要的东西。有人说,什么东西是软件架构?最重要的东西就是你的软件架构!因此,在遇见这类由规则来主导整个应用的问题的时候,规则管理系统就变成了这个应用的架构。现实世界中很多问题都是这一类应用。因此,一些推理引擎就应运而生了。人们最早可能从80年代开始就有推理引擎的意识了。如果我们能够较好的应用推理引擎,就可以使我们能够脱身于编写繁杂的推理逻辑代码。例如对一系列数据的遍历,if-else判断,等等。有了基于规则而推理的引擎,我们就可以把重心放在想办法将已知条件抽象成事实,将逻辑推理抽象成规则的任务上。这就让我们可以放开推理部分的构建,能够

大幅度缩短开发周期。

我花了一些时间去阅读pyke的文档,从中基本理解了pyke的应用逻辑和语法。并开始尝试了自己使用pyke推理引擎来做一些实际上的小测试。下面陈列一些在这段时间内,我发现的需要特别注意的一些事项:

知识库文件名必须不同名(后缀名不算)

在pyke的框架下,初始化推理引擎的时候会指定一个目录。然后pyke会在那个目录下递归地寻找所有的*.kfb, *.krb和*.kqb文件,并把这些文件中记录的fact,rule和question都编译或者准备好。这些文件统称为pyke的知识库。并且pyke会以这些文件名(不包含后缀)为该知识库的名称。正因为如此,在pyke遍历的过程中如果遇见两个文件名相同的时候就会产生冲突。哪怕是后缀名不同都不行。例如base.kfb和base.krb同时存在是不允许的。

关于知识库实例的归属问题

在pyke的框架下每一个知识库实例都是属于某个知识库之下的。它们具有通用的从属关系。这一点在不同类型的知识库的体现方式有所不一样。

在.kfb中的每个事实实例都是属于与该.kfb文件名对应的那个知识库下的。例如base.kfb中的所有实例都是属于 base 名下的。我们假设 base.kfb 中有一条fact是sonOf(david, jim),那么在整个框架下定位这一条fact时,则需要用base.sonOf来指定。要注意的是,在base.kfb文件中,并没有显示的指明sonOf是属于base知识库的。

在.krb中的规则实例用来表述与知识库的从属关系时又不一样。在正向推理的规则中都要写明每一个知识库实例所属的知识库。例如:

son_of
    foreach
        family.son_of($child, $father, $mother)
    assert
        family.child_parent($child, $father, father, son)
        family.child_parent($child, $mother, mother, son)

在这段规则中,每一条前提和结论都显式的写名了其所属的知识库为family。

但是在逆向推理的规则中,我们在use声明中省去知识库的标识符。并且在when中也可以选择性的缺省知识库的标识符。例如:

parent_and_child
    use child_parent($child, $parent, (), $parent_type, $child_type)
    when
        child_parent($child, $parent, $parent_type, $child_type)

这一个反向推理的规则中,use下的child_parent没有像前面正向推理一样写明知识库。而且在use下是必须省略的。省略掉它,是因为反向推理规则都有一个默认的知识库,那就是这个规则库的基本规则类类名所对应的知识库。我们假设这个规则库文件名是myRule.krb,而它是继承自fatherRule.krb,那么这里所缺省的知识库名就是fatherRule。也就是说,如果把这个示例中的use写全,那么应该就是

use fatherRule.child_parent($child, ...)

但pyke规定了,这种情况下必须缺省知识库。然后在when的声明中,每个规则实例是可以指定知识库名也可以不指定。在指定的情况下,其默认知识库名的方法与use相同。

fedora下挂载lvm盘

挂载硬盘是linux下常用的一个操作。但是最近在挂载lvm盘的时候稍微遇见些小麻烦。使用lsblk命令显示硬盘信息如下:

loop0                            7:0    0    20K  1 loop /var/lib/snapd/snap/hello-world/29
loop1                            7:1    0  32.3M  1 loop /var/lib/snapd/snap/snapd/11588
loop2                            7:2    0  61.7M  1 loop /var/lib/snapd/snap/core20/975
loop3                            7:3    0  99.1M  1 loop /var/lib/snapd/snap/core/10958
sda                              8:0    0 931.5G  0 disk
├─sda1                           8:1    0   600M  0 part /boot/efi
├─sda2                           8:2    0     1G  0 part /boot
└─sda3                           8:3    0 929.9G  0 part /home
sdb                              8:16   0 931.5G  0 disk
├─sdb1                           8:17   0    10G  0 part
├─sdb2                           8:18   0     1G  0 part
├─sdb3                           8:19   0 911.5G  0 part
│ ├─fedora-home                253:1    0 775.5G  0 lvm  /home/super_stone
│ ├─fedora-root                253:2    0   120G  0 lvm
│ └─fedora-swap                253:3    0    16G  0 lvm
└─sdb4                           8:20   0     9G  0 part
  └─fedora_localhost--live-var 253:0    0     9G  0 lvm
sdc                              8:32   0 931.5G  0 disk
└─sdc1                           8:33   0 931.5G  0 part /opt/backup
zram0                          252:0    0     4G  0 disk [SWAP]

此时我们的目的是挂载sdb/sdb3下面的fedora-root硬盘。此时如果我们直接mount sdb3或者mount sdb3/fedora-root都会出现错误。正确的做法是通过fdisk -l查找到该虚拟盘的真实位置。我通过fdisk -l命令找到了fedora-root相关信息如下:

Disk /dev/mapper/fedora-root: 120.01 GiB, 128857407488 bytes, 251674624 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes

于是,我执行如下命令成功挂载了该虚拟盘:

mount /dev/mapper/fedora-root /opt/old_disk

配置VPN客户端时出现连接成功但却无效的情况

最近在配置VPN时,我遇见过这样的案例。openvpn的客户端,在无线的环境下连接成功后,vpn网络正常工作,计算机可以正常通过vpn访问网络。但时,同一台机器,在有线连接的环境下却不行了。在那种情况下,就像是vpn没有使用,而仍然是使用着原先的网络一样。并且还会出现各种各样的奇怪的情况(访问google的网页,却收到facebook的证书)。

这是网络链路竞争的结果,计算机有多个网络接口。在windows操作系统下也可以说是由多个网络适配器。当我们连接好openvpn后,原先的interface/network-adaptor也会同时处于工作状态。这样,在计算机访问网络时,就同时有多个选择。在默认情况下,操作系统有自己的判断机制,来按照最优线路访问网络。在实际的测试情况下,windows系统在默认的情况下,几个网络适配器的优先级是:有线网络 > openvpn > 无线网络。

这个优先级的顺序就导致了第一段的现象。在有线的情况下,即使连接了openvpn,系统仍然会走有线网络的适配器,而不是走openvpn的适配器。遇见这种情况,我们可以通过手动调节网络适配器的优先级来解决问题。在ipv4协议中,网络数据会根据网络链路的一个结点数来寻找最优线路,我们可以通过手动配置网络适配器ipv4协议的结点数来解决问题。配置方式如下:

  • 通过控制面板或者网络配置界面进入更改网络适配器选项的界面。如下图所示:
  • 然后“右击某网络适配器->属性->选择ipv4协议->属性->高级”打开高级tcp/ip设置,如下图所示:
  • 在这个界面,撤销掉“自动跃点”的选项,给此适配器手动配置一个接口跃点数。这个跃点数配置得跃高,该适配器在其余适配器中的优先级就越靠后。

如果我们使用以上的方法,把openvpn适配器的接口跃点数配置成5,而把有线网络适配器的接口跃点数配置成10,这样就可以让openvpn优先级大于有线连接了。问题到此就能解决了。