系列文章:

用OpenCV實現Photoshop演算法(四): 色階調整

三、曲線調整( Curves Adjustment )

曲線調整是Photoshop的最常用的重要功能之一。

網上關於曲線技術原理的材料都不完整。經過一個多月的探索、不斷實驗,我用OpenCV實現了曲線功能,基本算是揭開了“曲線之謎“。

(一)曲線原理

對於一個RGB影象,  可以對R,  G,  B 通道進行獨立的曲線調整,即,對三個通道分別使用三條曲線(Curve)。還可以再增加一條曲線對 三個通道進行整體調整。 因此,對一個影象,可以用四條曲線調整。最終的結果,是四條曲線調整後合併產生的結果。

我們先來分析對單通道一條曲線的原理,比如:對紅色通道定義一條曲線如下:

 

圖中,橫軸是輸入,比左到右分別表示0到255.  縱軸是輸出,從下到上分別表示0到255.

該曲線由三個點定義,座標分別為:  點1(0,0),  點2(127,154),點3(255,255)

點1和點3是預設產生的,  點2是我們新增加的。在這三個點中畫出一條曲線(Spline).

調整的實現:    當輸入(紅色通道值)為X1時,將輸出值(新的紅色通道值)設為曲線對應的值  Y1.

程式碼實現: 對圖片的所有畫素點進行掃描, 取紅色值 X1,   換為 對應的 Y1.  其它兩個通道值(綠藍)不變。

         比如:  畫素點的RGB= (127,  230, 220),  其中紅色值為 X1 = 127,    對應曲線上的值Y1 = 154, 則對該通道曲線調整後 畫素點的RGB= (154,  230, 220)

如果曲線僅是一條由左下角到右上角的45度斜線,則 X1 總是等於 Y1, 則曲線調整後 圖片不變。

對紅、綠、藍三個獨立通道調整方式都與上述演算法相同。各通道調整是互不相關的。

然後,我們再來分析對RGB通道進行整體調整的原理。

 比如:  畫素點的RGB= (127,  230, 220),  對RGB通道進行整體調整, 則根據該曲線同時對R, G, B三個值進行調整。

     R = 127 作為輸入值,  計算曲線上的 對應輸出值  R1 

     G = 230作為輸入值,   計算曲線上的 對應輸出值  G1 

     B = 220作為輸入值計算曲線上的 對應輸出值  B1 

     則新的畫素點的RGB =(R1, G1, B1)

用幾條曲線同時調整時,先對紅、綠、藍三個獨立通道分別進行調整,最後對RGB總通道進行調整。

由於曲線調整僅僅是數值替換,可以用一個轉換表進行快速運算, 因此,曲線調整的速度是很快的。

(二)曲線的生成

Photoshop使用的曲線是一種SPline 曲線。這種曲線表現力很強,特點是:僅需要定義幾個控制點,就可以定義一條平滑的曲線,且曲線同時通過所有控制點。生成曲線時,只需要給出幾個控制點,呼叫曲線生成函式即可。

SPline的具體數學原理我就不講了,生成函式可以看下面的原始碼Curves.cpp中的spline()函式

(三)曲線調整的OpenCV實現

我用opencv寫了兩個 C++ 類: Curves類實現了多通道的曲線的定義、繪製、實施調整。  Curve類是一個通道的曲線定義類。

原始碼共兩個檔案:    Curves.hpp,  Curves.cpp,    原始碼及使用例程可在這裡下載: 曲線演算法原始碼

原始碼有一定的長度,不具體解釋了,請見註釋。補充說明幾點:

1, Curves類中定義了四個Curve物件(即四個通道),分別是RedChannel, GreenChannel, BlueChannel 和 RGBChannel.

2,  Curves類支援用滑鼠生成曲線,使用方法見例程。

2, Curves.cpp中的spline()函式是生成曲線數值的,即輸入一串控制點,通過插值運算,生成一系列的輸出值。  

3, 除了用滑鼠生成曲線以外, 也可以用程式程式碼直接生成曲線:

     先使用Curve類的clearPoints()方法清除所有控制點,再呼叫addPoint()方法逐個新增控制點即可。

(四)例程

寫一個例程,使用Curves類,實現曲線調整。

程式中定義了兩個視窗,一個是圖片視窗,一個是曲線視窗。

/*
 * test_Curves.cpp
 *
 *  Created on: 2016年9月11日
 *      Author: Administrator
 */


#include <cstdio>
#include <iostream>
#include "opencv2/core.hpp"
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include "Curves.hpp"

using namespace std;
using namespace cv;

static string window_name = "Photo";
static Mat src;

static string curves_window = "Adjust Curves";
static Mat curves_mat;
static int channel = 0;
Curves  curves;

static void invalidate()
{
	curves.draw(curves_mat);
	imshow(curves_window, curves_mat);

	Mat dst;
	curves.adjust(src, dst);
	imshow(window_name, dst);

	int y, x;
	uchar *p;

	y = 150; x = 50;
	p = dst.ptr<uchar>(y) + x * 3;
	cout << "(" << int(p[2]) << ", " << int(p[1]) << ", " << int(p[0]) << ")  ";

	y = 150; x = 220;
	p = dst.ptr<uchar>(y) + x * 3;
	cout << "(" << int(p[2]) << ", " << int(p[1]) << ", " << int(p[0]) << ")  ";

	y = 150; x = 400;
	p = dst.ptr<uchar>(y) + x * 3;
	cout << "(" << int(p[2]) << ", " << int(p[1]) << ", " << int(p[0]) << ")  " << endl;
}

static void callbackAdjustChannel(int , void *)
{
	switch (channel) {
	case 3:
		curves.CurrentChannel = &curves.BlueChannel;
		break;
	case 2:
		curves.CurrentChannel = &curves.GreenChannel;
		break;
	case 1:
		curves.CurrentChannel = &curves.RedChannel;
		break;
	default:
		curves.CurrentChannel = &curves.RGBChannel;
		break;
	}


	invalidate();
}

static void callbackMouseEvent(int mouseEvent, int x, int y, int flags, void* param)
{
	switch(mouseEvent) {
	case CV_EVENT_LBUTTONDOWN:
		curves.mouseDown(x, y);
		invalidate();
		break;
	case CV_EVENT_MOUSEMOVE:
		if ( curves.mouseMove(x, y) )
			invalidate();
		break;
	case CV_EVENT_LBUTTONUP:
		curves.mouseUp(x, y);
		invalidate();
		break;
	}
	return;
}


int main()
{
	//read image file
	src = imread("building.jpg");
	if ( !src.data ) {
		cout << "error read image" << endl;
		return -1;
	}

	//create window
	namedWindow(window_name);
	imshow(window_name, src);

	//create Mat for curves
	curves_mat = Mat::ones(256, 256, CV_8UC3);

	//create window for curves
	namedWindow(curves_window);
	setMouseCallback(curves_window, callbackMouseEvent, NULL );
	createTrackbar("Channel", curves_window, &channel,  3, callbackAdjustChannel);


// 範例:用程式程式碼在RedChannel中定義一條曲線
//	curves.RedChannel.clearPoints();
//	curves.RedChannel.addPoint( Point(10,  10) );
//	curves.RedChannel.addPoint( Point(240, 240) );
//	curves.RedChannel.addPoint( Point(127, 127) );

	invalidate();

	waitKey();

	return 0;
}

執行效果如下:

原圖:


對紅色通道(Channel 1)進行曲線調整


然後,對RGB通道(Channel 0)來一個經典的S型曲線調整


呵呵,有點味道了

.