ImagePy解耦SCIWX,便于开发者树立独立品牌 - 知乎

https://zhuanlan.zhihu.com/p/107473067html


本文基于ImagePy做者给群友回答问题的讨论中整理而来,阅读须要发散思惟,若有不解,能够查看代码连接。前端

正文:

ImagePy的canvas解耦对程序员是好事,而后imagepy能够再与后面的深度学习结合,这对小白是好事,由于深度学习有不少的工做是前面对图像的处理,好比裁剪、旋转以及标注等。如今的深度学习库着眼于后面的训练,前面的图像操做是通常用的是第三方库,好比cv,二者是分离的。node

其实imagepy有个问题是,开发者用容易喧宾夺主。基础功能太多了,本身加的插件都不太起眼。若是能解耦,又能快速集成,就会有开发者愿意选择.就是打开没那么多功能,就只有本身加的少数功能,python

就是把现有的插件都干掉,只保留框架.我能深入体会这句话,以前就想用imagepy作开发,可是工具太多了,给别人用也找不到,git

分红两个项目,一个空壳,一个插件系统。程序员

不是重量问题,有一些开发者的目的是,树立品牌概念。github

作一个插件太不起眼,没动力。算法

若是基础组件能像plt和napari那样使用,而后又能够在空壳上快速搭建插件应用。就比较好了,canvas

其实如今的imagepy只要把menus菜单里面的东西删光,也是同样的。。。后端

那就变成了带标记和显示功能的ui。

sciwx初步完成

https://github.com/Image-Py/sciwx/tree/master/sciwxgithub.com

sciwx是相似matplotlib的一套绘图库,不一样的是组件化更好,能够快速搭建上层应用。

其实就是把imagepy里面的canvas,3dmyvi,gridtable,markdown,还有直方图,曲线面板,鹰眼什么的作成了独立控件。

各类组件,画布,表格,参数对话框生成,都放到sciwx里面了。

主要是imagepy里面的ui部分,独立出来了


又加了一个matplotlib式的用法

如今imagepy里面的三维,二维面板,均可以相似matplotlib的简单语法独立调用了。

还能够经过实现接口,set给工具栏实现交互。


除了napari的三维功能,其余的把imagepy的canvas独立出来,均可以实现。

其实napari也是用了一个比较成熟的三维可视化工具包装的

画图和普通的处理有点区别,其余的处理能够用很长时间作处理,最后刷新一次。

可是画图是动一下鼠标就要更新一次,就要不停的从cpu送显存。因此napari那种用显卡可视化的,就有问题了。

除非开发局部纹理节点更新,这个水就深了。

图像在opengl里面是纹理。画图若是高效一些,应该局部更新纹理数据。

这个应该不是python层面能够解决的问题了

Amira优化的彷佛还不错,不过是商业软件了,无法比

路线不一样,napari是gpu渲染,我如今的是cpu渲染。

不过优化得还不错了,最近支持了复数,对数展现。

复数,不是提早求模,log送过去的。

画布实时作采样,求模,对数。依然有上百帧率。全屏状态下40帧。

其实z和c的支持都已经很成熟了,独立出来应该仍是有用。

就像plt那样,三五行,show出来,多图层多通道,能够设置假彩色。

canvas里面都是很极端的代码,一切性能优先

box只是控制显示区域逻辑,还不涉及太多性能

imutil里面,写法都是在我能力范围内,优化到极致了。

复数的场景是用在什么地方?傅里叶变换吧,其余的我也没想到


toolbar重构

此次重构的目的是

1. 组件能够当成wx的panel,放在其余UI项目中使用

2. 能够像plt,napari那样,简短的代码show出图像或表格

3. 测试插件或工具能够不须要拷贝到特定目录,能够用代码快捷加载测试

4. 能够从纯净版环境快速搭建特定的简单应用,好比标记工具

5. 现有插件所有以一个插件管理项目外部加载到纯净版

简单应用若是不想准备图标,能够用一个字母代替做为工具的logo


imagepy的定位能够是“一站式图像处理及智能识别库”


https://github.com/Image-Py/cellpose-plgs

cellpose这个例子很好,imagepy正好做为胶水框架,把整个流程串起来:前端提供缩放、裁剪、标注、显示,后端集成深度学习模型,提供训练和预测

如今只是一个训练的模型,而后cellpose本身也带一个简单的ui,上面有标记功能,很简陋。先把这个提交了,而后问一下做者标记以后怎么用。

https://github.com/MouseLand/cellpose/issues/11


cellpose的那个UI,若是在ImagePy里面作,很轻松的。插件作好了,最多再作一个widget,就能够作到很定制化的程度了。

不过他这个我记得半径那个参数挺重要的,有些比较难的例子还须要调一下

那个不是磁性套索,就是鼠标轨迹。不过感受不是用来修复的


他们这个工做有几个很是简单的点比较有意思,第一个是他们的训练数据不是一个类别的,是很是多,很是广块状物体,第二个是他们输入以前的缩放,针对不一样块状物体,能够先缩放大小再输入,输出再缩放回来。

他们好像就是用的cellprofiler+cellpose标记的他们本身的数据


for i in range(n):

mask = img==n

xxxx 找mask的轮廓

这么写,若是图很大,而且里面标记又多,就很是慢了。

mask = img==n 这一句要反复分配大内存。

没有工程化的优化思惟的

毕竟重点在机器学习

不过那个写法真的不是电脑好能抢回来的。若是图像2048平方,里面1024个碎片。这个目测就几十秒了。

为了分析一个小块label,先复制整个图像,而后作成bool掩膜。

我估计他们可能不care这些,因此问了一下需不须要帮忙优化。用的话就套个近乎,不用就算了。

github图看不到问题

