计算机视觉图像点运算【灰度直方图均衡化图形界面实操理解 +开源代码】
对一个数字图像处理系统来说,一般的处理过程为三个步骤:图像预处理、特征抽取、图像识别和分析。图像的点运算就是预处理过程中的重要一步,点运算是对图像的灰度级进行变换。
图像点运算概念
点运算是指对图像的每个像素依次进行相同的灰度变换,然后得出的图像就是输出图像。那么对于RGB彩色图像的点运算是怎么处理的呢?是有多种的处理方案的,有简单对R、G、B分别操作的,也有转换为灰度操作,再还原为彩色的,读者可以自行查阅资料。
点运算几个形式以及作用
对比度以及亮度调整
线性变换:通过y=kx+d实现
- 对比度增强(k>1) 拓展灰度分布范围,突出细节差异,例如医学影像中通过增大斜率是的病灶边缘更清晰
- 亮度调节(k=1 && d != 0) 整体平移灰度值,用于低曝光图像全局提亮
- 灰度反转(k=−1,d=255):用于突出暗区细节,如X光片中的骨骼结构显示。
分段线性变换
对特定灰度区间进行选择性拉伸,例如增强中间灰度区域的对比度,保留极亮/极暗区域的信息
动态范围优化
非线性变换
- 对数变换:压缩高灰度区域,扩展低灰度细节,适用于傅里叶频谱图等动态范围过宽的图像。
- 伽马变换(幂次变换):
- γ<1:扩展暗部细节,适用于背光场景的修复。
- γ>1:增强亮部层次,如卫星云图中云层纹理的区分
- 灰度直方图均衡化
像素、灰度、灰度级、灰度直方图
像素点是最小的图像单元,一张图片由很多的像素点组成。比如之后我们用到的Kun图片就是939*554像素的。
灰度是表明图像明暗的数值,即黑白图像中点的颜色深度,范围一般从0到255,白色为255 ,黑色为0,故黑白图片也称灰度图像。灰度值指的是单个像素点的亮度。灰度值越大表示越亮。
灰度就是没有色彩,RGB色彩分量全部相等。图像的灰度化就是让像素点矩阵中的每一个像素点都满足关系:R=G=B,此时的这个值叫做灰度值。如RGB(100,100,100)就代表灰度值为100,RGB(50,50,50)代表灰度值为50
灰度值与像素值的关系:黑白图像的灰度值就是像素值,彩色图像则需要进行一定的映射才可以得到灰度
灰度级:不同灰度的种类个数
灰度直方图:对灰度级进行统计
灰度直方图均衡化实操(JAVA实例附源码)
JavaSwing进行图片展示
先上网找一张我家哥哥的图片,初始化一下Java项目,然后就可以准备开始了。
利用JavaSwing进行图形化显示,注意代码里边标注的,他的图片读取默认目录是项目根目录:
代码实现
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.WritableRaster;
import java.io.*;
public class Main {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("图片展示");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(800, 600);
Image image=null;
try {
//这里注意读取的默认位置是根目录
InputStream is = new BufferedInputStream(
new FileInputStream("./asset/caixukun.png"));
Image originImage = ImageIO.read(is);
image=originImage;
//image=originImage.getScaledInstance(400,300,Image.SCALE_SMOOTH);
// BufferedImage bimage=utils.toBufferedImage(image);
// WritableRaster raster=bimage.getRaster();
// ColorModel colorModel=bimage.getColorModel();
//image=originImage;
// 使用JLabel显示图片
JLabel label = new JLabel(new ImageIcon(image));
frame.add(new JScrollPane(label));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
} catch (IOException e) {
// 增强错误处理
JOptionPane.showMessageDialog(frame,
"图片加载失败: " + e.getMessage(),
"错误",
JOptionPane.ERROR_MESSAGE);
System.exit(1);
}
});
}
}
观察图片效果良好,这里读者可能会想把图像规范到一个固定的长宽比如400*300,作者在这里尝试调用了Image的scale库,最后的结果是bug预警,后续调试观察RGB图像矩阵的时候像素值发生巨变,通道数也变成了4,总而言之就是不太看的明白的故障,最终取消选择手工裁剪图片尺寸。
好,这里打上断点查看一下图片格式:
在raster(栅格)属性下的data栏目,发现里边包括了RGB三个通道的所有点。
彩色图像映射成灰度图像
- 加权平均法:由于人眼对不同颜色的敏感度不同,对绿色的最高,红色次之,对蓝色最低,所以通常的权重设置是:R=0.299, G=0.587; B=0.114;
- 上课讲了U和V???
- 简单平均法:G=(R+G+B)/3;
这里采用加权映射方法
代码实现
public static BufferedImage toGrayscaleImage(Image image) {
// 先将 Image 转换为 BufferedImage
BufferedImage bufferedImage = toBufferedImage(image);
int width = bufferedImage.getWidth();
int height = bufferedImage.getHeight();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
Color color = new Color(bufferedImage.getRGB(x, y));
int grayLevel = (int) (color.getRed() * 0.299 + color.getGreen() * 0.587 + color.getBlue() * 0.114);
Color grayColor = new Color(grayLevel, grayLevel, grayLevel);
bufferedImage.setRGB(x, y, grayColor.getRGB());
}
}
return bufferedImage;
}
这里返回的bufferImage是image的子类,可以在main函数里直接赋值给image看看转换之后的效果:
灰度直方图绘制
utils里边增添Java代码:
代码实现[Bug预警]
public static BufferedImage drawGrayscaleHistogram(BufferedImage grayscaleImage) {
int width = grayscaleImage.getWidth();
int height = grayscaleImage.getHeight();
// 统计每个灰度级的像素数量
int[] histogram = new int[256];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
Color color = new Color(grayscaleImage.getRGB(x, y));
int grayLevel = color.getRed();
histogram[grayLevel]++;
}
}
// 找到直方图中的最大像素数量
int maxCount = 0;
for (int count : histogram) {
if (count > maxCount) {
maxCount = count;
}
}
int histogramWidth = 512;
int histogramHeight = 400;
BufferedImage histogramImage = new BufferedImage(histogramWidth, histogramHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = histogramImage.createGraphics();
g2d.setColor(Color.WHITE);
g2d.fillRect(0, 0, histogramWidth, histogramHeight);
g2d.setColor(Color.BLACK);
for (int i = 0; i < 256; i++) {
int barHeight = (int) ((double) histogram[i] / maxCount * histogramHeight);
g2d.drawLine(i, histogramHeight, i, histogramHeight - barHeight);
}
g2d.dispose();
return histogramImage;
}
直方图展示
其实这个图画的不是很好看,点开数值看一下分布,各个地方都有,应该是纵轴尺度太大了。
对数概率代码实现(没有意义,只是证明一下各个像素其实都有)
public static BufferedImage drawGrayscaleHistogram(BufferedImage grayscaleImage) {
int width = grayscaleImage.getWidth();
int height = grayscaleImage.getHeight();
// 统计每个灰度级的像素数量
int[] histogram = new int[256];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
Color color = new Color(grayscaleImage.getRGB(x, y));
int grayLevel = color.getRed();
histogram[grayLevel]++;
}
}
// 找到直方图中的最大像素数量
int maxCount = 0;
for (int count : histogram) {
if (count > maxCount) {
maxCount = count;
}
}
int histogramWidth = 512;
int histogramHeight = 400;
BufferedImage histogramImage = new BufferedImage(histogramWidth, histogramHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = histogramImage.createGraphics();
g2d.setColor(Color.WHITE);
g2d.fillRect(0, 0, histogramWidth, histogramHeight);
g2d.setColor(Color.BLACK);
double logMaxCount = Math.log(maxCount + 1);
for (int i = 0; i < 256; i++) {
// 计算对数尺度下的柱子高度
double logCount = Math.log(histogram[i] + 1);
int barHeight = (int) ((logCount / logMaxCount) * histogramHeight);
g2d.drawLine(i * 2, histogramHeight, i * 2, histogramHeight - barHeight);
}
g2d.dispose();
return histogramImage;
}
注:最后发现这么稀疏的原因是因为int除法向下取整,全是0:修改utils里边的这一小部分
for (int i = 0; i < 256; i++) {
//因该是这里的问题
int barHeight = (int)(((histogram[i] / (double)maxCount) * histogramHeight));
g2d.drawLine(i * 2, histogramHeight, i * 2, histogramHeight - barHeight);
}
经过调试还是因为这张图本身不是太好,确实太集中了,没能画出好图,读者感兴趣可以换张图,代码全都相同(下边贴一张作者换图之后的直方图)
换图之后的效果
灰度直方图分布含义
观察灰度直方图是为了对其进行操作,在文章开始就有提到,图像点运算是为了去做数据增强。
这里就得牵扯到一个对于灰度的理解。
整体灰度都偏向于一个区域的话大多数时候不是有利的,会让画面看起来灰蒙蒙
较黑的图像
较亮的图像
灰度直方图均衡化
【直方图均衡化的核心思想:亮度和对比度的改善】
并非灰度很高的图片或者灰度很低的图片看起来就会很清晰,而是对比度高的图片看起来才会清晰,特征分明,这也就方便了各种识别任务。
过亮或者过暗的图片经过直方图均衡化会变得清晰。
直方图均衡化是一种简单有效的图像增强技术,通过改变图像的直方图来改变图像中各像素的灰度,主要用于增强动态范围偏小的图像的对比度。原始图像由于其灰度分布可能集中在较窄的区间,造成图像不够清晰。例如,过曝光图像的灰度级集中在高亮度范围内,而曝光不足将使图像灰度级集中在低亮度范围内。采用直方图均衡化,可以把原始图像的直方图变换为均匀分布(均衡)的形式,这样就增加了像素之间灰度值差别的动态范围,从而达到增强图像整体对比度的效果。换言之,直方图均衡化的基本原理是:对在图像中像素个数多的灰度值(即对画面起主要作用的灰度值)进行展宽,而对像素个数少的灰度值(即对画面不起主要作用的灰度值)进行归并,从而增大对比度,使图像清晰,达到增强的目的,如下图所示。
如果一幅图像整体偏暗或者偏亮,那么直方图均衡化的方法很适用。但直方图均衡化是一种全局处理方式,它对处理的数据不加选择,可能会增加背景干扰信息的对比度并且降低有用信号的对比度(如果图像某些区域对比度很好,而另一些区域对比度不好,那采用直方图均衡化就不一定适用)。此外,均衡化后图像的灰度级减少,某些细节将会消失;某些图像(如直方图有高峰),经过均衡化后对比度不自然的过分增强。针对直方图均衡化的缺点,已经有局部的直方图均衡化方法出现。
灰度直方图均衡化适用范围
直方图均衡化在需要增强局部细节的任务中表现优异(比如识别任务),但其对全局亮度分布的破坏性处理可能影响依赖整体光照语义的应用(如图生文)
贴几组均衡化的例子
左边是均衡化之前,右边是之后
发现问题
上边的一系列例子里,我们发现最后一张黑猫图片是个例外,试图发现共性找出第二张可能有这种情况的例子;这样的情况是由于黑白图片吗,是由于过暗的地方有光影吗?现在是什么情况呢?可能原图的色调太单一,就那么几个,所以均衡化之后
似乎黑白图片效果也很好和另一张有光影的图片也很好
灰度直方图均衡化思考
灰度直方图呈现的是一种图像整体的状态,因为灰度级统计的是全部的图片,所以造成上述结果的应该是一种“局部的特征”。我要不要找一找他的灰度分布情况呢?
回答:实时证明,均衡化对于噪声的鲁棒性很差,有噪声的时候就会导致颗粒感很足,出来的画面会很奇怪。可以检测一下图片里的噪声
不难看出,不是所有的灰度图像经过均衡化都会变得更加清晰的,那么灰度直方图均衡化到底在哪些图上可能效果比较好呢?我们看看他的操作方案
灰度直方图均衡化公式解释
如果把映射的分子分母上的-cdfMin去掉,那是不是立马就能看懂了,就是一个拉伸;一个简单的解释是为了让映射范围有0,所以这个公式的映射范围就变成了0~255之间。
最后的最后,为了方便进行对比展示,搭建了一个UI界面,代码如下:
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.Arrays;
public class ImageProcessorGUI extends JFrame {
private BufferedImage originalImage;
private BufferedImage processedImage;
private JLabel imageLabel;
private JComboBox<String> operationComboBox;
private JButton fileSelectButton;
private final String[] OPERATIONS = {
"原始图像", "灰度转换", "灰度直方图",
"均衡化后图片", "均衡化后直方图", "噪声点统计"
};
public ImageProcessorGUI() {
super("图像处理器");
initializeUI();
loadDefaultImage();
}
private void initializeUI() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(1000, 800);
setLayout(new BorderLayout());
// 控制面板
JPanel controlPanel = new JPanel();
fileSelectButton = new JButton("选择图片");
operationComboBox = new JComboBox<>(OPERATIONS);
JButton processButton = new JButton("执行");
JButton refreshButton = new JButton("重置");
controlPanel.add(fileSelectButton);
controlPanel.add(new JLabel("选择操作:"));
controlPanel.add(operationComboBox);
controlPanel.add(processButton);
controlPanel.add(refreshButton);
// 图像显示区域
imageLabel = new JLabel() {
@Override
public Dimension getPreferredSize() {
return getIcon() == null ? new Dimension(0, 0) : super.getPreferredSize();
}
};
imageLabel.setHorizontalAlignment(SwingConstants.CENTER);
imageLabel.setVerticalAlignment(SwingConstants.CENTER);
JScrollPane scrollPane = new JScrollPane(imageLabel);
add(controlPanel, BorderLayout.NORTH);
add(scrollPane, BorderLayout.CENTER);
// 事件监听
fileSelectButton.addActionListener(e -> selectImageFile());
processButton.addActionListener(e -> processImage());
refreshButton.addActionListener(e -> showOriginalImage());
}
private void loadDefaultImage() {
loadImage("./asset/tanzhilang.png");
}
private void selectImageFile() {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setCurrentDirectory(new File("./asset"));
fileChooser.setFileFilter(new FileNameExtensionFilter("图像文件", "jpg", "jpeg", "png", "gif"));
if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
loadImage(fileChooser.getSelectedFile().getAbsolutePath());
}
}
private void loadImage(String path) {
try (InputStream is = new BufferedInputStream(new FileInputStream(path))) {
originalImage = ImageIO.read(is);
showOriginalImage();
} catch (IOException ex) {
JOptionPane.showMessageDialog(this, "图片加载失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
}
}
private void showOriginalImage() {
processedImage = originalImage;
updateImageDisplay();
}
private void processImage() {
int selectedIndex = operationComboBox.getSelectedIndex();
try {
switch (selectedIndex)
{
case 1:
processedImage = ImageUtils.toGrayscale(originalImage);
break;
case 2:
processedImage=utils.drawGrayscaleHistogram(ImageUtils.toGrayscale(originalImage));
break;
case 3:
processedImage = utils.histogramEqualization(ImageUtils.toGrayscale(originalImage));
break;
case 4:
processedImage = utils.histogramEqualization(ImageUtils.toGrayscale(originalImage));
processedImage=utils.drawGrayscaleHistogram(processedImage);
break;
case 5:
int noisePoints = utils.countNoisePoints(ImageUtils.toGrayscale(originalImage), 30);
JOptionPane.showMessageDialog(this, "噪声点数量: " + noisePoints);
return;
default:
processedImage = originalImage;
}
updateImageDisplay();
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "处理失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
}
}
private void updateImageDisplay() {
if (processedImage == null) return;
Dimension viewSize = ((JScrollPane) getContentPane().getComponent(1)).getViewport().getExtentSize();
int maxWidth = Math.max(viewSize.width - 50, 100);
int maxHeight = Math.max(viewSize.height - 50, 100);
Image scaledImage = processedImage.getScaledInstance(
Math.min(processedImage.getWidth(), maxWidth),
Math.min(processedImage.getHeight(), maxHeight),
Image.SCALE_SMOOTH);
imageLabel.setIcon(new ImageIcon(scaledImage));
imageLabel.revalidate();
imageLabel.repaint();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
ImageProcessorGUI app = new ImageProcessorGUI();
app.setLocationRelativeTo(null);
app.setVisible(true);
});
}
}
class ImageUtils {
// 灰度转换
public static BufferedImage toGrayscale(BufferedImage image) {
BufferedImage result = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB);
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
Color color = new Color(image.getRGB(x, y));
int gray = (int) (color.getRed() * 0.299 + color.getGreen() * 0.587 + color.getBlue() * 0.114);
result.setRGB(x, y, new Color(gray, gray, gray).getRGB());
}
}
return result;
}
}