原文地址:http://blog.csdn.net/hjimce/article/details/44916869
作者:hjimce
一、相关理论
Seam Carving 算法是2007年siggraph上的一篇paper,名为《Seam Carving for Content-Aware Image Resizing》,这篇paper提出了一种内容感知的图像缩放算法,继这篇paper的思想,后来近几年siggraph上面也有好几篇都是内容感知的图像缩放算法。内容感知的图像缩放算法一般用于图像的裁剪,就是有的时候,你觉得一张照片有点大,你希望把它裁剪的小一些,但是你又想保留照片中的物体,这个时候就要用到内容感知的突刺昂缩放算法了。
原图 裁剪结果
垂直方向裁剪
该开始学习这个算法的时候,看起来好像很高深莫测的样子,理论看起来好像牛逼哄哄,什么能量,动态规划,都把我吓到了,最后理论的东西我实在是受不鸟了,于是果断直接解读代码,看了代码以后,才知道这个算法其实非常简单,难度就比普通的图像缩放难那么一点点,其实算法的原理很简单。废话不多说,这边只介绍缩小算法,且是图像宽度缩小的算法,对于图像高度缩小,只需对图像转置处理,总之paper中其它功能的实现,大同小异,整个过程说的简单一点就是通过寻找一条线,如上面的图1所示,找到了线以后,把那条线上的像素点全部删除,从而实现缩小操作。
该过程是通过迭代实现的,每次只能移除一列,迭代直到宽度满足要求。下面介绍寻找移除能量线的方法,需要注意的是,每次移除一列,都要重新计算能量图,也就是说,梯度图要重新计算,才能进行下一轮的移除工作。
算法流程
1、算法第一步:计算图像能量图
能量图一般是图像像素的梯度模值,为了简化计算可先转换成灰度图像,然后直接采用如下公式(直接用x、y方向上的差分取绝对值,然后相加),其实这一步就是相当于边缘检测算法一样:
2、算法第二步:寻找最小能量线
最小能量线指的是需要被移除的那一列:首先需要以图像第一行或最后一行为开始行进行迭代。下面实现的为从图像最后一行开始,往上迭代,
//先找出图片最底部的一行中,能量最小的像素点作为起始点
int min_x = 0;
int width = (*Source).Width();
int height = (*Source).Height();
for( int x = 0; x < width; x++ )
{
if( (*Source)(x,height-1)->energy < (*Source)(min_x,height-1)->energy )
{
min_x = x;
}
}
找出最后一行需要被移除的像素点后,设其坐标为P(x,y),然后往上一行寻找,寻找的点为P点的在y-1行中的三个相邻像素点中的能量最小值像素。也就是寻找的坐标为(x-1,y-1)、(x,y-1)、(x+1,y-1);
//min_x是图片最底部的一行的能量值最小的像素点 因此遍历的时候,要往上寻找
void Generate_Path( CML_image_ptr * Energy, int min_x, int * Path )
{
int min;
int x = min_x;//起始行的能量最小点
int height = (*Energy).Height();
//从底部往上寻找最小能量线
for( int y = height - 1; y >= 0; y-- ) //从下往上
{
min = x; //首个顶点迭代更新
//计算上一行中 三个邻接像素中能量值最小的像素点 作为上一行的能量最小点
int Maxy= Get_Max( Energy, min, y );
if( Get_Max( Energy, x-1, y ) < Maxy ) //
{
min = x - 1;
}
if( Get_Max( Energy, x+1, y ) < Maxy ) //up-right
{
min = x + 1;
}
Path[y] = min;
x = min;
}
}
3、算法第三步:移除得到的最小能量线,让图片的宽度缩小一个像素
移除最小能量线,同时所有位于最小能量线右边的像素点左移一个单位,从而实现图像缩小宽度缩小一个单位。
移除的时候 为了让图像看起来自然,需要在移除缝线的地方进行平均,假设移除坐标为P(x,y),那么移除后P(x-1,y)的像素值为P(x-1,y)与P(x,y)的像素值的平均。P(x+1,y)的像素值为P(x-1,y)与P(x,y)的像素值的平均,然后才能把P(x+1,y)移动到P(x,y)的位置。
int height=(*Source).Height();
int width=(*Source).Width();
//移除函数
for( int y =0; y < height; y++ )
{
int remove = (Path)[y];
(*(Source))(remove,y)->removed = true;
//移除的时候 为了让图像看起来自然,需要在移除缝线的地方进行平均,假设移除坐标为P(x,y),那么
//移除后P(x-1,y)的像素值为P(x-1,y)与P(x,y)的像素值的平均
//P(x+1,y)的像素值为P(x-1,y)与P(x,y)的像素值的平均,然后才能把P(x+1,y)移动到P(x,y)的位置
if( (remove - 1) > 0 )
{
if( (*(Source))(remove,y)->weight >= 0 )
{
(*(Source))(remove-1,y)->image = Average_Pixels( (*(Source))(remove,y)->image,
(*(Source)).Get(remove-1,y)->image );
}
(*(Source))(remove-1,y)->gray = Grayscale_Pixel( &(*(Source))(remove-1,y)->image );
}
if( (remove + 1) < (*(Source)).Width() )
{
if( (*(Source))(remove,y)->weight >= 0 )
{
(*(Source))(remove+1,y)->image = Average_Pixels( (*(Source))(remove,y)->image,
(*(Source)).Get(remove+1,y)->image );
}
(*(Source))(remove+1,y)->gray = Grayscale_Pixel( &(*(Source))(remove+1,y)->image );
}
(*(Source)).Shift_Row( remove + 1, y, -1 );
}
对于图像的放大算法原理一样,先找到最小能量线,设能量线上点的坐标为P(x,y),则在P(x,y)、P(x+1,y)中心位置插入新的像素,像素值为P(x,y)与P(x+1,y)的平均。
参考文献:
1、《Seam Carving for Content-Aware Image Resizing》
*****************本文地址: 作者:hjimce 联系qq:1393852684 更多资源请关注我的博客:http://blog.csdn.net/hjimce 原创文章,转载请保留这几行信息***************
Matlab版本实现:
%%%%%%%%%%%%%%%%
%
%使用动态规划的方法实现seam carving
%
% 2016.12.09 南京
%
%%%%%%%%%%%%%%%%
clc;
clear;
Im=imread('ori.jpg');
gauss=fspecial('gaussian',[3,3],2);
Blur=imfilter(Im,gauss,'same');
[m,n,c]=size(Im);
%灰度图
Gray=rgb2gray(Im);
%求梯度图
hy=fspecial('sobel');
hx=hy';
Iy=imfilter(double(Gray),hy,'replicate');
Ix=imfilter(double(Gray),hx,'replicate');
Gradient=sqrt(Ix.^2+Iy.^2);
%归一化
max1=max(max(Gradient)');
Gradient=Gradient./max1;
%能量值图
Energy=zeros(m,n);
%路径图
Path=zeros(m,n);
tmp=0;
for i=1:m
for j=1:n
if(i==1)
Energy(i,j)=Gradient(i,j);
Path(i,j)=0;
else
if(j==1)
tmp=which_min2(Energy(i-1,j),Energy(i-1,j+1));
Energy(i,j)=Gradient(i,j)+Energy(i-1,j+tmp);
Path(i,j)=tmp;
elseif(j==n)
tmp=which_min2(Energy(i-1,j-1),Energy(i-1,j));
Energy(i,j)=Gradient(i,j)+Energy(i-1,j-1+tmp);
Path(i,j)=tmp-1;
else
tmp=which_min3(Energy(i-1,j-1),Energy(i-1,j),Energy(i-1,j+1));
Energy(i,j)=Gradient(i,j)+Energy(i-1,j-1+tmp);
Path(i,j)=tmp-1;
end
end
end
end
%归一化
%归一化
max2=max(max(Energy)');
Energy=Energy./max2;
%能量最小路径的最后一行的纵坐标
lastCol=find(Energy(m,:)==min(Energy(m,:)));
col=lastCol(1);
%描画出分割线
Line=Im;
for i=m:-1:1
Line(i,col,:)=[0,255,0];
col=col+Path(i,col);
end
%消除路径上的点
Im=Im(:);
for i=m:-1:1
Im(1*i+col)=[];
Im(2*i+col)=[];
Im(3*i+col)=[];
col=col+Path(i,col);
end
Im=reshape(Im,m,n-1,3);
%Gradient是梯度图
figure,imshow(Gradient);title('Gradient Image');
%Energy是能量图
figure,imshow(Energy);title('Energy Image');
%Line是标注了分割线的图
figure,imshow(Line);title('Image with Seam');
%在Im上将分割线切掉
figure,imshow(Im);title('after Cut Seam');
function tmp=which_min(x,y)
if(min([x,y])==x)
tmp=0;
else
tmp=1;
end
function tmp=which_min3(x,y,z)
if(min([x,y,z])==y)
tmp=1;
elseif(min([x,y,z])==x)
tmp=0;
else
tmp=2;
end
C++版本(opencv)实现:
#include "opencv2/opencv.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <stdio.h>
#define NUM 220
void calculateEnergy(cv::Mat& srcMat,cv::Mat& dstMat,cv::Mat& traceMat)
{
srcMat.copyTo(dstMat); //不用“=”,防止两个矩阵指向的都是同一个矩阵,现在只需要传里面的数值
for (int i = 1;i < srcMat.rows;i++) //从第2行开始计算
{
//第一列
if (dstMat.at<float>(i-1,0) <= dstMat.at<float>(i-1,1))
{
dstMat.at<float>(i,0) = srcMat.at<float>(i,0) + dstMat.at<float>(i-1,0);
traceMat.at<float>(i,0) = 1; //traceMat记录当前位置的上一行应取那个位置,上左为0,上中1,上右为2
}
else
{
dstMat.at<float>(i,0) = srcMat.at<float>(i,0) + dstMat.at<float>(i-1,1);
traceMat.at<float>(i,0) = 2;
}
//中间列
for (int j = 1;j < srcMat.cols-1;j++)
{
float k[3];
k[0] = dstMat.at<float>(i-1,j-1);
k[1] = dstMat.at<float>(i-1,j);
k[2] = dstMat.at<float>(i-1,j+1);
int index = 0;
if (k[1] < k[0])
index = 1;
if (k[2] < k[index])
index = 2;
dstMat.at<float>(i,j) = srcMat.at<float>(i,j) + dstMat.at<float>(i-1,j-1+index);
traceMat.at<float>(i,j) = index;
}
//最后一列
if (dstMat.at<float>(i-1,srcMat.cols-1) <= dstMat.at<float>(i-1,srcMat.cols-2))
{
dstMat.at<float>(i,srcMat.cols-1) = srcMat.at<float>(i,srcMat.cols-1) + dstMat.at<float>(i-1,srcMat.cols-1);
traceMat.at<float>(i,srcMat.cols-1) = 1;
}
else
{
dstMat.at<float>(i,srcMat.cols-1) = srcMat.at<float>(i,srcMat.cols-1) + dstMat.at<float>(i-1,srcMat.cols-2);
traceMat.at<float>(i,srcMat.cols-1) = 0;
}
}
}
// 找出最小能量线
void getMinEnergyTrace(const cv::Mat& energyMat,const cv::Mat& traceMat,cv::Mat& minTrace)
{
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;
} // end if
} // end for i = ...
// 以下根据traceMat,得到minTrace,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;
}
}
}
// 删掉一列
void delOneCol(cv::Mat& srcMat,cv::Mat& dstMat,cv::Mat& minTrace,cv::Mat& beDeletedLine)
{
for (int i = 0;i < dstMat.rows;i++)
{
int k = minTrace.at<float>(i,0);
for (int j = 0;j < k;j++)
{
dstMat.at<cv::Vec3b>(i,j)[0] = srcMat.at<cv::Vec3b>(i,j)[0];
dstMat.at<cv::Vec3b>(i,j)[1] = srcMat.at<cv::Vec3b>(i,j)[1];
dstMat.at<cv::Vec3b>(i,j)[2] = srcMat.at<cv::Vec3b>(i,j)[2];
}
for (int j = k;j < dstMat.cols-1;j++)
{
if (j == dstMat.cols-1)
{
int a = 1;
}
dstMat.at<cv::Vec3b>(i,j)[0] = srcMat.at<cv::Vec3b>(i,j+1)[0];
dstMat.at<cv::Vec3b>(i,j)[1] = srcMat.at<cv::Vec3b>(i,j+1)[1];
dstMat.at<cv::Vec3b>(i,j)[2] = srcMat.at<cv::Vec3b>(i,j+1)[2];
}
{
beDeletedLine.at<cv::Vec3b>(i,0)[0] = srcMat.at<cv::Vec3b>(i,k)[0];
beDeletedLine.at<cv::Vec3b>(i,0)[1] = srcMat.at<cv::Vec3b>(i,k)[1];
beDeletedLine.at<cv::Vec3b>(i,0)[2] = srcMat.at<cv::Vec3b>(i,k)[2];
}
}
}
void run(cv::Mat& image,cv::Mat& outImage,cv::Mat& outMinTrace,cv::Mat& outDeletedLine)
{
cv::Mat image_gray(image.rows,image.cols,CV_8U,cv::Scalar(0));
cv::cvtColor(image,image_gray,CV_BGR2GRAY); //彩色图像转换为灰度图像
cv::Mat gradiant_H(image.rows,image.cols,CV_32F,cv::Scalar(0));//水平梯度矩阵
cv::Mat gradiant_V(image.rows,image.cols,CV_32F,cv::Scalar(0));//垂直梯度矩阵
cv::Mat kernel_H = (cv::Mat_<float>(3,3) << 0, 0, 0, 0, 1, -1, 0, 0, 0); //求水平梯度所使用的卷积核(赋初始值)
cv::Mat kernel_V = (cv::Mat_<float>(3,3) << 0, 0, 0, 0, 1, 0, 0, -1, 0); //求垂直梯度所使用的卷积核(赋初始值)
cv::filter2D(image_gray,gradiant_H,gradiant_H.depth(),kernel_H);
cv::filter2D(image_gray,gradiant_V,gradiant_V.depth(),kernel_V);
cv::Mat gradMag_mat(image.rows,image.rows,CV_32F,cv::Scalar(0));
cv::add(cv::abs(gradiant_H),cv::abs(gradiant_V),gradMag_mat);//水平与垂直滤波结果的绝对值相加,可以得到近似梯度大小
如果要显示梯度大小这个图,因为gradMag_mat深度是CV_32F,所以需要先转换为CV_8U
//cv::Mat testMat;
//gradMag_mat.convertTo(testMat,CV_8U,1,0);
//cv::imshow("Image Show Window2",testMat);
//计算能量线
cv::Mat energyMat(image.rows,image.cols,CV_32F,cv::Scalar(0));//累计能量矩阵
cv::Mat traceMat(image.rows,image.cols,CV_32F,cv::Scalar(0));//能量最小轨迹矩阵
calculateEnergy(gradMag_mat,energyMat,traceMat);
//找出最小能量线
cv::Mat minTrace(image.rows,1,CV_32F,cv::Scalar(0));//能量最小轨迹矩阵中的最小的一条的轨迹
getMinEnergyTrace(energyMat,traceMat,minTrace);
//显示最小能量线
cv::Mat tmpImage(image.rows,image.cols,image.type());
image.copyTo(tmpImage);
for (int i = 0;i < image.rows;i++)
{
int k = minTrace.at<float>(i,0);
tmpImage.at<cv::Vec3b>(i,k)[0] = 0;
tmpImage.at<cv::Vec3b>(i,k)[1] = 0;
tmpImage.at<cv::Vec3b>(i,k)[2] = 255;
}
cv::imshow("Image Show Window (A)",tmpImage);
//删除一列
cv::Mat image2(image.rows,image.cols-1,image.type());
cv::Mat beDeletedLine(image.rows,1,CV_8UC3);//记录被删掉的那一列的值
delOneCol(image,image2,minTrace,beDeletedLine);
cv::imshow("Image Show Window",image2);
image2.copyTo(outImage);
minTrace.copyTo(outMinTrace);
beDeletedLine.copyTo(outDeletedLine);
}
void recoverOneLine(cv::Mat& inImage,cv::Mat&inTrace,cv::Mat& inDeletedLine,cv::Mat& outImage)
{
cv::Mat recorvedImage(inImage.rows,inImage.cols+1,CV_8UC3);
for (int i = 0; i < inImage.rows; i++)
{
int k = inTrace.at<float>(i);
for (int j = 0; j < k; j++)
{
recorvedImage.at<cv::Vec3b>(i,j)[0] = inImage.at<cv::Vec3b>(i,j)[0];
recorvedImage.at<cv::Vec3b>(i,j)[1] = inImage.at<cv::Vec3b>(i,j)[1];
recorvedImage.at<cv::Vec3b>(i,j)[2] = inImage.at<cv::Vec3b>(i,j)[2];
}
recorvedImage.at<cv::Vec3b>(i,k)[0] = inDeletedLine.at<cv::Vec3b>(i,0)[0];
recorvedImage.at<cv::Vec3b>(i,k)[1] = inDeletedLine.at<cv::Vec3b>(i,0)[1];
recorvedImage.at<cv::Vec3b>(i,k)[2] = inDeletedLine.at<cv::Vec3b>(i,0)[2];
for (int j = k + 1;j < inImage.cols + 1; j++)
{
recorvedImage.at<cv::Vec3b>(i,j)[0] = inImage.at<cv::Vec3b>(i,j-1)[0];
recorvedImage.at<cv::Vec3b>(i,j)[1] = inImage.at<cv::Vec3b>(i,j-1)[1];
recorvedImage.at<cv::Vec3b>(i,j)[2] = inImage.at<cv::Vec3b>(i,j-1)[2];
}
}
//显示恢复的轨迹
cv::Mat tmpImage(recorvedImage.rows,recorvedImage.cols,recorvedImage.type());
recorvedImage.copyTo(tmpImage);
for (int i = 0;i < tmpImage.rows;i++)
{
int k = inTrace.at<float>(i,0);
tmpImage.at<cv::Vec3b>(i,k)[0] = 0;
tmpImage.at<cv::Vec3b>(i,k)[1] = 255;
tmpImage.at<cv::Vec3b>(i,k)[2] = 0;
}
cv::imshow("Image Show Window (B)",tmpImage);
recorvedImage.copyTo(outImage);
}
int main(int argc,char* argv)
{
cv::Mat image = cv::imread("1.jpg");
cv::namedWindow("Original Image");
cv::imshow("Original Image",image);
cv::Mat tmpMat;
image.copyTo(tmpMat);
cv::Mat traces[NUM];
cv::Mat deletedLines[NUM];
cv::Mat outImage;
cv::waitKey(2000);
for (int i = 0;i < NUM;i++)
{
run(tmpMat,outImage,traces[i],deletedLines[i]);
tmpMat = outImage;
cv::waitKey(50);
}
cv::Mat tmpMat2;
outImage.copyTo(tmpMat2);
for (int i = 0; i < NUM; i++)
{
recoverOneLine(tmpMat2,traces[NUM-i-1],deletedLines[NUM-i-1],outImage);
tmpMat2 = outImage;
cv::waitKey(50);
}
cv::waitKey(115000);
return 0;
}