https://github.com/Image-Py/cellpose-plgs 我刚写的,七牛云,我这里看不到图,不知道什么缘由。

github作图床,怎么拿到绝对连接呢

在issue里传图,传完后就有个markdown格式的连接

githubusercontent.com 可能被墙了


推荐阅读ImagePy_Learn学习系列

土盐:ImagePy智能标注工具源码学习记录

土盐:ImagePy_Learn | 图形学绘制代码学习:core\draw\fill.py

土盐:ImagePy_Learn | 图形学绘制代码学习:paint.py

土盐:ImagePy_Learn | 图形学绘制代码学习:core\draw\polygonfill.py

土盐:基于ImagePy编写插件之迷途探索WXPython

土盐:ImagePy起手式IPy.py代码解析


金属材料的熔池图像弱分界线提取问题

底下这根分界线有什么方法能识别出来?

https://forum.image.sc/t/weak-boundaries-extraction/34048/2

https://forum.image.sc/t/weak-boundaries-extraction/34048forum.image.sc


方法1 最短路径方法

其实最短路径更直接可行一些


https://scikit-image.org/docs/stable/api/skimage.graph.html#shortest-path


from skimage.io import imread
from skimage.graph import route_through_array
import numpy as np
import matplotlib.pyplot as plt

arr = imread('path.png')
arr = (arr-arr.min())/np.ptp(arr)
arr[:,[0,-1]] = 0

indices, weight = route_through_array(arr, (0, 0), (-1,-1))
path = np.array(indices).T

plt.imshow(arr)
plt.plot(path[1], path[0], 'red')
plt.show()

路径计算大概0.5s吧


ImagePy宏命令

8-bit>None
Max>{'num': 170.0}
DOG>{'sigma1': 0.0, 'sigma2': 3.0, 'uniform': True}

作一些预处理,也就是一个全局最小值限定

拉通的意思就是求从左到右的最短路径

思路是,对图像进行预处理,使得黑白尽可能分明,而且排除上方特别黑的干扰。而后缩放到0-1之间,再把最左边最右边两条竖线设定成0,意思是能够没有代价的移动。而后求左到右的最短路径。

这个graph的方法,就考虑了路径代价。有了路径代价的约束,就加入了线条平滑的这个先验信息。

像素亮度表明通过的代价,从左到右作路径规划。

可是起点钟点很差设定,因此人工把左右竖条设置成0,意思是无成本移动。而后左上角到右下角作路径规划。

图像,矢量,图论 三条腿走路

不少形态学,距离,轮廓描述,特征检测,实际上是矢量问题。须要用计算几何算法。

图像的距离为何是矢量问题,不是像素之间计算么?

单次距离运算确定是矢量更快,图像是采样过的距离场。

好比你刚才那个图,标注的几个参数,就应该用矢量运算。


idx = np.argmin(np.linalg.norm(left_broundary - imgpoint, axis=1))
the left_broundary[idx] is the start point. (the right is similar)

拿到上轮廓,而后中下方虚拟点,求左右两侧的最近点

这个已是很纯正的矢量问题了

拿到上下轮廓,后续的全部测量,都是矢量问题了。

能够虚拟一个中间下方较远的一个点,而后起点,终点分别就是上轮廓,左右两边,最靠近虚拟位置的点。

若是必定用图像实现,是这样的.可是就很是低效了

图像的距离是计算距离场,少数点的距离,用矢量更高效。

用这两个点作最短路径,应该稳了

扩充功能,零碎时间都好说。重构这种事情,没弄完会分散精力。



又一个找起点的方法 反正是求最近点,因此仍是能够用这个工具。

不能在原图上作,阈值,或者和某个数max一下。目的是消除特别暗的影响。 dog实际上是把尺度映射成亮度的方法

若是走最大值,先乘以-1,而后减最小值,+单步距离代价。

其实平时写的也比较随意,以前给亓总的,

(arr-arr.min())/np.ptp(arr)

不过放到imagepy里面,我通常都会作内存优化。

内置一个sobel,就是磁性套索工具了

方法2 水平集方法

https://github.com/Image-Py/imagepy/blob/e82f80ba4d8ccfe562c83f6dd878902223727e2d/imagepy/menus/Process/Segment/active_plgs.py

https://imagej.net/Level_Sets

imagej 和imagepy的水平集


方法3 构造方向滤波器

https://mp.weixin.qq.com/s/8qmw62tqZuMs3Vm5yXS5OA


ImagePy路径分析插件一教一实现之旅

接着上面的那个素材整合到ImagePy

https://github.com/Image-Py/imagepy/blob/master/imagepy/menus/Analysis/Skeleton%20Network/graph_plgs.pygithub.com
https://github.com/Image-Py/imagepy/blob/master/imagepy/menus/Analysis/Region%20Analysis/regionprops_plgs.pygithub.com

起点终点用roi就能够

ips.roi能够拿到,而后load里面判断一下是否是point类型而且只有两个点,若是知足,run里面求路径,而且高亮绘制。

那里面其实还有一个原理,就是dog能够凸显特定尺度的目标。

加载analysis菜单下面吧

Shortest Route


这两个要作个参数弹窗么

支持单通道8-bit, 16-bit, int, float

还有req_roi,而且还不够,还要再load里面断定一下

必须是point类型,只有2点,不然alert一下

其实再细致一点,应该先检测起点,终点

想着点两个点起码比描个线要轻松吧

这个确定有办法很稳定的全自动完成的

不用角点,用最两端的也好啊

其实中间两个点也好说,这就是昨天我说的,图像,矢量,图论,三条腿走路。

属于filter类

能够参考分水岭那个,加一个输出选项

三个选项:掩膜,高亮在原图上,擦除背景

