├── .gitignore ├── haclon.jpg ├── .idea └── vcs.xml ├── var_threshold.py ├── adatptive_threshold.py ├── README.md └── BoxFilter.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.pyc 3 | -------------------------------------------------------------------------------- /haclon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hdmjdp/var_threshold/HEAD/haclon.jpg -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /var_threshold.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cv2 3 | from BoxFilter import boxFilter, boxFilter_MeanStd 4 | 5 | 6 | def adaptive_threshold_var(src, maxValue=255, # 二值化,线性表中只有2个值 要么0 要么就是maxValue 7 | blockSize=7, absThreshold=3, stdDevScale=0.0, debug=True): 8 | assert (blockSize % 2 == 1 and blockSize > 1) 9 | height, width = src.shape 10 | if debug: 11 | print(width, height) 12 | 13 | dst = src.copy() 14 | 15 | if maxValue < 0: 16 | # 二值化后值小于0,图像都为0 17 | dst[...] = 0 18 | return dst 19 | 20 | # 计算平均值和标准差作为比较值 21 | mean, stdv = boxFilter_MeanStd(src, blockSize) 22 | 23 | imaxval = np.clip(maxValue, 0, 255) 24 | 25 | for i in range(height): 26 | for j in range(width): 27 | idelta = max(stdDevScale * stdv[i, j], absThreshold) 28 | if int(src[i, j]) - int(mean[i, j]) > -idelta: 29 | dst[i, j] = imaxval 30 | else: 31 | dst[i, j] = 0 32 | return dst 33 | 34 | 35 | if __name__ == "__main__": 36 | src = cv2.imread('/Volumes/Transcend/高内涵/master/MDA-MB-231-20171212-4x-4D-M2.jpg', 0) 37 | src = cv2.resize(src, (400, 300), interpolation=cv2.INTER_AREA) 38 | import time 39 | st = time.time() 40 | bw1 = adaptive_threshold_var(src, blockSize=201, absThreshold=10, stdDevScale=1.4, debug=False) 41 | st1 = time.time() 42 | print(time.time() - st1) 43 | 44 | cv2.imshow("1", bw1) 45 | cv2.waitKey(0) 46 | -------------------------------------------------------------------------------- /adatptive_threshold.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | import numpy as np 3 | import cv2 4 | from BoxFilter import boxFilter 5 | import time 6 | 7 | def adaptive_threshold(src, maxValue=255, blockSize=7, delta=3, debug=False): 8 | assert(blockSize % 2 == 1 and blockSize > 1) 9 | height, width = src.shape 10 | if debug: 11 | print(width, height) 12 | 13 | dst = src.copy() 14 | 15 | if maxValue < 0: 16 | dst[...] = 0 17 | return dst 18 | 19 | # 计算平均值作为比较值 20 | # mean = cv2.boxFilter(src, -1, (blockSize, blockSize), normalize=True, borderType=cv2.BORDER_REPLICATE) 21 | mean = boxFilter(src, blockSize) 22 | 23 | imaxval = np.clip(maxValue, 0, 255) 24 | idelta = delta 25 | tab = np.zeros(768, dtype=src.dtype) 26 | # 构建查找表,index 就是像素值的大小(key),对应的值就是阈值比较过后应该是0还是255 27 | for i in range(768): 28 | if i - 255 > -idelta: 29 | tab[i] = imaxval 30 | else: 31 | tab[i] = 0 32 | 33 | if debug: 34 | print("tab:", tab, tab.shape) 35 | 36 | # dst = src.astype(int) - mean.astype(int) + 255 37 | # zz = cv2.LUT(dst, tab) 38 | # 逐像素计算src[j] - mean[j] + 255,并查表得到结果 39 | for i in range(height): 40 | for j in range(width): 41 | dst[i, j] = tab[int(src[i, j]) - int(mean[i, j]) + 255] 42 | return dst 43 | 44 | 45 | if __name__ == "__main__": 46 | src = cv2.imread("/Volumes/Transcend/高内涵/image/明场/10ms/PRO1_HCT-116_M_Day10_4x_Bright_10ms_10ms_20180411131703_O.bmp") 47 | src = cv2.resize(src, (400, 300), interpolation=cv2.INTER_AREA) 48 | src_gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) 49 | st = time.time() 50 | bw = adaptive_threshold(src_gray, blockSize=201, delta=30) 51 | print("elapsed: ", time.time() - st) 52 | cv2.imshow("1", bw) 53 | cv2.waitKey(0) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # var_threshold 2 | 3 | 实现 halcon 版本的动态均方差阈值分割算法, 不依赖opencv 4 | 5 | 修复上一版结果偏差问题 6 | 7 | 基于积分图进行加速计算 重新实现boxfileter 8 | 9 | 10 | # halcon var_threshold说明: 11 | ![Image text](https://github.com/hdmjdp/var_threshold/blob/master/haclon.jpg) 12 | 13 | 14 | 网上说明 https://www.cnblogs.com/xh6300/p/6384542.html 15 | var_threshold 16 | 17 | 18 | 先看var_threshold算子的签名: 19 | 20 | var_threshold(Image : Region : MaskWidth, MaskHeight, StdDevScale, AbsThreshold, LightDark : ) 21 | 22 | MaskWidth、 MaskHeight是用于滤波平滑的掩膜单元;StdDevScale是标准差乘数因子(简称标准差因子);AbsThreshold是设定的绝对阈值;LightDark有4个值可选,'light'、'dark'、'equal'、'not_equal'。 23 | 24 | 25 | 26 | 需要强调的是var_threshold算子和dyn_threshold算子极为类似。不同的是var_threshold集成度更高,并且加入了“标准差×标准差因子”这一变量。 27 | 28 | 举例: 29 | 30 | 1 read_image (Image, 'C:/1.png') 31 | 2 var_threshold (Image, Region, 4, 4, 0.2, 12, 'dark') 32 | 33 | 34 | 在该程序中,先用4×4的掩膜在图像上逐像素游走,用原图中的当前像素和对应掩膜中16个像素的灰度均值对比,找出暗(dark)的区域。当原图像素灰度比对应的掩膜灰度均值低(0.2,12)个灰阶时,该区域被分割出来。本程序中StdDevScale = 0.2, AbsThreshold = 12,问题的关键就是理解如何通过StdDevScale和AbsThreshold来确定用于分割的阈值。 35 | 36 | 37 | var_threshold的帮助文档中是这么写的: 38 | 39 | 40 | 说明: 41 | 42 | 1、d(x,y)指的是遍历每个像素时,掩膜覆盖的那些像素块(本例中是4×4 = 16个像素)灰度的标准差;StdDevScale 是标准差因子。 43 | 44 | 2、当标准差因子StdDevScale ≥ 0 时,v(x,y) 取(StdDevScale ×标准差)和AbsThreshold 中较大的那个。 45 | 46 | 3、当标准差因子StdDevScale < 0 时,v(x,y) 取(StdDevScale ×标准差)和AbsThreshold 中较小的那个。实测发现,这里的比较大小是带符号比较,由于标准差是非负数,当StdDevScale < 0 时,(StdDevScale ×标准差)≤ 0恒成立。所以此时的取值就是(StdDevScale ×标准差)。 47 | 48 | 49 | 文档是这么说的: 50 | 51 | If StdDevScale*dev(x,y) is below AbsThreshold for positive values of StdDevScale or above for negative values StdDevScale, AbsThreshold is taken instead. 52 | 53 | 大致意思是: 54 | 55 | 当StdDevScale为正时,如果StdDevScale*dev(x,y) 低于 AbsThreshold,则采用AbsThreshold。 56 | 57 | 当StdDevScale为负时,如果StdDevScale*dev(x,y) 高于 AbsThreshold,则采用AbsThreshold。 58 | 59 | 60 | 61 | 我找了一块黑白过渡处4×4的像素块,求得它的灰度标准差为51.16(或49.53): 62 | 63 | 64 | 帮助文档中StdDevScale 的推荐值范围是-1~1,一般通过上面的例子可知,一般的明显的黑白过度处的标准差在50左右,乘以StdDevScale即-50 ~ 50 ,50的灰度差异,对于分割来说一般是够了的。 65 | 66 | 文档还说:推荐的值是0.2,如果参数StdDevScale太大,可能分割不出任何东西;如果参数StdDevScale太小(例如-2),可能会把整个图像区域全部输出,也就说达不到有效分割的目的。(……with 0.2 as a suggested value. If the parameter is too high or too low, an empty or full region may be returned.) 67 | 68 | 69 | 最后再看看是怎么分割像素的: 70 | 71 | ![Image text](https://images2015.cnblogs.com/blog/1002191/201702/1002191-20170210161338885-896535788.png) 72 | 73 | 74 | 其中g(x,y)指的是原始图像当前像素的灰度值;m(x,y)指的是遍历像素时,掩膜覆盖的像素的平均灰度值(mean)。 75 | 76 | 以LightDark = ‘dark’为例,当满足m(x,y) - g(x,y) ≥ v(x,y)时(即原始图像对应像素灰度比掩膜像素灰度均值低v(x,y)个灰度值以上),相应的灰度值低的暗像素被分割出来。 77 | 78 | 79 | 80 | 最后看几个例子体会一下:(对比之前的例子var_threshold (Image, Region, 4, 4, 0.2, 12, 'dark')的效果) 81 | 82 | ① 将AbsThreshold 由12改成30,此时分割出的区域变小。 83 | 84 | 1 read_image (Image, 'C:/1.png') 85 | 2 var_threshold (Image, Region, 4, 4, 0.2, 30, 'dark') 86 | 87 | ![Image text](https://images2015.cnblogs.com/blog/1002191/201702/1002191-20170210162704697-1048977266.jpg) 88 | 89 | 90 | ② AbsThreshold 保持12不变,将StdDevScale由0.2改成0.7,此时分割出的区域变小。 91 | ![Image text](https://images2015.cnblogs.com/blog/1002191/201702/1002191-20170210163008588-1599035080.jpg) 92 | 93 | 94 | ③ 将参数改为var_threshold (Image, Region, 4, 4, -0.01, 12, 'dark'),此时分割出的区域大大增加,由前面的分析可知,此时参数AbsThreshold = 12无效,事实上,此时将AbsThreshold 改为1、50甚至200都对最终结果没有任何影响。 95 | ![Image text](https://images2015.cnblogs.com/blog/1002191/201702/1002191-20170210163505651-1275175898.jpg) 96 | 97 | 98 | 通过本人的分析,我认为StdDevScale取负值意义不大,因为它会分割出大量的不需要的区域,故一般推荐使用该算子时,StdDevScale取正值。 99 | 需要强调的是:在黑白过渡处,一般掩膜覆盖的像素的标准差较大,而在其他平缓的地方,标准差较小;因此最终采用的分割阈值随着掩膜在不断遍历像素的过程中,在(StdDevScale×标准差)和AbsThreshold 之间不断切换。 100 | 101 | 102 | var_threshold和dyn_threshold的区别和联系: 103 | dyn_threshold是将原图和滤波平滑后的图对比,var_threshold是将原图和对应像素掩膜覆盖的像素的平均灰度值对比。 104 | 105 | 在算子var_threshold中,如果参数StdDevScale = 0,那么就可以用动态阈值的方式非常近似地模拟。以下两种算法的效果极为类似: 106 | 1 read_image (Image, 'C:/1.png') 107 | 2 var_threshold (Image, Region, 4, 4, 0, 12, 'dark') 108 | 1 read_image (Image, 'C:/1.png') 109 | 2 mean_image (Image, ImageMean, 4, 4) 110 | 3 dyn_threshold (Image, ImageMean, RegionDynThresh, 12, 'dark') 111 | 两种方法的效果图: 112 | ![Image text](https://images2015.cnblogs.com/blog/1002191/201702/1002191-20170210165031776-383419088.jpg) 113 | 114 | 115 | 那么当StdDevScale > 0 时,var_threshold对比dyn_threshold还存在什么优点呢?我认为是在黑白过渡处能减少分割出不需要的区域的概率。(因为黑白过渡处标准差大,当然前提是StdDevScale 不能设置得太小) 116 | 117 | 118 | -------------------------------------------------------------------------------- /BoxFilter.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Grayscale box filter. 4 | USAGE: BoxFilter.py [--filter_size] [] 5 | 6 | Takes an image filename and filter size (which must be an odd number) and returns blurred image based on filter size. 7 | """ 8 | __author__ = 'hdmjdp' 9 | import numpy as np 10 | import cv2 11 | from math import sqrt 12 | 13 | 14 | def readImage(filename): 15 | """ 16 | Read in an image file, errors out if we can't find the file 17 | :param filename: The image filename. 18 | :return: The img object in matrix form. 19 | """ 20 | img = cv2.imread(filename, 0) 21 | if img is None: 22 | print('Invalid image:' + filename) 23 | return None 24 | else: 25 | print('Image successfully read...') 26 | return img 27 | 28 | 29 | def integralImage(img): 30 | """ 31 | Returns the integral image/summed area table. See here: https://en.wikipedia.org/wiki/Summed_area_table 32 | :param img: 33 | :return: 34 | """ 35 | height = img.shape[0] 36 | width = img.shape[1] 37 | int_image = np.zeros((height, width), np.uint64) 38 | for y in range(height): 39 | for x in range(width): 40 | up = 0 if (y - 1 < 0) else int_image.item((y - 1, x)) 41 | left = 0 if (x - 1 < 0) else int_image.item((y, x - 1)) 42 | diagonal = 0 if (x - 1 < 0 or y - 1 < 0) else int_image.item((y - 1, x - 1)) 43 | val = img.item((y, x)) + int(up) + int(left) - int(diagonal) 44 | int_image.itemset((y, x), val) 45 | return int_image 46 | 47 | 48 | def adjustEdges(height, width, point): 49 | """ 50 | This handles the edge cases if the box's bounds are outside the image range based on current pixel. 51 | :param height: Height of the image. 52 | :param width: Width of the image. 53 | :param point: The current point. 54 | :return: 55 | """ 56 | newPoint = [point[0], point[1]] 57 | if point[0] >= height: 58 | newPoint[0] = height - 1 59 | 60 | if point[1] >= width: 61 | newPoint[1] = width - 1 62 | return tuple(newPoint) 63 | 64 | 65 | def findArea(int_img, a, b, c, d): 66 | """ 67 | Finds the area for a particular square using the integral image. See summed area wiki. 68 | :param int_img: The 69 | :param a: Top left corner. 70 | :param b: Top right corner. 71 | :param c: Bottom left corner. 72 | :param d: Bottom right corner. 73 | :return: The integral image. 74 | """ 75 | height = int_img.shape[0] 76 | width = int_img.shape[1] 77 | a = adjustEdges(height, width, a) 78 | b = adjustEdges(height, width, b) 79 | c = adjustEdges(height, width, c) 80 | d = adjustEdges(height, width, d) 81 | 82 | a = 0 if (a[0] < 0 or a[0] >= height) or (a[1] < 0 or a[1] >= width) else int_img.item(a[0], a[1]) 83 | b = 0 if (b[0] < 0 or b[0] >= height) or (b[1] < 0 or b[1] >= width) else int_img.item(b[0], b[1]) 84 | c = 0 if (c[0] < 0 or c[0] >= height) or (c[1] < 0 or c[1] >= width) else int_img.item(c[0], c[1]) 85 | d = 0 if (d[0] < 0 or d[0] >= height) or (d[1] < 0 or d[1] >= width) else int_img.item(d[0], d[1]) 86 | 87 | return a + d - b - c 88 | 89 | 90 | def boxFilter(img, filterSize): 91 | """ 92 | Runs the subsequent box filtering steps. Prints original image, finds integral image, and then outputs final image 93 | :param img: An image in matrix form. 94 | :param filterSize: The filter size of the matrix 95 | :return: A final image written as finalimage.png 96 | """ 97 | # print("Printing original image...") 98 | # print(img) 99 | height = img.shape[0] 100 | width = img.shape[1] 101 | intImg = integralImage(img) 102 | finalImg = np.zeros((height, width), np.uint64) 103 | # print("Printing integral image...") 104 | # print(intImg) 105 | # cv2.imwrite("integral_image.png", intImg) 106 | loc = filterSize // 2 107 | for y in range(height): 108 | for x in range(width): 109 | finalImg.itemset((y, x), findArea(intImg, (y - loc - 1, x - loc - 1), (y - loc - 1, x + loc), 110 | (y + loc, x - loc - 1), (y + loc, x + loc)) / (filterSize ** 2)) 111 | # print("Printing final image...") 112 | # # print(finalImg) 113 | # 114 | # cv2.imwrite("finalimage.png", finalImg) 115 | return finalImg 116 | 117 | 118 | def boxFilterStd(img, filterSize): 119 | """ 120 | Runs the subsequent box filtering steps. Prints original image, finds integral image, and then outputs final image 121 | :param img: An image in matrix form. 122 | :param filterSize: The filter size of the matrix 123 | :return: A final image written as finalimage.png 124 | """ 125 | height = img.shape[0] 126 | width = img.shape[1] 127 | intImg_sum = integralImage(img) 128 | finalImg = np.zeros((height, width), np.uint64) 129 | img_square = np.square(img.astype(np.uint64)) 130 | intImg_square = integralImage(img_square) 131 | loc = filterSize // 2 132 | for y in range(height): 133 | for x in range(width): 134 | s1 = findArea(intImg_square, (y - loc - 1, x - loc - 1), (y - loc - 1, x + loc), 135 | (y + loc, x - loc - 1), (y + loc, x + loc)) 136 | s2_sum = findArea(intImg_sum, (y - loc - 1, x - loc - 1), (y - loc - 1, x + loc), 137 | (y + loc, x - loc - 1), (y + loc, x + loc)) 138 | s2 = s2_sum*s2_sum/(filterSize**2) 139 | variance = s1 - s2 140 | std_deviation = sqrt(variance/(filterSize**2)) 141 | finalImg.itemset((y, x), std_deviation) 142 | # cv2.imwrite("finalimage11.png", finalImg) 143 | return finalImg 144 | 145 | 146 | def boxFilter_MeanStd(img, filterSize): 147 | """ 148 | Runs the subsequent box filtering steps. Prints original image, finds integral image, and then outputs final image 149 | :param img: An image in matrix form. 150 | :param filterSize: The filter size of the matrix 151 | :return: A final image written as finalimage.png 152 | """ 153 | height = img.shape[0] 154 | width = img.shape[1] 155 | intImg_sum = integralImage(img) 156 | finalImg_mean = np.zeros((height, width), np.uint64) 157 | finalImg_stdv = np.zeros((height, width), np.uint64) 158 | 159 | img_square = np.square(img.astype(np.uint64)) 160 | intImg_square = integralImage(img_square) 161 | loc = filterSize // 2 162 | for y in range(height): 163 | for x in range(width): 164 | s1 = findArea(intImg_square, (y - loc - 1, x - loc - 1), (y - loc - 1, x + loc), 165 | (y + loc, x - loc - 1), (y + loc, x + loc)) 166 | s2_sum = findArea(intImg_sum, (y - loc - 1, x - loc - 1), (y - loc - 1, x + loc), 167 | (y + loc, x - loc - 1), (y + loc, x + loc)) 168 | mean = s2_sum / (filterSize ** 2) 169 | s2 = s2_sum * mean # s2_sum*s2_sum/(filterSize**2) 170 | variance = s1 - s2 171 | std_deviation = sqrt(variance/(filterSize**2)) 172 | finalImg_stdv.itemset((y, x), std_deviation) 173 | finalImg_mean.itemset((y, x), mean) 174 | # cv2.imwrite("finalImg_mean.png", finalImg_mean) 175 | # cv2.imwrite("finalImg_stdv.png", finalImg_stdv) 176 | return finalImg_mean, finalImg_stdv 177 | 178 | 179 | def main(): 180 | """ 181 | Reads in image and handles argument parsing 182 | :return: None 183 | """ 184 | 185 | img = readImage("/Volumes/Transcend/高内涵/master/MDA-MB-231-20171212-4x-4D-M2.jpg") 186 | src = cv2.resize(img, (400, 300), interpolation=cv2.INTER_AREA) 187 | # finalImg = boxFilter(src, 201) 188 | # finalImg = boxFilterStd(src, 201) 189 | finalImg_mean, finalImg_stdv = boxFilter_MeanStd(src, 201) 190 | cv2.imshow("finalImg", finalImg_mean) 191 | cv2.waitKey() 192 | 193 | 194 | if __name__ == "__main__": 195 | main() 196 | --------------------------------------------------------------------------------