返回:OpenCV系列文章目录(持续更新中......)
上一篇:使用 OpenCV 添加(混合)两个图像
下一篇:如何利用OpenCV4.9离散傅里叶变换
目标
在本教程中,您将学习如何:
- 访问像素值
- 用零初始化矩阵
- 了解 cv::saturate_cast 的作用以及它为什么有用
- 获取有关像素转换的一些很酷的信息
- 在实际示例中提高图像的亮度
理论
注意
下面的解释属于 Richard Szeliski 的《计算机视觉:算法和应用》一书
图像处理
- 通用图像处理运算符是获取一个或多个输入图像并生成输出图像的函数。
- 图像变换可以看作是:
- 点运算符(像素变换)
- 邻域(基于区域)运营商
像素变换
- 在这种图像处理转换中,每个输出像素的值仅取决于相应的输入像素值(可能还取决于一些全局收集的信息或参数)。
- 此类运算符的示例包括亮度和对比度调整以及颜色校正和转换。
亮度和对比度调整
-
代码:
- 可下载代码: 点击这里
C++:
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include <iostream>
// we're NOT "using namespace std;" here, to avoid collisions between the beta variable and std::beta in c++17
using std::cin;
using std::cout;
using std::endl;
using namespace cv;
int main( int argc, char** argv )
{
CommandLineParser parser( argc, argv, "{@input | lena.jpg | input image}" );
Mat image = imread( samples::findFile( parser.get<String>( "@input" ) ) );
if( image.empty() )
{
cout << "Could not open or find the image!\n" << endl;
cout << "Usage: " << argv[0] << " <Input image>" << endl;
return -1;
}
Mat new_image = Mat::zeros( image.size(), image.type() );
double alpha = 1.0; /*< Simple contrast control */
int beta = 0; /*< Simple brightness control */
cout << " Basic Linear Transforms " << endl;
cout << "-------------------------" << endl;
cout << "* Enter the alpha value [1.0-3.0]: "; cin >> alpha;
cout << "* Enter the beta value [0-100]: "; cin >> beta;
for( int y = 0; y < image.rows; y++ ) {
for( int x = 0; x < image.cols; x++ ) {
for( int c = 0; c < image.channels(); c++ ) {
new_image.at<Vec3b>(y,x)[c] =
saturate_cast<uchar>( alpha*image.at<Vec3b>(y,x)[c] + beta );
}
}
}
imshow("Original Image", image);
imshow("New Image", new_image);
waitKey();
return 0;
}
Jav a:
import java.util.Scanner;
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.highgui.HighGui;
import org.opencv.imgcodecs.Imgcodecs;
class BasicLinearTransforms {
private byte saturate(double val) {
int iVal = (int) Math.round(val);
iVal = iVal > 255 ? 255 : (iVal < 0 ? 0 : iVal);
return (byte) iVal;
}
public void run(String[] args) {
String imagePath = args.length > 0 ? args[0] : "../data/lena.jpg";
Mat image = Imgcodecs.imread(imagePath);
if (image.empty()) {
System.out.println("Empty image: " + imagePath);
System.exit(0);
}
Mat newImage = Mat.zeros(image.size(), image.type());
double alpha = 1.0; /*< Simple contrast control */
int beta = 0; /*< Simple brightness control */
System.out.println(" Basic Linear Transforms ");
System.out.println("-------------------------");
try (Scanner scanner = new Scanner(System.in)) {
System.out.print("* Enter the alpha value [1.0-3.0]: ");
alpha = scanner.nextDouble();
System.out.print("* Enter the beta value [0-100]: ");
beta = scanner.nextInt();
}
byte[] imageData = new byte[(int) (image.total()*image.channels())];
image.get(0, 0, imageData);
byte[] newImageData = new byte[(int) (newImage.total()*newImage.channels())];
for (int y = 0; y < image.rows(); y++) {
for (int x = 0; x < image.cols(); x++) {
for (int c = 0; c < image.channels(); c++) {
double pixelValue = imageData[(y * image.cols() + x) * image.channels() + c];
pixelValue = pixelValue < 0 ? pixelValue + 256 : pixelValue;
newImageData[(y * image.cols() + x) * image.channels() + c]
= saturate(alpha * pixelValue + beta);
}
}
}
newImage.put(0, 0, newImageData);
HighGui.imshow("Original Image", image);
HighGui.imshow("New Image", newImage);
HighGui.waitKey();
System.exit(0);
}
}
public class BasicLinearTransformsDemo {
public static void main(String[] args) {
// Load the native OpenCV library
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
new BasicLinearTransforms().run(args);
}
}
Python
from __future__ import print_function
from builtins import input
import cv2 as cv
import numpy as np
import argparse
# Read image given by user
parser = argparse.ArgumentParser(description='Code for Changing the contrast and brightness of an image! tutorial.')
parser.add_argument('--input', help='Path to input image.', default='lena.jpg')
args = parser.parse_args()
image = cv.imread(cv.samples.findFile(args.input))
if image is None:
print('Could not open or find the image: ', args.input)
exit(0)
new_image = np.zeros(image.shape, image.dtype)
alpha = 1.0 # Simple contrast control
beta = 0 # Simple brightness control
# Initialize values
print(' Basic Linear Transforms ')
print('-------------------------')
try:
alpha = float(input('* Enter the alpha value [1.0-3.0]: '))
beta = int(input('* Enter the beta value [0-100]: '))
except ValueError:
print('Error, not a number')
# Do the operation new_image(i,j) = alpha*image(i,j) + beta
# Instead of these 'for' loops we could have used simply:
# new_image = cv.convertScaleAbs(image, alpha=alpha, beta=beta)
# but we wanted to show you how to access the pixels :)
for y in range(image.shape[0]):
for x in range(image.shape[1]):
for c in range(image.shape[2]):
new_image[y,x,c] = np.clip(alpha*image[y,x,c] + beta, 0, 255)
cv.imshow('Original Image', image)
cv.imshow('New Image', new_image)
# Wait until user press some key
cv.waitKey()
解释
我们使用 cv::imread 加载图像并将其保存在 Mat 对象中:
CommandLineParser parser( argc, argv, "{@input | lena.jpg | input image}" );
Mat image = imread( samples::findFile( parser.get<String>( "@input" ) ) );
if( image.empty() )
{
cout << "Could not open or find the image!\n" << endl;
cout << "Usage: " << argv[0] << " <Input image>" << endl;
return -1;
}
Java:
String imagePath = args.length > 0 ? args[0] : "../data/lena.jpg";
Mat image = Imgcodecs.imread(imagePath);
if (image.empty()) {
System.out.println("Empty image: " + imagePath);
System.exit(0);
}
Python :
parser = argparse.ArgumentParser(description='Code for Changing the contrast and brightness of an image! tutorial.')
parser.add_argument('--input', help='Path to input image.', default='lena.jpg')
args = parser.parse_args()
image = cv.imread(cv.samples.findFile(args.input))
if image is None:
print('Could not open or find the image: ', args.input)
exit(0)
- 现在,由于我们将对这个图像进行一些转换,我们需要一个新的 Mat 对象来存储它。此外,我们希望它具有以下功能:
- 初始像素值等于零
- 与原始图像的大小和类型相同
c++:
new_image = np.zeros(image.shape, image.dtype)
Java
Mat newImage = Mat.zeros(image.size(), image.type());
python
new_image = np.zeros(image.shape, image.dtype)
我们观察到 cv::Mat::zeros 返回一个基于 image.size()和 image.type()的 Matlab 风格的零初始值设定项
- 我们现在问的价值观a和b由用户输入:
C++
double alpha = 1.0; /*< Simple contrast control */
int beta = 0; /*< Simple brightness control */
cout << " Basic Linear Transforms " << endl;
cout << "-------------------------" << endl;
cout << "* Enter the alpha value [1.0-3.0]: "; cin >> alpha;
cout << "* Enter the beta value [0-100]: "; cin >> beta;
Java
double alpha = 1.0; /*< Simple contrast control */
int beta = 0; /*< Simple brightness control */
System.out.println(" Basic Linear Transforms ");
System.out.println("-------------------------");
try (Scanner scanner = new Scanner(System.in)) {
System.out.print("* Enter the alpha value [1.0-3.0]: ");
alpha = scanner.nextDouble();
System.out.print("* Enter the beta value [0-100]: ");
beta = scanner.nextInt();
}
Python
alpha = 1.0 # Simple contrast control
beta = 0 # Simple brightness control
# Initialize values
print(' Basic Linear Transforms ')
print('-------------------------')
try:
alpha = float(input('* Enter the alpha value [1.0-3.0]: '))
beta = int(input('* Enter the beta value [0-100]: '))
except ValueError:
print('Error, not a number')
C++:
for( int y = 0; y < image.rows; y++ ) {
for( int x = 0; x < image.cols; x++ ) {
for( int c = 0; c < image.channels(); c++ ) {
new_image.at<Vec3b>(y,x)[c] =
saturate_cast<uchar>( alpha*image.at<Vec3b>(y,x)[c] + beta );
}
}
}
Java
byte[] imageData = new byte[(int) (image.total()*image.channels())];
image.get(0, 0, imageData);
byte[] newImageData = new byte[(int) (newImage.total()*newImage.channels())];
for (int y = 0; y < image.rows(); y++) {
for (int x = 0; x < image.cols(); x++) {
for (int c = 0; c < image.channels(); c++) {
double pixelValue = imageData[(y * image.cols() + x) * image.channels() + c];
pixelValue = pixelValue < 0 ? pixelValue + 256 : pixelValue;
newImageData[(y * image.cols() + x) * image.channels() + c]
= saturate(alpha * pixelValue + beta);
}
}
}
newImage.put(0, 0, newImageData);
Python:
for y in range(image.shape[0]):
for x in range(image.shape[1]):
for c in range(image.shape[2]):
new_image[y,x,c] = np.clip(alpha*image[y,x,c] + beta, 0, 255)
请注意以下事项(仅限 C++ 代码):
- 要访问图像中的每个像素,我们使用以下语法:image.at<Vec3b>(y,x)[c],其中 y 是行,x 是列,c 是 B、G 或 R(0、1 或 2)。
- 自操作以来a.p(ij),可以给出超出范围或不是整数的值(如果a是浮点数),我们使用 cv::saturate_cast 来确保值有效。
- 最后,我们创建窗口并按照通常的方式显示图像。
# Show stuff
cv.imshow('Original Image', image)
cv.imshow('New Image', new_image)
# Wait until user press some key
cv.waitKey()
注意
与其使用 for 循环来访问每个像素,不如简单地使用以下命令:
image.convertTo(new_image, -1, alpha, beta);
其中 cv::Mat::convertTo 将有效地执行 *new_image = a*image + beta*。但是,我们想向您展示如何访问每个像素。无论如何,这两种方法都给出相同的结果,但 convertTo 更优化并且工作速度更快。
结果
$ ./BasicLinearTransforms lena.jpg
Basic Linear Transforms
-------------------------
* Enter the alpha value [1.0-3.0]: 2.2
* Enter the beta value [0-100]: 50
我们得到这个:
实例
在本段中,我们将把我们所学到的知识付诸实践,通过调整图像的亮度和对比度来纠正曝光不足的图像。我们还将看到另一种校正图像亮度的技术,称为伽马校正。
亮度和对比度调整
直方图表示每个颜色级别的像素数。深色图像将具有许多低色值的像素,因此直方图将在其左侧呈现一个峰值。添加恒定偏置时,直方图向右移动,因为我们已向所有像素添加了恒定偏置。
伽玛校正
伽玛校正可用于通过在输入值和映射的输出值之间使用非线性变换来校正图像的亮度:
由于这种关系是非线性的,因此所有像素的效果都不会相同,并且取决于它们的原始值。
绘制不同 gamma 值的图
校正曝光不足的图像
作者:Visem (Own work) [CC BY-SA 3.0], via Wikimedia Commons
整体亮度有所提高,但您可以注意到,由于所用实现的数值饱和度(摄影中的高光剪切),云层现在非常饱和。
作者:Visem (Own work) [CC BY-SA 3.0], via Wikimedia Commons
伽玛校正应该倾向于增加较少的饱和效应,因为映射是非线性的,并且不可能像以前的方法那样出现数值饱和。
左图:alpha 后的直方图,beta 校正;中心:原始图像的直方图;右图:伽马校正后的直方图
上图比较了三个图像的直方图(三个直方图之间的 y 范围不同)。您可以注意到,大多数像素值都位于原始图像直方图的下部。后a、b校正后,由于饱和度以及右移,我们可以观察到 255 处的大峰值。伽玛校正后,直方图向右移动,但暗区域的像素比亮区域的像素偏移更多(参见伽马曲线图)。
在本教程中,您介绍了两种调整图像对比度和亮度的简单方法。它们是基本技术,不能用作光栅图形编辑器的替代品!
代码:
本教程的代码在这里。伽玛校正代码:
Mat lookUpTable(1, 256, CV_8U);
uchar* p = lookUpTable.ptr();
for( int i = 0; i < 256; ++i)
p[i] = saturate_cast<uchar>(pow(i / 255.0, gamma_) * 255.0);
Mat res = img.clone();
LUT(img, lookUpTable, res);
查找表用于提高计算性能,因为只需计算一次 256 个值。
参考文献:
1、《Changing the contrast and brightness of an image!》----Ana Huamán