对了,图像最大最小值,能够用minv, maxv = ips.range得到

由于对于其余类型,可能不是0,255

route_through_array函数返回的是坐标,我想的是新建一个全是0的矩阵,而后把对应坐标的值写255

索引,img[rs,cs] = 255

这个应用不必新建数组吧,能够原图上操做。

为何ips.roi.body获取的点的坐标有小数?

roi是矢量数据,有小数正常。

截取一下吧,而且不保证在图像内部。

不在图像内部的问题,能够暂时忽略


我理解的用户从坐标上鼠标选取的应该是某一个像素的坐标,理论上不该该是小数把

若是通过放大,再选取呢?或者是从shapefile,gps等导入的地理数据呢


其实应该这样作的,用line对象

而后依次计算,连线

好比一个圆环,能够点三个点

这样也不必限定两个了

反正有几条,就顺着依次作

能够支持多线

判断一下是line类型的roi,就能够放过去了

不用支持point了

point也能够用一条单线

支持多条多段line就能够了,不支持point

一个line上的是一组?

对,两重循环,第一层是每条line,第二层是每条line上的点。


可是有个问题,就是一个line上有3个point的话,好比p0,p1,p2。那要算多少次?

两次啊

0-1, 1-2

p0,p2不算一次?那若是是想获得个封闭的路径就要P0,P2再算一次?

不用了


若是须要闭合,用户本身再点会起点就能够了


1-2,2-3,3-4 配对,能够这样写

for p1,p2 in zip(line[:-1], line[1:])

一条肯定了,其实接下来应该是描边,而后完成

效果和polygon tool效果基本同样的

就是加了删除点和拖动

polygon也支持拖动,拖动以后,要动态作相邻的轨迹更新

polygon能够总体拖动,也能够节点拖动

拖动摄取,你用的x,y直接计算的吧

不用总体拖动




展现就暂时这两种能够吗?要加个mark的么?

filter貌似无法另外show

由于若是是批量,就会爆屏

因此,都在原图上动吧

我记得是有个白线获得mark的功能,若是须要获得mark直接用那个功能就行,在流体插件好像

不须要吧,这里面更简单

1. img[r,c] = max

2. img[:] = 0, img[r,c] = max

3. img[:] = 0, img[r,c] = snape[r,c]

这三种方式展现

0其实应该是min,不是0


line的坐标放在哪一个属性了?我打印了ips.roi.body是空的

就是body吧

右键落下去才算完成,这个只是mark

我说颜色怎么不同




可是若是勾选了preview,再换ouput的mode的话会出错

run里面加上img[:] = snap

其实应该分析用snap,展现用img

还能够作一个tool,在智能画笔旁边

就是动态点点,而后中间用mark绘制轨迹,还能够设置线条宽度,最后确认了再绘图。

实时绘制路径


至关于智能套索了,加上填充应该就全了
是的,能够用快捷键加上自动闭合,内部填充,描边等逻辑
而后还要设定一下,是最小值,最大值,仍是梯度最大值吧
梯度最大值
先求梯度,再取反,再求最优路径.结果就是沿着变化大的地方走
仍是能够左键加控制点,而后实时计算的只是当前距离上一个控制点之间的。





智能套锁就是用的这个路径?我记得ps有个磁性套锁

磁性套锁感受应该用mark

其实内部的数据结构是本身维护的,mark只是用来展现

最后路径出个mark而后用户还能够手工调整

本身维护两个序列,一个控制点序列,一个智能路线。

控制点序列能够手工拖动

其实内存维护的主要是控制点序列,顺带产生了轨迹序列。

由于mark的结构不必定知足咱们的交互逻辑

[控制点1, 控制点2, 。。。]

[序列1, 序列2, 序列3 。。。]

当前鼠标点,当前点到最后控制点之间的序列

有imagepy以后,已经必定程度上隔离了。若是直接面对wx,qt,要看的东西还不少。

画个图都要双缓冲,还有各类闪屏,绘图性能问题

其实绘制能够分层。

1. 绘制肯定的路径

2. 绘制肯定的控制点

3. 绘制末端路径

用三个mark就能够了,一旦落下鼠标,随即当前点加入控制点,末端路径加入肯定的路径

move里面始终计算当前位置到最后一个控制点

不要用mark当核心数据结构,只是须要展现的时候临时组装的。

取消能够用右键啊,好比判断右键点击了,就把最后一个控制点弹出,也把最后一段路径弹出。

而后从新构建mark,set给ips

mark只是负责展现的

核心数据结构确定是根据需求本身维护,可是这个转换过程应该尽可能节省内存。就是打包的时候应该是不须要大量的内存复制的。

本身维护这个逻辑,最后只是数据打包成mark展现

因此维护的轨迹,就能够按照mark能够直接用的方式来维护。mark其实只是一个重载draw方法的替代

若是要本身draw,就须要用gdi,就直接和界面打交道了。

mark底层仍是用dc绘图的

界面绘图底层确定是dc,或者opengl

roi的颜色不对劲。。

直接dog不行,估计会找到上轮廓

须要新作相似二值化的操做,或者抑制一下特别黑的。

dog只是凸显特定尺度的目标

roi右键是做为结束的,由于默认状况支持多段线。就是不停的左键,会产生多段线

我怎么从鼠标事件获取坐标

仍是要获取wx的panel

继承工具,engine下面的Tool

除了写widget应该没有须要直接写wx的地方了


磁性套索我看了下ps的操做方式,ps的好像是按下鼠标左键防止一个锚点,而后用户沿着轮廓放锚点,能够用delete删除前一个点。

我想还支持下放置的锚点能够修改,就鼠标移动到锚点时候鼠标变成一个手,能够拖动

