曲线调整( 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类,实现曲线调整。
程序中定义了两个窗口,一个是图片窗口,一个是曲线窗口。
1 /*
2 * test_Curves.cpp
3 *
4 * Created on: 2016年9月11日
5 * Author: Administrator
6 */
7
8
9 #include <cstdio>
10 #include <iostream>
11 #include "opencv2/core.hpp"
12 #include "opencv2/imgproc.hpp"
13 #include "opencv2/highgui.hpp"
14 #include "Curves.hpp"
15
16 using namespace std;
17 using namespace cv;
18
19 static string window_name = "Photo";
20 static Mat src;
21
22 static string curves_window = "Adjust Curves";
23 static Mat curves_mat;
24 static int channel = 0;
25 Curves curves;
26
27 static void invalidate()
28 {
29 curves.draw(curves_mat);
30 imshow(curves_window, curves_mat);
31
32 Mat dst;
33 curves.adjust(src, dst);
34 imshow(window_name, dst);
35
36 int y, x;
37 uchar *p;
38
39 y = 150; x = 50;
40 p = dst.ptr<uchar>(y) + x * 3;
41 cout << "(" << int(p[2]) << ", " << int(p[1]) << ", " << int(p[0]) << ") ";
42
43 y = 150; x = 220;
44 p = dst.ptr<uchar>(y) + x * 3;
45 cout << "(" << int(p[2]) << ", " << int(p[1]) << ", " << int(p[0]) << ") ";
46
47 y = 150; x = 400;
48 p = dst.ptr<uchar>(y) + x * 3;
49 cout << "(" << int(p[2]) << ", " << int(p[1]) << ", " << int(p[0]) << ") " << endl;
50 }
51
52 static void callbackAdjustChannel(int , void *)
53 {
54 switch (channel) {
55 case 3:
56 curves.CurrentChannel = &curves.BlueChannel;
57 break;
58 case 2:
59 curves.CurrentChannel = &curves.GreenChannel;
60 break;
61 case 1:
62 curves.CurrentChannel = &curves.RedChannel;
63 break;
64 default:
65 curves.CurrentChannel = &curves.RGBChannel;
66 break;
67 }
68
69
70 invalidate();
71 }
72
73 static void callbackMouseEvent(int mouseEvent, int x, int y, int flags, void* param)
74 {
75 switch(mouseEvent) {
76 case CV_EVENT_LBUTTONDOWN:
77 curves.mouseDown(x, y);
78 invalidate();
79 break;
80 case CV_EVENT_MOUSEMOVE:
81 if ( curves.mouseMove(x, y) )
82 invalidate();
83 break;
84 case CV_EVENT_LBUTTONUP:
85 curves.mouseUp(x, y);
86 invalidate();
87 break;
88 }
89 return;
90 }
91
92
93 int main()
94 {
95 //read image file
96 src = imread("building.jpg");
97 if ( !src.data ) {
98 cout << "error read image" << endl;
99 return -1;
100 }
101
102 //create window
103 namedWindow(window_name);
104 imshow(window_name, src);
105
106 //create Mat for curves
107 curves_mat = Mat::ones(256, 256, CV_8UC3);
108
109 //create window for curves
110 namedWindow(curves_window);
111 setMouseCallback(curves_window, callbackMouseEvent, NULL );
112 createTrackbar("Channel", curves_window, &channel, 3, callbackAdjustChannel);
113
114
115 // 范例:用程序代码在RedChannel中定义一条曲线
116 // curves.RedChannel.clearPoints();
117 // curves.RedChannel.addPoint( Point(10, 10) );
118 // curves.RedChannel.addPoint( Point(240, 240) );
119 // curves.RedChannel.addPoint( Point(127, 127) );
120
121 invalidate();
122
123 waitKey();
124
125 return 0;
126 }
运行效果如下:
原图:
对红色通道(Channel 1)进行曲线调整
然后,对RGB通道(Channel 0)来一个经典的S型曲线调整
呵呵,有点味道了