这一章在干嘛?
把前面学过的「矩阵就是数据」这个直觉推到极致 —— 任何图像都是矩阵,矩阵里的数就是颜色;任何声音都是向量,向量里的数就是振幅。学完你就能用 MATLAB 读写图片、做滤镜、把语音合成出来,再用一个会响应点击的小程序把它们打包起来。整章分四块:图像(13.1~13.3)、GUI(13.4~13.7)、声音(13.8)。
13.4 GUI 基础:figure 与 uicontrol
13.7 GUIDE 与 App Designer:可视化搭界面
13.1 图像就是矩阵
「图像就是矩阵」是这一章最重要的认知 —— 一旦接受这个前提,后面所有 image、imshow、imread、imwrite 都只是矩阵操作的一种新写法而已。这跟你在前面章节里学的「数据 = 矩阵」「信号 = 向量」是同一种世界观:把现实信号数字化成最基本的数组,让 MATLAB 把数组画出来 / 算出来。
像素(pixel,picture element)是图像的最小单元。把一张图按行按列拆开,每个像素就是一个数。整张图就是一个二维矩阵:
- 一张 的灰度图 = 一个 的矩阵;
- 一张 的彩色图(RGB,真彩色)= 一个 的三维矩阵;
- 一张 的索引图(indexed image)= 一个 的整数矩阵 + 一个 的「调色板」。
下面这张示意图把索引图的两层结构画得很清楚 —— 左边图像矩阵里每个格子是个「指向调色板的指针」,右边的调色板每一行存储一种真实颜色:

这张图想说明的是:图像矩阵和调色板矩阵是「两张表」,图像矩阵本身不存颜色,只存「去查表的下标」。这种设计在科学可视化里非常常见 —— 温度场、压力场、应力场只需要几十种「色阶」,用索引图比每个像素直接存 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)
切到 jet 调色板会看到完全不同的彩虹渐变(蓝→青→绿→黄→红):
>> colormap(jet) % 把当前调色板换成 jet
>> image(imagemat) % 同一矩阵,色彩立即改变
对比上面两张图:调色板不一样,同一个整数矩阵显示出来天差地别。这说明图像矩阵本身不含颜色,颜色是由矩阵 + colormap 共同决定的。你要保存为图片发给同事,要么发带 colormap 的 .fig 文件,要么把它先转成 RGB 矩阵再 imwrite。
随手生成一张「伪图像」也很容易 —— 把 1~颜色数之间的随机整数塞进矩阵即可:
>> mat = randi(64, 5); % 5x5 矩阵,元素是 1~64 的随机整数
>> image(mat)
把网格放大到 看上去才像「真的图」:
>> mat = randi(64, 500);
>> image(mat)
你也可以自己造调色板。下面只用三种颜色(黑、白、红)做一张 40×40 的随机图:
>> mycolormap = [0 0 0; 1 1 1; 1 0 0]; % 黑、白、红
>> colormap(mycolormap)
>> mat = randi(3, 40); % 元素只能是 1/2/3
>> image(mat)
调色板的颜色分量不必是整数。下面用 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)
调色板不只服务于 image,任何把数值映射到颜色的 plot 函数(surf、mesh、imagesc)都能用 colormap 切换:
>> [x, y, z] = sphere(20); % 单位球坐标
>> colormap(pink)
>> surf(x, y, z)
>> title('Pink sphere')
>> colorbar
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');

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)
想单独看某条通道?把对应层抽出来做一张「只剩红/绿/蓝」的图:
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 通道')
把三张通道子图相加(重叠的部分会变白)就回到原图。这说明 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)
常见坑:colorbar 上 0
1 与 0255 的混淆colormap 矩阵用 double(0
1);RGB 图像用 uint8(0255)。colorbar显示的是 colormap 的值(01),而1),要么在画 RGB 图前image显示的是矩阵元素。要保持一致,要么把所有数据归一化到 double(0caxis([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)
想「整体压暗」,最直接的方式就是乘 0.5(uint8 矩阵乘以 0.5 会自动截断到 0~255):
>> dimmer = 0.5 * porchimage;
>> imwrite(dimmer, 'dimporch.JPG'); % 把处理后的图写回文件
工程实战:从代码到图像处理的 3 个动作
- 读:
I = imread('pic.jpg');- 改:所有你想得到的 numpy 矩阵操作都能用 ——
I(end:-1:1, :, :)上下翻转、I(:, 100:300, :)横向裁剪、rgb2gray(I)转灰度。- 写:
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);
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'); % 颜色反转
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);
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);