tool类里面只有鼠标和滚轮的的事件,若是我想用delete删除一个点的话,可能要获取键盘的事件

imagepy有相似机制么?我看有的用组合键盘shift键和鼠标左键,可是这样要放在鼠标事件里面才会刷新,单独用键盘好像刷新不了

磁性套索我看了下ps的操做方式,ps的好像是按下鼠标左键防止一个锚点,而后用户沿着轮廓放锚点,能够用delete删除前一个点。

我想还支持下放置的锚点能够修改,就鼠标移动到锚点时候鼠标变成一个手,能够拖动

tool类里面只有鼠标和滚轮的的事件,若是我想用delete删除一个点的话,可能要获取键盘的事件

imagepy有相似机制么?我看有的用组合键盘shift键和鼠标左键,可是这样要放在鼠标事件里面才会刷新,单独用键盘好像刷新不了

键盘事件还没加,除了ctrl, alt, shift

要不你先用功能键作吧,事后再加键盘支持。

好比shift+click,用来删除点

事件函数里面的key, 能够key['alt']来获取是否按下,key['shift'], key['ctrl']

roi是有功能的,好比生成选区掩膜,还要实现交并补运算。

mark只是用来显示的


距离怎么算的?直接用坐标会有问题.就是你昨天问的,为何roi有小数

key['canvas'].scale能够拿到比例,画布当前的比例尺,会随着放大镜改变,就是真实的屏幕坐标和像素坐标之间的关系。

5个像素支内算选中

可是若是不带比例尺,就会随着画布缩放变化。

key['canvas'].scale

就是选中某个点的逻辑,直接用x,y计算,不太对

由于x,y会受到画布比例尺影响

拖动要判断是否选中吧,就须要计算距离。


由于x,y都是像素坐标系。因此摄取的时候会出问题

好比你放大到很大的时候,离很远就选中了。

除以canvas.scale就回到屏幕坐标系了。


dog问题

经过调控sigma能够凸显不一样尺度的目标,那个问题是用dog凸显细线,而后再作路径规划。

dog会遇到类型溢出问题,要不你就转float吧,而后

img - gaussian(img, 2)

以后标准化到0-1,两端擦除



Shortest Route理论

图像最小值若是是0,那么0表明无代价

这样会致使线路曲折,只要有通路,就钻空子。

可是若是总体加一个常数,这个表明距离代价

意思是,通过实际距离是有代价的。不能无限制的任意走。

这个常数n,比如最优路径,加上几何距离*n

这个n其实必定程度上,是限制线条流畅性的参数。若是n很是大,那么规化出来的路线,就趋于两点之间的直线。这个能够理解吗?

好比原图原本是0-1之间波动的,若是我全图都+10,那么规划出来的路径,就会成为直线。由于相对10来讲,波动很小,那么决策比如尽可能走少的路。

因此,那个插件,建议在note里面加上'2int',而后添加一个step cost参数。

但愿路径除了代价小,还但愿路径是越短越好

常数越大,越接近直线,由于绕路不是0成本的

这个,画了一条干扰线


0代价的时候,确定就是从这条干扰线通过了

加上路径成本,就对了


这个值人工选取,能够自动选取么?

在图像的值域内,无法彻底自动,这个约束就是,你但愿路径直的约束力度

越大其实就越接近直线

路径对角线走和正交走,都是通过一个像素

加了个反转


掩膜问题

process > classify 下面有一个label panel

专门用于标记的,不过有点绕,提示没作好。

大体原理是,对当前图像新建一个掩膜,把原图设置成掩膜的背景,而后选择一下混合模式。

那个上面能够方便的擦除,覆盖,不会破坏原图


ipy的plg除run方法外,支持singleton吗?

dl第一次加载model都很耗时,每次run都是第一次

其实应该作成类成员

Plugin.model = xxx

下次判断是否是已经加载

不过比较扯的是,这里有多种模型,而且每次是根据用户选择加载的。因此就没作处理。

单例调用意义不大,由于ipy主要就是作交互上的事情。却是能够当字典用,把代码拷贝出来。

用字典cache下就行


这里能够查看每一个功能的源码,通常都很是简短。

这种类成员cache,须要写成

类名.xxx的方式,就不会被释放了。

https://github.com/Image-Py/sciwx

新建了一个项目,准备用来剥离ImagePy的组件,能够关注一下,有精力能够作些测试。

一个兼容性错误是这部分的功能

https://forum.image.sc/t/imagepy-add-some-feature-to-simplified-a-network-structure/23643/2

SCIWX问题

demo中少引入了一个包?

from skimage.draw import line

使用pencil工具时报line错误

多是少了,须要加一下pythonpath

或者在demo上面加上sys.path.append('../../')


分水岭讲解

其实分水岭就是从各个种子点你们同步上涨,在领地发生冲突的地方划定界线。因此领地冲突是一个重要条件

0表明还没占领的,

0xffff是边界,

0xfffe是已经肯定是分水岭的,

msk[p]是本身已经占领的

若是是line=True,就划定界线

其实neibours的形状,也决定了界线的形状

若是neighbor是4邻域,界线就是4邻域,neighbor是8邻域,界线就是8邻域

rgb和cmyk概念

lab是公认的,坐标系距离和视觉感知差别最一致的模型。

rgb和cmyk之间,实际上是逻辑或,逻辑且的关系

lab其实只有数值概念,无法可视化。

要可视化也只有把l,a,b强制当成r,g,b.值域要本身变换一下


Shortest Route源码

https://github.com/Image-Py/imagepy/blob/master/imagepy/menus/Analysis/Skeleton%20Network/graph_plgs.py

