奇技淫巧:只用numpy ufunc实现图片二值化

图片二值化的逻辑是这样的:
(伪python代码)

forin 灰度图片:
  if.灰度 < 限度:= 黑点
  else:= 白点

要想二值化一个图片,按照正常的脑回路,我就要将上述逻辑转换为代码:将图片转为numpy array,然后遍历每个点,灰度小于限度的,灰度变为0,灰度大于限度的,灰度变为255。

然而,众所周知,对一整个图片这么大的array,遍历的资源开销很大。
而在numpy中,有一类函数叫做ufunc,这类函数会对数组中的全部元素进行同一操作。多个数据,同一操作,这不就是CPU的各种SIMD指令集干的事吗!numpy没理由不用这些指令集优化ufunc。因此我猜测,用ufunc代替遍历,就可节省资源开销。

那么问题来了:怎么把遍历改成ufunc?这堆ufunc似乎都不带条件判断啊?
好在虽然ufunc都不提供条件判断,但是有的ufunc存在“逻辑层面的条件判断”:比如floor(向下取整)这个函数。
虽然floor的实现不一定用了条件判断,但是它可以表示下面的逻辑:

for 一个整数 in 全体整数:
  if 数字 >= 一个整数 and 数字 < 一个整数+1:
    return 一个整数

因此,我们只要用一些奇妙的转换将大小的判断转化成floor内部的逻辑判断,就可用ufunc代替条件遍历。

上代码:

#img:一张灰度图片对应的numpy array
#threshold:灰度限度
def binarray(img,threshold):
  a = img-np.array([[threshold]])
  b = np.floor(a/np.array([[256]]))
  c = b+np.array([[1]],dtype = np.int16)
  bfilter = c.astype('uint8')
  result = bfilter*np.array([[255]],dtype = np.uint8)
  return result

解释一下上边的abc都是什么
a:用广播机制让每一个像素点的灰度都减去threshold,小于threshold的值除后为负,其他数字为正
b:用广播机制全体地板除256,将数组中的所有值压缩到小数范围内,因为灰度的范围就是0~255,小于threshold的值地板除后全变-1,其他数字地板除后全部变0
c:广播机制,全体+1,现在正数是1,负数是0
bfilter:数据类型还原为图像的uint8
result:广播机制,全体x255,二值化完毕

但是,还不够快,乘除还是太慢。
那么floor是怎么实现不用条件判断完成其功能的?答案就是位运算,直面计算机的二进制。
浮点数由符号位、指数段、尾数段组成。(参考https://zhuanlan.zhihu.com/p/339949186)根据浮点数的原理,要向下取整,即削去小数部分,可以通过以下几步实现(不考虑负数):

  1. 取指数段代表的值,减去中间值127,设为N
  2. 将尾数段左移N位,将小数部分截掉(左移负数即右移正数)
  3. 将指数段设为中间值127

虽然以上对floor的分析和下面的代码没啥实际关系,但我确实被自己想出的这种“骚操作”惊到了,然后在不知为何的启发下想到了以下思路:

def binarray(img,threshold):
  a = img.astype('int16')-np.array([[threshold]],dtype = np.int16)
  b = np.right_shift(a,15)
  c = b+np.array([[1]],dtype = np.int16)
  bfilter = c.astype('uint8')
  result = bfilter*np.array([[255]],dtype = np.uint8)
  return result

我不知怎的就想起了高一时候信息老师说的:“整数的头一位表示正负”(要是这记性我能用在语文上就好了QwQ)于是有了以下过程:
a:将原来的数据类型uint8转换为int16(不用int8,因为装不下),再用广播机制让每一个像素点的灰度都减去threshold,小于threshold的值除后为负,其他数字为正
b:右移15位,只保留第一位。此处有个坑:正数右移后,前面的位用0补,负数右移后,前面的位用1补,所以负数右移15位的结果不是1,是-1
c:同上
bfilter:同上
result:同上

有没有更快呢,计一下时:

位运算:0.0484583999999999
地板除:0.15314919999999987

成倍地快了!

现在我们再加入两组对照:

  #从另一篇文章中拿来的
  a = np.where((img < threshold), img, white)
  b = np.where((a >= threshold), a, black)
  result = b
  #opencv
  result = cv2.threshold(img,threshold,255,cv2.THRESH_BINARY)

然后再比一次:

np ufunc 位运算 0.05511350000000004
np ufunc 地板除 0.15484640000000005
opencv-python 0.006221599999999938
np.where 筛选 0.03421509999999994

非常的遗憾,位运算终究没比过numpy的内建方法,opencv更是一骑绝尘。
这告诉我们什么?越C越强大啊OwO 真求快那就学C啊
(可惜我这种知识有限的还只能学py,令人感叹)

你可能感兴趣的:(经验,numpy,python,人工智能)