这一章在干嘛?

把前面学过的「矩阵就是数据」这个直觉推到极致 —— 任何图像都是矩阵,矩阵里的数就是颜色;任何声音都是向量,向量里的数就是振幅。学完你就能用 MATLAB 读写图片、做滤镜、把语音合成出来,再用一个会响应点击的小程序把它们打包起来。整章分四块:图像(13.1~13.3)、GUI(13.4~13.7)、声音(13.8)。

13.1 图像就是矩阵

13.2 颜色的两种编码方式:索引图与 RGB

13.3 图像文件的读写与基本处理

13.4 GUI 基础:figure 与 uicontrol

13.5 文本框、按钮与滑块

13.6 按钮组:单选与互斥

13.7 GUIDE 与 App Designer:可视化搭界面

13.8 声音文件与音效合成

13.1 图像就是矩阵

「图像就是矩阵」是这一章最重要的认知 —— 一旦接受这个前提,后面所有 image、imshow、imread、imwrite 都只是矩阵操作的一种新写法而已。这跟你在前面章节里学的「数据 = 矩阵」「信号 = 向量」是同一种世界观:把现实信号数字化成最基本的数组,让 MATLAB 把数组画出来 / 算出来。

像素(pixel,picture element)是图像的最小单元。把一张图按行按列拆开,每个像素就是一个数。整张图就是一个二维矩阵:

  • 一张 的灰度图 = 一个 的矩阵;
  • 一张 的彩色图(RGB,真彩色)= 一个 的三维矩阵;
  • 一张 的索引图(indexed image)= 一个 的整数矩阵 + 一个 的「调色板」。

下面这张示意图把索引图的两层结构画得很清楚 —— 左边图像矩阵里每个格子是个「指向调色板的指针」,右边的调色板每一行存储一种真实颜色:

图像矩阵中每个像素指向调色板的对应行,箭头展示 (x, x) → colormap 第 x 行

这张图想说明的是:图像矩阵和调色板矩阵是「两张表」,图像矩阵本身不存颜色,只存「去查表的下标」。这种设计在科学可视化里非常常见 —— 温度场、压力场、应力场只需要几十种「色阶」,用索引图比每个像素直接存 RGB 省 90% 内存。

为什么工程师喜欢用 MATLAB 处理图像?

因为它把像素处理降维成了矩阵运算 —— 你想给整张图加亮,只需 image = image + 30;想左右翻转,只需 image(:, end:-1:1, :);想做卷积滤波,直接调用 conv2。这是 OpenCV 或 C 写一行循环才能搞定的事。MATLAB 之所以能在图像/信号处理领域几十年长盛不衰,本质原因就是它把「像素」「采样点」「单元格」全都统一成了「矩阵元素」这一种数据结构。

常见坑:uint8 与 double 的取值范围

真彩色图像常常以 uint8 类型存储,每个通道取值 0~255。但很多图像处理函数(卷积、归一化、灰度变换)默认要求 double 取值 0~1。把它们混用就会出现「加出来全是白色」或「plot 出来一条直线」之类的诡异现象。解决套路:处理前先 im2double 转成 0~1 的 double,处理完要保存再 im2uint8 转回去。后面 13.3 节还会用阳台雪景图演示这条 pipeline。


13.2 颜色的两种编码方式:索引图与 RGB

颜色编码是这一节的硬核。理解清楚「索引图」和「RGB 图」的区别,等于掌握了 90% 的图像处理编程模型。

13.2.1 索引图(colormap 方式)

索引图的思路是「调色板复用」:颜色信息不存到每个像素里,而是放在一张叫 colormap 的 矩阵里( 是颜色总数)。图像矩阵的每个元素是一个整数下标(1~),指出「这个像素的颜色在调色板的第几行」。

MATLAB 自带多个命名调色板:默认 parula(64 色)、jet(蓝→青→绿→黄→红,64 色)、pink、hot、gray、hsv 等等。colormap 不带参数时返回当前调色板,带参数时切换。

>> map = colormap;       % 把当前调色板存成 64x3 的 double 矩阵
>> [r, c] = size(map)
r = 64
c = 3
>> map(60:64, :)         % 看最后 5 行:黄到亮黄的渐变
ans =
    0.9626    0.8705    0.1309
    0.9589    0.8949    0.1132
    0.9598    0.9218    0.0948
    0.9661    0.9514    0.0755
    0.9763    0.9831    0.0538

注意 colormap 里的颜色分量都是 double,范围 01(不是 0255)。这是约定 —— 索引图和 RGB 矩阵类型约定不同:索引图 + colormap 走 double(01),而 RGB 矩阵走 uint8(0255)。

最直观的演示是把 1~64 排成一个 矩阵喂给 image,就能看到整个 parula 调色板:

>> imagemat = reshape(1:64, 8, 8);
>> image(imagemat)