from imagepy.core.engine import Filter, Simple
from imagepy.ipyalg.graph import sknw
import numpy as np
from numpy.linalg import norm
import networkx as nx, wx
from imagepy import IPy
from numba import jit
import pandas as pd

# build   statistic  sumerise   cut  edit
class Mark:
    def __init__(self, graph):
        self.graph = graph

    def draw(self, dc, f, **key):
        dc.SetPen(wx.Pen((255,255,0), width=3, style=wx.SOLID))
        dc.SetTextForeground((255,255,0))
        font = wx.Font(8, wx.FONTFAMILY_DEFAULT, 
                       wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False)
        dc.SetFont(font)

        ids = self.graph.nodes()
        pts = [self.graph.nodes[i]['o'] for i in ids]
        pts = [f(i[1], i[0]) for i in pts]
        dc.DrawPointList(pts)
        dc.DrawTextList([str(i) for i in ids], pts)

class BuildGraph(Filter):
    title = 'Build Graph'
    note = ['8-bit', 'not_slice', 'not_channel', 'auto_snap']

    #process
    def run(self, ips, snap, img, para = None):
        ips.data = sknw.build_sknw(img, True)
        sknw.draw_graph(img, ips.data)
        ips.mark = Mark(ips.data)

class Statistic(Simple):
    title = 'Graph Statistic'
    note = ['all']

    def load(self, ips):
        if not isinstance(ips.data, nx.MultiGraph):
            IPy.alert("Please build graph!");
            return False;
        return True;

    def run(self, ips, imgs, para = None):
        edges, nodes = [], []
        ntitles = ['PartID', 'NodeID', 'Degree','X', 'Y']
        etitles = ['PartID', 'StartID', 'EndID', 'Length']
        k, unit = ips.unit
        comid = 0
        for g in nx.connected_component_subgraphs(ips.data, False):
            for idx in g.nodes():
                o = g.nodes[idx]['o']
                print(idx, g.degree(idx))
                nodes.append([comid, idx, g.degree(idx), round(o[1]*k,2), round(o[0]*k,2)])
            for (s, e) in g.edges():
                eds = g[s][e]
                for i in eds:
                    edges.append([comid, s, e, round(eds[i]['weight']*k, 2)])
            comid += 1

        IPy.show_table(pd.DataFrame(nodes, columns=ntitles), ips.title+'-nodes')
        IPy.show_table(pd.DataFrame(edges, columns=etitles), ips.title+'-edges')

class Sumerise(Simple):
    title = 'Graph Summarise'
    note = ['all']

    para = {'parts':False}
    view = [(bool, 'parts', 'parts')]

    def load(self, ips):
        if not isinstance(ips.data, nx.MultiGraph):
            IPy.alert("Please build graph!");
            return False;
        return True;

    def run(self, ips, imgs, para = None):
        titles = ['PartID', 'Noeds', 'Edges', 'TotalLength', 'Density', 'AveConnect']
        k, unit = ips.unit
        
        gs = nx.connected_component_subgraphs(ips.data, False) if para['parts'] else [ips.data]
        comid, datas = 0, []
        for g in gs:
            sl = 0
            for (s, e) in g.edges():
                sl += sum([i['weight'] for i in g[s][e].values()])
            datas.append([comid, g.number_of_nodes(), g.number_of_edges(), round(sl*k, 2), 
                round(nx.density(g), 2), round(nx.average_node_connectivity(g),2)][1-para['parts']:])
            comid += 1
        IPy.show_table(pd.DataFrame(datas, columns=titles[1-para['parts']:]), ips.title+'-graph')

class CutBranch(Filter):
    title = 'Cut Branch'
    note = ['8-bit', 'not_slice', 'not_channel', 'auto_snap', 'preview']

    para = {'lim':10, 'rec':False}
    view = [(int, 'lim', (0,1e6), 0, 'limit', 'uint'),
            (bool, 'rec', 'recursion')]

    def load(self, ips):
        if not isinstance(ips.data, nx.MultiGraph):
            IPy.alert("Please build graph!");
            return False;
        self.buf = ips.data
        return True;

    def run(self, ips, snap, img, para = None):
        g = ips.data = self.buf.copy()
        k, unit = ips.unit
        while True:
            rm = []
            for i in g.nodes():
                if g.degree(i)!=1:continue
                s,e = list(g.edges(i))[0]
                if g[s][e][0]['weight']*k<=para['lim']:
                    rm.append(i)
            g.remove_nodes_from(rm)
            if not para['rec'] or len(rm)==0:break
        img *= 0
        sknw.draw_graph(img, g)

    def cancel(self, ips):
        if 'auto_snap' in self.note:
            ips.swap()
            ips.update()
        ips.data = self.buf

class RemoveIsolate(Filter):
    title = 'Remove Isolate Node'
    note = ['all', 'not_slice', 'not_channel', 'auto_snap']

    def load(self, ips):
        if not isinstance(ips.data, nx.MultiGraph):
            IPy.alert("Please build graph!");
            return False;
        return True;

    def run(self, ips, snap, img, para = None):
        g = ips.data
        for n in list(g.nodes()):
            if len(g[n])==0: g.remove_node(n)
        img *= 0
        sknw.draw_graph(img, g)
        ips.mark = Mark(ips.data)

