一、算法目标:
实现这种背景去除增强的效果,特别是在“全能扫描王”中该算法得到了典型的应用。
二、使用PS进行模拟
图像处理算法很多时候就是对成熟经验的模拟和复现。首先我们来看PS下的处理。
1.手机拍一张需要电子版的纸质文件:
2.打开PS,复制背景,得到图层1:
3.对图层1使用滤镜:高斯模糊,半径100像素:
4.改变图层1混合模式:划分:
5.添加曲线调整层,压暗文字:
6.合并可见图层。
7.柔角画笔擦掉彩色噪点和边缘露出的拍摄背景:
那么,这个效果的确实很好的,关键问题就是“划分”是什么操作?
PS中划分模式的计算公式:
结果色 = (基色 / 混合色) * 255。
结果色 = (基色 / 混合色) * 255。
分析每个颜色通道的数值,并用基色分割混合色,基色数值大于或等于混合色数值,混合出的颜色为白色。基色数值小于混合色,结果色比基色更暗。因此结果色对比非常强。
两个一样的图层(复制基色图层),图层混合模式改为划分结果色就是白色
混合色为白色,结果色就是基色;混合色为黑色,结果色就是白色;
公式验证:r值(255/79)*255=823;结果色大于255,系统就默认为最大值255;以公式推断基色数值大于或等于混合色数值,混合出的颜色为白色。
两个一样的图层(复制基色图层),图层混合模式改为划分结果色就是白色
混合色为白色,结果色就是基色;混合色为黑色,结果色就是白色;
公式验证:r值(255/79)*255=823;结果色大于255,系统就默认为最大值255;以公式推断基色数值大于或等于混合色数值,混合出的颜色为白色。
三、整理流程,转换为代码
图像A,B,为同一场景在不一样的光照拍摄图片,那么: 光照分布 L = A / B 如果已知 A, L ,则 B = A / L (B 为A去光照的结果) 这里L约等于 gaussFilter(A, 大核)
再进一步转换为代码:
Mat src = imread("t1.jpeg"); src.convertTo(src,CV_32FC3,1.0/255); Mat gauss; Mat dst = src.clone(); cv::GaussianBlur(src,gauss,Size(101,101),0); //划分 //如果混合色与基色相同则结果色为白色 //如混合色为白色则结果色为基色不变 //如混合色为黑色则结果色为白色 dst = src/gauss; waitKey();
在这个基础上,进一步进行封装:
//USM图像增 void ImageSharp(Mat &src,Mat &dst,int nAmount = 1000) { double sigma = 3; int threshold = 1; float amount = nAmount/100.0f; Mat imgBlurred; GaussianBlur(src, imgBlurred, Size(), sigma, sigma); Mat lowContrastMask = abs(src-imgBlurred)<threshold; dst = src*(1+amount)+imgBlurred*(-amount); src.copyTo(dst, lowContrastMask); } int main() { Mat src = imread("E:\\未来项目\\白板增强\\images\\t1.jpeg"); src.convertTo(src,CV_32FC3,1.0/255); Mat gauss; Mat dst1 = src.clone(); Mat dst2 = src.clone(); cv::GaussianBlur(src,gauss,Size(101,101),0); dst1 = src/gauss; blur(src,gauss,Size(101,101)); dst2 = src/gauss; //划分 ImageSharp(dst2,dst2,101); waitKey(); return 0; }
效果更好
四、原理解析
算法的效果不错,下一步就是要分析为什么了。
关键点在于blur的话就可以思考得更清楚了,在101*101的格子里面算平均值,然后前景和其进行比较,对于那些有字的,肯定是小于平均值,关键是对于背景,肯定大于平均值,这样一除就变成了全白,问题也就清楚了。
我也找到了ImageShop作者对这个算法的分析,他是专门做增强的,分析的比较有道理:
“这个算法在此类图像中能够成功的核心是:在原图中比较黑的文字部分,占用的整体是比较少的,当大半径模糊时,模糊的值是接近纸张之类的颜色的,也就是比较靠近白色,所以结果基本上没什么变化,而纸张那些地方的颜色,因为模糊的值和他们的原始值基本差不多,所以Src/Blur基本接近1,在乘以255,所以结果就变为白色了。”
五、补充材料
1、发现OpenCV自带Divide算法,未进一步检测,我自信目前的解决是不错的。
divide Performs per-element division of two arrays or a scalar by an array. C++: void divide(InputArray src1, InputArray src2, OutputArray dst, double scale=1, int dtype=-1) C++: void divide(double scale, InputArray src2, OutputArray dst, int dtype=-1) Python: cv2.divide(src1, src2[, dst[, scale[, dtype]]]) → dst Python: cv2.divide(scale, src2[, dst[, dtype]]) → dst C: void cvDiv(const CvArr* src1, const CvArr* src2, CvArr* dst, double scale=1) Python: cv.Div(src1, src2, dst, scale=1) → None Parameters: src1 – first input array. src2 – second input array of the same size and type as src1. scale – scalar factor. dst – output array of the same size and type as src2. dtype – optional depth of the output array; if -1, dst will have depth src2.depth(), but in case of an array-by-array division, you can only pass -1 when src1.depth()==src2.depth(). The functions divide divide one array by another: or a scalar by an array when there is no src1 : When src2(I) is zero, dst(I) will also be zero. Different channels of multi-channel arrays are processed independently.