Hoodlum1980 (fafa)'s Technological Blog

Languages mainly using and digging: C / CPP, ASM, C#, Python. Other languages:Java.

博客园 首页 新随笔 联系 订阅 管理

            在上一篇文章里,我们讲解了为滤镜添加术语资源,从而使我们的滤镜可以被PS的scripting system感知和描述,这样即友好支持了PS的“动作”面板。在这一篇文章中,我们将对此前的DEMO进行进一步的细化,例如在参数对话框上增加实时预览的小缩略图等。对话框的引入主要是给用户一个机会和接口,设置或调节滤镜使用的图像处理算法。通常作为UI的友好性,在对话框上应该提供预览图,这样可以直观的把参数对结果产生的影响反馈给用户,指导他们调整参数。而不是要用户必须反复执行滤镜命令才能看到效果然后去调节参数。

            此前我觉得“添加缩略图”这样的功能应该不是很困难,但当我尝试这样去做,我很快发现它的难度远远超过了以往我写的文章中的讲解。因为当我们尝试使用PS提供的回调函数去显示缩略图时,我们必须对PS提供的接口细节完全清楚,包括影响缩放的参数设置,数据分布,扫描行等细节。不能够有一分一毫的差错,否则我们就可能看到不正常的显示,甚至会不小心的使内存越界。

            在引入缩略图之前,我先对滤镜算法做一些有趣的改进,进行了一点增强,使它更加实用化。

 

            (1)引入“像素随机抖动”参数和算法。

             此前我们设置像素时,输入和输出的位置本来是完全一致的,即 Dest(i, j) = f (Src(i, j))。

            现在我们考虑对上式进行一点改动,把源像素进行随机抖动,即Dest(i, j) = f (Src(i+dx, j+dy))。

            我们设置抖动距离为distance(像素)参数,这样我们取源像素时,在以当前像素为中心向外围扩展distance的正方形内随机选取某个点作为源像素。从而使他们在结果图中具有一种“溶解”或“腐蚀”效果。如下图所示:

            

            因此我们在滤镜参数中增加了distance参数,表示上图中的随机抖动距离。这样当我们设置位于 (i,j)位置的像素时,我们取的源像素坐标是:

            x = i + rand()%(2*distance+1) - distance;

            y = j + rand()%(2*distance+1) - distance;

            在实际处理时,我们还需要考虑上述结果x,y可能会超出有效数据边界,因此需要把x,y限定在 filterRect 内部。

            而由于我们采用的是贴片(Tile)处理方法,因此我们的调整需要一点技巧性。我们将改动我们向PS请求的inRect,即每次贴片时,outRect依然保持和此前一致,而把 inRect 尝试向四周扩张distance像素距离,这样可以保证我们每次贴片时都能拿到有效数据(当贴片位于filterRect内部时),除非贴片位于filterRect的边缘。

            必须注意的是,由于 inRect 比 outRect 要“大一圈”,所以这时候两个Rect的像素已经不是大小一致完全重叠关系了,而是有一定偏移的!在代码中我们必须考虑两个矩形之间的偏移关系。这里可以参考我的源代码,就不详细讲解该处理方法了。

 

            (2)为对话框增加“缩略图”(Proxy)。

            我们增加了一个参数,然后在对话框左侧空出一个较小区域用于显示缩略图,为了方便,我在缩略图位置放置了一个隐藏的STATIC 控件(Proxy Banner),它的主要用处是使我可以在运行时获取到 “缩略图”的边界(客户区坐标)。修改后的对话框如下图所示:

            

 

            (2.1) displayPixles 回调函数 和 PSPixelMap 结构;

             在显示缩略图时,我们使用的是 gFilterRecord 中的 displayPixels 回调函数,这个函数的原型如下(函数指针的typedef):

             typedef MACPASCAL OSErr (*DisplayPixelsProc) (

                    const PSPixelMap *source,
                    const VRect *srcRect,
                    int32 dstRow,
                    int32 dstCol,
                    void *platformContext);

 

            第一个参数是一个 PSPixelMap 结构体的指针用以描述一块像素数据区,它相当于BitBlt中的源图,srcRect参数描述的是源图矩形;

            dstRow 和 dstCol 描述的是目标区域的“目标行”,“目标列”坐标,请注意其逻辑意义与我们通常使用的参数的区别。这里dstRow相当于 destY, dstCol就相当于 destX 参数,即(dstCol, dstRow) 是在目标区域中的起始坐标,这一点需要注意。

            最后一个参数 platformContext 在 windows系统中也就是 HDC 。

            其中第一个参数我们还需要再做简单介绍,即 PSPixelMap 的定义和 PS 对该数据区的分布要求。PSPixelMap的定义如下:

