>  기사  >  웹 프론트엔드  >  포토샵 유화 효과 필터

포토샵 유화 효과 필터

高洛峰
高洛峰원래의
2017-02-18 13:33:162418검색

이 필터는 제가 PS SDK를 사용하여 개발했습니다. 필터 알고리즘을 제안한 사람이 누구인지는 알 수 없습니다. 이 알고리즘의 주요 참조 소스는 이 프로젝트입니다. , Jason Waltman 작성(2001년 4월 18일). 또한 C# 언어로 작성된 또 다른 국산 소프트웨어인 PhotoSprite(버전 3.0, 2006, Lian Jun 작성)의 유화 필터 알고리즘도 전자(또는 다른 동종 코드)에서 인용되어야 합니다. 본 글의 알고리즘에 대한 개념적 설명은 필자의 이해와 해석에 속한다. 하지만 이 알고리즘의 효율성은 높지 않습니다. 템플릿 크기에 관한 시간 복잡도가 O(n^2)에서 선형 복잡도(n)로 개선되었으며, 동일한 테스트 샘플(특정 1920 * 1200 픽셀 RGB 이미지)의 경우, 동일한 매개변수의 처리 속도가 약 35초에서 약 3초로 감소하며, 처리 속도는 일정합니다. 속도는 약 10~12배(대략적인 추정)로 증가합니다.

이 글은 포토샵 유화효과 필터(OilPaint)를 주로 게재하고 있습니다. 알고리즘은 제가 제안한 것이 아닙니다. 이 기사의 참고 자료를 참조하세요. 이 필터는 C#으로 개발된 국내 소프트웨어 PhotoSprite에서 볼 수 있다. 저는 2010년에 이 필터를 개발하는 데 도움을 달라는 요청을 받았으며, 지금은 며칠 동안 이를 개발하고 무료로 제공하고 있습니다.

(1) 유화 필터 알고리즘의 개념적 설명