8x8 矩阵展示 parula 调色板的 64 种颜色,按列序号映射颜色

切到 jet 调色板会看到完全不同的彩虹渐变(蓝→青→绿→黄→红):

>> colormap(jet)            % 把当前调色板换成 jet
>> image(imagemat)          % 同一矩阵,色彩立即改变

8x8 矩阵展示 jet 调色板的 64 种颜色,从深蓝到深红的彩虹条

对比上面两张图:调色板不一样,同一个整数矩阵显示出来天差地别。这说明图像矩阵本身不含颜色,颜色是由矩阵 + colormap 共同决定的。你要保存为图片发给同事,要么发带 colormap 的 .fig 文件,要么把它先转成 RGB 矩阵再 imwrite。

随手生成一张「伪图像」也很容易 —— 把 1~颜色数之间的随机整数塞进矩阵即可:

>> mat = randi(64, 5);      % 5x5 矩阵,元素是 1~64 的随机整数
>> image(mat)

5x5 随机方块图,每个方块从 jet 调色板随机抽色

把网格放大到 看上去才像「真的图」:

>> mat = randi(64, 500);
>> image(mat)

500x500 随机彩色像素,看起来就像电子噪点或抽象画

你也可以自己造调色板。下面只用三种颜色(黑、白、红)做一张 40×40 的随机图:

>> mycolormap = [0 0 0; 1 1 1; 1 0 0];   % 黑、白、红
>> colormap(mycolormap)
>> mat = randi(3, 40);                    % 元素只能是 1/2/3
>> image(mat)

自定义 3 色调色板(黑/白/红)下的 40x40 随机图,是像素艺术的雏形

调色板的颜色分量不必是整数。下面用 6 级深红展示 colormap 是怎么「渐变」的:

>> colors = [0 0 0;
            0.2 0 0;
            0.4 0 0;
            0.6 0 0;
            0.8 0 0;
            1 0 0];
>> colormap(colors)
>> vec = 1:length(colors);
>> image(vec)

6 级红色渐变条,每级用 0~1 之间的实数表示红色分量强度

调色板不只服务于 image,任何把数值映射到颜色的 plot 函数(surf、mesh、imagesc)都能用 colormap 切换:

>> [x, y, z] = sphere(20);   % 单位球坐标
>> colormap(pink)
>> surf(x, y, z)
>> title('Pink sphere')
>> colorbar

球面用 pink 调色板着色,附带右侧颜色条显示数值与颜色对应关系

R2014b 之后还可以让一个 Figure 里不同子图用不同调色板 —— 把 axes 句柄传给 colormap:

[x, y, z] = sphere(20);
ax1 = subplot(1, 2, 1);
colormap(ax1, pink);    % 左子图:粉色调
surf(x, y, z); title('Pink sphere');
 
ax2 = subplot(1, 2, 2);
colormap(ax2, jet);     % 右子图:jet 彩虹
surf(x, y, z); title('Jet sphere');

同一个 figure 中左球用 pink、右球用 jet,对比鲜明

同一个 figure 中左球用 pink 调色板、右球用 jet 调色板,互不干扰

13.2.2 真彩色(RGB / truecolor)

真彩色绕开了调色板,直接把每个像素的 R/G/B 三个分量都存在矩阵里:

  • 三维矩阵的尺寸是 ;
  • (:,:,1) 是红色层,(:,:,2) 是绿色层,(:,:,3) 是蓝色层;
  • 一个 8-bit RGB 图的元素是 uint8,每个通道取值 0~255(0 = 该通道无光,255 = 该通道最亮)。

下面创建一个 的「四色像素图」:(1,1) 红、(1,2) 蓝、(2,1) 绿、(2,2) 黑:

>> mat = zeros(2, 2, 3);     % 预分配黑色
>> mat(1, 1, 1) = 255;        % 左上:R=255 → 纯红
>> mat(1, 2, 3) = 255;        % 右上:B=255 → 纯蓝
>> mat(2, 1, 2) = 255;        % 左下:G=255 → 纯绿
>> mat = uint8(mat);
>> image(mat)

2x2 RGB 矩阵生成的图像:左上红、右上蓝、左下绿、右下黑

想单独看某条通道?把对应层抽出来做一张「只剩红/绿/蓝」的图:

matred   = uint8(zeros(2,2,3));  matred(:,:,1) = mat(:,:,1);
matgreen = uint8(zeros(2,2,3));  matgreen(:,:,2) = mat(:,:,2);
matblue  = uint8(zeros(2,2,3));  matblue(:,:,3)  = mat(:,:,3);
 
subplot(2,2,1); image(mat);        title('原图')
subplot(2,2,2); image(matred);     title('R 通道')
subplot(2,2,3); image(matgreen);   title('G 通道')
subplot(2,2,4); image(matblue);    title('B 通道')

