感谢分享!
English is here (KuKuXia translates into English)
https://github.com/KuKuXia/Image_Processing_100_Questions
Chinese is here (gzr2017, my ex-colleague, translates into Chinese)
https://github.com/gzr2017/ImageProcessing100Wen
My git:
https://github.com/jamery-huang/ImageProcessing100Wen.git
基于c++版本
原code错误:
- Question_01_10/answers_cpp/answer_6.cpp
一、通道交换
RGB -> BGR
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>
// Channel swap
cv::Mat channel_swap(cv::Mat img){
// get height and width
int width = img.cols;
int height = img.rows;
// prepare output
cv::Mat out = cv::Mat::zeros(height, width, CV_8UC3);
// each y, x
for (int y = 0; y < height; y++){
for (int x = 0; x < width; x++){
// R -> B
out.at<cv::Vec3b>(y, x)[0] = img.at<cv::Vec3b>(y, x)[2];
// B -> R
out.at<cv::Vec3b>(y, x)[2] = img.at<cv::Vec3b>(y, x)[0];
// G -> G
out.at<cv::Vec3b>(y, x)[1] = img.at<cv::Vec3b>(y, x)[1];
}
}
return out;
}
int main(int argc, const char* argv[]){
// read image
cv::Mat img = cv::imread("imori.jpg", cv::IMREAD_COLOR);
// channel swap
cv::Mat out = channel_swap(img);
//cv::imwrite("out.jpg", out);
cv::imshow("img", img);
cv::imshow("out", out);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
二、灰度化
RGB ->YUV
来源:https://juejin.im/post/6844903640377884679
// BGR -> Gray
cv::Mat BGR2GRAY(cv::Mat img){
// get height and width
int width = img.cols;
int height = img.rows;
// prepare output
cv::Mat out = cv::Mat::zeros(height, width, CV_8UC1);
// each y, x
for (int y = 0; y < height; y++){
for (int x = 0; x < width; x++){
// BGR -> Gray
out.at<uchar>(y, x) = 0.2126 * (float)img.at<cv::Vec3b>(y, x)[2] \
+ 0.7152 * (float)img.at<cv::Vec3b>(y, x)[1] \
+ 0.0722 * (float)img.at<cv::Vec3b>(y, x)[0];
}
}
return out;
}
三、二值化(Thresholding)
将灰度图的亮度值,大于阈值设置为255,小于阈值设置为0。
// Gray -> Binary
cv::Mat Binarize(cv::Mat gray, int th){
int width = gray.cols;
int height = gray.rows;
// prepare output
cv::Mat out = cv::Mat::zeros(height, width, CV_8UC1);
// each y, x
for (int y = 0; y < height; y++){
for (int x = 0; x < width; x++){
// Binarize
if (gray.at<uchar>(y, x) > th){
out.at<uchar>(y, x) = 255;
} else {
out.at<uchar>(y, x) = 0;
}
}
}
return out;
}
四、大津二值化(Otsu’s Method)
小于阈值为前景(0),大于阈值为背景(255)
m0:前景平均灰度
m1:背景平均灰度
w0:前景像素数量占总像素数量的比例(概率)
w1:背景像素数量占总像素数量的比例(概率)
平均灰度m:m0 * w0 + m1 * w1
类间方差:w0 * (m0 - m) ^ 2 + w1 * (m1 - m) ^ 2
计算得到:w0 * w1 * (m0 - m1) ^ 2
统计其最大值即是阈值
// Gray -> Binary
cv::Mat Binarize_Otsu(cv::Mat gray){
int width = gray.cols;
int height = gray.rows;
// determine threshold
double w0 = 0, w1 = 0;
double m0 = 0, m1 = 0;
double max_sb = 0, sb = 0;
int th = 0;
int val;
// Get threshold
for (int t = 0; t < 255; t++){
w0 = 0;
w1 = 0;
m0 = 0;
m1 = 0;
for (int y = 0; y < height; y++){
for (int x = 0; x < width; x++){
val = (int)(gray.at<uchar>(y, x));
if (val < t){
w0++;
m0 += val;
} else {
w1++;
m1 += val;
}
}
}
m0 /= w0; //前景平均灰度
m1 /= w1; //背景平均灰度
w0 /= (height * width); //前景像素数量占总像素数量的比例(概率)
w1 /= (height * width); //背景像素数量占总像素数量的比例(概率)
sb = w0 * w1 * pow((m0 - m1), 2);
if(sb > max_sb){
max_sb = sb;
th = t;
}
}
std::cout << "threshold:" << th << std::endl;
// prepare output
cv::Mat out = cv::Mat::zeros(height, width, CV_8UC1);
// each y, x
for (int y = 0; y < height; y++){
for (int x = 0; x < width; x++){
// Binarize
if (gray.at<uchar>(y, x) > th){
out.at<uchar>(y, x) = 255;
} else {
out.at<uchar>(y, x) = 0;
}
}
}
return out;
}
五、RGB与HSV变换
- RGB色彩空间:是最常见的一种颜色模型,被称为设备相关的色彩空间。RGB色彩系统之所以能够用来表示色彩,归根结底还是因为人眼中的锥状细胞和棒状细胞对红色、蓝色和绿色特别敏感。
RGB颜色模型对应笛卡尔坐标系中的一个立方体,R、G、B分别代表三个坐标轴。当3个分量的取值范围都是0~255之间的整数时,可以表示256³种颜色。
- HSV颜色空间:
使用色相(Hue)、饱和度(Saturation)、明度(Value)来表示色彩的一种方式。
(1). 色相:将颜色使用 到 表示,就是平常所说的颜色名称,如红色、蓝色。色相与数值按下表对应:
(2). 饱和度:是指色彩的纯度,饱和度越低则颜色越黯淡(0 ≤ S<1)红 黄 绿 青 蓝 品红 红 0° 60° 120° 180° 240° 300° 360°
(3). 明度:即颜色的明暗程度。数值越高越接近白色,数值越低越接近黑色(0 ≤ V<1)
1、RGB转换HSV
- RGB的取值范围量化为[0, 1];
- 计算Max与Min
- 计算HSV
// BGR -> HSV
cv::Mat BGR2HSV(cv::Mat img){
// get height and width
int width = img.cols;
int height = img.rows;
float r, g, b;
float h, s, v;
float _max, _min;
// prepare output
cv::Mat hsv = cv::Mat::zeros(height, width, CV_32FC3);
// each y, x
for (int y = 0; y < height; y++){
for (int x = 0; x < width; x++){
// BGR -> HSV
r = (float)img.at<cv::Vec3b>(y, x)[2] / 255;
g = (float)img.at<cv::Vec3b>(y, x)[1] / 255;
b = (float)img.at<cv::Vec3b>(y, x)[0] / 255;
_max = fmax(r, fmax(g, b));
_min = fmin(r, fmin(g, b));
// get Hue
if(_max == _min){
h = 0;
} else if (_min == b) {
h = 60 * (g - r) / (_max - _min) + 60;
} else if (_min == r) {
h = 60 * (b - g) / (_max - _min) + 180;
} else if (_min == g) {
h = 60 * (r - b) / (_max - _min) + 300;
}
// get Saturation
s = _max - _min;
// get Value
v = _max;
hsv.at<cv::Vec3f>(y, x)[0] = h;
hsv.at<cv::Vec3f>(y, x)[1] = s;
hsv.at<cv::Vec3f>(y, x)[2] = v;
}
}
return hsv;
}
// inverse Hue 色相反转(色相加180)
cv::Mat inverse_hue(cv::Mat hsv){
int height = hsv.rows;
int width = hsv.cols;
for(int y = 0; y < height; y++){
for(int x = 0; x < width; x++){
hsv.at<cv::Vec3f>(y, x)[0] = fmod(hsv.at<cv::Vec3f>(y, x)[0] + 180, 360);
}
}
return hsv;
}
2、HSV转换RGB
- step1
- step2
- step3
// HSV -> BGR
cv::Mat HSV2BGR(cv::Mat hsv){
// get height and width
int width = hsv.cols;
int height = hsv.rows;
float h, s, v;
double c, _h, _x;
double r, g, b;
// prepare output
cv::Mat out = cv::Mat::zeros(height, width, CV_8UC3);
// each y, x
for (int y = 0; y < height; y++){
for (int x = 0; x < width; x++){
h = hsv.at<cv::Vec3f>(y, x)[0];
s = hsv.at<cv::Vec3f>(y, x)[1];
v = hsv.at<cv::Vec3f>(y, x)[2];
c = s;
_h = h / 60;
_x = c * (1 - abs(fmod(_h, 2) - 1));
r = g = b = v - c;
if (_h < 1) {
r += c;
g += _x;
} else if (_h < 2) {
r += _x;
g += c;
} else if (_h < 3) {
g += c;
b += _x;
} else if (_h < 4) {
g += _x;
b += c;
} else if (_h < 5) {
r += _x;
b += c;
} else if (_h < 6) {
r += c;
b += _x;
}
out.at<cv::Vec3b>(y, x)[0] = (uchar)(b * 255);
out.at<cv::Vec3b>(y, x)[1] = (uchar)(g * 255);
out.at<cv::Vec3b>(y, x)[2] = (uchar)(r * 255);
}
}
return out;
}
色相反转(色相加180)结果:
六、减色处理
我们将图像的值由256³压缩至4³,即将RGB的值只取{32, 96, 160, 224}。这被称作色彩量化。(256 / 4 / 2 = 32)
色彩的值按照下面的方式定义:
// Dedcrease color
cv::Mat decrease_color(cv::Mat img){
// int height = img.cols;
// int width = img.rows;
// error: 原文宽高反了
int width = img.cols;
int height = img.rows;
int channel = img.channels();
cv::Mat out = cv::Mat::zeros(height, width, CV_8UC3);
for(int y = 0; y < height; y++){
for(int x = 0; x < width; x++){
for(int c = 0; c < channel; c++){
out.at<cv::Vec3b>(y, x)[c] = (uchar)(floor((double)img.at<cv::Vec3b>(y, x)[c] / 64) * 64 + 32);
}
}
}
return out;
}
七、平均池化(Average Pooling)
将图片按照固定大小网格分割,网格内的像素值取网格内所有像素的平均值。
我们将这种把图片使用均等大小网格分割,并求网格内代表值的操作称为池化(Pooling)。
池化操作是卷积神经网络(Convolutional Neural Network)中重要的图像处理方式。
定义:
// average pooling
cv::Mat average_pooling(cv::Mat img){
int height = img.rows;
int width = img.cols;
int channel = img.channels();
// prepare output
cv::Mat out = cv::Mat::zeros(height, width, CV_8UC3);
int r = 8; // 使用8x8的网格做平均池化
double v = 0;
for (int y = 0; y < height; y+=r){
for (int x = 0; x < width; x+=r){
for (int c = 0; c < channel; c++){
v = 0;
for (int dy = 0; dy < r; dy++){
for (int dx = 0; dx < r; dx++){
v += (double)img.at<cv::Vec3b>(y + dy, x + dx)[c];
}
}
v /= (r * r);
for (int dy = 0; dy < r; dy++){
for (int dx = 0; dx < r; dx++){
out.at<cv::Vec3b>(y + dy, x + dx)[c] = (uchar)v;
}
}
}
}
}
return out;
}
八、最大池化(Max Pooling)
// max pooling
cv::Mat max_pooling(cv::Mat img){
int height = img.rows;
int width = img.cols;
int channel = img.channels();
// prepare output
cv::Mat out = cv::Mat::zeros(height, width, CV_8UC3);
int r = 8; // 使用8x8的网格做最大池化
double v = 0;
for (int y = 0; y < height; y+=r){
for (int x = 0; x < width; x+=r){
for (int c = 0; c < channel; c++){
v = 0;
for (int dy = 0; dy < r; dy++){
for (int dx = 0; dx < r; dx++){
v = fmax(img.at<cv::Vec3b>(y + dy, x + dx)[c], v);
}
}
for (int dy = 0; dy < r; dy++){
for (int dx = 0; dx < r; dx++){
out.at<cv::Vec3b>(y + dy, x + dx)[c] = (uchar)v;
}
}
}
}
}
return out;
}
九、高斯滤波(Gaussian Filter)
高斯滤波器是一种可以使图像平滑的滤波器,用于去除噪声。
可用于去除噪声的滤波器还有中值滤波器(参见问题十),平滑滤波器(参见问题十一)、LoG滤波器(参见问题十九)。
高斯滤波器将中心像素周围的像素按照高斯分布加权平均进行平滑化。这样的(二维)权值通常被称为卷积核(kernel)或者滤波器(filter)。但是,由于图像的长宽可能不是滤波器大小的整数倍,因此我们需要在图像的边缘补 。这种方法称作Zero Padding。并且权值 (卷积核)要进行归一化操作( Σg=1)。
缺点:对椒盐噪声基本无能为力。
按高斯分布公式计算权值:
使用高斯滤波器(3x3大小,标准差=1.3)进行滤波,计算近邻高斯滤波器:
// gaussian filter (sigma = 1.3, kernel_size = 3)
cv::Mat gaussian_filter(cv::Mat img, double sigma, int kernel_size){
int height = img.rows;
int width = img.cols;
int channel = img.channels();
// prepare output
cv::Mat out = cv::Mat::zeros(height, width, CV_8UC3);
// prepare kernel
int pad = floor(kernel_size / 2);
int _x = 0, _y = 0;
double kernel_sum = 0;
// get gaussian kernel
float kernel[kernel_size][kernel_size];
for (int y = 0; y < kernel_size; y++){
for (int x = 0; x < kernel_size; x++){
_y = y - pad;
_x = x - pad;
kernel[y][x] = 1 / (2 * M_PI * sigma * sigma) * exp( - (_x * _x + _y * _y) / (2 * sigma * sigma));
kernel_sum += kernel[y][x];
}
}
for (int y = 0; y < kernel_size; y++){
for (int x = 0; x < kernel_size; x++){
kernel[y][x] /= kernel_sum;
}
}
// filtering
double v = 0;
for (int y = 0; y < height; y++){
for (int x = 0; x < width; x++){
for (int c = 0; c < channel; c++){
v = 0;
for (int dy = -pad; dy < pad + 1; dy++){
for (int dx = -pad; dx < pad + 1; dx++){
if (((x + dx) >= 0) && ((y + dy) >= 0)){
v += (double)img.at<cv::Vec3b>(y + dy, x + dx)[c] * kernel[dy + pad][dx + pad];
}
}
}
out.at<cv::Vec3b>(y, x)[c] = v;
}
}
}
return out;
}
十、中值滤波(Median Filter)
中值滤波算法以某像素的领域图像区域中的像素值的排序为基础,将像素领域内灰度的中值代替该像素的值。中值滤波对处理椒盐噪声非常有效。
缺点:易造成图像的不连续性
// median filter
cv::Mat median_filter(cv::Mat img, int kernel_size){
int height = img.rows;
int width = img.cols;
int channel = img.channels();
// prepare output
cv::Mat out = cv::Mat::zeros(height, width, CV_8UC3);
// prepare kernel
int pad = floor(kernel_size / 2);
// filtering
double v = 0;
int vs[kernel_size * kernel_size];
int count = 0;
for (int y = 0; y < height; y++){
for (int x = 0; x < width; x++){
for (int c = 0; c < channel; c++){
v = 0;
count = 0;
for (int i = 0; i < kernel_size * kernel_size; i++){
vs[i] = 999;
}
// get neighbor pixels
for (int dy = -pad; dy < pad + 1; dy++){
for (int dx = -pad; dx < pad + 1; dx++){
if (((y + dy) >= 0) && ((x + dx) >= 0)){
vs[count++] = (int)img.at<cv::Vec3b>(y + dy, x + dx)[c];
}
}
}
// get and assign median
std::sort(vs, vs + (kernel_size * kernel_size));
out.at<cv::Vec3b>(y, x)[c] = (uchar)vs[int(floor(count / 2)) + 1];
}
}
}
return out;
}