既然排序过程是图像中值滤波处理的瓶颈,能不能抛开它,用其它手段实现呢?这就是本文要探讨的问题。有朋友可能会有疑问,不排序怎么获取中间值呢,是否采用网上有些文章介绍的近似值来代替?不,本文介绍的方法决不是近似中间值,而是的的确确的“精确”中间值。
我是自学统计大专毕业,图像中值滤波中的中间值。在统计学中叫做中位数,是平均数指标的一种。平均数指标按统计的复杂程度可分为简单平均数和加权平均数,所谓简单平均数就是对统计总体的每个个体进行累计计算后求得;而加权平均数则是先对统计总体的所有个体进行分组,然后以各个组的大小作为权数后进行累计计算所得。中位数既然是平均数指标的一种,当然也存在两种计算方法。加权中位数和加权算术平均数的区别在于后者是对各个分组用权数相乘进行累积后除以总体个数,而前者只需要对各组权数进行累积后除以2,累积权数大于或等于这个数的组的值就是中位数。传统中值滤波实现代码中采用的排序手段实质就是简单中位平均数统计方法,而本文要介绍的是采用分组加权的方法来获取中位平均数,即中值滤波的中间值。
采用分组加权统计方法的前提条件是对无限统计总体进行有限的分组,例如,人口年龄统计,就是首先确定各个年龄段,这个年龄段可以是一岁、五岁、十岁等。而图像像素的R、G、B值在0 -- 255之间,正好符合有限分组的前提条件。说到这里,很多人可能已经明白我要表达的意思了:
1、按R、G、B分别定义一个256大小的灰度统计数据;
2、将图像像素总体的每个个体像素的R、G、B值进行归类;
3、确定累积中间值权数的大小;
4、从数组左端或者右端开始权数累计,累积到大于或等于中间值权数的那个灰度组就是中间值!
从前面几个步骤不难看出,前3个步骤就是图像处理中灰度统计的分类方法,第4个累积步骤与灰度统计累积计算有2个不同点,一是灰度统计累积的是权数*灰度,而这里只累积灰度;二是灰度统计累积需要全部完成,而中值累积只要达到中间值权数那个组就可以终止。在这4个步骤中,前3个步骤是相当快的(实际上只是第二个步骤),因为其中既无乘除运算,也没有比较转移,更没有元素交换或移动等动作。而制约数据处理速度的瓶颈就在第4步,因为对每个像素都必须分别按R、G、B通道对一共768个元素大小的数据进行累积,哪怕是每个通道除了2个加法运算和唯一的一次比较判断外,没有其它运算,也不需要每次都必须累积到位,但仍然是比较耗时的,不过本文在代码实现过程中,尽可能地作了一些弥补。最后结果同排序方法比起来,可算是相当快捷了,而且滤波半径越大,差距越明显,几十倍的差距绝不是天方夜谭!就算是同我前面所说的半径为1的改进排序算法比较来,大多数情况下,也略有胜出。
下面是用BCB2007写的全部实现代码(头文件略):
1
//
---------------------------------------------------------------------------
2
unsigned GetMedianGray(
const
BitmapData
*
data)
3
{
4
unsigned buffer[
256
];
5
6
memset(buffer,
0
,
256
*
sizeof
(unsigned));
7
PRGBTRIPLE p
=
(PRGBTriple)data
->
Scan0;
8
unsigned width
=
data
->
Width
>>
3
;
9
unsigned height
=
data
->
Height
>>
3
;
10
int
offset
=
(data
->
Stride
<<
3
)
-
(width
<<
3
)
*
sizeof
(RGBTRIPLE);
11
for
(unsigned y
=
0
; y
<
height; y
++
, (
char
*
)p
+=
offset)
12
{
13
for
(unsigned x
=
0
; x
<
width; x
++
, p
+=
8
)
14
{
15
buffer[p
->
rgbtBlue]
++
;
16
buffer[p
->
rgbtGreen]
++
;
17
buffer[p
->
rgbtRed]
++
;
18
}
19
}
20
unsigned median
=
(height
*
width
*
3
)
>>
1
;
21
unsigned v
=
0
;
22
unsigned
*
pi
=
buffer;
23
while
(v
<
median)
24
v
+=
*
pi
++
;
25
return
(pi
-
buffer
-
1
);
26
}
27
//
---------------------------------------------------------------------------
28
void
GetExpendData(
const
BitmapData
*
data, unsigned radius, BitmapData
*
exp)
29
{
30
exp
->
Width
=
data
->
Width
+
(radius
<<
1
);
31
exp
->
Height
=
data
->
Height
+
(radius
<<
1
);
32
exp
->
Stride
=
(exp
->
Width
*
sizeof
(RGBTRIPLE)
+
3
)
&
-
4
;
33
exp
->
Scan0
=
(
void
*
)
new
char
[exp
->
Stride
*
exp
->
Height];
34
PRGBTRIPLE ps
=
(PRGBTriple)data
->
Scan0;
35
PRGBTRIPLE pd
=
(PRGBTriple)((
char
*
)exp
->
Scan0
+
radius
*
exp
->
Stride);
36
PRGBTRIPLE pt
=
pd;
37
unsigned width
=
data
->
Width;
38
unsigned height
=
data
->
Height;
39
int
dstOffset
=
exp
->
Stride
-
exp
->
Width
*
sizeof
(RGBTRIPLE);
40
int
srcOffset
=
data
->
Stride
-
(width
-
1
)
*
sizeof
(RGBTRIPLE);
41
unsigned x, y;
42
for
(y
=
0
; y
<
height; y
++
, (
char
*
)pd
+=
dstOffset, (
char
*
)ps
+=
srcOffset)
43
{
44
for
(x
=
0
; x
<
radius;
*
pd
++
=
*
ps, x
++
);
45
for
(x
=
0
; x
<
width;
*
pd
++
=
*
ps
++
, x
++
);
46
for
(x
=
0
, ps
--
; x
<
radius;
*
pd
++
=
*
ps, x
++
);
47
}
48
PRGBTRIPLE pb
=
(PRGBTriple)((
char
*
)pd
-
exp
->
Stride);
49
PRGBTRIPLE pd0
=
(PRGBTriple)exp
->
Scan0;
50
for
(y
=
0
; y
<
radius; y
++
, (
char
*
)pd
+=
dstOffset, (
char
*
)pd0
+=
dstOffset)
51
{
52
PRGBTRIPLE p0
=
pt;
53
PRGBTRIPLE p1
=
pb;
54
for
(x
=
0
; x
<
exp
->
Width;
*
pd0
++
=
*
p0
++
,
*
pd
++
=
*
p1
++
, x
++
);
55
}
56
}
57
//
---------------------------------------------------------------------------
58
void
ImageMedianValue(BitmapData
*
data, unsigned radius)
59
{
60
unsigned buffer[
768
];
61
unsigned
*
pb
=
buffer;
62
unsigned
*
pg
=
pb
+
256
;
63
unsigned
*
pr
=
pb
+
512
;
64
unsigned
*
pbOff,
*
pgOff,
*
prOff;
65
int
delta;
66
//
获取图像总体1/8样本的灰度中间值
67
if
(GetMedianGray(data)
<
128
)
68
{
69
//
如果总体灰度中间值小于128,从缓冲区左边开始累计
70
pbOff
=
pb
-
1
;
71
pgOff
=
pg
-
1
;
72
prOff
=
pr
-
1
;
73
delta
=
1
;
74
}
75
else
76
{
77
//
否则,从缓冲区右边开始累计
78
pbOff
=
pb
+
256
;
79
pgOff
=
pg
+
256
;
80
prOff
=
pr
+
256
;
81
delta
=
-
1
;
82
}
83
84
if
(radius
==
0
) radius
=
1
;
85
unsigned size
=
(radius
<<
1
)
+
1
;
//
中值滤波窗口大小
86
unsigned median
=
(size
*
size
+
1
)
>>
1
;
//
缓冲区累计中值权数
87
//
获取按半径扩展边框的备份图
88
BitmapData exp;
89
GetExpendData(data, radius,
&
exp);
90
91
int
stride
=
exp.Stride;
92
int
rowOffset
=
stride
-
size
*
sizeof
(RGBTRIPLE);
93
int
mOffset
=
stride
*
size
-
sizeof
(RGBTRIPLE);
94
95
PRGBTRIPLE ps
=
(PRGBTriple)exp.Scan0;
96
PRGBTRIPLE pd
=
(PRGBTriple)data
->
Scan0;
97
unsigned width
=
data
->
Width;
98
unsigned height
=
data
->
Height;
99
int
dstOffset
=
data
->
Stride
-
width
*
sizeof
(RGBTRIPLE);
100
int
srcOffset
=
exp.Stride
-
(width
-
1
)
*
sizeof
(RGBTRIPLE);
101
unsigned i, j, v;
102
unsigned
*
pv;
103
104
for
(unsigned y
=
0
; y
<
height; y
++
, (
char
*
)ps
+=
srcOffset, (
char
*
)pd
+=
dstOffset)
105
{
106
//
对每行首像素按中值滤波窗口大小作一次灰度统计
107
memset(buffer,
0
,
768
*
sizeof
(unsigned));
108
PRGBTRIPLE p
=
ps;
109
for
(i
=
0
; i
<
size; i
++
, (
char
*
)p
+=
rowOffset)
110
{
111
for
(j
=
0
; j
<
size; j
++
, p
++
)
112
{
113
pb[p
->
rgbtBlue]
++
;
//
蓝色灰度统计
114
pg[p
->
rgbtGreen]
++
;
//
绿色灰度统计
115
pr[p
->
rgbtRed]
++
;
//
红色灰度统计
116
}
117
}
118
unsigned x
=
0
;
119
while
(TRUE)
120
{
121
//
按R、G、B灰度统计缓冲区分别进行累计,
122
//
找出大于或等于median的灰度组作为中值
123
for
(v
=
0
, pv
=
pbOff; v
<
median; pv
+=
delta, v
+=
*
pv);
124
pd
->
rgbtBlue
=
pv
-
pb;
125
for
(v
=
0
, pv
=
pgOff; v
<
median; pv
+=
delta, v
+=
*
pv);
126
pd
->
rgbtGreen
=
pv
-
pg;
127
for
(v
=
0
, pv
=
prOff; v
<
median; pv
+=
delta, v
+=
*
pv);
128
pd
->
rgbtRed
=
pv
-
pr;
129
pd
++
;
130
if
(
++
x
==
width)
break
;
131
//
从灰度统计缓冲区中剔除上个像素滤波窗口最左边一列,
132
//
将当前像素滤波窗口最右边一列追加到灰度统计缓冲区
133
p
=
ps
+
size;
134
for
(i
=
0
; i
<
size; i
++
, (
char
*
)ps
+=
stride, (
char
*
)p
+=
stride)
135
{
136
pb[ps
->
rgbtBlue]
--
;
137
pg[ps
->
rgbtGreen]
--
;
138
pr[ps
->
rgbtRed]
--
;
139
pb[p
->
rgbtBlue]
++
;
140
pg[p
->
rgbtGreen]
++
;
141
pr[p
->
rgbtRed]
++
;
142
}
143
(
char
*
)ps
-=
mOffset;
144
};
145
}
146
delete[] exp.Scan0;
147
}
148
//
---------------------------------------------------------------------------
实现代码中,在前面所说的4个步骤基础上作了3点完善:
1、并非对图像的所有像素都按4个步骤进行。除了每行行首像素作了完整的分类统计外,其它像素只是从统计数组中剔除前一像素临近值的最左边一列和追加当前像素临近值的最右边一列,这无疑加快了分类统计速度,而且滤波半径越大,效果越明显。
2、在对图像进行滤波处理前,对整个图像像素总体的1/8样本求了一次中间值权数,其作用是确定像素分类后的累积方向,如果这个总体中间值权数小于128,从灰度统计数据左边(低端)开始累积,否则则从灰度统计数据右边(高端)开始累积。通过这个总体中间值权数就可以大致确定该图像处理速度的快慢,如果这个值接近两端,处理速度相对就快些,反之如靠近128附近,处理速度则相应会慢些,同样大小的图片,因这个原因,可能造成成倍的差距,但是最坏的情况下,对灰度统计数组累积时的平均值也不会超过一半。
3、制作滤波备份源图时按滤波半径对图像边缘作了扩充,这样有利于对图像边缘进行滤波处理。有些人在写中值滤波代码时往往对图像边缘略过不作处理,这在小半径滤波范围内还无所谓,但是对于较大半径的滤波处理后,由于未进行滤波处理的边缘较宽。会使得图像很难看。
正式由于第2点中所说的差距原因,本文也不好给出具体的测试数据,但有一点是肯定的:同等条件下(语言、程序员水平及编译器优化程度等),比传统排序法实现的中值滤波要快不少,滤波半径越大,差距越大!如果要求更快的处理速度,可用汇编对核心代码进行优化,我自己使用的就是如此。
最后还是贴个中值滤波测试代码和效果图:前面是源图,后面是50半径中值滤波图:
1
void
__fastcall TForm1::Button1Click(TObject
*
Sender)
2
{
3
Bitmap
*
bmp
=
new
Bitmap(WideString(
"
d:\\source.bmp
"
));
4
BitmapData data, exp;
5
Gdiplus::Rect r(
0
,
0
, bmp
->
GetWidth(), bmp
->
GetHeight());
6
bmp
->
LockBits(
&
r, ImageLockModeRead
|
ImageLockModeWrite, PixelFormat24bppRGB,
&
data);
7
ImageMedianValue(
&
data,
50
);
8
bmp
->
UnlockBits(
&
data);
9
Gdiplus::Graphics
*
g
=
new
Gdiplus::Graphics(Canvas
->
Handle);
10
g
->
DrawImage(bmp,
0
,
0
);
11
delete g;
12
delete bmp;
13
}