走航车
这几天再接触一个走航车项目。我的主要任务是,负责对摄像头采集的图片进行处理,由于图片是tiff无损压缩格式,所以存在很多噪声。
目标任务:因为摄像头是那种普通摄像头,不能通过红外拍摄夜景图像。所以会产生存在大量噪点点图像,为此决定在设置摄像头拍摄时间的基础上(不拍摄夜晚的图像),决定分析图片的信息,排除掉那种高噪声的图像。
1.OpenCV方式
1.1代码
public class DeviceCameraUsbService {
public void captureImage(String outputPath, String fileName) throws Exception {
// 打开默认摄像头
OpenCVFrameGrabber grabber = new OpenCVFrameGrabber(0);
try{
grabber.setImageWidth(1920); // 设置图像宽度
grabber.setImageHeight(1080); // 设置图像高度
grabber.start();
try{
Thread.sleep(3000);
}catch (Exception e){}
// 抓拍图像
Frame frame = grabber.grab();
if (frame == null) {
throw new RuntimeException("DeviceCameraUsbService captureImage error: Frame not captured");
}
// 转换为Mat对象
OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat();
Mat mat = converter.convert(frame);
// 检测图像是否为噪声
Mat mean = new Mat();
Mat stdDev = new Mat();
opencv_core.meanStdDev(mat, mean, stdDev);
// 判断标准差是否过低
boolean isLowStdDev = stdDev.createIndexer().getDouble(0) < 150.0;
// 判断黑色像素比例是否过高
int blackPixelCount = 0;
int totalPixelCount = mat.rows() * mat.cols();
UByteIndexer indexer = mat.createIndexer();
for (int y = 0; y < mat.rows(); y++) {
for (int x = 0; x < mat.cols(); x++) {
int r = indexer.get(y, x, 2);
int g = indexer.get(y, x, 1);
int b = indexer.get(y, x, 0);
if (r < 50 && g < 50 && b < 50) { // 这里的阈值可以根据实际情况调整
blackPixelCount++;
}
}
}
double blackPixelRatio = (double) blackPixelCount / totalPixelCount;
boolean isMostlyBlack = blackPixelRatio > 0.1; // 黑色像素比例阈值
// 如果标准差过低且黑色像素比例过高,则认为图像为噪声,设置为全黑
if (isLowStdDev && isMostlyBlack) {
System.out.println("DeviceCameraUsbService captureImage detected noise with mostly black pixels, setting image to black.");
mat = new Mat(mat.rows(), mat.cols(), opencv_core.CV_8UC3, new Scalar(0, 0, 0, 0));
}
// 确保frame的大小至少为1080x1080
if (mat.cols() >= 1080 && mat.rows() >= 1080) {
// 裁剪图像中心区域
int width = mat.cols();
int height = mat.rows();
int cropSize = 1080;
int x = (width - cropSize) / 2;
int y = (height - cropSize) / 2;
// 确保ROI在图像范围内
if (x >= 0 && y >= 0 && x + cropSize <= width && y + cropSize <= height) {
Rect roi = new Rect(x, y, cropSize, cropSize);
Mat cropped = new Mat(mat, roi);
// 确保目录存在
File dir = new File(outputPath);
if (!dir.exists()) {
dir.mkdirs();
}
// 保存为TIFF格式
String outputFileName = outputPath + File.separator + fileName;
opencv_imgcodecs.imwrite(outputFileName, cropped);
System.out.println("DeviceCameraUsbService captureImage success:" + outputFileName);
} else {
throw new RuntimeException("DeviceCameraUsbService captureImage error: ROI is out of image bounds");
}
} else {
throw new RuntimeException("DeviceCameraUsbService captureImage error: Image size is too small for the desired crop size");
}
}catch (Exception e){
e.printStackTrace();
throw new RuntimeException(e.getMessage());
} finally {
grabber.stop();
}
}
}
1.2核心代码
这段代码的功能是检测图像是否为噪声,并在检测到噪声时将图像设置为全黑。它主要通过两种方式来判断图像是否为噪声:计算图像的标准差和黑色像素的比例。下面是对代码的逐步解释:
-
计算图像的均值和标准差:
Mat mean = new Mat(); Mat stdDev = new Mat(); opencv_core.meanStdDev(mat, mean, stdDev);
这里使用OpenCV的
meanStdDev
方法计算图像的均值和标准差,结果存储在mean
和stdDev
对象中。 -
判断标准差是否过低:
boolean isLowStdDev = stdDev.createIndexer().getDouble(0) < 150.0;
通过访问标准差对象
stdDev
的第一个值,判断标准差是否小于150.0。如果标准差较低,说明图像的颜色变化不大,可能是噪声图像。 -
计算黑色像素的比例:
int blackPixelCount = 0; int totalPixelCount = mat.rows() * mat.cols(); UByteIndexer indexer = mat.createIndexer(); for (int y = 0; y < mat.rows(); y++) { for (int x = 0; x < mat.cols(); x++) { int r = indexer.get(y, x, 2); int g = indexer.get(y, x, 1); int b = indexer.get(y, x, 0); if (r < 50 && g < 50 && b < 50) { // 这里的阈值可以根据实际情况调整 blackPixelCount++; } } }
遍历图像的每个像素,统计RGB值都小于50的黑色像素数,并计算黑色像素的比例。
-
判断黑色像素比例是否过高:
java double blackPixelRatio = (double) blackPixelCount / totalPixelCount; boolean isMostlyBlack = blackPixelRatio > 0.1; // 黑色像素比例阈值
计算黑色像素在总像素中的比例,如果比例大于0.1,说明图像中黑色像素过多。
-
检测到噪声时将图像设置为全黑:
if (isLowStdDev && isMostlyBlack) { System.out.println("DeviceCameraUsbService captureImage detected noise with mostly black pixels, setting image to black."); mat = new Mat(mat.rows(), mat.cols(), opencv_core.CV_8UC3, new Scalar(0, 0, 0, 0)); }
如果标准差过低且黑色像素比例过高,则认为图像为噪声,并将图像设置为全黑。
总结:
- 代码首先计算图像的标准差和均值。
- 然后判断标准差是否过低。
- 接着遍历图像所有像素,计算黑色像素的比例。
- 最后,如果图像的标准差过低且黑色像素比例过高,则认为图像为噪声,并将图像设置为全黑。
缺点:采用这中方式大概可以排除掉90%的图像,但还是有部分图像漏掉。
2.亮度和信息熵
2.1代码方案
为了解决漏检的问题,我决定采用亮度+信息熵的方式实现。
因为java代码不便对图像进行处理,因此采用python代码实现。
import numpy as np
from PIL import Image
import os
# 定义亮度和信息熵的阈值(R、G、B各通道)
brightness_thresholds = {'R': 94.4, 'G': 127.9, 'B': 169.1} # 亮度阈值
entropy_thresholds = {'R': 7.1, 'G': 6.7, 'B': 6.4} # 信息熵阈值
# 指定输入和输出文件夹路径
input_folder = '/Users/gebaokang/Pictures/20240610-1/10'
output_folder = '/Users/gebaokang/Pictures/outputPath'
def calculate_entropy(data):
# 计算数据的直方图
hist = np.histogram(data, bins=256, range=(0, 256))[0]
# 计算概率分布
prob = hist / float(np.sum(hist))
# 移除零概率以避免log(0)错误
prob = prob[prob > 0]
# 计算信息熵
entropy = -np.sum(prob * np.log2(prob))
return entropy
def calculate_rgb_entropy(image_path):
# 打开图像并将其转换为RGB模式
img = Image.open(image_path).convert('RGB')
# 将图像转换为NumPy数组
img_array = np.array(img)
# 分离RGB通道
red_channel = img_array[:, :, 0].flatten()
green_channel = img_array[:, :, 1].flatten()
blue_channel = img_array[:, :, 2].flatten()
# 计算每个通道的信息熵
red_entropy = calculate_entropy(red_channel)
green_entropy = calculate_entropy(green_channel)
blue_entropy = calculate_entropy(blue_channel)
return red_entropy, green_entropy, blue_entropy
# 确保输出文件夹存在
if not os.path.exists(output_folder):
os.makedirs(output_folder)
# 遍历指定文件夹下的所有TIFF图片文件
for filename in os.listdir(input_folder):
if filename.endswith('.tiff') or filename.endswith('.tif'):
# 读取TIFF图像
img_path = os.path.join(input_folder, filename)
img = Image.open(img_path)
# 计算图像R、G、B通道的亮度值和信息熵
img_array = np.array(img)
r_channel = img_array[:, :, 0].flatten()
g_channel = img_array[:, :, 1].flatten()
b_channel = img_array[:, :, 2].flatten()
brightness_r = np.mean(r_channel)
brightness_g = np.mean(g_channel)
brightness_b = np.mean(b_channel)
entropy_r = calculate_entropy(r_channel)
entropy_g = calculate_entropy(g_channel)
entropy_b = calculate_entropy(b_channel)
# 判断是否需要处理图像
if not ((brightness_r < brightness_thresholds['R'] or
brightness_g < brightness_thresholds['G'] or
brightness_b < brightness_thresholds['B']) and
(entropy_r < entropy_thresholds['R'] or
entropy_g < entropy_thresholds['G'] or
entropy_b < entropy_thresholds['B'])):
# 图像满足条件,保存到输出文件夹
img.save(os.path.join(output_folder, filename))
2.2代码含义
代码中涉及的两个重要算法的详细解释:亮度计算和信息熵计算。
- 亮度计算
亮度计算通过求平均值来实现。对于图像的每个通道(R、G、B),分别计算其像素值的平均值,作为该通道的亮度值。
# 获取图像的R、G、B通道数据
r_channel = img_array[:, :, 0].flatten()
g_channel = img_array[:, :, 1].flatten()
b_channel = img_array[:, :, 2].flatten()
# 计算R、G、B通道的亮度(即平均值)
brightness_r = np.mean(r_channel)
brightness_g = np.mean(g_channel)
brightness_b = np.mean(b_channel)
- 信息熵计算
信息熵用于度量数据的不确定性或随机性。在图像处理中,信息熵反映了图像中像素值的分布情况。以下是信息熵计算的步骤:
- 计算直方图:统计像素值的出现频率。
- 计算概率分布:将直方图转换为概率分布。
- 移除零概率:避免在计算对数时出现log(0)错误。
- 计算信息熵:根据概率分布计算信息熵。
def calculate_entropy(data):
# 计算数据的直方图
hist = np.histogram(data, bins=256, range=(0, 256))[0]
# 计算概率分布
prob = hist / float(np.sum(hist))
# 移除零概率以避免log(0)错误
prob = prob[prob > 0]
# 计算信息熵
entropy = -np.sum(prob * np.log2(prob))
return entropy
对于每个通道(R、G、B),分别计算其信息熵:
# 计算每个通道的信息熵
entropy_r = calculate_entropy(r_channel)
entropy_g = calculate_entropy(g_channel)
entropy_b = calculate_entropy(b_channel)
判断条件
最后,通过亮度和信息熵的阈值来判断图像是否需要处理:
if not ((brightness_r < brightness_thresholds['R'] or
brightness_g < brightness_thresholds['G'] or
brightness_b < brightness_thresholds['B']) and
(entropy_r < entropy_thresholds['R'] or
entropy_g < entropy_thresholds['G'] or
entropy_b < entropy_thresholds['B'])):
# 图像满足条件,保存到输出文件夹
img.save(os.path.join(output_folder, filename))
具体的判断逻辑是,如果图像的任何一个通道的亮度低于相应的阈值,并且任何一个通道的信息熵低于相应的阈值,则认为该图像不需要处理,否则将图像保存到输出文件夹。
重点:最重要的就是这里判断逻辑的设置,以及阈值设置。
阈值是通过计算所有高噪声图像的亮度、信息熵,最后进行设置。