直方图绘制:从imhist到自定义实现

图像直方图这东西,说白了就是统计每个灰度级上有多少个像素。你想想看,一张图亮不亮、对比度高不高,看直方图一眼就能判断。我刚开始做图像处理时,总觉得直方图太简单,后来才发现——很多高级算法的基础都在这里。

这一章,我们就把直方图的四种绘制方法讲透。从最省事的 imhist,到完全自己动手写,每一步我都会把坑指出来。

1. imhist函数——最快上手

imhist 是MATLAB自带的直方图计算函数。用法极其简单:

% 读取图像
I = imread('pout.tif');
% 计算并显示直方图
imhist(I);

嗯,这里要注意——imhist 默认只接受灰度图。如果你传了彩色图,它会报错。我在项目里见过有人直接拿RGB图往里塞,结果一脸懵。

小技巧: 如果图像是uint8类型,灰度级范围是0-255;如果是double类型,范围是0-1。用之前最好确认一下数据类型。

如果你想获取直方图数据而不是直接显示,可以这样:

[counts, binLocations] = imhist(I);
% counts: 每个灰度级的像素个数
% binLocations: 对应的灰度级

我个人习惯拿到 counts 后先看一眼最大值,判断图像有没有过曝或欠曝。

2. bar函数——柱状图展示

imhist 画的是竖线图,但有时候我们需要柱状图——尤其是做报告时,柱状图看起来更直观。

I = imread('cameraman.tif');
[counts, bins] = imhist(I);
bar(bins, counts, 'BarWidth', 1, 'FaceColor', [0.3 0.6 0.9]);
xlabel('灰度级');
ylabel('像素个数');
title('直方图 - bar方式');

这里 BarWidth 控制柱子宽度,设为1就是紧挨着,看起来像连续分布。我曾经在给客户做演示时,柱子太细显得稀疏,客户说“你这数据是不是不全?”——从那以后我默认都用 BarWidth 调满。

避坑: 如果图像是16位或更高位,灰度级范围很大,用 bar 会画出密密麻麻的柱子,基本看不清。这时候建议先降采样再画。

3. stem函数——火柴棍图

stem 画出来像火柴棍——每个灰度级上竖一根线,顶端带个圆圈。适合展示离散分布:

I = imread('coins.png');
[counts, bins] = imhist(I);
stem(bins, counts, 'Marker', 'o', 'LineStyle', '-', 'Color', [0.8 0.2 0.2]);
xlabel('灰度级');
ylabel('像素个数');
title('直方图 - stem方式');

说实话,stem 在实际项目中用得不多。它更适合展示稀疏数据——比如只有几个灰度级有像素的情况。我记得有一次做二值化分析,图像只有0和255两个灰度级,用 stem 画出来特别清晰。

绘制方式 适用场景 视觉效果 数据密集时表现
imhist 快速查看 竖线图 好
bar 报告/演示 柱状图 一般(需调BarWidth)
stem 稀疏数据 火柴棍图 差(密集时重叠)

4. 自定义直方图绘制——从零开始

为什么要自己写?因为有时候你需要控制细节——比如只统计某个灰度范围,或者做加权直方图。MATLAB自带的函数做不到这些。

核心思路就三步:

  1. 统计每个灰度级的像素个数
  2. 归一化(可选)
  3. 绘制

来看代码:

% 自定义直方图
I = imread('pout.tif');
I = im2uint8(I);  % 确保是uint8

% 第一步:统计
counts = zeros(256, 1);
for i = 0:255
    counts(i+1) = sum(I(:) == i);
end

% 第二步:归一化(变成概率分布)
prob = counts / numel(I);

% 第三步:绘制
figure;
bar(0:255, prob, 'BarWidth', 1, 'FaceColor', [0.2 0.7 0.3]);
xlabel('灰度级');
ylabel('概率');
title('自定义直方图(归一化)');
关键点: 用 sum(I(:) == i) 这种写法虽然直观,但循环256次在大图上会很慢。实际项目中我会用 histcounts 或 accumarray 来加速。

如果你想做加权直方图——比如给图像中心区域的像素更高权重——自定义方法就派上用场了:

% 加权直方图示例
[M, N] = size(I);
weight = zeros(M, N);
centerX = N/2; centerY = M/2;
for x = 1:N
    for y = 1:M
        dist = sqrt((x - centerX)^2 + (y - centerY)^2);
        weight(y, x) = exp(-dist / 100);  % 高斯权重
    end
end

weighted_counts = zeros(256, 1);
for i = 0:255
    mask = (I == i);
    weighted_counts(i+1) = sum(weight(mask));
end

嗯,这个加权直方图在目标检测里很常用——中心区域权重高,边缘权重低,能减少背景干扰。

5. 四种方法对比总结

我把四种方法的核心差异整理成一张表,方便你选型:

方法 代码量 灵活性 性能 推荐指数
imhist 1行 低 快 ★★★★★
bar 3行 中 快 ★★★★
stem 3行 中 快 ★★★
自定义 10+行 高 中(循环慢) ★★★★(特殊场景)

我个人建议:日常快速分析用 imhist,做报告用 bar,遇到特殊需求(加权、局部统计)再自己写。别为了炫技而造轮子。

经验之谈: 画直方图时,记得把坐标轴范围固定——xlim([0 255]) 和 ylim([0 max(counts)*1.1])。不然MATLAB自动缩放会误导你。
直方图绘制方法知识结构 直方图绘制 imhist 函数 bar 柱状图 stem 火柴棍图 自定义绘制 1行代码 快速查看 无自定义空间 适合报告 可调BarWidth 密集时需优化 稀疏数据 离散分布 密集时重叠 完全灵活 加权/局部 循环较慢 选型原则:日常用imhist,报告用bar 特殊需求用自定义,stem仅限稀疏场景

这张图把四种方法的关系和适用场景串起来了。你想想看——从最省事的 imhist 到完全自定义,本质上是在「开发效率」和「灵活度」之间做权衡。没有的好坏,只有合不合适。