├── .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 | 
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 | 
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 | 
88 |
89 |
90 | ② AbsThreshold 保持12不变,将StdDevScale由0.2改成0.7,此时分割出的区域变小。
91 | 
92 |
93 |
94 | ③ 将参数改为var_threshold (Image, Region, 4, 4, -0.01, 12, 'dark'),此时分割出的区域大大增加,由前面的分析可知,此时参数AbsThreshold = 12无效,事实上,此时将AbsThreshold 改为1、50甚至200都对最终结果没有任何影响。
95 | 
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 | 
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 |
--------------------------------------------------------------------------------