typedef struct PSPixelMap
{
    int32 version;
    VRect bounds;
    int32 imageMode;
    int32 rowBytes;
    int32 colBytes;
    int32 planeBytes;
    
void *baseAddr;
    
    
//---------------------------------------------------------------------------
    
// Fields new in version 1:
    
//---------------------------------------------------------------------------    
    PSPixelMask *mat;
    PSPixelMask 
*masks;
    
    
// Use to set the phase of the checkerboard:
    int32 maskPhaseRow;
    int32 maskPhaseCol;

    
//---------------------------------------------------------------------------
    
// Fields new in version 2:
    
//---------------------------------------------------------------------------    
    PSPixelOverlay *pixelOverlays;
    unsigned32 colorManagementOptions;

} PSPixelMap;

 

            ◆ version: 结构体版本。

            对于PS CS版本来说,它要求我们把它设置为1。PS的未来版本可能会扩展它并提升此版本号。

            ◆ bounds:像素数据所占据的矩形。

            ◆ imageMode:数据区的图像模式。

            它支持以下模式: grayscale, RGB, CMYK, Lab。

            ◆ rowBytes: 相邻行之间的字节数距离。

            相当于扫描行宽度(重要),以字节为单位,必须设置正确。

            ◆ colBytes: 相邻列像素数据的字节数距离。

            由于数据是“集中分布”的,所以这个属性的值主要取决于像素的色深度。对于每个通道每个像素使用一个字节的普通24位深度的图像来说,这个距离为1byte。 

            ◆ planeBytes: 相邻通道的字节数距离。

            由于数据是“集中分布”的,所以这个属性通常是 rowBytes * 图像高度。

            ◆ baseAddr:数据区起始地址。

            我曾在此前的文章中讲过,PS提供给我们的 inData 和 outData 是通道交叉分布的(interleave)。而这里PSPixelMap中的数据的分布要求则不同,它要求数据是通道集中分布的。

            例如对于RGB图像来说, 当我们请求所有通道时,inData/outData中的数据分布是:

            R | G | B | R | G | B | R | G | B | ..... (interleave)

            而对于 PSPixelMap 中的数据来说,则要求按如下分布:

            R | R | R | .... G | G | G | ... B | B | B .... (集中分布)

            也就是说对inData 和 outData, 所有通道数据交叉分布,同一个通道(plane)的数据在数据区中是跳跃式存在的。

            而对于 PSPixelMap 来说,同一个通道(plane)的数据是集中在一起的,先列出所有第一个通道数据,再列出所有第二个通道数据,等等。       

            同时我们进行像素定位和预测缓冲区大小时,和普通的Bitmap像素定位一样,必须使用滤镜参数中的 inRowBytes 属性(相当于扫描行宽度)进行在“行”间定位,而不能假设或自行计算“行宽度”。

           【注意】我们必须清楚数据分布的细节,这样才能正确定位到指定位置的像素。

 

            (2.2)控制缩放:FilterRecord中的 inputRate 和 inputPadding 属性;

            由于PS处理的图像大小是多种多样的,因此我们显示缩略图时必然面对的一个问题是缩放问题。大多数情况下,由于缩略图只是定性展示,对数据精确性要求可以有所降低,并且考虑到性能因素,因此一般缩略图的尺寸可以设置的较小,当原图(filterRect)比缩略图(bannerRect)大时,我们希望图像缩小显示在缩略图上。而当原图比缩略图小时,我们就采用实际原图大小(即缩放因子=1)即可。因此现在我们需要了解如何把原图缩小到缩略图大小。在GDI中我们知道我们可以使用 StretchBlt 函数来完成缩放的。而在这里我们获取源图数据是从PS传递给我们的inData得到的,当我们希望得到缩小的原图时,我们即通过设置 FilterRecord 参数中的 inputRate (采样率)属性来完成缩小。

 

            (2.2.1)Fixed inputRate;

            ==================================================

            【关于 Fixed 类型】

            Fixed在PS中被定义为 long 类型:

            typedef long Fixed;

            但是Fixed在PS中的实际意义是一个(定点)小数。所谓Fixed是相对 float(浮点小数)来说的。在float中小数点的位置是不固定的,因此称为浮动点。而 Fixed 则把一个小数拆解为整数部分和小数部分,分别存储到一个高16位和低16位。即其含义是 "16.16"。

            例如假设有一个小数是3.00f;则相应的Fixed数是 0x00030000;

            从浮点数转换成 Fixed 类型的方法是:

    

            double  _factor;

             Fixed    _fixed = ( Fixed ) ( _factor * 0x10000 ) ;

 

 

    从 Fixed 类型转换成浮点类型的方法是 ( 备注:1 / 0x10000 = 0.0000152587890625 ) :

 

 

    Fixed   _fixed;

    double  _factor = _fixed * 0.0000152587890625 ;

 

            ==================================================

    【补充】

           根据我对 Photoshop CS 的观察,当设置 inputRate 时,PS 会把这个值进行了取整处理。也就是说,如果 inputRate < 1.0, 则 inputRate = 1.0; 相当于 Photoshop 对插件设置的 inputRate 做了如下处理:

int nInputRate = (pFilterRecord->inputRate + 0x8000) >> 16;
if(nInputRate < 1) nInputRate = 1;

           --hoodlum1980, on 2017-6-22。

            ==================================================

            inputRate表示采样率,在逻辑意义上是一个小数,我们通过设置它的值来实现得到的inData是对原图的缩放结果。在默认情况下,PS设置的inputRate就是1,也就没有任何缩放。我们在获取缩略图数据时,需要计算缩放因子(factor),然后把inputRate设置为factor(注意数据类型的转换方法)。

            这样设置后,我们得到的 inData 的实际图像坐标将是 inRect * inputRate,即

            (inRect.left * inputRate, inRect.top* inputRate, 

                        inRect.right * inputRate, inRect.bottom * inputRate)

            例如,当 inputRate 为 2.0 时,则每两个像素采样一个点,则 inRect, inData 的关系如下图所示:

              

            在上面这幅图中,展示了在缩放时应该如何设置inRect,请注意为了获取我们需要的矩形,我们需要把我们希望的inRect(粉红色矩形区域)的坐标除以 inputRate(图中假设inputRate = 2),才是需要提交给PS的 inRect(上图中的蓝色矩形)。然后我们使用advanceProc回调函数,即可得到 inData 为上图中右侧的图像数据,可见它的尺寸在两个方向上都缩小了一半。

           【注意1】在处理完毕缩略图并关闭对话框时,必须把inputRate恢复为 0x00010000 (1.0)。否则它将会继续影响后续的实际处理中的 inData!使处理结果产生意料外的结果。

           【注意2】滤镜参数必须考虑图像缩放所带来的影响。和缩放有关的参数也要相应的映射到缩略图尺寸上(例如本例中的随机抖动距离要同比例缩小)。和缩放无关的参数(例如本例中的不透明度百分比,填充色)可不考虑缩放影响。

 

            (2.2.2)int16 inputPadding;

            当PS提供 inData 时,它可以被补齐。可以指定补齐的像素的值(0~255),也可以设置为以下选项(它们被定义为负数,以和用户设置像素值区别):

 

            plugInWantsEdgeReplication: 复制边缘像素。

            plugInDoesNotWantPadding:随机值(不确定的值)。

            plugInWantsErrorOnBoundsException:(默认值)请求边界外数据时标记一个错误。

 

            当请求区域超出边界,PS将使用上述选项设置 inData 的数据。

 

            (2.2.3)显示缩略图。

            为了显示缩略图,我们需要请求PS为我们分配缓冲区。我们首先需要预测我们的缓冲区的大小,并在Prepare调用时通知 PS 我们的需求。

            考虑到当用户在对话框上进行参数调整时,我们应该实时的更新缩略图显示,以反馈当前参数效果。所以我们需要两份缩略图数据,一份是缩略图的原始数据,它作为算法的输入,在创建对话框时获取到源图数据,然后在整个对话框生命期间保持数据不会改变。另一份是我们用于处理 WM_PAINT 消息时使用的绘制数据,即可以实时改变的缩略图实际显示数据。

            因此我们评估缩略图的尺寸,然后使用以下估计值:

 

            bufferSize = 缩略图最大宽度 * 缩略图最大高度 * 通道数 * 2;

 

            在 Prepare 调用期间,我们把这个值(bufferSize)设置到 FilterRecord 的 bufferSpace 和 maxSpace 属性中,这表示我们(PlugIn)和PS(Host)进行内存需求“协商”,使 PS 了解到我们预期的内存开销,然后尝试准备足够内存以供我们后续的申请。

 

            真正显示对话框是在 start 调用中,我们在对话框的初始化消息时准备请PS为我们申请缓冲区。基本方式如下:

 

//获取 buffer 回调函数集指针
BufferProcs *bufferProcs = gFilterRecord->bufferProcs;

//请PS为我们申请内存
bufferProcs->allocateProc(bufferSize, &m_ProxyData.bufferId0);


