图像处理 Seam Carving算法-Siggraph 2007

原文地址: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;
 
}

 

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值