class Remove2Node(Simple):
    title = 'Remove 2Path Node'
    note = ['all']

    def load(self, ips):
        if not isinstance(ips.data, nx.MultiGraph):
            IPy.alert("Please build graph!");
            return False;
        return True;

    def run(self, ips, imgs, para = None):
        g = ips.data
        for n in list(g.nodes()):
            if len(g[n])!=2 or n in g[n]: continue 
            (k1, e1), (k2, e2) = g[n].items()
            if isinstance(g, nx.MultiGraph):
                if len(e1)!=1 or len(e2)!=1: continue
                e1, e2 = e1[0], e2[0]
            l1, l2 = e1['pts'], e2['pts']
            d1 = norm(l1[0]-g.nodes[n]['o']) > norm(l1[-1]-g.nodes[n]['o'])
            d2 = norm(l2[0]-g.nodes[n]['o']) < norm(l2[-1]-g.nodes[n]['o'])
            pts = np.vstack((l1[::[-1,1][d1]], l2[::[-1,1][d2]]))
            l = np.linalg.norm(pts[1:]-pts[:-1], axis=1).sum()
            g.remove_node(n)
            g.add_edge(k1, k2, pts=pts, weight=l)
        ips.img[:] = 0
        sknw.draw_graph(ips.img, g)
        ips.mark = Mark(ips.data)

@jit(nopython=True)
def floodfill(img, x, y):
    buf = np.zeros((131072,2), dtype=np.uint16)
    color = img[int(y), int(x)]
    img[int(y), int(x)] = 0
    buf[0,0] = x; buf[0,1] = y;
    cur = 0; s = 1;

    while True:
        xy = buf[cur]
        for dx in (-1,0,1):
            for dy in (-1,0,1):
                cx = xy[0]+dx; cy = xy[1]+dy
                if cx<0 or cx>=img.shape[1]:continue
                if cy<0 or cy>=img.shape[0]:continue
                if img[cy, cx]!=color:continue
                img[cy, cx] = 0
                buf[s,0] = cx; buf[s,1] = cy
                s+=1
                if s==len(buf):
                    buf[:len(buf)-cur] = buf[cur:]
                    s -= cur; cur=0
        cur += 1
        if cur==s:break

class CutROI(Filter):
    title = 'Cut By ROI'
    note = ['8-bit', 'not_slice', 'not_channel', 'auto_snap', 'preview']

    def run(self, ips, snap, img, para = None):
        msk = ips.get_msk(3) * (img>0)
        r,c = np.where(msk)
        for x,y in zip(c,r):
            if img[y,x]>0:
                floodfill(img, x, y)
        
class ShortestPath(Simple):
    title = 'Graph Shortest Path'
    note = ['all']

    para = {'start':0, 'end':1}
    view = [(int, 'start', (0,1e8), 0, 'start', 'id'),
            (int, 'end',   (0,1e8), 0, 'end', 'id')]

    def load(self, ips):
        if not isinstance(ips.data, nx.MultiGraph):
            IPy.alert("Please build graph!");
            return False;
        return True;

    def run(self, ips, imgs, para = None):
        nodes = nx.shortest_path(ips.data, source=para['start'], target=para['end'], weight='weight')
        path = zip(nodes[:-1], nodes[1:])
        paths = []
        for s,e in path:
            ps = ips.data[s][e].values()
            pts = sorted([(i['weight'], i['pts']) for i in ps])
            paths.append(((s,e), pts[0]))
        sknw.draw_graph(ips.img, ips.data)
        for i in paths:
            ips.img[i[1][1][:,0], i[1][1][:,1]] = 255
            IPy.write('%s-%s:%.4f'%(i[0][0], i[0][1], i[1][0]), 'ShortestPath')
        IPy.write('Nodes:%s, Length:%.4f'%(len(nodes), sum([i[1][0] for i in paths])), 'ShortestPath')



plgs = [BuildGraph, Statistic, Sumerise, '-', RemoveIsolate, Remove2Node, CutBranch, CutROI, '-', ShortestPath]


https://github.com/Image-Py/imagepy/blob/master/imagepy/menus/Analysis/Region%20Analysis/regionprops_plgs.py

# -*- coding: utf-8 -*-
"""
Created on Tue Dec 27 01:06:59 2016
@author: yxl
"""
from imagepy import IPy
import numpy as np
from imagepy.core.engine import Simple, Filter
from imagepy.core.manager import ImageManager, ColorManager
from scipy.ndimage import label, generate_binary_structure
from skimage.measure import regionprops
from imagepy.core.mark import GeometryMark
import pandas as pd