//请PS为我们锁定内存(禁止内存整理)
//[ 1 ]函数返回被锁定的内存起始地址。
//[ 2 ]第二个参数是BOOL moveHigth,对windows平台将被忽略。
m_ProxyData.data0 = bufferProcs->lockProc(m_ProxyData.bufferId0, TRUE);

//=============================
//  这里是处理和更新缓冲区的期间
//=============================

//使用结束后,释放和解锁缓冲区。
//解锁
gFilterRecord->bufferProcs->unlockProc(m_ProxyData.bufferId0);
//释放内存
gFilterRecord->bufferProcs->freeProc(m_ProxyData.bufferId0);

 

            我们使用 lockProc 锁定缓冲区这块内存,主要是防止操作系统在我们处理数据期间进行内存整理,从而破坏缓冲区资源。

            【注意】这里加锁和解锁使用的是“引用计数”机制,即解锁次数 必须 匹配加锁次数才能使缓冲区真正得到解锁。

 

            为了显示缩略图,并能够实时反馈用户的调节,我们准备了下面的四个函数(其中CreateProxyBuffer 和 UpdateProxy 难度最大):

            ● CreateProxyBuffer

            计算缩略图实际大小和缩放因子,委托PS为我们申请缓冲区,同时也初始化了原始数据(即把inData拷贝到PsPixelMap中),在处理 WM_INITDIALOG 时调用。

            ● UpdateProxy

            当用户在对话框上修改了某个参数时(WM_COMMAND)被调用,用于更新缩略图显示数据,并刷新缩略图显示。会引起对 PaintProxy 函数的间接调用。

            ● PaintProxy

            绘制缩略图,通过 displayPixels 回调函数完成,在处理 WM_PAINT 消息时调用。

            ● DeleteProxyBuffer

            释放我们申请的缓冲区,在对话框退出前(WM_DESTROY)调用。

 

            现在总结一下上面四个函数的调用时机,使我们对这四个函数的分工具有一个明确的认识,如下表:

            

窗口消息

事件

被调用的函数

说明

WM_INITDIALOG

创建对话框

CreateProxyBuffer

申请缩略图缓冲区并初始化

WM_COMMAND

修改参数值

UpdateProxy

更新缩略图,将间接调用PainProxy

WM_PAINT

窗口绘制

PaintProxy

绘制缩略图

WM_DESTROY

退出对话框

DeleteProxyBuffer

释放缩略图缓冲区

 

            【注意】把 inData 拷贝到 PSPixelMap, 是一个难度很大,并且特别需要注意的地方。两块数据的通道数据的分布不同,因此像素定位方式也完全不同。并且涉及到缓冲区大小的计算和申请。 复制缓冲区时是使用指针进行访问的,而这非常容易因为引发错误(将导致PS进程崩溃)。

            在CreateProxyBuffer中,我们的主要任务是分配缓冲区,然后把源图数据(inData)相应的拷贝到我们的缓冲区(绘制时设置给PSPixelMap结构)。由于这是一个有难度的地方,因此我特别把这个函数代码放在此处展示,代码如下:

 