이것은 FilterExplorer 소스 코드를 읽고 이해한 내용입니다. 이 필터에는 두 개의 매개변수가 있습니다. 하나는 템플릿 반경(반경)이고, 템플릿 크기는 (반경 * 2 + 1) * (반경 * 2 + 1) 크기입니다. 즉, 현재 픽셀을 중심으로 바깥쪽으로 확장됩니다. 반경별 픽셀의 직사각형 영역을 검색 범위로 일시적으로 "템플릿"이라고 부릅니다. (실제로 이 알고리즘은 가우시안 블러나 사용자 정의 필터와 같은 표준 템플릿 방법이 아니며 처리 과정이 유사할 뿐입니다. , 나중에 소개할 최적화를 구현할 수 있습니다.

또 다른 매개변수는 부드러움인데, 이는 실제로 회색조 버킷의 수입니다. 픽셀(0 ~ 255)의 회색조/밝기가 평활도 간격으로 균일하게 나누어진다고 가정하고 여기서는 각 간격을 버킷이라고 부르므로 일시적으로 버킷 배열(버킷)이라고 하는 많은 버킷이 있습니다.

이 알고리즘은 이미지의 각 픽셀을 순회하며 현재 위치(x, y) 픽셀에 대해 템플릿 범위 내의 모든 픽셀을 회색조, 즉 이미지를 회색조 이미지로 바꾼 다음 픽셀 값 추가적인 이산화는 픽셀의 회색 레벨이 떨어지는 간격에 따라 템플릿의 픽셀을 차례로 해당 버킷에 넣는 것입니다. 그런 다음 그 안에 들어가는 픽셀 수가 가장 많은 버킷을 찾고 해당 버킷에 있는 모든 픽셀의 색상을 평균하여 위치 (x, y)의 결과 값으로 구합니다.

위의 알고리즘 설명은 다음의 개략도로 표현됩니다. 가운데 이미지는 그레이스케일 + 원본 이미지의 이산화 결과(Photoshop의 톤 분리와 동일)이며, 작은 상자는 템플릿을 나타냅니다. 아래는 버킷 배열(8개의 버킷, 즉 0부터 255까지의 회색값을 8개의 간격으로 구분한 것)입니다.

 Photoshop 油画效果滤镜

  (2) 외국인 기존 코드의 효율성 제고

기존 코드를 PS 필터에 그대로 이식하는 것은 어렵지 않습니다. 기본적으로 디버깅에 성공하려면 1~2일 정도 여유 시간이 필요했습니다. 하지만 외국인의 소스코드를 읽어보니 원본 코드가 충분히 효율적이지 않다는 느낌이 확연히 느껴졌습니다. 이 알고리즘은 이미지를 한 번 순회함으로써 완성될 수 있으며, 각 픽셀의 처리는 일정한 시간이므로 픽셀 수(이미지 길이 * 이미지 너비)에 대한 복잡도는 O(n)이지만 원본 코드의 상수 계수는 예를 들어, 픽셀 결과를 계산할 때마다 템플릿 범위 내 픽셀의 회색조를 다시 계산하여 버킷에 넣어야 하므로 실제로 많은 반복 계산이 발생합니다.

 2.1 이를 위해 첫 번째 개선 사항은 PS에서 현재 전체 이미지 패치를 그레이스케일하고 이산화하여(버킷에 넣음) 템플릿을 사용할 때 패치를 순회할 때 , 그레이스케일을 반복적으로 계산하고 이산화할 필요가 없습니다. 이는 알고리즘의 실행 속도를 대략 두 배로 늘립니다(특정 샘플의 경우 처리 속도가 20초 이상에서 약 10초로 증가합니다).

 2.2 하지만 아직까지 속도 향상 효과는 미미합니다. 그래서 저는 템플릿 크기의 복잡성을 정사각형에서 선형 복잡성으로 줄이는 또 다른 더 중요한 최적화를 수행했습니다. 이는 템플릿이 현재 행 사이에서 왼쪽에서 오른쪽으로 이동한다는 점을 고려하면 결과에서 템플릿 중간(인접한 두 템플릿의 교차점)에 있는 픽셀의 통계는 변경되지 않고 유지된다는 사실을 기반으로 합니다. 가장 왼쪽 열만 템플릿 밖으로 이동하고 가장 오른쪽 열은 템플릿에 들어가므로 이미지를 탐색할 때 템플릿 중간에 있는 픽셀에 대해 걱정할 필요가 없으며 이미지의 두 가장자리만 처리하면 됩니다. 주형. 아래와 같이(반경은 2, 템플릿 크기는 5 * 5픽셀):

 Photoshop 油画效果滤镜

패치의 오른쪽 가장자리에 도달하면 캐리지 리턴이나 라인 피드처럼 줄의 시작 부분으로 재설정하지 않고 이동합니다. 템플릿을 한 줄 아래로 내리고 다음 줄의 꼬리를 입력한 다음 왼쪽으로 이동하면 템플릿의 궤적이 구불구불한 계단 궤적이 됩니다. 이러한 개선 후에는 픽셀을 탐색할 때 템플릿의 두 가장자리 픽셀만 처리하면 됩니다. 이러한 방식으로 템플릿 크기(매개변수의 반경)가 O(n^2)에서 O(n)으로 줄어들어 알고리즘의 연산 속도가 크게 향상됩니다. Optimization 2.1과 결합되어 최종적으로 알고리즘의 연산 속도가 향상됩니다. (이 값은 대략적인 추정치일 뿐이며 많은 수의 샘플에서 테스트되지 않았습니다.) 대형 이미지에 대한 최적화된 알고리즘의 처리 시간도 허용됩니다.

[참고] 이러한 최적화를 이룰 수 있는 이유는 필터 알고리즘이 표준 템플릿 알고리즘이 아니기 때문입니다. 그 본질은 템플릿 범위 내에서 통계 정보를 얻는 것입니다. 결과는 픽셀의 템플릿 좌표와 아무 관련이 없습니다. 이는 마치 특정 지역의 인구 수, 남녀 비율 등의 정보를 얻고 싶은 것과 같습니다. 따라서 위의 방법에 따라 최적화합니다.

템플릿의 이동 궤적은 구불구불한 계단입니다. 예:

→ → → → → →

아래 필터의 핵심 알고리즘 코드, 모든 코드는 Algorithm.cpp에 있습니다:

code_FilterData_OilPaint

#include "Algorithm.h"

//=========================================
//    缩略图和实际处理共享的滤镜算法
//=========================================
//
// 默认把数据当作是RGB, GrayData 是单通道数据,矩形和 InRect 一致
//
// bInitGray: 是否需要算法重新计算灰度数据
// rowBytes: inData/outData, 扫描行宽度
// colBytes: inData/outData, 对于interleave分布,等于通道数,集中分布时该为1
// planeBytes: 每个通道的字节数(对于interleave分布,该参数的值为1)
// grayData: 由于仅一个通道,所以grayColumnBytes一定是1;
// buckets: 灰度桶; 每个灰度占据4个UINT,0-count,1-redSum,2-greenSum,3-blueSum
// abortProc: 用于测试是否取消的回调函数(在滤镜处理过程中,即测试用户是否按了Escape)
//              在缩略图中用于测试是否已经产生了后续的Trackbar拖动事件
// retVal:如果没有被打断,返回TRUE,否则返回FALSE(说明被用户取消或后续UI事件打断)
//

BOOL FilterData_OilPaint(
    uint8* pDataIn, Rect& inRect, int inRowBytes, int inColumnBytes, int inPlaneBytes,
    uint8* pDataOut, Rect& outRect, int outRowBytes, int outColumnBytes, int outPlaneBytes,
    uint8* pDataGray, int grayRowBytes, BOOL bInitGray,
int radius, 
int smoothness,
    UINT* buckets,
    TestAbortProc abortProc
    )
{
int indexIn, indexOut, indexGray, x, y, i, j, i2, j2, k;    //像素索引
     uint8 red, green, blue;

//设置边界
     int imaxOut = (outRect.right - outRect.left);
int jmaxOut = (outRect.bottom - outRect.top);
int imaxIn = (inRect.right - inRect.left);
int jmaxIn = (inRect.bottom - inRect.top);

//获取两个矩形(inRect和outRect)之间的偏移,即 outRect 左上角在 inRect 区中的坐标
     int x0 = outRect.left - inRect.left;
int y0 = outRect.top - inRect.top;

// 灰度离散化应该作为原子性操作,不应该分割
     if(bInitGray)
    {
//把 In 贴片灰度化并离散化
         double scale = smoothness /255.0;

for(j =0; j < jmaxIn; j++)
        {
for(i =0; i < imaxIn; i++)
            {
                indexIn = i * inColumnBytes + j * inRowBytes; //源像素[x, y]
                
                red = pDataIn[indexIn];
                green = pDataIn[indexIn + inPlaneBytes];
                blue = pDataIn[indexIn + inPlaneBytes*2];

                pDataGray[grayRowBytes * j + i] = (uint8)(GET_GRAY(red, green, blue) * scale);
            }
        }
    }

if(abortProc != NULL && abortProc())
return FALSE;

// 模板和统计数据
// 灰度桶 count, rSum, gSum, bSum
//
    memset(buckets, 0, (smoothness +1) *sizeof(UINT) *4);

int colLeave, colEnter, yMin, yMax;
int rowLeave, rowEnter, xMin, xMax;
int direction;

//初始化第一个模板位置的数据
     yMin = max(-y0,              -radius);
    yMax = min(-y0 + jmaxIn -1, radius);
    xMin = max(-x0,              -radius);
    xMax = min(-x0 + imaxIn -1, radius);

for(j2 = yMin; j2 <= yMax; j2++)
    {
for(i2 = xMin; i2 <= xMax; i2++)
        {
            indexIn = (j2 + y0) * inRowBytes + (i2 + x0) * inColumnBytes;
            indexGray = (j2 + y0) * grayRowBytes + (i2 + x0);

            buckets[ pDataGray[indexGray] *4 ]++; //count
            buckets[ pDataGray[indexGray] *4+1 ] += pDataIn[indexIn]; //redSum
            buckets[ pDataGray[indexGray] *4+2 ] += pDataIn[indexIn + inPlaneBytes]; //greenSum
            buckets[ pDataGray[indexGray] *4+3 ] += pDataIn[indexIn + inPlaneBytes*2]; //greenSum
        }
    }

if(abortProc != NULL && abortProc())
return FALSE;

//进入模板的蛇形迂回循环
    
for(j =0; j < jmaxOut; j++)
    {
if(abortProc != NULL && abortProc())
return FALSE;

//direction:水平移动方向( 1 - 向右移动; 0 - 向左移动)
         direction =1- (j &1);

//找到最大的那个像素
         GetMostFrequentColor(buckets, smoothness, &red, &green, &blue);
if(direction)
        {
            indexOut = j * outRowBytes;
        }
else
        {
            indexOut = j * outRowBytes + (imaxOut -1) * outColumnBytes;
        }
        pDataOut[ indexOut                     ] = red;  
        pDataOut[ indexOut + outPlaneBytes     ] = green;
        pDataOut[ indexOut + outPlaneBytes *2 ] = blue;

        i = direction?1 : (imaxOut -2);

for(k =1; k < imaxOut; k++) //k 是无意义的变量,仅为了在当前行中前进
         {
//每 64 个点测试一次用户取消 ( 在每行中间有一次测试 )
            if((k &0x3F) ==0x3F&& abortProc != NULL && abortProc())
            {
return FALSE;
            }

if(direction) //向右移动
              {
                colLeave = i - radius -1;
                colEnter = i + radius;
            }
else//向左移动
              {
                colLeave = i + radius +1;
                colEnter = i - radius;
            }

            yMin = max(-y0,              j - radius);
            yMax = min(-y0 + jmaxIn -1, j + radius);

//移出当前模板的那一列
              if((colLeave + x0) >=0&& (colLeave + x0) < imaxIn)
            {
for(j2 = yMin; j2 <= yMax; j2++)
                {
                    indexIn = (j2 + y0) * inRowBytes + (colLeave + x0) * inColumnBytes;
                    indexGray = (j2 + y0) * grayRowBytes + (colLeave + x0);

                    buckets[ pDataGray[indexGray] *4     ]--; //count
                    buckets[ pDataGray[indexGray] *4+1 ] -= pDataIn[indexIn]; //redSum
                    buckets[ pDataGray[indexGray] *4+2 ] -= pDataIn[indexIn + inPlaneBytes]; //greenSum
                    buckets[ pDataGray[indexGray] *4+3 ] -= pDataIn[indexIn + inPlaneBytes*2]; //greenSum
                }
            }

//进入当前模板的那一列
              if((colEnter + x0) >=0&& (colEnter + x0) < imaxIn)
            {
for(j2 = yMin; j2 <= yMax; j2++)
                {
                    indexIn = (j2 + y0) * inRowBytes + (colEnter + x0) * inColumnBytes;
                    indexGray = (j2 + y0) * grayRowBytes + (colEnter + x0);

                    buckets[ pDataGray[indexGray] *4     ]++; //count
                    buckets[ pDataGray[indexGray] *4+1 ] += pDataIn[indexIn]; //redSum
                    buckets[ pDataGray[indexGray] *4+2 ] += pDataIn[indexIn + inPlaneBytes]; //greenSum
                    buckets[ pDataGray[indexGray] *4+3 ] += pDataIn[indexIn + inPlaneBytes*2]; //greenSum
                }
            }

//找到最大的那个像素
              GetMostFrequentColor(buckets, smoothness, &red, &green, &blue);

//目标像素[i, j]
            indexOut =  j * outRowBytes + i * outColumnBytes; 
            pDataOut[ indexOut                     ] = red;  
            pDataOut[ indexOut + outPlaneBytes     ] = green;
            pDataOut[ indexOut + outPlaneBytes *2 ] = blue;

            i += direction?1 : -1;
        }

//把模板向下移动一行
         rowLeave = j - radius;
        rowEnter = j + radius +1;

if(direction)
        {
            xMin = max(-x0,              (imaxOut -1) - radius);
            xMax = min(-x0 + imaxIn -1, (imaxOut -1) + radius);
            indexOut = (j +1) * outRowBytes + (imaxOut -1) * outColumnBytes; //目标像素[i, j]
        }
else
        {
            xMin = max(-x0,              -radius);
            xMax = min(-x0 + imaxIn -1, radius);
            indexOut = (j +1) * outRowBytes; //目标像素[i, j]
        }

//移出当前模板的那一列
         if((rowLeave + y0) >=0&& (rowLeave + y0) < jmaxIn)
        {
for(i2 = xMin; i2 <= xMax; i2++)
            {
                indexIn = (rowLeave + y0) * inRowBytes + (i2 + x0) * inColumnBytes;
                indexGray = (rowLeave + y0) * grayRowBytes + (i2 + x0);

                buckets[ pDataGray[indexGray] *4     ]--; //count
                buckets[ pDataGray[indexGray] *4+1 ] -= pDataIn[indexIn]; //redSum
                buckets[ pDataGray[indexGray] *4+2 ] -= pDataIn[indexIn + inPlaneBytes]; //greenSum
                buckets[ pDataGray[indexGray] *4+3 ] -= pDataIn[indexIn + inPlaneBytes*2]; //greenSum
            }
        }

//进入当前模板的那一列
         if((rowEnter + y0) >=0&& (rowEnter + y0) < jmaxIn)
        {
for(i2 = xMin; i2 <= xMax; i2++)
            {
                indexIn = (rowEnter + y0) * inRowBytes + (i2 + x0) * inColumnBytes;
                indexGray = (rowEnter + y0) * grayRowBytes + (i2 + x0);

                buckets[ pDataGray[indexGray] *4     ]++; //count
                buckets[ pDataGray[indexGray] *4+1 ] += pDataIn[indexIn]; //redSum
                buckets[ pDataGray[indexGray] *4+2 ] += pDataIn[indexIn + inPlaneBytes]; //greenSum
                buckets[ pDataGray[indexGray] *4+3 ] += pDataIn[indexIn + inPlaneBytes*2]; //greenSum
            }
        }
    }
return TRUE;
}

//从灰度桶阵列中,提取出最多像素的那个桶,并把桶中像素求平均值作为 RGB 结果。
void GetMostFrequentColor(UINT* buckets, int smoothness, uint8* pRed, uint8* pGreen, uint8* pBlue)
{
    UINT maxCount =0;
int i, index =0;
for(i =0; i <= smoothness; i++)
    {
if(buckets[ i *4 ] > maxCount)
        {
            maxCount = buckets[ i *4 ];
            index = i;
        }
    }
if(maxCount >0)
    {
*pRed =   (uint8)(buckets[ index *4+1 ] / maxCount);    //Red  
        *pGreen = (uint8)(buckets[ index *4+2 ] / maxCount);    //Green 
        *pBlue =  (uint8)(buckets[ index *4+3 ] / maxCount);    //Blue
    }
}

 2.3 원래 코드에서는 반경 범위를 (1 ~ 5)로 제한했는데, 코드를 최적화했기 때문에 설정하면 반경 범위를 크게 늘릴 수 있습니다. 반경을 100으로 너무 큰 반경을 갖는 것은 의미가 없다는 것을 알았습니다. 원본 이미지가 무엇인지 말하기가 거의 불가능하기 때문입니다.

[요약] 다수의 하위 수준 포인터 작업과 서로 다른 직사각형(입력 패치, 출력 패치, 템플릿) 간의 좌표 위치 지정을 포함하여 개선된 코드가 더욱 기술적이고 까다롭습니다. 코드의 가독성이 약간 떨어질 수 있지만 위의 원칙을 이해하는 한 코드의 가독성은 여전히 ​​좋습니다. 또한 알고리즘을 개선하고 템플릿을 직사각형에서 "원"으로 변경하고 템플릿 반경과 버킷 수의 두 매개 변수가 이미지를 탐색할 때 무작위로 지터되도록 하는 것도 생각했지만 이러한 개선으로 인해 2.2의 개선 최적화가 실패하면 알고리즘 속도가 더 낮은 수준으로 떨어집니다.

(3) 멀티 스레딩 기술을 사용하여 썸네일 표시 효율성을 높이고 UI 스레드의 상호 작용에 영향을 주지 않습니다.

매개변수에 표시 대화 상자 썸네일 기술에 대해서는 PS 필터 작성에 대한 이전 튜토리얼의 네 번째 기사를 참조하세요. 여기서는 이에 대해 설명하지 않겠습니다. 여기서 제가 말하는 것은 썸네일 업데이트 시 UI 상호작용 개선과 확대/축소 및 이동 기술입니다.

아래 사진은 포토샵에서 이 필터를 호출하면 나타나는 매개변수 설정 대화상자입니다. 사용자는 슬라이더 컨트롤(TrackBar, Slider라고도 함)을 드래그하거나 뒤에 있는 텍스트 상자에 직접 입력하여 매개변수를 변경할 수 있습니다. 새 매개변수를 반영하기 위해 썸네일이 실시간으로 업데이트됩니다. 원래 필터 구현에서는 대화 상자 UI와 동일한 스레드에 썸네일 업데이트 처리를 배치했습니다. 이렇게 하면 사용자가 슬라이더 컨트롤을 빠르게 드래그하면 매개변수가 빠르게 변경되고 UI 스레드가 썸네일 데이터를 처리하는 중이어서 짧은 시간 동안 "차단"되어 즉시 응답할 수 없게 됩니다. 후속 컨트롤 이벤트, 즉 슬라이더 컨트롤의 드래그가 충분히 부드럽지 않고 점프, 좌절, 부진이 발생하고 마우스 드래그에 대한 피드백이 충분히 민감하지 않습니다.

 

이 문제를 개선하고 UI 스레드에 영향을 주지 않기 위해 시간이 많이 걸리는 썸네일 처리를 넣을 계획입니다. 스레드가 축소판 처리를 완료하면 해당 보기를 업데이트하도록 대화 상자에 알립니다. 트랙바를 드래그하면 UI 스레드는 "서지"와 같은 매우 높은 빈도로 제어 알림을 받게 됩니다. 이를 위해서는 도착하는 후속 UI 이벤트로 인해 실행 중인 스레드 작업이 빠르게 종료되고 종료될 수 있어야 합니다.

실제 필터 처리와 썸네일 업데이트 시 이 기능을 공유할 수 있도록 필터 알고리즘을 공유 기능으로 추출했습니다. PS의 실제 필터 호출과 썸네일 업데이트 중에 필터 알고리즘은 실제로 "작업 취소" 이벤트를 정기적으로 감지하는 데 필요합니다. 예를 들어 PS가 필터를 호출할 때 사용자가 ESC 키를 누르면 시간이 많이 걸리는 필터 작업이 즉시 중단됩니다. 썸네일을 업데이트할 때 UI 이벤트가 급증하면 처리 스레드도 빠르게 종료될 수 있어야 합니다.

필터의 핵심 알고리즘 기능에서는 PS에서 필터를 호출할 때와 썸네일을 업데이트할 때 테스트 취소 방법이 다르기 때문에 "작업 취소" 이벤트를 정기적으로 감지합니다. 콜백 함수 매개변수(TestAbortProc)를 추가했습니다. 이와 같이 PS가 실제 처리를 위해 필터를 호출할 때 PS에 내장된 콜백 함수를 사용하여 취소 이벤트를 감지합니다. 대화 상자의 썸네일을 업데이트할 때 제가 직접 제공하는 콜백 함수를 사용하여 취소 이벤트를 감지합니다( 이 함수는 처리를 기다리는 새로운 UI 이벤트가 있는지 여부를 알기 위해 부울 변수를 감지합니다.

썸네일 처리에 단일 스레드를 사용합니다. 즉, 각각의 새로운 UI 이벤트가 도착할 때 썸네일 처리 스레드가 실행 중인지 감지해야 하며, 그렇다면 새 UI 이벤트에 대한 표시를 설정한 다음 이전 스레드가 종료된 후 스레드가 종료될 때까지 기다립니다. , UI 이벤트가 계속해서 도착할 때 너무 많은 스레드를 여는 대신 항상 하나의 스레드만 썸네일 처리할 수 있도록 새 스레드를 시작하겠습니다. 이 방법의 장점은 논리가 명확하고 제어하기 쉽다는 것입니다. 스레드가 너무 많아서 유지 관리가 불가능한 상황에 빠지지 않게 됩니다. 단점은 스레드가 정기적으로 취소 이벤트를 감지하더라도 스레드를 종료하는 데 여전히 약간의 시간이 걸린다는 것입니다. 이로 인해 UI 스레드가 여전히 약간의 "일시 중지"를 가지게 되지만, 썸네일을 업데이트하는 것과 비교하면 사소한 일입니다. UI 스레드를 근본적으로 개선하세요.

개선 후에는 매개변수 대화상자에서 두 개의 슬라이더를 매우 빠른 속도로 드래그할 수 있습니다. 비록 이 필터의 핵심 알고리즘에는 많은 양의 계산이 필요하지만 여전히 매개변수 대화상자가 표시되는 것을 볼 수 있습니다. 많은 기능이 있습니다.

(4) 썸네일 줌 및 팬 기능

사실 줌이나 패닝에 관계없이 썸네일 데이터를 업데이트하는 것은 어렵지 않습니다. 그 자체 . 어려운 점은 주로 썸네일 패닝에 있습니다. 마우스 상호 작용이 필요하기 때문에 매우 견고한 Windows 프로그래밍 기술과 Windows 프로그램의 기본 메커니즘에 대한 이해가 필요하기 때문입니다.

썸네일을 드래그하는 방법은 두 가지가 있습니다.

4.1 결과 이미지를 직접 드래그합니다.

두가지 방법으로 나누어집니다. 하나는 비교적 완벽한 드래그 효과이지만 일정량의 공간과 시간을 낭비하는 대가가 따르며 코딩도 까다롭습니다. 즉, 썸네일의 입력 데이터를 9배 크기로 확장하고 결과 이미지를 메모리에 얻는다. 표시 시 결과 그래프의 중앙 부분만 표시됩니다. 드래그하면 썸네일에 빈 공간이 나타나지 않습니다.

또 다른 방법은 현재 결과 이미지를 드래그하면서 스냅샷(스크린샷)을 찍은 후, 드래그하면서 스크린샷 결과를 화면의 해당 위치에 붙여넣는 것입니다. 이것이 더 효율적이지만, 드래그할 때 썸네일 옆에 빈 공간이 보일 수 있다는 단점이 있습니다. 이 방법은 벡터 드로잉과 같이 뷰 업데이트 비용이 클 때 자주 사용됩니다. 이 필터에서 구현한 방법이 이 범주에 속합니다.

 4.2 이미지를 원본 입력 이미지로 드래그하세요.

즉, 드래그할 때 결과 사진이 아닌 원본 데이터를 사용하는 것 역시 데이터 업데이트 비용을 줄이기 위한 절충안입니다. 예를 들어 이 방법은 Photoshop의 내장 필터 Gaussian Blur에 사용됩니다. 썸네일을 드래그할 때 표시되는 썸네일은 원본 이미지이며, 미리보기 효과는 마우스를 놓은 후에만 표시됩니다. 이것이 더 경제적이고 효과적입니다. 원본 데이터를 요청하는 데 드는 비용은 높지 않지만 필터를 사용하여 썸네일을 한 번 처리하는 데 드는 비용이 높기 때문입니다.

몇 가지 추가 기술 사항은 다음과 같습니다. 마우스가 클라이언트 영역 밖으로 이동할 수 있으므로(음수가 될 수 있음) LOWORD(lParam) 및 HIWORD(lParam)를 직접 사용하여 클라이언트 영역을 얻을 수 없습니다. (WORD는 부호 없는 숫자이므로) 사용하기 전에 부호 있는 숫자(약식)로 변환해야 합니다. 올바른 방법은 windowsx.h 헤더 파일의 매크로(GET_X_LPARAM 및 GET_Y_LPARAM)를 사용하는 것입니다.

(5) 이 필터에 대한 다운로드 링크(첨부 파일에는 제가 작성한 PS 플러그인 설치 도구가 포함되어 있어 사용자 설치를 단순화할 수 있습니다)

[이 플러그인- in은 최근 2013년에 출시되었습니다. -4-1 업데이트, 향상된 UI 상호작용 성능】

   //내가 개발한 최신 PS 플러그인 모음(ICO, OilPaint, DrawTable 등 포함)

                                                                                      | lum1980/PsPlugIns_V2013.zip

Photoshop을 설치하고 다시 시작합니다.

메뉴에서 이 필터를 호출합니다. Filter - Hoodlum1980 - OilPaint.

메뉴에서 도움말 - 플러그인 정보 - OilPaint...에서 정보 대화 상자를 볼 수 있습니다. (모양은 제가 개발한 ICO 파일 형식 플러그인의 정보 대화 상자와 거의 동일합니다.)

메뉴: 도움말 - 시스템 정보에서 "OilPaint" 항목이 로드되었는지 여부와 해당 버전 정보를 확인할 수 있습니다.

(6) 덜 중요한 보충 지침

6.1 미러 처리 과정에서 사용한 출력 패치 크기는 128 * 128 픽셀입니다. , Photoshop 상태 표시줄에 표시되는 진행률 표시줄의 각 단계는 출력 패치의 완료를 나타냅니다. 입력 패치는 일반적으로 출력 패치보다 크거나 같고, 입력 패치 크기는 필터 매개변수의 반경(템플릿 반경이 네 방향으로 바깥쪽으로 확장되는 픽셀 거리)과 관련됩니다.

 6.2 필터 코어 알고리즘에서는 취소 이벤트에 대한 민감도를 높이기 위해 현재 행(행의 픽셀 인덱스 & 0x0F == 0x0F)에서 처리된 16픽셀마다 취소를 감지합니다. 취소는 각 행이 처리된 후(열 루프에서) 한 번 감지됩니다. 그러나 이 감지 빈도는 약간 너무 빈번합니다. 너무 자주 발생하면 함수 호출 비용이 증가할 수 있습니다.

 6.3 동일한 이미지, 동일한 매개변수를 사용하여 필터, FilterExplorer, PhotoSprite를 별도로 처리한 다음 Photoshop에서 비교했습니다. 내 알고리즘은 FilterExplorer의 소스 코드를 기반으로 하고 해당 알고리즘이 개선되었기 때문에 내 알고리즘은 FilterExplorer와 동일하지만 더 효율적이므로 결과는 정확히 동일합니다. 하지만 내 필터와 FilterExplorer의 전반적인 효과는 PhotoSprite의 효과와 매우 유사하지만 결과는 약간 다릅니다. PhotoSprite의 코드를 확인해보니 이미지의 그레이스케일 알고리즘의 차이로 인해 발생하는 것으로 나타났습니다(PhotoSprite의 그레이스케일 알고리즘을 FilterExplorer의 그레이스케일 알고리즘과 동일하게 조정하면 처리 결과가 동일해졌습니다).

PhotoSprite에서 회색조 픽셀에 사용되는 방법은 다음과 같습니다.
grey = (byte) ( ( 19661 * R + 38666 * G + 7209 * B ) >> 16 ) ;

FilterExplorer/제가 개발한 필터에서 회색조 픽셀에 사용되는 방법은 다음과 같습니다.
grey = (byte) (0.3 * R + 0.59 * G + 0.11 * B ) ;

 PhotoSprite의 회색조 방법은 부동 소수점 곱셈을 정수 곱셈으로 변환하므로 효율성이 약간 향상될 수 있지만 여기서는 성능이 크게 향상되지 않습니다.

(7) 참고자료

7.1 FilterExplorer 소스코드.

 7.2 Photoshop 6.0 SDK 문서.

 7.3 FillRed 및 IcoFormat 플러그인 소스 코드, 작성자:hoodlum1980(본인).

(8) 수정 내역

8.01 [H] Continue 호출에서 그레이스케일 버킷 메모리 할당 시 필터 통과 문제 수정 메모리 크기가 잘못되었습니다. (회색조 비트맵의 공간 크기로 잘못 설정됨) 이 BUG는 문서 크기가 너무 작거나 반경 매개변수가 작고 매끄러움 매개변수가 큰 경우에 쉽게 발생합니다. 이러한 요인으로 인해 패치가 너무 작아질 수 있습니다. 이때 그레이 버킷 공간의 할당이 실제 필요한 크기보다 작기 때문에 후속 코드가 메모리 제한을 초과하여 PS 프로세스가 예기치 않게 종료될 수 있습니다. 2011-1-19 19:01.

 8.02 [M] 새로운 썸네일 확대/축소 버튼 및 기능을 추가하고, 확대/축소 시 깜박임을 줄이기 위해 확대/축소 버튼 코드를 최적화했습니다. 2011-1-19 19:06.

 8.03 [M] 썸네일 마우스 드래그 기능을 추가하고 썸네일 직사각형을 수정하는 코드를 추가로 조정하여 확대/축소 시 깜박임을 완전히 방지했습니다. 2011-1-19 23:50.

 8.04 [L] 썸네일 위에 마우스를 올려 드래그 앤 드롭하면 커서가 손을 뻗거나 잡은 모양으로 바뀌는 새로운 기능입니다. 이는 PS의 Suite PEA UI Hooks 제품군에 있는 함수를 호출하여 수행됩니다. 즉, 썸네일에 표시되는 것은 PS 내부의 커서입니다. 2011-1-20 1:23.

 8.05 [M] 매개변수 대화상자에서 확대, 축소 버튼 클릭 시 반경 매개변수가 잘못 변환되는 버그를 수정했습니다. 이 버그로 인해 확대 또는 축소 버튼을 클릭한 후 축소판이 잘못 표시됩니다. 2011-1-20 1:55.

 8.06 [L] 정보 대화 상자의 URL 링크를 SysLink 컨트롤로 조정하여 정보 대화 상자의 창 프로시저 코드를 크게 단순화할 수 있습니다. 2011-1-20 18:12.

 8.07 [L] 여러 플러그인을 한 번에 설치할 수 있도록 플러그인 설치 도우미 도구를 업데이트합니다. 2011-1-20 21:15.

 8.08 [L] 썸네일 크기 조정 시 계산 오류로 인해(이유는 알 수 없음, 부동 소수점 계산 오류일 수 있음) 가장자리 근처 오른쪽 하단에 있는 이미지가 썸네일 보기로 변환되지 않을 수 있습니다. 이므로 변환은 범위에 오류 버퍼를 추가했습니다. 2011-1-27.

 8.09 [L] 미화: 필터 매개변수 대화 상자의 확대/축소 버튼을 DirectUI 기술(새 클래스 CImgButton 추가)을 사용하도록 변경하면 원래 버튼 컨트롤을 사용하는 것보다 인터페이스 효과가 더 좋습니다. 2011-2-14.

 8.10 [M] 성능: 플러그인이 매개변수 대화 상자에 있을 때 반경 및 평활도 매개변수를 조정하기 위한 슬라이더의 대화형 성능을 조정했습니다. 2013-4-1.


더 많은 포토샵 유화효과 필터 관련 글은 PHP 중국어 홈페이지를 주목해주세요!


성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.