# center, area, l, extent, cov
class RegionCounter(Simple):
    title = 'Geometry Analysis'
    note = ['8-bit', '16-bit', 'int']
    para = {'con':'8-connect', 'center':True, 'area':True, 'l':True, 'extent':False, 'cov':False, 'slice':False,
            'ed':False, 'holes':False, 'ca':False, 'fa':False, 'solid':False}
    view = [(list, 'con', ['4-connect', '8-connect'], str, 'conection', 'pix'),
            (bool, 'slice', 'slice'),
            ('lab', None, '=========  indecate  ========='),
            (bool, 'center', 'center'),
            (bool, 'area', 'area'),
            (bool, 'l', 'perimeter'),
            (bool, 'extent', 'extent'),
            (bool, 'ed', 'equivalent diameter'),
            (bool, 'ca', 'convex area'),
            (bool, 'holes', 'holes'),
            (bool, 'fa', 'filled area'),
            (bool, 'solid', 'solidity'),
            (bool, 'cov', 'cov')]

    #process
    def run(self, ips, imgs, para = None):
        if not para['slice']:imgs = [ips.img]
        k = ips.unit[0]

        titles = ['Slice', 'ID'][0 if para['slice'] else 1:]
        if para['center']:titles.extend(['Center-X','Center-Y'])
        if para['area']:titles.append('Area')
        if para['l']:titles.append('Perimeter')
        if para['extent']:titles.extend(['Min-Y','Min-X','Max-Y','Max-X'])
        if para['ed']:titles.extend(['Diameter'])
        if para['ca']:titles.extend(['ConvexArea'])
        if para['holes']:titles.extend(['Holes'])
        if para['fa']:titles.extend(['FilledArea'])
        if para['solid']:titles.extend(['Solidity'])
        if para['cov']:titles.extend(['Major','Minor','Ori'])
        buf = imgs[0].astype(np.uint32)
        data, mark = [], {'type':'layers', 'body':{}}
        strc = generate_binary_structure(2, 1 if para['con']=='4-connect' else 2)
        for i in range(len(imgs)):
            label(imgs[i], strc, output=buf)
            ls = regionprops(buf)

            dt = [[i]*len(ls), list(range(len(ls)))]
            if not para['slice']:dt = dt[1:]

            layer = {'type':'layer', 'body':[]}
            texts = [(i.centroid[::-1])+('id=%d'%n,) for i,n in zip(ls,range(len(ls)))]
            layer['body'].append({'type':'texts', 'body':texts})
            if para['cov']:
                ellips = [i.centroid[::-1] + (i.major_axis_length/2,i.minor_axis_length/2, i.orientation+np.pi/2) for i in ls]
                layer['body'].append({'type':'ellipses', 'body':ellips})
            mark['body'][i] = layer

            if para['center']:
                dt.append([round(i.centroid[1]*k,1) for i in ls])
                dt.append([round(i.centroid[0]*k,1) for i in ls])
            if para['area']:
                dt.append([i.area*k**2 for i in ls])
            if para['l']:
                dt.append([round(i.perimeter*k,1) for i in ls])
            if para['extent']:
                for j in (0,1,2,3):
                    dt.append([i.bbox[j]*k for i in ls])
            if para['ed']:
                dt.append([round(i.equivalent_diameter*k, 1) for i in ls])
            if para['ca']:
                dt.append([i.convex_area*k**2 for i in ls])
            if para['holes']:
                dt.append([1-i.euler_number for i in ls])
            if para['fa']:
                dt.append([i.filled_area*k**2 for i in ls])
            if para['solid']:
                dt.append([round(i.solidity, 2) for i in ls])
            if para['cov']:
                dt.append([round(i.major_axis_length*k, 1) for i in ls])
                dt.append([round(i.minor_axis_length*k, 1) for i in ls])
                dt.append([round(i.orientation*k, 1) for i in ls])

            data.extend(list(zip(*dt)))
        ips.mark = GeometryMark(mark)
        IPy.show_table(pd.DataFrame(data, columns=titles), ips.title+'-region')

# center, area, l, extent, cov
class RegionFilter(Filter):
    title = 'Geometry Filter'
    note = ['8-bit', '16-bit', 'int', 'auto_msk', 'auto_snap','preview']
    para = {'con':'4-connect', 'inv':False, 'area':0, 'l':0, 'holes':0, 'solid':0, 'e':0, 'front':255, 'back':100}
    view = [(list, 'con', ['4-connect', '8-connect'], str, 'conection', 'pix'),
            (bool, 'inv', 'invert'),
            ('lab', None, 'Filter: "+" means >=, "-" means <'),
            (int, 'front', (0, 255), 0, 'front color', ''),
            (int, 'back', (0, 255), 0, 'back color', ''),
            (float, 'area', (-1e6, 1e6), 1, 'area', 'unit^2'),
            (float, 'l', (-1e6, 1e6), 1, 'perimeter', 'unit'),
            (int, 'holes', (-10,10), 0, 'holes', 'num'),
            (float, 'solid', (-1, 1,), 1, 'solidity', 'ratio'),
            (float, 'e', (-100,100), 1, 'eccentricity', 'ratio')]

    #process
    def run(self, ips, snap, img, para = None):
        k, unit = ips.unit
        strc = generate_binary_structure(2, 1 if para['con']=='4-connect' else 2)

        lab, n = label(snap==0 if para['inv'] else snap, strc, output=np.uint32)
        idx = (np.ones(n+1)*(0 if para['inv'] else para['front'])).astype(np.uint8)
        ls = regionprops(lab)
        
        for i in ls:
            if para['area'] == 0: break
            if para['area']>0:
                if i.area*k**2 < para['area']: idx[i.label] = para['back']
            if para['area']<0:
                if i.area*k**2 >= -para['area']: idx[i.label] = para['back']

        for i in ls:
            if para['l'] == 0: break
            if para['l']>0:
                if i.perimeter*k < para['l']: idx[i.label] = para['back']
            if para['l']<0:
                if i.perimeter*k >= -para['l']: idx[i.label] = para['back']

        for i in ls:
            if para['holes'] == 0: break
            if para['holes']>0:
                if 1-i.euler_number < para['holes']: idx[i.label] = para['back']
            if para['holes']<0:
                if 1-i.euler_number >= -para['holes']: idx[i.label] = para['back']

        for i in ls:
            if para['solid'] == 0: break
            if para['solid']>0:
                if i.solidity < para['solid']: idx[i.label] = para['back']
            if para['solid']<0:
                if i.solidity >= -para['solid']: idx[i.label] = para['back']

        for i in ls:
            if para['e'] == 0: break
            if para['e']>0:
                if i.minor_axis_length>0 and i.major_axis_length/i.minor_axis_length < para['e']: 
                    idx[i.label] = para['back']
            if para['e']<0:
                if i.minor_axis_length>0 and i.major_axis_length/i.minor_axis_length >= -para['e']: 
                    idx[i.label] = para['back']

        idx[0] = para['front'] if para['inv'] else 0
        img[:] = idx[lab]

