具体实施方式
下面结合附图对本发明进行详细的描述。
本发明所提供的任意区域的色彩校正特技调节方法,是在视频界面上采用三次Bezier曲线描述一个或多个区域,在界面上绘制若干条闭合的Bezier曲线,将特技效果只作用于闭合的Bezier曲线所包含的区域内。
几何学中连续的Bezier曲线由若干端点组成,每个端点带有一个或两个控制点,端点构成曲线的位置,控制点控制曲线在某端点处的走向,这样的Bezier曲线可以描述任意形状的曲线。
Bezier曲线的推导公式如下所示:
表示一个矢量,既是端点,也是控制点,当n=3时,上式变形为:
视频一般是动态的、序列化的,在不同的关键帧上,某一选定的区域在画面中的位置很有可能会发生变化,本发明使用直线插值的方法可以达到动态跟踪区域位置变化的效果。
直线插值(或者叫作线性插值)方法为一种公知技术,下面对该方法进行一下介绍:
假设在时序上加了两个关键帧,每个关键帧上保存了不同的要渲染的图形形状,如第一个关键帧上的图形如图3中a所示,第二个关键帧上的图形如b所示,两图中上面两个点不动,调整下面一点,则在两个关键帧内时序上某一帧上的图像就需要按这两个关键帧上的点的位置用线性插值的方法计算出来。
假设:图3中a上要调节的点(左下角点)的坐标为(x1,y1),图3中b上要调节的点(右下角点)的坐标为(x2,y2);a、b、c对应的时序分别为t1、t2,t3。则图3中c上要调整的点(中下点)的坐标为:
x3=x1+(x2-x1)*t
y3=y1+(y2-y1)*t
其中t=(t3-t1)/(t2-t1)
用上述的方法,播放序列时可以看到要调节的点会呈直线移动。
在介绍Bezier曲线模块的设计之前,首先介绍一下该模块会用到的两种数据结构:nodelist与红黑树(red-black tree)。
node list是一种通用的用于构建链表的数据结构,它不同于我们一般使用的像Clist或者STL中的list那样的链表数据结构,此两者是把要加入链表的对象的指针或者对象本身加入到链表中,然后由链表类的成员函数来遍历这些对象,这样的链表虽然简单直观,但不适合于构建复杂的链表数据结构,比如当一个对象需要同时位于多个链表中,并且随时可能在不同链表中遍历其前面的或者后面的对象的时候,上述两种链表类就显得有些捉襟见肘了。而nodelist则不同,它直接将用于构建链表的数据结构---链表节点,放置在对象自身之中,然后将这些链表节点(而不是对象本身)链接起来,而当找到了一个链表节点之后则通过该链表节点在对象之中的地址偏移量找到对象本身的地址。
node list的数据结构如图2所示,一共有三个对象Obj0,Obj1与Obj2,它们分别被链接入三个链表中,这三个链表的表头(也是一个链表节点)分别是:HEAD NODE0,HEAD NODE1以及HEAD NODE2。从图中可以看出每个对象为了能够链接入这三个链表,都为相应的链表准备了一个链表节点,分别是NODE0,NODE1与NODE2,它们依次对应前述链表头的构成的链表。每个链表以其链表头为开始,按一定顺序将链表节点链接起来(实际上相当于将对象链接起来)。比如对于上图中的第二个链表,它依次将Obj1,Obj2和Obj0链接起来。
链表节点的数据结构如下所示:
struct SNsListNode{
SNsListNode*pNext;
SNsListNode*pPrev;
};
每个链表节点具有指向其后继节点以及前导节点的指针。对于节点的初始化,插入、删除操作有一套统一的操作,这些操作不会因具体对象的数据结构而异。假设有一个数据结构SNsNonsense,其结构如下:
struct SNsNonsense{
uint32_t nChatId;
uint32_t nTimestamp;
SNsListNode lstNode;
wchar_t wszNonsense[32];
};
有许多SNsNonsense的对象通过ls tNode这个链表节点被链接入一个链表中,而在某一刻沿着该链表遍历时得到了一个链表节点的指针pNode,它的类型为SNsListNode*,那么只要通过下面的语句就可以得到其所在对象的指针:
SNsNonsense*pNonsense=NS_LIST_ENTRY(pNode,SNsNonsense,lstNode);
上面语句中的NS_LIST_ENTRY是一个宏,它的定义如下:
#define NS_GET_CONTAINER(PTR,TYPE,MEMBER)\
((TYPE*)((char*)PTR-(size_t)(&((TYPE*)0)->MEMBER)))
#define NS_LIST_ENTRY(PTR,TYPE,MEMBER)NS_GET_CONTAINER(PTR,TYPE,MEMBER)
将pNode指针的值减去lstNode这个成员在SNsNonsense结构中的偏移量(上面的例子中这个偏移量是8)得到的指针,这个指针强制转换为SNsNonsense*就是指向这个链表节点所在对象的指针。
红黑树是一颗二叉搜索树,它在传统的二叉搜索树的基础上增加了一些规则使得它更适合于数据的检索。红黑树有一个重要的性质:一颗有n个内部节点的红黑树它的树高h满足:h<=2log(n+1)。由于这个性质使得红黑树的插入、删除与搜索的时间复杂度均为l og(n)。
本发明所使用的红黑树的数据结构很类似于前面提到的list node,只不过它最终把对象以二叉树的形式链接起来(而非一个线性的链表)。这样的数据结构可以极大地提高排序数据的插入,删除和搜索速度。
本发明所提供的方法依赖于Bezier模块的设计,该模块中有两个最重要的数据结构:SNsEndPoint和SNsSpline。
SNsEndPoint用于描述曲线中的端点,它主要有如下内容:
1)记录了端点的位置,选中状态等等信息。
2)记录了端点的前后两个控制点的信息(有时可能只有一个控制点)。
3)它还有两个指向SNsSpline结构的指针,分别指向该端点的前后两个子曲线的数据结构。
4)每个端点都会被链接入一个端点的顺序链表中,端点在这个链表中的顺序决定了端点在整个Bezier曲线中的顺序。
5)每个被选中的端点还会被链接入一个所有被选中的端点的链表。
6)每个端点还会被链接入一个排序的链表中和一颗红黑树中,这个排序的链表对端点以其x轴坐标的数值进行从小到大的排序,在红黑树中也同样以x坐标排序。
对于每一个新的端点,它被插入到上面提到的排序链表和红黑树中的时间复杂度为log(n)。这是因为首先这个端点被插入到红黑树中的时间复杂度为log(n),当确定了这个端点在红黑树中位置后,在红黑树中找到x轴坐标大小顺序上该端点的下一个端点的时间复杂度也是log(n)(因为这步操作也是对红黑树进行搜索的过程),当找到了“下一个端点”后,直接将新的端点在排序的链表中链接在“下一个端点”的前面即可(注意,这个链接的过程是不需要再进行搜索的,因为所使用的链表都是nodelist,数据结构本身就决定了它在链表中的位置)。因此总的时间复杂度仍为log(n)。
之所以要将一个端点同时链接到x轴坐标的排序链表和红黑树中是为了在使用者调用框选端点函数时缩小搜索范围。比如使用者想要框选横坐标范围在[x0,x1],纵坐标范围在[y0,y1]内的矩形区域内的所有端点,那么实现该功能的函数会通过x轴坐标的红黑树定位满足x≥x0中x值最小的那个端点(算法复杂度log(n)),然后再通过红黑树定位满足x≤x1中x值最大的那个端点(算法复杂度log(n)),这时再通过x轴坐标的排序链表遍历这两个端点之间的所有端点,选中其中纵坐标范围在[y0,y1]的那些端点即可。
SNsSpline数据结构用于描述Bezier子曲线(后面简称子曲线)。它主要有如下内容:
1)两个指向SNsEndPoint型数据结构的指针。分别指向这个子曲线的起始端点和结束端点的数据结构。
2)记录子曲线的矩形边界的边界信息(上下左右四个边界)。
3)每个子曲线会被链接入四个排序链表和四颗红黑树中,它们分别是:分别以上下左右四个边界的数值排序的排序链表;分别以上下左右四个边界的数值排序的红黑树。
4)子曲线的一阶导函数与二阶导函数的系数。
5)子曲线的驻点与拐点的位置。
之所以要引入SNsSpline这个数据结构是为了在查询某点是否在曲线上或端点/控制点上时提高速度。如果在子曲线上或者在某子曲线的端点/控制点上那么它必然要在该曲线的边界矩形之内,所以首先要找到矩形区域包含要查询的点的子曲线(可能不只一条),然后再检查该点是否真的在某个子曲线上或端点/控制点上(关于检查的方法后面会介绍)。为了找到这些子曲线,最简单的方法就是对所有的子曲线的数据结构进行遍历,逐一判断每条子曲线的边界矩形是否包含要查询的点。这种方法虽然简单,但是效率不高,因为在最坏情况下可能要将所有的子曲线遍历一遍才能找到答案。本发明所采用的方法简单解释如下:
假如使用者要查询点(x0,y0)是否位于某条子曲线上或端点/控制点上,而且x0比较接近于整个Bezier矩形边界的右边界,而y0接近于整个Bezier矩形边界的上下边界的中心点。那么在这种情况下,首先利用以子曲线的右边界排序的红黑树找到所有右边界坐标≥x0中最小的那个子曲线(算法复杂度为log(n)),注意所有右边界<x0的子曲线是不可能包含(x0,y0)点的,因此找到那条子曲线后,沿着子曲线右边界的那条排序链表向右边界数值增大的方向遍历子曲线,并判断(x0,y0)是否位于某条子曲线上或端点/控制点上。
下面介绍一下如何判断给定点(x0,y0)是否位于某条子曲线上或端点/控制点上。判断给定点是否于某条子端点/控制点上很简单,只要检查该点与这条子曲线中的端点/控制点的距离是否在一个给定的误差范围内即可。但判断给定点是否位于某条子曲线上则复杂要复杂一些,大致思路是这样的:首先求出一条值为y0的水平线或者值为x0的垂直线与子曲线的交点(注意三次Bezier曲线最多可能有三个交点),然后判断这些交点与(x0,y0)的距离是否在一个给定的误差范围内即可。这里最复杂的操作是求交点操作,下面简要介绍一下。
一条二维的子曲线可以用参数方程表达为x=x(t),y=y(t),t∈[0,1],求交相当于求出x(t)=x0或者y(t)=y0的根t。求根的操作使用牛顿迭代法,牛顿迭代法对于初始值的要求是比较严格的,有如下定理:
定理:设f(x)在[a,b]上满足下列条件:
(1)f(a)·f(b)<0;
(2)f’(x)≠0;
(3)f″(x)存在且不变号;
(4)取x0∈[a,b],使得f″(x)·f(x0)>0
则牛顿迭代序列收敛于f(x)在[a,b]上的唯一根x*。
由于已经将每一条子曲线在[0,1]范围内的驻点与拐点求了出来,那么在求根之前,先把[0,1]区间根据这些驻点与拐点划分为若干区间,接下来则分别在这些区间之内利用牛顿迭代法求根。假设某个区间为[a,b],方程为f(x)=0,求根的大致步骤如下:
1)判断f(a)*f(b)<0是否为真,如果不是则进行下一个区间的处理;如果是则进行下一步处理。注意:有可能a或者b就是方程的根,这种情况要特殊处理,但在这里就不赘述,有兴趣可以查看NsBezier.cpp中CNsBezier::CalcRoots的求根具体实现。
2)取初值x0为(1-p)*a+p*b,其中p=f(a)/(f(a)+f(b))。这样取初值是为了尽可能让初值接近于根。注意,此时前面定理的(1),(2),(3)项都已经满足,但(4)不一定满足。
3)判断f″(x)·f(x0)>0是否成立。如果是,则进行牛顿迭代直到迭代结束;如果不是则进行下一步处理。
4)如果f″(x)·f(a)>0,则取x0=(a+x0)/2,跳转到(3);否则取x0=(x0+b)/2,跳转到(3)。注意:鉴于前面的子区间的划分原则,f″(x)必然不等于0,而且f″(x)在[a,b]区间内符号不变,因此判断f″(x)·f(a)>0只须一次就可以。
本发明所提供的任意区域的色彩校正特技调节系统的结构组成如图1所示,包括以下几个模块:
1、Bezier曲线数据模块,该Bezier曲线模块负责描述一条由若干段3次Bezier子曲线连接而成的曲线,其主要的功能如下:
1)产生Bezier曲线及曲线组;
2)从曲线组中得到曲线,或向曲线组中加入新的曲线;
3)修改曲线,包括添加点、封闭曲线、修改各个点、删除点等等;
4)查询某点是否位于某端点、控制点或者子曲线上;
5)对某些控制点进行选中或者取消选中操作;
6)求取曲线与某条水平直线的交点,得到特技渲染区域。
2、曲线绘制模块,该模块主要是提供操作界面及按钮,调用Bezier曲线数据模块产生曲线、绘制曲线以及精确修改曲线,其主要功能如下:
1)调出与隐藏曲线区域绘制界面;
2)提供操作工具,用户可以准确的生成、细致的修改曲线,达到所见即所得的操作效果;
3)提供定制椭圆、矩形的功能;
4)选择曲线绘制及控制形式,包括平滑曲线、折线、控制点直线连动、控制点独立等;
5)从曲线区域数据处理模块中读取曲线实例,或者将曲线保存到曲线区域数据处理模块;
3、曲线区域数据处理模块,主要有以下功能:
1)保存及读取曲线实例;
2)对关键帧上的曲线进行直线插值,求取非关键帧上曲线区域的位置。
4、特技设置接口模块,主要功能是传递曲线组给渲染模块。
5、色彩校正特技模块,用于将任意特技区域作为其参数封装起来。
6、特技渲染模块,用于在接到特技设置接口模块传下来曲线区域后在区域内渲染特技。
本发明所述的方法并不限于具体实施方式中所述的实施例,本领域技术人员根据本发明的技术方案得出其他的实施方式,同样属于本发明的技术创新范围。