这两张图的中央像素颜色完全相同,但因为周围环境不同,肉眼会感觉左边中央更亮、右边更暗。这就是著名的「亮度恒常错觉」(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)');

随机游走在物理上模拟的是森林火灾蔓延、枝晶生长、布朗运动等扩散现象。把模拟结果当「图」画出来,本质上还是个矩阵赋值 + 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
几个细节值得点出:
'Visible', 'off'让窗口在搭建阶段不可见,避免出现「GUI 一半一半」的尴尬,最后才'on'。Position的四个数字是[left bottom width height],单位默认像素。movegui(f, 'center')把窗口挪到屏幕正中。'Normalized'单位:把坐标改成 0~1 的相对值,让窗口在不同屏幕分辨率下看起来一致(推荐用)。
13.4.2 事件驱动与回调(callback)
GUI 程序和普通程序最大的不同是事件驱动编程:用户的每一次点击、拖动、按键都会触发一个事件,事件会调用你写好的「回调函数」(callback function)。MATLAB 的回调约定是:用户写一个子函数,系统自动传两个参数进去:
hObject:触发事件的那个控件的句柄eventdata:事件相关数据(一般不用,可以用~跳过)
% simpleGuiNormalized.m — 使用 Normalized 单位的版本
function simpleGuiNormalized
f = figure('Visible', 'off', 'Color', 'white', ...
'Units', 'Normalized', ...
'Position', [.25 .5 .35 .3]); % 占屏幕左中 35%×30%
htext = uicontrol('Style', 'text', ...
'Units', 'Normalized', ...
'Position', [.45 .2 .2 .1], ...
'String', 'My First GUI string');
f.Name = 'Simple GUI Normalized';
movegui(f, 'center');
f.Visible = 'on';
endNormalized 单位下,Position = [.25 .5 .35 .3] 表示「窗口左下角在屏幕的 25% 横向、50% 纵向处,宽 35%、高 30%」。窗口放大缩小,里面的控件会按比例缩放。
为什么推荐 Normalized?
你的 GUI 在实验室 1080p 显示器上调好,放在 4K 投影仪上用像素坐标会挤成左上角一小块。Normalized 单位能让你「写一次,到处能看」。
13.5 文本框、按钮与滑块
13.5.1 可编辑文本框(edit)—— 第一次交互
下面这个 GUI 让用户在文本框输入字符串,回车后大红色字体显示出来。回调函数用嵌套函数写在外层 function 内部,这样可以共享外层的句柄变量。
% guiWithEditbox.m
function guiWithEditbox
% 一个可编辑文本框 + 回调函数把用户输入显示成红色大字
f = figure('Visible', 'off', 'Color', 'white', ...
'Units', 'Normalized', 'Position', [.25 .5 .4 .2]);
f.Name = 'GUI with editable text';
movegui(f, 'center');
% 提示文字
hsttext = uicontrol('Style', 'text', ...
'BackgroundColor', 'white', ...
'Units', 'Normalized', 'Position', [.2 .6 .6 .2], ...
'String', 'Enter your string here');
% 可编辑文本框 + 回调
huitext = uicontrol('Style', 'edit', ...
'Units', 'Normalized', 'Position', [.3 .3 .4 .2], ...
'Callback', @callbackfn); % 关键:'Callback' 指向回调句柄
f.Visible = 'on';
% ——— 嵌套回调 ———
function callbackfn(~, ~)
% 隐藏原控件
set([hsttext huitext], 'Visible', 'off');
% 取输入内容(也可以用 huitext.String)
printstr = huitext.String;
% 大红色字体再显示
hstr = uicontrol('Style', 'text', ...
'BackgroundColor', 'white', ...
'Units', 'Normalized', ...
'Position', [.1 .3 .8 .4], ...
'String', printstr, ...
'ForegroundColor', 'Red', ...
'FontSize', 30);
hstr.Visible = 'on';
end
end
按回车后回调函数触发,原控件消失,文字以 30 号红字出现:

注意几个工程要点:
- 回调函数声明是
callbackfn(hObject, eventdata),两个输入参数由 GUI 系统自动传。如果不用,可以写callbackfn(~, ~)。 - 嵌套函数可以直接闭包外层的
hsttext、huitext句柄,不必通过guidata传来传去。 get和set是句柄对象的标准读写接口:huitext.String是 R2014b 后的点语法糖,等价于get(huitext, 'String')。
13.5.2 按钮(pushbutton)—— 不再「回车即触发」
把上面那个例子改成「必须点按钮才触发」:
% guiWithPushbutton.m(节选关键差异)
hbutton = uicontrol('Style', 'pushbutton', ...
'String', 'Push me!!', ...
'Units', 'Normalized', ...
'Position', [.6 .1 .3 .2], ...
'Callback', @callbackfn); % 回调挂在按钮上
按钮回调里别忘了处理「空字符串」的情况,否则用户没输入就点会出现空白:
function callbackfn(~, ~)
set([hsttext huitext hbutton], 'Visible', 'off');
printstr = huitext.String;
if isempty(printstr)
printstr = 'Enter something next time!';
end
hstr = uicontrol('Style', 'text', ...
'BackgroundColor', 'white', ...
'Units', 'Normalized', 'Position', [.1 .3 .8 .4], ...
'String', printstr, ...
'ForegroundColor', 'Red', 'FontSize', 30);
hstr.Visible = 'on';
end13.5.3 滑块(slider)—— 连续输入
滑块适合做亮度、音量、阈值这类「在范围内连续调」的参数。下面做一个英寸↔厘米换算器:滑块拖到 1.0,按钮按下后显示「1.0 in = 2.54 cm」。
% slider 关键代码(按教材练习 13.3 改写)
slhan = uicontrol('Style', 'slider', ...
'Units', 'Normalized', 'Position', [.3 .3 .4 .1], ...
'Min', 0, 'Max', 10, 'Value', 1, ...
'Callback', @callbackfn);
% 回调:把滑块当前值换算成厘米
function callbackfn(~, ~)
inches = slhan.Value;
cm = inches * 2.54;
set(resulttext, 'String', sprintf('%.2f in = %.2f cm', inches, cm));
end下面三个截图演示了完整的滑块应用 —— 滑块在小范围内移动,下方实时显示数值:



常见坑:滑块的
SliderStep不设就用默认值
SliderStep是个[小步 大步]二元组,默认是[0.01 0.10]。想做「一次加 1」的整数滑块,要么手动改SliderStep = [1/(Max-Min) 1/(Max-Min)],要么在回调里把Value取整round(slhan.Value)。
13.5.4 多个按钮 vs 一个回调服务多个按钮
可以让两个按钮各自挂自己的回调,也可以两个按钮挂同一个回调,靠判断 hObject 是哪个:
% 一个回调处理两个按钮
function callbackfn(hObject, ~)
set([hbutton1 hbutton2], 'Visible', 'off');
hstr = uicontrol('Style', 'text', ...
'BackgroundColor', 'white', ...
'Units', 'Normalized', 'Position', [.4 .5 .2 .2], ...
'FontSize', 30);
if hObject == hbutton1
hstr.String = '!!!!!';
hstr.ForeColor = 'red';
else
hstr.String = '*****';
hstr.ForeColor = 'blue';
end
hstr.Visible = 'on';
end