# center, area, l, extent, cov
class PropertyMarker(Filter):
    title = 'Property Marker'
    note = ['8-bit', '16-bit', 'auto_msk', 'auto_snap','preview']
    para = {'con':'4-connect', 'pro':'area', 'cm':'gray'}
    view = [(list, 'con', ['4-connect', '8-connect'], str, 'conection', 'pix'),
            (list, 'pro', ['area', 'perimeter', 'solid', 'eccentricity'], str, 'property', ''),
            ('cmap', 'cm', 'color map')]

    def load(self, ips): 
        self.lut = ips.lut
        return True

    def cancel(self, ips):
        ips.lut = self.lut
        Filter.cancel(self, ips)

    #process
    def run(self, ips, snap, img, para = None):
        strc = generate_binary_structure(2, 1 if para['con']=='4-connect' else 2)

        lab, n = label(snap, strc, output=np.uint32)
        idx = (np.zeros(n+1)).astype(np.uint8)
        ls = regionprops(lab)
        
        if para['pro'] == 'area': ps = [i.area for i in ls]
        if para['pro'] == 'perimeter': ps = [i.perimeter for i in ls]
        if para['pro'] == 'solid': ps = [i.solidity for i in ls]
        if para['pro'] == 'eccentricity': ps = [i.major_axis_length/i.minor_axis_length for i in ls]

        ps = np.array(ps)
        if ps.max() != ps.min():
            ps = (ps - ps.min()) / (ps.max() - ps.min())
        else: ps = ps / ps.max()
        idx[1:] = ps * 245 + 10
        img[:] = idx[lab]
        ips.lut = ColorManager.get_lut(para['cm'])

plgs = [RegionCounter, RegionFilter, PropertyMarker]

toolbar源码

import wx

def make_logo(cont, obj):
    if isinstance(obj, str) and len(obj)>1:
        bmp = wx.Bitmap(obj)
    if isinstance(obj, str) and len(obj)==1:
        bmp = wx.Bitmap.FromRGBA(16, 16)
        dc = wx.BufferedDC(wx.ClientDC(cont), bmp)
        dc.SetBackground(wx.Brush((255,255,255)))
        dc.Clear()
        dc.SetTextForeground((0,0,150))
        font = dc.GetFont()
        font.SetPointSize(12)
        dc.SetFont(font)
        w, h = dc.GetTextExtent(obj)
        dc.DrawText(obj, 8-w//2, 8-h//2)
        rgb = bytes(768)
        bmp.CopyToBuffer(rgb)
        a = memoryview(rgb[::3]).tolist()
        a = bytes([255-i for i in a])
        bmp = wx.Bitmap.FromBufferAndAlpha(16, 16, rgb, a)
    img = bmp.ConvertToImage()
    img.Resize((20, 20), (2, 2))
    return img.ConvertToBitmap()

class ToolBar(wx.Panel):
    def __init__(self, parent, vertical=False):
        wx.Panel.__init__( self, parent, wx.ID_ANY,  wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
        sizer = wx.BoxSizer( (wx.HORIZONTAL, wx.VERTICAL)[vertical] )
        self.SetSizer( sizer )
        self.toolset = []

    def bind(self, btn, tol):
        btn.Bind( wx.EVT_LEFT_DOWN, lambda x, obj=tol: obj().start())
            
    def add_tool(self, tool, logo):
        btn = wx.BitmapButton(self, wx.ID_ANY, make_logo(self, logo), 
            wx.DefaultPosition, (32,32), wx.BU_AUTODRAW|wx.RAISED_BORDER )
        self.bind(btn, tool)
        self.GetSizer().Add(btn, 0, wx.ALL, 1)

    def add_tools(self, name, tools, fixed=True):
        if not fixed: self.toolset.append((name, []))
        for tool, logo in tools:
            btn = wx.BitmapButton(self, wx.ID_ANY, make_logo(self, logo), 
                wx.DefaultPosition, (32,32), wx.BU_AUTODRAW|wx.RAISED_BORDER )
            self.bind(btn, tool)
            self.GetSizer().Add(btn, 0, wx.ALL, 1)
            if not fixed: self.toolset[-1][1].append(btn)
        if fixed:
            line = wx.StaticLine( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_VERTICAL )
            self.GetSizer().Add( line, 0, wx.ALL|wx.EXPAND, 2 )

    def active_set(self, name):
        for n, tools in self.toolset:
            print('select', name, n)
            for btn in tools:
                if n==name: btn.Show()
                if n!=name: btn.Hide()
        self.Layout()
        
    def add_pop(self, logo, default):
        self.GetSizer().AddStretchSpacer(1)
        btn = wx.BitmapButton(self, wx.ID_ANY, make_logo(self, logo), 
                wx.DefaultPosition, (32,32), wx.BU_AUTODRAW|wx.RAISED_BORDER )
        btn.Bind(wx.EVT_LEFT_DOWN, self.menu_drop)
        self.GetSizer().Add(btn, 0, wx.ALL, 1)
        self.active_set(default)

    def menu_drop(self, event):
        menu = wx.Menu()
        for name, item in self.toolset:
            item = wx.MenuItem(menu, wx.ID_ANY, name, wx.EmptyString, wx.ITEM_NORMAL )
            menu.Append(item)
            f = lambda e, name=name:self.active_set(name)
            menu.Bind(wx.EVT_MENU, f, id=item.GetId())
        self.PopupMenu( menu )
        menu.Destroy()
        
if __name__ == '__main__':
    path = 'C:/Users/54631/Documents/projects/imagepy/imagepy/tools/drop.gif'
    app = wx.App()
    frame = wx.Frame(None)
    tool = ToolBar(frame)
    tool.add_tools('A', [(None, 'A')] * 3)
    tool.add_tools('B', [(None, 'B')] * 3, False)
    tool.add_tools('C', [(None, 'C')] * 3, False)
    tool.add_pop('P', 'B')
    tool.Layout()
    frame.Fit()
    frame.Show()
    app.MainLoop()
相关文章
相关标签/搜索