Seam Carving 算法(采用c++实现全部功能)

《Seam Carving for Content-Aware Image Resizing》论文复现(注:采用c++实现)

原文链接:论文原文

该论文提出了一种基于内容感知的图片缩放以及目标移除保护的方法,那么你一定很好奇到底是如何进行感知的呢?该算法能感知出图像上的“关键部分”和“不重要部分”,从而使得随意改变一个图像的高宽比但“不会让图像看起来很奇怪”。

1.Seam Carving 算法实现效果

  • 1.先来看下缩小(利用 Seam Carving 算法我们可以将原本照片中离得很远的两个物体挨在一起)

Input的图片:
Seam Carving 算法(采用c++实现全部功能)_第1张图片
三种方法对图片进行缩小:第一种为直接裁剪,发现裁剪到目标大小后小船都被裁剪没了。第二张为直接缩放,发现小船已经变形了。而第三种方法是seam carving。可以看出来保留了重要的部分小船,而且图片没有变形。
Seam Carving 算法(采用c++实现全部功能)_第2张图片 Seam Carving 算法(采用c++实现全部功能)_第3张图片 Seam Carving 算法(采用c++实现全部功能)_第4张图片

  • 2.图片的放大(利用 Seam Carving 算法我们可以将原本窄镜头的照片,修改成广角镜头的照图片片,且照片不会因为图片拉宽而变形)

①input图片以及长宽均放大后的图片,可以看出人眼主要关注的部分基本没有变形。
Seam Carving 算法(采用c++实现全部功能)_第5张图片
Seam Carving 算法(采用c++实现全部功能)_第6张图片
②input的图片以及输出的图片,看起来有没有像加了广角镜头哈哈哈:
Seam Carving 算法(采用c++实现全部功能)_第7张图片

Seam Carving 算法(采用c++实现全部功能)_第8张图片

  • 3.图片中目标的保护及移除(这个应用就很广泛了,比如下图直接把人p掉且不影响图片质量)

Seam Carving 算法(采用c++实现全部功能)_第9张图片

Seam Carving 算法(采用c++实现全部功能)_第10张图片

好啦,如果感觉很有趣的话可以接着看下去了,第二部分就是论文中原理的分析,最后会附上完整代码。每个人都能成为一个小ps设计师。

2.论文分析:

该篇论文的主要思想就是:通过寻找图片中的最小能量线进行插入或者删除,可以对图片的缩放产生的影响降至最小。那么图片的最小能量线用什么来衡量并找出来呢?接下来就是我根据论文中的方法进行的分析及处理。

1.裁剪的处理流程

读过论文后,我主要将算法实现的过程分为以下几步:
Seam Carving 算法(采用c++实现全部功能)_第11张图片
可以看出主要流程:
1.计算图片的梯度矩阵
2.根据梯度矩阵求得最小能量线
3.删除最小能量线
4.循环1-3步骤,直至达到设定次数(原图宽/高 – 目标图宽/高)

2.实现分析

其中主要的问题就是:最小能量线怎么衡量呢?如何根据梯度矩阵找到最小能量线呢?
1.首先第一个问题怎么衡量最小能量线,这里很显然使用梯度图来计算的,求出梯度图得到每个像素点的能量,之后计算从一行往下一直到最后一行的累积能量。
2.第二个问题:最小能量线的话直接找最后一行中累积能量值最小的列,进行回溯即可得到整条线。
对于灰度处理以及梯度矩阵就不再赘述,利用论文中的公式即可求得梯度矩阵。其中最重要的部分就是寻找最小能量线。那么从梯度矩阵中怎么计算能量呢。论文中也给出了方法,就是先计算最小能量矩阵。比如说求列最小能量线,那么就是根据梯度矩阵从第二行开始计算,比如说第二行中向上相邻的三个梯度值选取最小的加上当前这个梯度值既是第二行的能量值,最后求到最后一行,那么最后一行的元素中最小的元素进行回溯,既可以找到最小能量线。记录能量线,然后删除即可。直到删除次数等于设定次数时即可停止循环。

  1. 灰度处理程序如下:
cv::cvtColor(image, image_gray, CV_BGR2GRAY); //彩色图像转换为灰度图像
  1. 求图片的梯度值程序如下所示:
cv::filter2D(image_gray, gradiant_H, gradiant_H.depth(), kernel_H);//计算水平梯度
cv::filter2D(image_gray, gradiant_V, gradiant_V.depth(), kernel_V);//计算垂直梯度
cv::add(cv::abs(gradiant_H), cv::abs(gradiant_V), gradMag_mat);//水平与垂直结果的绝对值相加,可以得到近似梯度大小
  1. 求图片的能量矩阵程序如下:
for (int i = 1; i < gradMag_mat.rows; i++)  //从第2行开始计算
	{
		//第一列
		if (energyMat.at<float>(i - 1, 0) <= energyMat.at<float>(i - 1, 1))
		{
			energyMat.at<float>(i, 0) = gradMag_mat.at<float>(i, 0) + energyMat.at<float>(i - 1, 0);
			traceMat.at<float>(i, 0) = 1; //traceMat记录当前位置的上一行应取那个位置,上左为0,上中1,上右为2
		}
		else
		{
			energyMat.at<float>(i, 0) = gradMag_mat.at<float>(i, 0) + energyMat.at<float>(i - 1, 1);
			traceMat.at<float>(i, 0) = 2;
		}
		//中间列
		for (int j = 1; j < gradMag_mat.cols - 1; j++)
		{
			float k[3];
			k[0] = energyMat.at<float>(i - 1, j - 1);
			k[1] = energyMat.at<float>(i - 1, j);
			k[2] = energyMat.at<float>(i - 1, j + 1);

			int index = 0;
			if (k[1] < k[0])
				index = 1;
			if (k[2] < k[index])
				index = 2;
			energyMat.at<float>(i, j) = gradMag_mat.at<float>(i, j) + energyMat.at<float>(i - 1, j - 1 + index);
			traceMat.at<float>(i, j) = index;

		}
		//最后一列
		if (energyMat.at<float>(i - 1, gradMag_mat.cols - 1) <= energyMat.at<float>(i - 1, gradMag_mat.cols - 2))
		{
			energyMat.at<float>(i, gradMag_mat.cols - 1) = gradMag_mat.at<float>(i, gradMag_mat.cols - 1) + energyMat.at<float>(i - 1, gradMag_mat.cols - 1);
			traceMat.at<float>(i, gradMag_mat.cols - 1) = 1;
		}
		else
		{
			energyMat.at<float>(i, gradMag_mat.cols - 1) = gradMag_mat.at<float>(i, gradMag_mat.cols - 1) + energyMat.at<float>(i - 1, gradMag_mat.cols - 2);
			traceMat.at<float>(i, gradMag_mat.cols - 1) = 0;
		}
	}
  1. 找出最小能量线程序:
int row = energyMat.rows - 1;// 取的是energyMat最后一行的数据,所以行标是rows-1
	int index = 0;	// 保存的是最小那条轨迹的最下面点在图像中的列标
	// 获得index,即最后那行最小值的位置
	for (int i = 1; i < energyMat.cols; i++)
	{
		if (energyMat.at<float>(row, i) < energyMat.at<float>(row, index))
		{
			index = i;
		} 
	} 
	// 以下根据traceMat,得到minTrace
	{
		minTrace.at<float>(row, 0) = index;
		int tmpIndex = index;
		for (int i = row; i > 0; i--)
		{
			int temp = traceMat.at<float>(i, tmpIndex);// 当前位置traceMat所存的值
			if (temp == 0) // 往左走
			{
				tmpIndex = tmpIndex - 1;
			}
			else if (temp == 2) // 往右走
			{
				tmpIndex = tmpIndex + 1;
			} // 如果temp = 1,则往正上走,tmpIndex不需要做修改
			minTrace.at<float>(i - 1, 0) = tmpIndex;
		}
	}
  1. 删除最小能量线程序(这里是在输入的rgb图像上进行删除):
for (int i = 0; i < image2.rows; i++)
	{
		int k = minTrace.at<float>(i, 0);

		for (int j = 0; j < k; j++)
		{
			image2.at<cv::Vec3b>(i, j)[0] = image.at<cv::Vec3b>(i, j)[0];
			image2.at<cv::Vec3b>(i, j)[1] = image.at<cv::Vec3b>(i, j)[1];
			image2.at<cv::Vec3b>(i, j)[2] = image.at<cv::Vec3b>(i, j)[2];
		}
		for (int j = k; j < image2.cols - 1; j++)
		{
			if (j == image2.cols - 1)
			{
				int a = 1;
			}
			image2.at<cv::Vec3b>(i, j)[0] = image.at<cv::Vec3b>(i, j + 1)[0];
			image2.at<cv::Vec3b>(i, j)[1] = image.at<cv::Vec3b>(i, j + 1)[1];
			image2.at<cv::Vec3b>(i, j)[2] = image.at<cv::Vec3b>(i, j + 1)[2];
		}
	}

6.循环1-5的步骤,直至删除行数或者列数达到设定值停止。

若需要裁剪高度,则将图片矩阵进行转置即可。

2.图片的放大及目标移除

1.做图片放大时会遇到一个问题,就是如果每次添加一条线的话,每次添加的位置都是同一个位置,因为是按照最小能量线处进行添加的。如下图所示:
Seam Carving 算法(采用c++实现全部功能)_第12张图片
因此需要一次性找出n条线进行插入,其中n为你要扩大的像素列数。

2.目标移除其实也就是删除的过程,只需要把需要移除的物体的能量图中的能量改到足够小,让每次删除线都能经过目标移除物即可。

最后附上完整代码链接,程序改完图片读取和存储路径后可以直接运行:代码链接

你可能感兴趣的:(笔记,算法,c++,opencv,seam,carving,图片内容感知缩放)