3.6 分段线性变换
分段线性变换有很多种,包括灰度拉伸、灰度窗口变换等,本节仅讲述最为常用的灰度拉伸。
3.6.1 理论基础
利用分段线性变换函数来增强图像对比度的方法实际上是增强原图各部分的反差,即增强输入图像中感兴趣的灰度区域,相对抑制那些不感兴趣的灰度区域。分段线性函数的主要优势在于它的形式可任意合成,而其缺点是需要更多的用户输入。
分段线性变换的函数形式如下。
式(3-6)中最重要的参数是[x1, x2]和[y1, y2]。根据算法函数的描述,读者可以发现,其中x1和x2是给出需要转换的灰度范围,y1和y2参数决定线性变换的斜率。
当x1, x2, y1, y2分别取不同的值的组合时,可得到不同的变换效果。例如以下情况。
• 如果x1=y1, x2=y2,则f(x)为1条斜率为1的直线,增强图像将和原图像相同。
• 如果x1=x2, y1=0, y2=255,则增强图像只剩下2个灰度等级,分段线性变换起到了阈值化的效果,此时的对比度最大,但是细节丢失最多。
• x1、x2、y1、y2取一般值时的分段线性变换函数的图形如图3.15所示。
图3.15 灰度的分段线性变换
分段的灰度拉伸可以更加灵活地控制输出灰度直方图的分布,可以有选择地拉伸某段灰度区间,以改善输出图像。如果一幅图像灰度集中在较暗的区域而导致图像偏暗,可以用灰度拉伸功能来扩展(斜率>1)物体的灰度区间以改善图像;同样,如果图像灰度集中在较亮的区域而导致图像偏亮,也可以用灰度拉伸功能来压缩(斜率<1)物体灰度区间以改善图像质量。
灰度拉伸是通过控制输出图像中灰度级的展开程度来达到控制对比度的效果的。一般情况下都限制x1<x2, y1<y2,从而保证函数是单调递增的,以避免造成处理过的图像中灰度级发生颠倒。
3.6.2 MATLAB编程实现
文章编写了imgrayscaling()函数来实现灰度线性变换,它位于配套光盘“chapter3/code/”路径下的imgrayscaling.m文件中。
提示
作为本书的第1个由作者自己编写的MATLAB图像处理函数,imgrayscaling()具有很好的兼容性,它可以处理灰度、彩色、索引等不同类型,以及double、uint8等不同存储方式的图像。出于篇幅考虑,本书后面的所有的MATLAB函数都只将注意力集中在算法本身,不再考虑对各种类型图像的兼容性问题。如果读者需要处理多种类型的图像,可以参考imgrayscaling()函数的编写方法和技巧。
1.输入的处理
为了可以使用可变个数的参数,imgrayscaling()函数中使用了细胞数组(参见1.1.6小节),将函数的输入参数整体看作一个细胞数组。为此需要编写一个parse_inputs()函数来解析该细胞数组的内容,该函数的返回值为imgrayscaling()中所有可能由用户初始化的参数值。
parse_inputs()函数的完整实现如下,其中对未知个数的输入使用了参数varargin来表示。
function [A, map, x1, x2, y1, y2] = parse_inputs(varargin) % 这就是用来分析输入参数个数和有效性的函数parse_inputs % A 输入图像,RGB图 (3D), 灰度图 (2D), 或者索引图 (X) % map 索引图调色板 (:,3) % [x1, x2] 参数组 1,曲线中两个转折点的横坐标 % [y1, y2] 参数组 2,曲线中两个转折点的纵坐标 % 首先建立一个空的map变量,以免后面调用isempty(map)时出错 map = []; % IPTCHECKNARGIN(LOW, HIGH, NUM_INPUTS, FUNC_NAME) 检查输入参数的个数是否 % 符合要求,即NUM_INPUTS中包含的输入变量个数是否在LOW和HIGH所指定的范围 % 内。如果不在范围内,则此函数给出一个格式化的错误信息。 iptchecknargin(3,4, nargin, mfilename); % IPTCHECKINPUT(A, CLASSES, ATTRIBUTES, FUNC_NAME, VAR_NAME, ARG_POS) 检查给定 % 矩阵A中的元素是否属于给定的类型列表。如果存在元素不属于给定的类型,则给出 % 一个格式化的错误信息。 iptcheckinput(varargin{1}, ... {'uint8', 'uint16', 'int16', 'double'}, ... {'real', 'nonsparse'}, mfilename, 'I, X or RGB',1); % 根据参数个数的不同,分别确定相应的返回值 switch nargin case 3 % 可能是imgrayscaling(I, [x1, x2], [y1, y2]) 或imgrayscaling(RGB, [x1, x2], [y1, y2]) A = varargin{1}; x1 = varargin{2}(1); x2 = varargin{2}(2); y1 = varargin{3}(1); y2 = varargin{3}(2); case 4 A = varargin{1}; % imgrayscaling(X, map, [x1, x2], [y1, y2]) map = varargin{2}; x1 = varargin{2}(1); x2 = varargin{2}(2); y1 = varargin{3}(1); y2 = varargin{3}(2); end % 检测输入参数的有效性 % 检查RGB数组 if (ndims(A)= =3) && (size(A,3) ~=3) msg = sprintf('%s: 真彩色图像应当使用一个M-N-3维度的数组’, ... upper(mfilename)); eid = sprintf('Images:%s:trueColorRgbImageMustBeMbyNby3', mfilename); error(eid, '%s', msg); end if ~isempty(map) % 检查调色板 if (size(map,2) ~= 3) || ndims(map)>2 msg1 = sprintf('%s: 输入的调色板应当是一个矩阵’, ... upper(mfilename)); msg2 = ’并拥有三列’; eid = sprintf('Images:%s:inColormapMustBe2Dwith3Cols', mfilename); error(eid, '%s %s', msg1, msg2); elseif (min(map(:))<0) || (max(map(:))>1) msg1 = sprintf('%s: 调色板中各个分量的强度 ', upper(mfilename)); msg2 = ’应当在0和1之间’; eid = sprintf('Images:%s:colormapValsMustBe0to1', mfilename); error(eid, '%s %s', msg1, msg2); end end % 将int16类型的矩阵转换成uint16类型 if isa(A, 'int16') A = int16touint16(A); end
2.输出的处理
可以直接通过nargout这个参数判断用于接收结果的参数个数。参数nargout是由MATLAB自动赋值的,如果调用imgrayscaling()时没有使用变量接收返回值,则函数直接将结果通过imshow命令显示出来。
% 输出 if nargout= =0 % 显示结果 imshow(out); return; end
3.函数的实现
imgrayscaling()函数的完整实现如下。
function out = imgrayscaling(varargin) % IMGRAYSCALING 执行灰度拉伸功能 % 语法: % out = imgrayscaling(I, [x1, x2], [y1, y2]); % out = imgrayscaling(X, map, [x1, x2], [y1, y2]); % out = imgrayscaling(RGB, [x1, x2], [y1, y2]); % 这个函数提供灰度拉伸功能,输入图像应当是灰度图像,但如果提供的不是灰度 % 图像的话,函数会自动将图像转化为灰度形式。x1, x2, y1, y2应当使用双精度 % 类型存储,图像矩阵可以使用任何MATLAB支持的类型存储。 [A, map, x1 , x2, y1, y2] = parse_inputs(varargin{:}); % 计算输入图像A中数据类型对应的取值范围 range = getrangefromclass(A); range = range(2); % 如果输入图像不是灰度图,则需要执行转换 if ndims(A)= =3, % A矩阵为3维,RGB图像 A = rgb2gray(A); elseif ~isempty(map), % MAP变量为非空,索引图像 A = ind2gray(A, map); end % 对灰度图像则不需要转换 % 读取原始图像的大小并初始化输出图像 [M, N] = size(A); I = im2double(A); % 将输入图像转换为双精度类型 out = zeros(M, N); % 主体部分,双级嵌套循环和选择结构 for i=1:M for j=1:N if I(i, j)<x1 out(i, j) = y1 * I(i, j) / x1; elseif I(i, j)>x2 out(i, j) = (I(i, j)-x2)*(range-y2)/(range-x2) + y2; else out(i, j) = (I(i, j)-x1)*(y2-y1)/(x2-x1) + y1; end end end % 将输出图像的格式转化为与输入图像相同 if isa(A, 'uint8') % uint8 out = im2uint8(out); elseif isa(A, 'uint16') out = im2uint16(out); % 其他情况,输出双精度类型的图像 end % 输出: if nargout= =0 % 如果没有提供参数接受返回值 imshow(out); return; end
下面给出使用imgrayscaling()函数实现灰度线性变换的调用示例,程序实现中分别使用了两组不同的变换参数。代码如下。
>> I = imread('coins.png'); %读入原图像 >> J1 = imgrayscaling(I, [0.3 0.7], [0.15 0.85]); >> figure, imshow(J1, []); %得到图3.16左图 >> J2 = imgrayscaling(I, [0.15 0.85], [0.3 0.7]); >> figure, imshow(J2, []); %得到图3.16右图
上述程序的运行结果如图3.16和图3.17所示。
图3.16 对图3.7(a)的分段灰度变换的效果
图3.17 分段灰度变换对直方图的影响
从图3.16和图3.17可以看到第一组参数让图像灰度直方图上的非零区域扩展,而第二组参数让图像的灰度直方图非零区域压缩,这给目标图像带来了截然不同的效果。第一幅图像中的细节更加清晰,而第二幅图像更加柔和。
3.6.3 Visual C++编程实现
利用Visual C++实现图像的灰度分段线性变换的代码如下。
/************************************************** BOOL CImgProcess::ParLinTran(CImg * pTo, BYTE x1, BYTE x2, BYTE y1, BYTE y2) 功能: 图像的灰度分段线性变换 限制: x1 < x2 参数: CImg * pTo:输出CImg对象的指针 BYTE x1:分段线性变换第一点的横坐标 BYTE x2:分段线性变换第二点的横坐标 BYTE y1:分段线性变换第一点的纵坐标 BYTE y2:分段线性变换第二点的纵坐标 返回值: BOOL类型,TRUE为成功,FALSE为失败 ***************************************************/ BOOL CImgProcess::ParLinTran(CImg * pTo, BYTE x1, BYTE x2, BYTE y1, BYTE y2) { // 首先检查图像的类型 if(m_pBMIH->biBitCount ! = 8) return FALSE; // 不是8-bpp灰度图像,返回错误 // 检查参数范围 if (x1>x2) return FALSE; // 参数关系错误,返回错误 BYTE gray; // 临时变量,存储当前光标像素的灰度值 int target; // 临时变量,存储当前光标像素的目标值 for (int i=0; i<m_pBMIH->biHeight; i++) { for (int j=0; j<m_pBMIH->biWidth; j++) { gray = GetGray(j, i); // 按公式运算 if (gray<=x1) { target = y1 * gray / x1; } else if (gray<=x2) { target = (y2-y1)*(gray-x1)/(x2-x1) + y1; } else { target = (255-y2)*(gray-x2)/(255-x2) + y2; }; if (target < 0) target = 0; if (target > 255) target = 255; // 写入目标图像 pTo->SetPixel(j, i, RGB(target, target, target)); } } return TRUE; }
利用ParLinTran()函数实现分段线性变换的完整示例被封装在DIPDemo工程中的视图类函数void CDIPDemoView::OnPointStdlin()中,其中调用ParLinTran()函数的代码片断如下所示。
// 输出的临时对象 CImgProcess imgOutput = imgInput; // 分段线性变换 imgInput.ParLinTran(&imgOutput, dlg.m_bS1, dlg.m_bS2, dlg.m_bT1, dlg.m_bT2); // 将结果返回给文档类 pDoc->m_Image = imgOutput;
上述程序运行时会弹出对话框,要求用户设置变换参数。读者可以通过光盘中示例程序DIPDemo中的菜单命令“点运算→分段线性变换”来观察处理效果。