原作者 | Ana Huamán |
兼容性 | OpenCV >= 3.0 |
目标
在本教程中,您将学习如何
- 访问像素值
- 用零初始化矩阵
- 了解 cv::saturate_cast
- 获取有关像素变换的一些很酷的信息
- 通过实际例子提高图像亮度
理论
注释 以下解释属于《计算机视觉》(Computer Vision)一书: 算法与应用》一书中的内容。
图像处理
- 一般图像处理运算符是一个函数,它接收一个或多个输入图像并生成一个输出图像。
- 图像变换可分为
- 点算子(像素变换)
- 邻域(基于区域)算子
像素变换
- 在这种图像处理变换中,每个输出像素的值只取决于相应的输入像素值(可能还加上一些全局收集的信息或参数)。
- 这类运算符的例子包括亮度和对比度调整以及色彩校正和变换。
亮度和对比度调整
- 两个常用的点处理是与常数相乘和相加:
- 参数 α>0 和 β 通常被称为增益参数和偏置参数;有时这些参数也分别控制对比度和亮度。
- 可以将 f(x) 视为源图像像素,g(x) 视为输出图像像素。然后,我们可以更方便地将表达式写成
其中 i 和 j 表示像素位于第 i 行和第 j 列。
代码
- C++
- 可下载代码: 点击此处
- 以下代码实现上述公式
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include <iostream>
// 我们在这里不使用 "using namespace std;",以避免 c++17 中的 beta 变量与 std::beta 发生冲突
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;
}
- Java
- 可下载代码: 点击此处
- 以下代码实现上述公式
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) {
// 加载本地 OpenCV 库
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
# 读取用户提供的图像
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 # 简单的对比度控制
beta = 0 # 简单的亮度控制
# 初始化数值
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')
# 执行操作 new_image(i,j) = alpha*image(i,j) + beta
# 我们可以简单地使用以下方法来代替这些 "for "循环:
# new_image = cv.convertScaleAbs(image, alpha=alpha, beta=beta)
# 但我们想向您展示如何访问像素:)
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)
# 等待用户按下某个键
cv.waitKey()
解释
- 我们使用 cv::imread
- C++
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++
Mat new_image = Mat::zeros( image.size(), image.type() );
- Java
Mat newImage = Mat.zeros(image.size(), image.type());
}
- Python
new_image = np.zeros(image.shape, image.dtype)
我们注意到 cv::Mat::zeros 会根据 image.size()
- 现在我们要求用户输入 α 和 β 的值:
- C++
double alpha = 1.0; /*< 简单的对比度控制 */
int beta = 0; /*< 简单的亮度控制 */
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; /*< 简单的对比度控制 */
int beta = 0; /*< 简单的亮度控制 */
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 # 简单的对比度控制
beta = 0 # 简单的亮度控制
# 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')
- 现在,为了执行
操作,我们将访问图像中的每个像素。由于我们使用的是 BGR 图像,每个像素将有三个值(B、G 和 R),因此我们也将分别访问它们。代码如下 - 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(y,x)[c],其中 y 代表行,x 代表列,c 代表 B、G 或 R(0、1 或 2)。
- 由于操作 α⋅p(i,j)+β 可能会导致数值超出范围或不是整数(如果 α 是浮点数),因此我们使用 cv::saturate_cast
- 最后,我们按照常规方法创建窗口并显示图像。
- C++
imshow("Original Image", image);
imshow("New Image", new_image);
waitKey();
- Java
HighGui.imshow("Original Image", image);
HighGui.imshow("New Image", newImage);
HighGui.waitKey();
- Python
# 显示内容
cv.imshow('Original Image', image)
cv.imshow('New Image', new_image)
# 等待用户按下某个键
cv.waitKey()
注释
与其使用 for 循环来访问每个像素,我们还不如直接使用此命令:
- C++
image.convertTo(new_image, -1, alpha, beta);
- Java
image.convertTo(newImage, -1, alpha, beta);
- Python
new_image = cv.convertScaleAbs(image, alpha=alpha, beta=beta)
其中,cv::Mat::convertTo 将有效执行 new_image = aimage + beta*。不过,我们想向您展示如何访问每个像素。无论如何,两种方法都能得到相同的结果,但 convertTo 经过了更多优化,运行速度更快。
结果
- 运行我们的代码并使用 α=2.2 和 β=50
$ ./BasicLinearTransforms lena.jpg
Basic Linear Transforms
-------------------------
* Enter the alpha value [1.0-3.0]: 2.2
* Enter the beta value [0-100]: 50
我们得到这个:
实际例子
在本段中,我们将运用所学知识,通过调整图像的亮度和对比度来修正曝光不足的图像。我们还将看到另一种校正图像亮度的技术,即伽玛校正。
亮度和对比度调整
增加(/减少)β 值会给每个像素增加(/减少)一个恒定值。超出 [0 ; 255] 范围的像素值将被饱和(即高于(/低于)255(/0)的像素值将被箝制在 255(/0))。
浅灰色为原始图像的直方图,深灰色为 Gimp 中亮度 = 80 时的直方图
直方图表示每个色阶具有该色阶的像素数量。暗色图像中会有很多低色值的像素,因此直方图的左侧会出现一个峰值。在添加恒定偏置时,直方图会向右移动,因为我们对所有像素都添加了恒定偏置。
参数 α 将改变色阶的扩散方式。如果 α<1,色阶将被压缩,图像的对比度将降低。
浅灰色为原始图像的直方图,在 Gimp 中对比度 < 0 时为深灰色。 请注意,这些直方图是使用 Gimp 软件中的 "亮度-对比度 "工具绘制的。亮度工具应该与 β 偏置参数相同,但对比度工具似乎与 α 增益不同,后者的输出范围似乎是以 Gimp 为中心的(如上一张直方图所示)。
使用 β 偏置可以提高亮度,但同时由于对比度降低,图像会略显模糊。使用 α 增益可以减弱这种效果,但由于饱和度的原因,我们会丢失原来明亮区域的一些细节。
伽玛校正
伽玛校正可以通过在输入值和映射输出值之间进行非线性变换来校正图像的亮度:
由于这种关系是非线性的,因此对所有像素的效果并不相同,将取决于它们的原始值。
不同伽玛值的曲线图 当 γ<1 时,原来的暗部区域会更亮,直方图也会向右移动,而当γ>1 时,情况正好相反。
修正曝光不足的图像
以下图像的修正值为:α=1.3 和 β=40。
由 Visem(作品)[CC BY-SA 3.0]提供,通过维基共享资源
整体亮度有所改善,但您可以注意到,由于使用了数值饱和度的实现方法(摄影中的高光剪切),云彩现在已经非常饱和了。
下面的图像已用 γ=0.4.
由 Visem(作品)[CC BY-SA 3.0]提供,通过维基共享资源
伽玛校正应该会减少饱和效果,因为映射是非线性的,不可能像前一种方法那样进行数值饱和。
左图:阿尔法、贝塔校正后的直方图;中图:原始图像的直方图;右图:伽玛校正后的直方图 上图比较了三幅图像的直方图(三幅直方图的 y 范围不同)。可以看出,原始图像的大部分像素值都位于直方图的下部。经过 α、β 校正后,我们可以看到由于饱和度的原因,在 255 处出现了一个较大的峰值,并且向右移动。伽玛校正后,直方图向右移动,但暗部像素比亮部像素移动得更多(见伽玛曲线图)。
在本教程中,您将看到调整图像对比度和亮度的两种简单方法。它们都是基本技术,不能代替光栅图形编辑器!
代码
教程代码在此。
伽玛校正代码
- C++
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);
- Java
Mat lookUpTable = new Mat(1, 256, CvType.CV_8U);
byte[] lookUpTableData = new byte[(int) (lookUpTable.total()*lookUpTable.channels())];
for (int i = 0; i < lookUpTable.cols(); i++) {
lookUpTableData[i] = saturate(Math.pow(i / 255.0, gammaValue) * 255.0);
}
lookUpTable.put(0, 0, lookUpTableData);
Mat img = new Mat();
Core.LUT(matImgSrc, lookUpTable, img);
- Python
lookUpTable = np.empty((1,256), np.uint8)
for i in range(256):
lookUpTable[0,i] = np.clip(pow(i / 255.0, gamma) * 255.0, 0, 255)
res = cv.LUT(img_original, lookUpTable)