Code_CreateProxyBuffer

 

            在上面的函数(CreateProxyBuffer)中,我们首先按照下面的方法计算出缩略图的缩放因子:

 

            factor = ceiling (max(原图宽度 / 缩略图宽度, 原图高度 / 缩略图高度));

 

            然后我们计算了缩略图的起始点坐标(m_ProxyData.left, m_ProxyData.top)和采用上述缩放因子后的缩略图实际尺寸(m_ProxyData.width, m_ProxyData.height)。请注意,我们把 factor 向上取整(ceiling),这会使缩略图的实际尺寸是小于等于其 BANNER 尺寸的。通过设置左上角坐标,我们使缩略图的位置在 BANNER 矩形中居中。 

            然后我们委托 PS 为我们分配两块同样大小的缓冲区 data0 和 data1(一个原图数据拷贝,一个是用于即时显示)并锁定它们。我们使用了PS提供的 advanceState 回调去请求原图数据,我在此前的文章中已经介绍过这个最重要的回调函数之一,它的作用是请求 Photoshop 立即更新滤镜参数(FilterRecord)结构中的相关数据,包括inData,outData等等。请注意在上面的代码中,我们是逐个通道进行复制的,即我们每次请求PS为我们发送一个通道的数据,然后我们把这批数据一次性的完全拷贝到缓冲区(使用memcpy),这样就完成了通道数据的“集中分布”。其中每个通道字节数(planeBytes)计算方法如下:

 

            每个通道字节数(planeBytes) =  单一通道的扫描行宽度(inRowBytes) * 缩略图的图像高度(inRect高度);

            

            我们把缩略图数据的信息并保存在m_ProxyData参数中。在 PaintProxy 中,我们只需要把这些信息再设置并提交给 displayProxy 回调函数即可。显示缩略图(PaintProxy,UpdateProxy)的主要逻辑和代码原理,限于篇幅这里不详细讲述,可参考附件中的源代码。最后我们可以看下滤镜的对话框运行效果如下:

 

            

 

            当在上面的滤镜对话框中使用鼠标拖动或者键盘改变文本框数值时,左侧缩略图将会实时更新以反应当前的参数效果。在参数设置对话框中,我模拟了一个Photoshop中常见的UI特性,当你把鼠标悬停在数值文本框的左侧标签上时,光标变为一个拖动箭头的形状,这时按下鼠标,左右拖动,可以看到相应文本框的数据发生变化(这和操作滑杆控件非常类似)。在上面这个对话框中,你能够看到我如何模拟了PS的这种UI效果(在Photoshop看似朴素的外表下,隐藏着非常多让人惊叹的 UI 效果,而这只是它们中的其中一个,向强大的Photoshop致敬!)。

            

            (3)增加一个我们自己定义的“关于对话框”。

            在此前为了简单起见,在“关于”中我仅仅弹出了一个MessageBox。我们可以自定义一个关于对话框,同样这里我吸取了 PS 的关于对话框的建议和风格,即没有标题栏,没有任何按钮,对话框初始位置在其父窗口的中等偏上(上1/3)处。用户按Escape,回车键 或用鼠标点击任何位置即退出对话框。我的滤镜的关于对话框如下(在PS中点击菜单:帮助 -> 关于增效工具 -> FillRed Filter... ):

            

 

            这是一个普通的对话框,但我主要想介绍是当鼠标移动到我的博客的网址上时,光标变成(IDC_HAND)手形,点击即可使用默认浏览器打开网址。它是用过使用PS的回调函数集中的相应函数来完成的。因此这里我将示范 PS callback suites 的一种标准用法:

 

Code

 

            在上面的代码中我们可以看到, PS CALLBACK Suites的用法 和 COM 组件的 QueryInterface 的使用方法是完全类似的:先声明想获取的回调函数集(callback Suite,一个含有一组PS内部的函数指针的struct)的一个指针,然后把该指针的地址传递给 BasicSuite 的 AcquireSuite 函数,成功以后我们就可以通过该指针去调用PS提供给插件的相应回调函数。

 

            (4)总结。   

            到目前为止,我们已经完整的讲解了有关制作一个Photoshop滤镜的主要技术环节,从(1)创建项目,到(2)添加UI资源,再到(3)使Photoshop Scripting System知道我们的滤镜,并支持“动作”面板的对话框选项,以及本篇重点讲述的添加在对话框上的缩略图。涵盖了制作 Photoshop 滤镜插件的流程和重要知识,而Photoshop插件开发的技术细节以及插件种类仍然是非常繁复众多的,有待进一步的研究。

            我们开发Photoshop插件的一个主要原因是,PS是图形处理领域的重要软件,为第三方开放了插件扩展的接口。作为第三方开发者我们可以根据自己的需求,遵照PS的约定去以插件形式扩展PS。在PS的重要用户基础上,扩展和研究将会更有实际意义。

            制作滤镜的基本技术已经介绍完成,剩下的其他工作将主要是对图像处理算法的寻求和发掘。

            本例是以使用基于Platform SDK的Windows程序开发为基础的,但重点在于讲解PS插件开发,因此没有详细讲解Windows程序开发中的一些技术细节。

 

            (5)最后是源代码(增量更新)的下载链接:

            https://files.cnblogs.com/hoodlum1980/FillRed.rar

 

            (6)我的相关文章:

            《怎样编写一个Photoshop滤镜(1)》

            《怎样编写一个Photoshop滤镜(2)》

            《怎样编写一个Photoshop滤镜(3) -- Scriping PlugIn》

 

            【备注】有网友反应范例中的编译结果在PS CS2下无法加载,我发现编译好的滤镜文件仅有60KB左右(这样小感觉不太正常),而正常应该有至少有几百KB大小才对,可能是因为某些依赖未能静态链接。因此这就会导致这个DLL在我的机器环境上能够正常加载,但换个环境可能就会导致缺少依赖而无法加载。

            我把项目中的ATL选项设置为“静态链接到ATL”后,编译结果就成了几百KB。使用我同事的机器环境(安装了PS CS2版本)作为测试,这时果然能够正常被PS CS2加载到了。

 

 

posted on 2009-05-23 15:59  hoodlum1980  阅读(2318)  评论(1编辑  收藏  举报