目录
一,图像增强
图像增强的处理方法,包括空域方法、频域方法。
而空域方法包括点处理(变换)、模板处理(滤波)
二,空域方法——点处理
点运算都可以表示成,用一个一元函数,把所有像素点的灰度值进行映射,得到的新的图像。
1,反转变换
s = L-1 -r
[0,L-1]为图像的灰度级。作用:黑的变白,白的变黑
代码:
import cv2
def maxg(image):
ans = 0
for i in range(image.shape[0]):
for j in range(image.shape[1]):
ans = max(ans,image[i,j])
return ans
image = cv2.imread("D:/im.jpg",0)
cv2.imshow("old",image)
L = maxg(image)+1
for i in range(image.shape[0]):
for j in range(image.shape[1]):
image[i,j]=L-1-image[i,j]
cv2.imshow("new",image)
cv2.waitKey(0)
2,对数变换
有时原图的动态范围太大,超出某些显示设备的允许 动态范围,如直接使用原图,则一部分细节可能丢失。解决办法是对原图进行灰度压缩,如对数变换。
s = c log(1+r) ,其中c是常数
3,幂次变换
4,对比度拉伸
提高图像处理时灰度级的动态范围。
5,灰度级切片
利用切片扣出我们关心的部分,常见的两种切片:
6,位平面切片
假设图像中每个像素的灰度级是256,这可以用8位来表 示,假设图像是由8个1位平面组成,范围从位平面0到位 平面7。其中,位平面0包含图像中像素的最低位,位平面 7包含像素的最高位。
较高位(如前4位)包含大多数视觉重要数据,较低位(如后4位)对图像中的微小细节有作用。
三,图像运算
1,一元运算
(1)非
g(x,y) = 255 - f(x,y)
手动实现:
import cv2
image = cv2.imread("D:/im.jpg")
cv2.imshow("old",image)
L = 256
for i in range(image.shape[0]):
for j in range(image.shape[1]):
image[i,j]=L-1-image[i,j]
cv2.imshow("new",image)
cv2.waitKey(0)
也可以直接调用库函数:
cv2.bitwise_not(image,image2)
第一张图,熊猫,灰度级是0-255,非运算和反转运算相同。
第二张图,素描,灰度级是0-180,非运算出来的底色还是灰色,而反转运算出来的底色是纯黑色。
(2)数乘
手动实现:
for i in range(image.shape[0]):
for j in range(image.shape[1]):
image[i, j] *= 0.55
背景值从250变成 250*0.55=137
for i in range(image.shape[0]):
for j in range(image.shape[1]):
image[i, j] *= 10
背景值从250变成 250*10%256=196
调用库函数:
image2 = image*10
如果是乘以小数,会把图像转化成CV_64F的图像,所以需要转换:
image2 = cv2.cvtColor(numpy.uint8(image*0.8),cv2.CV_8UC1)
2,二元逻辑运算
(1)与
import cv2
image = cv2.imread("D:/im.jpg",0)
image2 = cv2.imread("D:/im2.jpg",0)
cv2.imshow("img",image)
cv2.imshow("img2",image2)
image3=image2
cv2.bitwise_and(image,image2,image3)
cv2.imshow("img3",image3)
cv2.waitKey(0)
(2)或
cv2.bitwise_or(image,image2,image3)
(3)异或
cv2.bitwise_xor(image,image2,image3)
3,算术运算
(1)加
python:
手动实现:
for i in range(image.shape[0]):
for j in range(image.shape[1]):
image2[i,j] += image[i,j]
或者直接用库函数:
image4 = image+image2
加法的结果如果超过上限值(如255),那么结果就mod 256,相当于和c语言中整数的溢出规则相同,减法同理,下文不再赘述。
经过统计发现,img和img2的背景白色值是250,img的背景白色是244
C++中是如果加完超过最大值255,那就结果就是最大值:
Mat img(2, 2, CV_8U);
img.at<char>(0, 0) = 1;
img.at<char>(0, 1) = 2;
img.at<char>(1, 0) = 3;
img.at<char>(1, 1) = 130;
Mat img2(2, 2, CV_8U);
img2.at<char>(0, 0) = 5;
img2.at<char>(0, 1) = 6;
img2.at<char>(1, 0) = 7;
img2.at<char>(1, 1) = 130;
Mat img3 = img + img2;
cout << int(img3.at<char>(0, 0)) << " " << int(img3.at<char>(0, 1)) << endl;
cout << int(img3.at<char>(1, 0)) << " " << int(img3.at<char>(1, 1)) << endl;
输出:
6 8
10 -1
-1即最大值255
(2)减
python:
image3 = image2-image
这个结果感觉和想象中的不太一样?
C++中是如果减完小于最小值0,那么结果就是0:
Mat img(2, 2, CV_8U);
img.at<char>(0, 0) = 1;
img.at<char>(0, 1) = 2;
img.at<char>(1, 0) = 3;
img.at<char>(1, 1) = 130;
Mat img2(2, 2, CV_8U);
img2.at<char>(0, 0) = 5;
img2.at<char>(0, 1) = 6;
img2.at<char>(1, 0) = 7;
img2.at<char>(1, 1) = 8;
Mat img3 = img2 - img;
cout << int(img3.at<char>(0, 0)) << " " << int(img3.at<char>(0, 1)) << endl;
cout << int(img3.at<char>(1, 0)) << " " << int(img3.at<char>(1, 1)) << endl;
输出:
4 4
4 0
(3)乘
python:
image3 = image*image2
C++的三种乘法:
矩阵乘法:
不允许CV_8U的图像矩阵相乘,32F可以:
Mat img(2, 2, CV_32F);
img.at<float>(0, 0) = 1;
img.at<float>(0, 1) = 2;
img.at<float>(1, 0) = 3;
img.at<float>(1, 1) = 4;
Mat img2(2, 2, CV_32F);
img2.at<float>(0, 0) = 5;
img2.at<float>(0, 1) = 6;
img2.at<float>(1, 0) = 7;
img2.at<float>(1, 1) = 8;
Mat img3 = img * img2;
cout << int(img3.at<float>(0, 0)) << " " << int(img3.at<float>(0, 1)) << endl;
cout << int(img3.at<float>(1, 0)) << " " << int(img3.at<float>(1, 1)) << endl;
输出:
19 22
43 50
PS:如果A*B中A的列数和B的行数不一致,则抛出异常。
向量内积:
Mat img(2, 2, CV_32F);
img.at<float>(0, 0) = 1;
img.at<float>(0, 1) = 2;
img.at<float>(1, 0) = 3;
img.at<float>(1, 1) = 4;
Mat img2(2, 2, CV_32F);
img2.at<float>(0, 0) = 5;
img2.at<float>(0, 1) = 6;
img2.at<float>(1, 0) = 7;
img2.at<float>(1, 1) = 8;
double x = img.dot(img2);
cout << x;
输出70
点乘:
Mat img(2, 2, CV_32F);
img.at<float>(0, 0) = 1;
img.at<float>(0, 1) = 2;
img.at<float>(1, 0) = 3;
img.at<float>(1, 1) = 4;
Mat img2(2, 2, CV_32F);
img2.at<float>(0, 0) = 5;
img2.at<float>(0, 1) = 6;
img2.at<float>(1, 0) = 7;
img2.at<float>(1, 1) = 8;
Mat img3 = img.mul(img2);
cout << int(img3.at<float>(0, 0)) << " " << int(img3.at<float>(0, 1)) << endl;
cout << int(img3.at<float>(1, 0)) << " " << int(img3.at<float>(1, 1)) << endl;
输出:
5 12
21 32
(4)除
一幅图像取反和另一幅图像相乘
image3 = image/image2
4,综合运算
(1)加权平均
image3 = image*0.5+image2*0.5
image3 = cv2.cvtColor(numpy.uint8(image3),cv2.CV_8UC1)
image3 = image*0.3+image2*0.7
image3 = cv2.cvtColor(numpy.uint8(image3),cv2.CV_8UC1)
四,直方图均衡、匹配
1 | 2 | 4 | 5 | 5 |
2 | 1 | 2 | 5 | 5 |
3 | 4 | 1 | 2 | 6 |
6 | 7 | 7 | 7 | 2 |
1,直方图表示方法
(1)像素个数
ps:常见的L=256
(2)像素密度(离散)
(3)像素密度(连续)
可以用一个定积分为1的曲线来描述像素密度,它和离散数据的折线图是相吻合的。
统计函数:
class EqualizeHistCalcHist_Invoker : public cv::ParallelLoopBody
{
//https://blog.csdn.net/nameofcsdn/article/details/120968236
};
int main()
{
Mat src = imread("D:/img.PNG",0);
Mutex histogramLockInstance;
const int hist_sz = EqualizeHistCalcHist_Invoker::HIST_SZ;
int hist[hist_sz] = { 0, };
EqualizeHistCalcHist_Invoker calcBody(src, hist, &histogramLockInstance);
cv::Range heightRange(0, src.rows);
calcBody(heightRange);
//cv::waitKey(0);
return 0;
}
heightRange是行的范围,支持只统计若干行的直方图。
src是输入图像,hist是出参直方图,仿函数是直方图统计。
2,直方图均衡
基本思想是把原始图的直方图变换为均匀分布的形式,这样就增加了像素灰度值的动态范围,从而达到增强图像整体对比度的效果。
(1)直方图均衡的条件
一个灰度变换操作是直方图均衡的充分必要条件是,它满足如下几个要求:
- 首先,直方图均衡应该是一个函数,即一个灰度不可能一部分变成灰度1,另一部分变成灰度2,这是没法实现的。
- 其次,我们不要求它是单射,因为这样对于一些图像就无法调整了。比如有256个像素点分别是0-255,剩下的几千个像素点都是150-160,那么直方图均衡可以把150-160拉开距离,而把很低的和很高的像素值合并。
- 再其次,这个映射函数应该是单调递增函数,直方图均衡需要保证较亮的区域调整后还是较亮。
- 最后,变换后的直方图应该非常接近均衡(折线图接近水平直线)。
(2)直方图的最大均衡
在满足上述条件的情况下,我们有一个常用的直方图均衡操作。
(2.1)书上的推导方法
ps:当前场景是单调函数,所以可以省掉绝对值。
结合这2个式子很容易证明,上述式子满足直方图均衡的条件,且s的概率密度函数是个完美的水平直线,所以这是最高程度的均衡。
(2.2)我的推导方法
书上的方法是直接给答案s=......,然后经过验证,这是最高均衡。
我的方法是根据最高均衡的条件推出答案。
有一个和上面的式(3.3-3)等价的表达方式:
我觉得这才是原始条件,根据积分上限函数的倒数,可以直接推出式(3.3-3)
现在,我们直接取最高均衡
则上式化成
(2.3)把连续的公式转化成离散的
总结:输出图像只由概率密度函数决定,最大均衡的概率密度函数是水平直线。
(2.4)公式不足
s的最大值总是能取到L-1的,但是最小值取决于最低灰度的像素数,如果这一级的像素数超过一半,那么s的最小值就超过了L/2。
库函数会把最低的灰度变成0,剩下其他的灰度用公式计算。
(3)库函数
python版:
image2=cv2.equalizeHist(image)
C++版:
我们可以把一个图二值化为只有0和1,这样的图看起来是全黑的,需要直方图均衡才能较好的显示:
int main()
{
Mat img = imread("D:/pw.png", 0);
Mat img2, img3;
threshold(img, img2, 100, 1, THRESH_BINARY);
equalizeHist(img2, img3);
cv::imshow("img", img);
cv::imshow("img2", img2);
cv::imshow("img3", img3);
cv::waitKey(0);
return 0;
}
ps:最后的图像不圆滑,是二值化时造成的,不是直方图均衡时造成的。
3,直方图匹配
如果我们想要某种特定的均衡(不一定要最均衡),那就需要直方图匹配,也叫直方图规定化。
是我们指定的输出图像的概率密度函数,那么输出图像只由
决定。
没有限制条件,所以输出图像可能比原图更均衡,也可能更不均衡,甚至可能变成纯色图。
原图、直方图均衡输出图、直方图匹配输出图三者之间都可以根据概率密度函数来互相转化
只要知道了pr和ps,就可以求出s,如果ps是水平直线,那就是直方图均衡。
void MarchHist(InputArray _src, InputArray _src2, OutputArray _dst)
{
CV_Assert(_src.type() == CV_8UC1);
if (_src.empty())
return;
Mat src = _src.getMat();
Mat src2 = _src2.getMat();
_dst.create(src.size(), src.type());
Mat dst = _dst.getMat();
Mutex histogramLockInstance;
Mutex histogramLockInstance2;
const int hist_sz = EqualizeHistCalcHist_Invoker::HIST_SZ;
int hist1[hist_sz] = { 0 };
int hist2[hist_sz] = { 0 };
int lut[hist_sz];
EqualizeHistCalcHist_Invoker calcBody(src, hist1, &histogramLockInstance);
EqualizeHistCalcHist_Invoker calcBody2(src2, hist2, &histogramLockInstance2);
EqualizeHistLut_Invoker lutBody(src, dst, lut);
cv::Range heightRange(0, src.rows);
cv::Range heightRange2(0, src2.rows);
if (EqualizeHistCalcHist_Invoker::isWorthParallel(src))
parallel_for_(heightRange, calcBody);
else
calcBody(heightRange);
if (EqualizeHistCalcHist_Invoker::isWorthParallel(src2))
parallel_for_(heightRange2, calcBody2);
else
calcBody2(heightRange2);
int s = hist2[0] - hist1[0];
for (int i = 0, j = 0; i < hist_sz && j < hist_sz;) { // 双指针
lut[i] = j;
if (s <= 0) {
s += hist2[++j];
}
else {
s -= hist1[++i];
}
}
lutBody(heightRange);
}
int main()
{
Mat img = imread("D:/img5.png", 0);
Mat img2 = imread("D:/img4.png", 0);
Mat img3;
MarchHist(img, img2,img3);
cv::imshow("img", img);
cv::imshow("img2", img2);
cv::imshow("img3", img3);
cv::waitKey(0);
return 0;
}
4,局部直方图处理
参考https://zhuanlan.zhihu.com/p/259886947
五,二值化
opencv提供的函数:
double threshold( InputArray src, OutputArray dst,
double thresh, double maxval, int type );
enum ThresholdTypes {
THRESH_BINARY = 0, //!< \f[\texttt{dst} (x,y) = \fork{\texttt{maxval}}{if \(\texttt{src}(x,y) > \texttt{thresh}\)}{0}{otherwise}\f]
THRESH_BINARY_INV = 1, //!< \f[\texttt{dst} (x,y) = \fork{0}{if \(\texttt{src}(x,y) > \texttt{thresh}\)}{\texttt{maxval}}{otherwise}\f]
THRESH_TRUNC = 2, //!< \f[\texttt{dst} (x,y) = \fork{\texttt{threshold}}{if \(\texttt{src}(x,y) > \texttt{thresh}\)}{\texttt{src}(x,y)}{otherwise}\f]
THRESH_TOZERO = 3, //!< \f[\texttt{dst} (x,y) = \fork{\texttt{src}(x,y)}{if \(\texttt{src}(x,y) > \texttt{thresh}\)}{0}{otherwise}\f]
THRESH_TOZERO_INV = 4, //!< \f[\texttt{dst} (x,y) = \fork{0}{if \(\texttt{src}(x,y) > \texttt{thresh}\)}{\texttt{src}(x,y)}{otherwise}\f]
THRESH_MASK = 7,
THRESH_OTSU = 8, //!< flag, use Otsu algorithm to choose the optimal threshold value
THRESH_TRIANGLE = 16 //!< flag, use Triangle algorithm to choose the optimal threshold value
};
前五个是全局阈值:
THRESH_BINARY:如果 src(x,y)>threshold ,dst(x,y) = max_value; 否则,dst(x,y)=0;
THRESH_BINARY_INV:如果 src(x,y)>threshold,dst(x,y) = 0; 否则,dst(x,y) = max_value.
THRESH_TRUNC:如果 src(x,y)>threshold,dst(x,y) = max_value; 否则dst(x,y) = src(x,y).
THRESH_TOZERO:如果src(x,y)>threshold,dst(x,y) = src(x,y) ; 否则 dst(x,y) = 0。
THRESH_TOZERO_INV:如果 src(x,y)>threshold,dst(x,y) = 0 ; 否则dst(x,y) = src(x,y).
使用方法:
int main()
{
Mat img = imread("D:/1.png", 0);
Mat img2;
threshold(img, img2, 100, 255, THRESH_BINARY);
cv::imshow("img", img);
cv::imshow("img2", img2);
cv::waitKey(0);
return 0;
}
可以看到一些线条丢失了,不同的地方亮度不同,很容易顾此失彼。
如果换成局部阈值THRESH_OTSU或者THRESH_TRIANGLE,运行效果:
因为右下角比较黑所以这块都黑了。
六,空域方法——模板处理
利用空间滤波器处理图像。
空间滤波器_nameofcsdn的博客-CSDN博客_滤波器csdn
七,频域方法
傅里叶变换 https://blog.csdn.net/nameofcsdn/article/details/118400482