13.5.5 把 plot 和 image 塞进 GUI
在 GUI 里画图只要先用 axes 在 Figure 上定一个子区域当画布,回调里照常用 plot、image:
% guiSliderPlot.m 节选:滑块控制 sin(x) 终点
axhan = axes('Position', [.2 .1 .6 .5]); % 在窗口下方留一块画布
function callbackfn(~, ~)
num = slhan.Value;
set(hsttext, 'Visible', 'on', 'String', num2str(num));
x = 0:num/50:num;
y = sin(x);
plot(x, y); xlabel('x'); ylabel('sin(x)');
end
把 plot 换成 image 就能做「拖滑块调亮度」的小工具(13.3 节的 dimmer):
function callbackfn(~, ~)
num = slhan.Value; % 滑块 0~1
set(hsttext, 'Visible', 'on', 'String', num2str(num));
myimage1 = imread('snowyporch.JPG');
dimmer = num * myimage1; % 0~1 的亮度因子
image(dimmer); % 直接显示
end
把科研 Demo 包装成 GUI
这是把算法「产品化」的最快路径 —— 把算法核心放在 callback 里,前端用
uicontrol暴露两三个旋钮、一个 plot 区域。30 行代码就能把命令行脚本变成可交互的小工具。
13.6 按钮组:单选与互斥
单选按钮(radio button)的特点是「一组里只能选一个」。MATLAB 用 uibuttongroup 把单选按钮圈起来实现互斥。
% guiWithButtongroup.m 节选
f = figure('Visible', 'off', 'Color', 'white', ...
'Units', 'Normalized', 'Position', [.2 .5 .4 .3]);
grouph = uibuttongroup('Parent', f, ...
'Units', 'Normalized', 'Position', [.2 .5 .4 .4], ...
'Title', 'Choose Color', ...
'SelectionChangedFcn', @whattodo); % 选谁触发谁
toph = uicontrol(grouph, 'Style', 'radiobutton', ...
'String', 'Blue', ...
'Units', 'Normalized', 'Position', [.2 .7 .4 .2]);
both = uicontrol(grouph, 'Style', 'radiobutton', ...
'String', 'Green', ...
'Units', 'Normalized', 'Position', [.2 .4 .4 .2]);
texth = uicontrol('Style', 'text', 'Units', 'Normalized', ...
'Position', [.6 .5 .3 .3], 'String', 'Hello', ...
'Visible', 'off', 'BackgroundColor', 'white');
grouph.SelectedObject = []; % 一开始谁都不选
f.Name = 'GUI with button group';
movegui(f, 'center');
f.Visible = 'on';
function whattodo(~, ~)
which = get(grouph, 'SelectedObject');
if which == toph
texth.ForeColor = 'blue';
else
texth.ForeColor = 'green';
end
texth.Visible = 'on';
end
end

注意 SelectionChangedFcn 是按钮组级别的回调 —— 不管选哪个按钮,都会触发它,函数内部用 grouph.SelectedObject 判断到底选了谁。教材练习 13.19 让你写风寒指数计算器、风速滑块等场景,思路都是一样的。
13.7 GUIDE 与 App Designer:可视化搭界面
手写 uicontrol 适合学习,工程上更常见的做法是用 MATLAB 自带的「所见即所得」GUI 构建工具。教材介绍了两个:
- GUIDE(R2016a 之前的标配):命令窗口敲
guide,进入拖拽式编辑器,自动生成两份文件(.m+.fig)。 - App Designer(R2016a 引入的现代替代):命令窗口敲
appdesigner,生成的代码全部是面向对象的,控件对应uifigure/uilabel/uislider/uibutton等专门函数而不是uicontrol('Style', 'slider', ...)。


13.7.1 App Designer 里的核心概念
每个 App Designer 工程是一个类(classdef),继承自 matlab.apps.AppBase。每个拖到画布上的控件对应类的 properties:
classdef App1 < matlab.apps.AppBase
properties (Access = public)
UIFigure matlab.ui.Figure % 顶层窗口
Label matlab.ui.control.Label % 拖入的 Label 自动变成属性
end
methods (Access = private)
function createComponents(app)
app.UIFigure = uifigure;
app.UIFigure.Position = [100 100 640 480];
app.UIFigure.Name = 'UI Figure';
% ... 各种 ui* 控件的创建
end
end
methods (Access = public)
function app = App1()
createComponents(app);
registerApp(app, app.UIFigure);
runStartupFcn(app, @startupFcn);
end
end
end跑起来后这个 App 就会显示一个 UI Figure,里面有一个写着「Hello」的 Label:

13.7.2 UI Figure 函数族
App Designer 抛弃了 uicontrol('Style', ...) 这种「一个函数所有控件」的老套路,每种控件都有自己的构造函数:
| 控件 | App Designer 函数 | 旧 uicontrol 风格 |
|---|---|---|
| 顶层窗口 | uifigure | figure |
| 静态文字 | uilabel | uicontrol('Style','text') |
| 可编辑文本框 | uitextarea / uieditfield | uicontrol('Style','edit') |
| 按钮 | uibutton | uicontrol('Style','pushbutton') |
| 滑块 | uislider | uicontrol('Style','slider') |
| 单选 / 复选 | uiradiobutton / uicheckbox | uicontrol('Style','radio'/'checkbox') |
| 绘图区 | uiaxes | axes |
举个例子,不用 App Designer,光在命令窗口就能现搭一个简单滑块窗口:
>> uif = uifigure;
>> uif.Name = 'Simple UI Fig Slider';
>> uislid = uislider(uif); % 默认 0~100 的整数刻度滑块
要给滑块加个标签显示数值,App Designer 的回调叫 ValueChangedFcn(旧版是 Callback):
% uislideruilabel.m
function uislideruilabel
uif = uifigure('Name', 'UI Fig Slider with Callback');
uislid = uislider(uif);
uilab = uilabel(uif, 'Position', [150 200 30 15], 'Text', '0');
uislid.ValueChangedFcn = @whatslid;
function whatslid(~, ~)
uilab.Text = num2str(round(uislid.Value));
end
end
App Designer 项目里把滑块和它的标签同时拖进去,会自动生成两个属性(Slider 和 LabelSlider)。在 Code View 右键 → Callbacks → 选 ValueChangedFcn,MATLAB 会自动生成回调骨架:
function SliderValueChanged(app)
app.LabelSlider.Text = num2str(round(app.Slider.Value));
end
下面两个图展示两个 App Designer 小项目跑起来的样子 —— 一个文本编辑器,一个红绿灯:


13.7.3 几个常用 App Designer 控件
- Drop Down:下拉菜单,用
Items列表预设选项,回调里读app.DropDown.Value。 - Spinner:数值微调按钮,回调里读
app.Spinner.Value,可以快速实现「点一下 +1」之类的输入。 - Lamp:状态指示灯,常用
app.Lamp.Color = 'red'/'green'表示系统状态。 - Gauge / Knob:仪表盘 / 旋钮控件,回调里读
Value,非常适合做工业控制 Demo。 - Date Picker:日期选择器,返回 datetime 类型。
工程建议:什么时候用 GUIDE,什么时候用 App Designer?
现在新项目永远从 App Designer 起手。GUIDE 只在维护老
.fig文件时才需要回来看一眼。教材后续章节如果做 GUI 练习,先在 App Designer 里拖出界面,再到 Code View 里写app.Slider.ValueChangedFcn这类回调。
13.8 声音文件与音效合成
声音信号在 MATLAB 里就是个一维向量:长度 = 采样点数,元素值 = 该时刻的振幅。配套的 Fs 是采样率(sampling rate,单位 Hz),常见值 8000(电话)、22050、44100(CD 音质)、48000(专业音频)。
13.8.1 采样与重建的基本直觉
采样定理(Nyquist–Shannon sampling theorem)告诉我们:要从离散采样无损重建一段最高频率为 的连续信号,采样率必须 。这意味着:
- 人耳最高能听到约 20 kHz,所以 CD 用 44.1 kHz 采样(略高于 40 kHz,留 10% 余量);
- 打电话只传 0~3.4 kHz,所以 8 kHz 采样就够;
- 超声检测要用 MHz 级采样才能还原几十 kHz 的回波信号。
13.8.2 内置声音与 sound 函数
MATLAB 自带几个示范用的 MAT 文件:chirp(频率递增的啁啾)、gong、laughter、splat、train、handel。每个文件里都有两个变量:
y:声音信号的离散采样向量Fs:采样率
>> load gong % 把 y 和 Fs 装进工作区
>> sound(y, Fs) % 按 Fs 速度播放想听到声音不一样?改播放速度:
>> sound(y, Fs*2) % 2 倍速播放:音调升高 8 度
>> sound(y, Fs*0.5) % 半速播放:音调降低 8 度注意 sound 的第二参数只是播放速度,不会改变数据本身 —— 数据向量 y 不变,只是「读得更快」或「读得更慢」。这就是为什么同一段声音在不同采样率下听起来像变调的 CarolBells。
下面是 chirp 和 train 两段内置声音的波形可视化,能看出 chirp 的「频率递增」与 train 的「火车经过的包络」特征:
% chirptrain.m
subplot(2,1,1)
load chirp
plot(y); ylabel('Amplitude'); title('Chirp')
subplot(2,1,2)
load train
plot(y); ylabel('Amplitude'); title('Train')