四个子图:原图(左上)、仅保留 R 的红色块(右上)、仅保留 G 的绿色块(左下)、仅保留 B 的蓝色块(右下)

把三张通道子图相加(重叠的部分会变白)就回到原图。这说明 RGB 表示法的本质就是「三层灰度图叠加」。理解这点后,对图像处理的算子就有直觉了 —— 比如「加红滤镜」就是把 R 通道整体加大,「去蓝」就是把 B 通道置零。

下面是一个练习题:构造一个 的 RGB 矩阵(绿色背景 + 红色方块):

>> pic = zeros(3,3,3,'uint8');
>> pic(:,:,1) = 200; pic(:,:,2) = 220; pic(:,:,3) = 80;   % 整张铺成黄绿色
>> pic(2,3,:) = [255 0 0];                                 % 中行右列塞个红块
>> image(pic)

3x3 真彩色矩阵:满屏黄绿色,中央右下角一个亮红方块

常见坑:colorbar 上 0 1 与 0255 的混淆

colormap 矩阵用 double(01);RGB 图像用 uint8(0255)。colorbar 显示的是 colormap 的值(01),而 image 显示的是矩阵元素。要保持一致,要么把所有数据归一化到 double(01),要么在画 RGB 图前 caxis([0 255])。

13.2.3 灰度图与强度图

工程上第三种常见图像是灰度图 / 强度图(intensity image)。它的矩阵元素是 0~1 的 double,表示灰度(0 = 黑,1 = 白)。rgb2gray 把 RGB 转灰度,im2double 把 uint8 转 double。强度图常常搭配 imagesc(image scale,自动缩放到当前 colormap)来画,例如画一个灰度热图:

>> A = magic(10);             % 一个 10x10 的「魔方矩阵」
>> imagesc(A);                % 自动按当前 colormap 着色
>> colorbar;

几个常见函数的混淆

  • image:纯粹按矩阵元素画,不缩放
  • imagesc:自动缩放到 [min max] 再映射到当前 colormap,画「按比例填色」
  • imshow:图像处理工具箱的图像显示函数,自动设置坐标轴比例(保持原图长宽比)

13.3 图像文件的读写与基本处理

真实工程里图片都是存成文件的(JPG、PNG、TIFF、BMP)。MATLAB 用 imread 读成矩阵、用 imwrite 把矩阵写成文件,imshow 是个比 image 更「图像友好」的显示函数(自动处理坐标轴、长宽比)。

>> porchimage = imread('snowyporch.JPG');    % 读入一张 JPG
>> size(porchimage)                          % 注意是 3 维的
ans =
        2848        4272           3        % 高 2848 × 宽 4272 × 3 通道
>> class(porchimage)
ans =
uint8

这张阳台雪景 4272 × 2848 ≈ 1200 万像素,是手机拍的照片。直接 image(porchimage) 显示:

>> image(porchimage)

读入 MATLAB 的 4272x2848 阳台雪景原图,色彩鲜艳

想「整体压暗」,最直接的方式就是乘 0.5(uint8 矩阵乘以 0.5 会自动截断到 0~255):

>> dimmer = 0.5 * porchimage;
>> imwrite(dimmer, 'dimporch.JPG');           % 把处理后的图写回文件

整张图乘以 0.5 后的暗化效果:色彩只剩一半亮度,阴影区域更暗

工程实战:从代码到图像处理的 3 个动作

  1. 读:I = imread('pic.jpg');
  2. 改:所有你想得到的 numpy 矩阵操作都能用 —— I(end:-1:1, :, :) 上下翻转、I(:, 100:300, :) 横向裁剪、rgb2gray(I) 转灰度。
  3. 写:imwrite(I, 'out.png'); 或 imshow(I); 只看不存。

没有「图像 API」这回事,就是矩阵。

下面这几道教材练习题涵盖了图像处理入门的高频操作,可以一一对照原图:

Practice 13.7(蓝绿渐变调色板)

「10 蓝 + 10 aqua + 10 绿」三段渐变,铺成 3×10 图像:

cmap = zeros(30, 3);
% 蓝色段:R=0,G=0,B 从 0.1 增到 1
cmap(1:10, 3) = linspace(0.1, 1, 10);
% aqua 段:R=0,G 与 B 都从 0.1 增到 1
cmap(11:20, 2:3) = repmat(linspace(0.1, 1, 10)', 1, 2);
% 绿色段:R=0,G 从 0.1 增到 1,B=0
cmap(21:30, 2) = linspace(0.1, 1, 10);
 
colormap(cmap);
img = [1:10; 11:20; 21:30];   % 3x10 矩阵,索引对应 cmap 各行
image(img);

三行三色渐变:上面一行是从黑到蓝的 10 级蓝,中间一行是从黑到青(aqua)的 10 级 aqua,下面一行是从黑到绿的 10 级绿。每一行的「浓度」逐级增加

Practice 13.6(黑白噪声 + 颜色反转)

bw = [0 1; 1 0];             % 黑白颜色:第 1 行黑,第 2 行白
colormap(bw);
mat = randi(2, 50, 50);      % 50x50 随机选 0/1
subplot(1,2,1); image(mat);  title('Yin');
subplot(1,2,2); image(1-mat); title('Yang');  % 颜色反转

左图标题 Yin、右图标题 Yang:两张 50x50 黑白噪声图,黑白像素的位置完全相反,像镜面映射的随机噪点

Practice 13.4(RGB 色块)

50x50 的 RGB 图,绿/黄/红三色方块互相重叠:

rgb = zeros(50, 50, 3, 'uint8');
% 绿块在左上
rgb(10:33, 10:33, 2) = 255; rgb(10:33, 10:33, 1) = 0; rgb(10:33, 10:33, 3) = 0;
% 红块在右下
rgb(20:42, 20:42, 1) = 255; rgb(20:42, 20:42, 2) = 0; rgb(20:42, 20:42, 3) = 0;
% 黄块 = 红 + 绿,在重叠处
rgb(20:33, 20:33, 1) = 255; rgb(20:33, 20:33, 2) = 255;
image(rgb);

50x50 RGB 图:绿色方块与红色方块部分重叠,重叠区域因为红+绿混合显示为黄色

Practice 13.9(亮度感知错觉)

「同一灰度在不同背景下显得不一样亮」。构造两个 3×3 RGB 图,中块同色、外圈不同色:

% 左图:外圈深蓝,中块稍亮
left  = zeros(3,3,3,'uint8'); left(:,:,3)  = 200; left(2,2,:) = [120 120 200];
% 右图:外圈草绿,中块同色稍亮
right = zeros(3,3,3,'uint8'); right(:,:,2) = 180; right(2,2,:) = [120 200 120];
subplot(1,2,1); image(left);
subplot(1,2,2); image(right);

左图:3x3 RGB 图,外圈深蓝、中块较亮的蓝色(看起来中块「更亮」)

右图:3x3 RGB 图,外圈草绿、中块较亮的蓝色(看起来中块「更暗」)

这两张图的中央像素颜色完全相同,但因为周围环境不同,肉眼会感觉左边中央更亮、右边更暗。这就是著名的「亮度恒常错觉」(brightness illusion),在图像处理做对比度增强时是绕不开的现象。

Exercise 26(随机游走纹理)

教材最后给了一个 ranwalk(n) 函数,模拟从中心出发的随机游走并把路径存成矩阵。运行两次( 与 )对比,能看到「确定性 + 随机性」是怎样长成两种完全不同的视觉形态的:

walk_small = ranwalk(8);     % 8x8 矩阵,色块稀疏
walk_big   = ranwalk(100);   % 100x100 矩阵,纹理丰富
 
figure;
subplot(1,2,1); image(walk_small); axis square; title('ranwalk(8)');
subplot(1,2,2); image(walk_big);   axis square; title('ranwalk(100)');

左图:8x8 随机游走纹理,深蓝/蓝色块呈锯齿状蔓延,色块大而稀疏

右图:100x100 随机游走纹理,整个画面长成彩色灌木丛,模拟森林火灾扩散或枝晶生长

随机游走在物理上模拟的是森林火灾蔓延、枝晶生长、布朗运动等扩散现象。把模拟结果当「图」画出来,本质上还是个矩阵赋值 + image。


13.4 GUI 基础:figure 与 uicontrol

GUI(Graphical User Interface,图形用户界面)让程序不再只是「跑完结束」,而是「等待用户点击、滑块拖动、文字输入」之后再跑。MATLAB 里一切 GUI 都是 11 章讲过的「句柄对象」的延伸 —— 一个 Figure 是父,下面挂着若干 uicontrol 子对象。

13.4.1 第一个 GUI

下面这个例子只做一件事:在窗口中央显示一行静态文字。

% simpleGui.m
function simpleGui
% 创建一个只有静态文本框的极简 GUI
% Format: simpleGui 或 simpleGui()
 
% 1) 先建一个隐形窗口
f = figure('Visible', 'off', ...
           'Color',    'white', ...
           'Position', [300 400 450 250]);   % [左 下 宽 高],单位像素
 
% 2) 在窗口里放一个静态文本框
htext = uicontrol('Style',     'text', ...
                  'Position',  [200 50 100 25], ...
                  'String',    'My First GUI string');
 
% 3) 给窗口起名字,挪到屏幕中央,最后才显示
f.Name = 'Simple GUI';
movegui(f, 'center');
f.Visible = 'on';     % 关键:在所有对象都建好之后才 Visible
end