如果你想拼接两段声音,先把它们的 y 向量竖向堆叠:
>> load gong; y_gong = y;
>> load chirp; y_chirp = y;
>> sound([y_gong; y_chirp], Fs); % 先 gong 后 chirp,串起来播放这就是教材练习 29 留的空:sound([y1; y2], Fs)。
13.8.3 现代音频 I/O:audioread / audiowrite / audioinfo
sound 是老接口(1990s),R2015b 起更推荐用 audioread / audiowrite / audioinfo 处理 .wav、.mp3、.flac 等文件:
>> [y, Fs] = audioread('myaudio.wav'); % 读:返回 y 和采样率
>> info = audioinfo('myaudio.wav') % 看:返回结构体,含时长、位深、通道数
>> audiowrite('copy.wav', y, Fs); % 写:保存为 wav立体声文件里 y 是 矩阵(左/右两个通道),sound(y, Fs) 会自动两声道播放。
13.8.4 自己合成音效
工程师经常会用 MATLAB 生成测试音(例如测扬声器频率响应、测耳机失真)。最基本的就是正弦波:
% 生成 2 秒、440 Hz 的标准 A 音(A4)
Fs = 44100; % CD 采样率
t = 0:1/Fs:2; % 2 秒时长
y = 0.5 * sin(2*pi*440*t); % 振幅 0.5 的正弦波
sound(y, Fs);实测中为了避免”咔哒”声(信号从 0 突然跳到正弦值),通常会加一个淡入淡出包络(fade in / fade out):
% 加 50ms 线性淡入淡出
fade = round(0.05 * Fs); % 50 ms 内的样本数
env = ones(size(t));
env(1:fade) = linspace(0, 1, fade);
env(end-fade+1:end) = linspace(1, 0, fade);
y_env = y .* env;
sound(y_env, Fs);进一步合成更复杂的「和弦 / 旋律」:
% C 大调和弦:C4 (262 Hz) + E4 (330 Hz) + G4 (392 Hz)
freqs = [262, 330, 392];
y = zeros(size(t));
for k = 1:length(freqs)
y = y + (1/length(freqs)) * sin(2*pi*freqs(k)*t); % 平均能量
end
sound(y, Fs);常见坑:振幅超过 ±1 会削顶
sound/audiowrite期望数据在 -1~+1 之间。如果你把多个声源简单相加(例如 sin+cos),结果可能超过 1.0,导致输出出现”噼啪”的硬削顶失真。解决:先用max(abs(y))看峰值,超过 1 就y = y / max(abs(y))归一化。
13.8.5 真实工程:心电图 / 振动信号的播放
心电信号本质是低频(0.5~40 Hz)的电信号。如果你测到一段 ECG,可以这样播放:
>> [y, Fs] = audioread('ecg.wav'); % 真实 ECG 数据
>> y = y / max(abs(y)); % 归一化到 -1~1
>> sound(y, Fs); % 听「心跳」测振动信号则要注意采样率 —— 轴承振动往往在 1 kHz 以上,必须用 ≥ 10 kHz 的采样率才能在频谱里看到峰值。
本章通关标准:
- 能在脑子里把「一张图像」直接翻译成「一个 或 的矩阵」;
- 能用
imread读入图像并通过矩阵运算修改像素(如压暗、翻转、灰度化)后imwrite写回;- 能写一个带按钮和滑块的 GUI,让用户点击触发一段处理逻辑。
1. 索引图和 RGB 图在存储上最大的区别是什么?
索引图有两个矩阵( 像素矩阵 + 调色板),每个像素只存 1 个整数索引;RGB 图只有 1 个 矩阵,每个像素存 3 个通道值(通常
uint8)。索引图省空间(适合调色板小、复用多的场景,比如科学可视化),RGB 图简单直接(适合照片)。
2. 为什么把
uint8的 RGB 矩阵乘 0.5 可以整体压暗?MATLAB 会自动把 0.5(double)广播到矩阵每个元素并按元素相乘,结果仍是
uint8但会被截断到 0255,所以像素值都减半,相当于每条通道亮度减半 → 整张图压暗。⚠️ 坑:如果原图某些像素已经是 1,乘 0.5 后变成 0,看起来像”噪点被磨平”;要做更精细的亮度调整,改用1 的 double 后处理。im2double转到 0
3. 写一个 GUI 让用户输入"英寸值"按按钮后显示"厘米值"的关键步骤是哪三步?
① 用
uicontrol('Style','edit',...)建可编辑文本框;② 用uicontrol('Style','pushbutton',...)建按钮并在'Callback', @callbackfn里挂回调;③ 回调函数里x = str2double(huitext.String); cm = x*2.54; set(htext,'String',sprintf('%.2f in = %.2f cm',x,cm));取值→换算→写回文本框。
4.
sound(y, Fs)和sound(y, 2*Fs)听起来有什么不同?为什么?第二段音调升高大约 8 度(一倍频),听起来像同一段旋律快进播放。原理:
sound按第二个参数采样率读 y,向量 y 本身不变,所以更高的播放速度等于”同样的波形数据在更短时间内播完”,耳朵感受成”频率翻倍 → 音调升高 8 度”。