The Savitzky–Golay method essentially performs a local polynomial regression (of degree $k$) on a series of values (of at least $k+1$ points which are treated as being equally spaced in the series) to determine the smoothed value for each point.

Savitzky–Golay 필터는 일정한 간격으로 주어진 데이터들이 있을 때(이들 데이터는 원래의 정보와 노이즈를 같이 포함한다), 각각의 점에서 주변의 점들을 가장 잘 피팅하는 $k$-차의 다항식을 최소자승법으로 찾아서 그 지점에서의 출력값을 결정하는 필터이다. 이 필터는 주어진 데이터에서의 극대나 극소, 또는 봉우리의 폭을 상대적으로 잘 보존한다.(주변 점들에 동등한 가중치를 주는 Moving Average Filter와 비교해 볼 수 있다).

간단한 예로, 2차의 다항식과 5개의 데이터 점

$$\{ (-2, d_0), (-1, d_1), (0, d_2), (1, d_3), (2, d_4)\}$$

을 이용해서 중앙에서의 값을 결정하는 방법을 살펴보자. 사용하려고 하는 다항식은

$$p(x)= a_0+ a_1 x + a_2 x^2$$

이다. 다항식의 계수는 다항식의 값과 실제 데이터의 값과의 차이를 최소화시키도록 선택해야 한다. 즉, 최소자승의 원리를 적용하여서 구하면 된다. 계산된 다항식의 값과 실제 데이터 값 사이의 차의 제곱을 구하면:

$$L = |a_0-2a_1 + 4a_2 -d_0|^2 +|a_0 -a_1 + a_2 -d |^2 + |a_0 -d_0|^2 \\+ |a_0 + a_1 + a_2 -d_3|^2 + |a_0 + 2 a_1 + 4 a_2 -d_4|^2$$

이 식의 $a_0, a_1, a_2$에 대한 극값을 가질 조건은 $$5 a_0+ 10 a_2 = d_0+ d_1 + d_2 + d_3 + d_4 \\ 10 a_1 = -2 d_0 – d_1 + d_3 + 2 d_4 \\ 10 a_0+ 34 a_2= 4d_0 + d_1+ d_3+ 4d_4$$

이 식을 만족시키는 $a_0$를 구하면, 필터의 출력(원도 중앙에서 값)이 결정된다.

$$\text{필터 출력} =a_0 = (-3d_0 + 12 d_1 + 17 d_2 + 12 d_3 - 3 d_4)/35$$

위에서 계수 $a_0$, $a_1$, $a_2$를 결정하는 방정식은 행렬로 정리하면 아래의 식과 같이 표현할 수 있다. 

좌변의 5행 3열 행렬을 $\mathbf{A}$, ${\mathbf{a}}=[a_0, a_1, a_2]^T$, ${\mathbf{d}}=[d_0, d_1, d_2, d_3, d_4]^T$로 놓으면, 이 행렬방정식은 $\mathbf{ A.a = d}$ 형태로 쓸 수 있다. $\mathbf{A}$가 정방행렬이 아니므로 역행렬을 바로 구할 수 없지만, $|\mathbf{ A \cdot a - d} |^2$을 최소로 하는 최소제곱해는
$$\bf (A^T\cdot A)\cdot a = A^T \cdot d$$를 만족시켜야 하므로 

$$\bf a = (A^T\cdot A)^{-1} \cdot (A^T \cdot d)$$로 주어짐을 알 수 있다.

이 식은 임의의 $k$-차 다항식을 이용한 경우에도 사용할 수 있다. 이 경우 행렬 $\bf A^T \cdot A$는 $(k+1)\times (k+1)$의 대칭행렬이 된다. 행렬 $\bf A$는 다항식의 찻수와 피팅에 사용이 될 데이터의 구간의 크기가 주어지면 정해지므로, 윗 식에서 $({\bf A}^T\cdot {\bf A})^{-1}\cdot {\bf A}^T$의 첫 행 ($a_0$을 $d$로 표현하는 식의 계수들)을 구하면 코드 내에서 결과를 lookup table로 만들어서 사용할 수 있다. 아래 표는 mathematica 를 이용해서 윈도 크기가 7 (7개 점)인 경우 2차 다항식을 사용할 때 계수를 구하는 과정이다.

 


2차 다항식일 때, 같은 방식으로 다양한 윈도 크기에 따른 계수를 구할 수 있다.
*크기($n$)에 따른 필터값 결정계수 (중앙에 대해 좌우대칭이다);
\begin{align} n=5;\quad &W[~] = \{-3, 12, \mathbf{17}, 12, -3\};\\ n=7;\quad &W[~] = \{-2, 3,  6, \mathbf{7}, 6, 3, -2\};\\ n=9;\quad &W[~] = \{-21, 14, 39, 54, \mathbf{59}, 54, 39, 14, -21\}; \\n=11;\quad&W[~]=\{-36,9,44,69,84,\mathbf{89},84,69,44,9,-36\};\\ n=13;\quad &W[~]=\{13,-11,0,9,16,21,24,\mathbf{25},24,21,16,9,0,-11,13\}\end{align} 

$$\text{필터 출력} =  \frac{\sum_i W[i]d[i]}{\sum_i W[i]}$$

int SavitzkyGolayFilter(std::vector<double>& data, double W[], int wsz, 
                        std::vector<double>& out) {
    int hwsz = wsz >> 1;
    wsz = (hwsz<<1) + 1;
    std::vector<double> data_pad(data.size() + 2 * hwsz);
    // reflective boundary conditions;   
    // [hw]...[1][0][1][2]...[hw]....[n-1+hw][n-2+hw][n-3+hw]....[n-1];
    for (int i = 0; i < hwsz; i++) { 
        data_pad[i]                  = data[hwsz-i];
        data_pad[i+data.size()+hwsz] = data[data.size()-2-i];
    }
    for (int i = data.size(); i-->0;) data_pad[i+hwsz] = data[i];
    //
    out.resize(data.size());
    double wsum = 0;
    for (int i = 0; i < wsz; i++) wsum += W[i]; 
    for (int i = data.size(); i-->0;) {
        double *pdata_pad = &data_pad[i];
        double fsum = 0;
        for (int k = 0; k < wsz; k++) fsum += pdata_pad[k] * W[k];
        out[i] = fsum / wsum;
    }
    return 1;
};

wsz=25, green=Savitzky-Golay, red=Moving average

 

728x90

'Image Recognition' 카테고리의 다른 글

Adaboost  (0) 2010.12.28
Blur Detection  (0) 2010.05.25
Retinex 알고리즘  (11) 2010.02.03
Gaussian Mixture Model & KMeans  (4) 2010.01.30
Image Morphing  (0) 2010.01.24
Posted by helloktk
,

Retinex 알고리즘은 영상의 contrast를 향상하거나, sharpness를 증진시킬 때 많이 사용한다. 또 픽셀 값의 dynamic range가 큰 경우에 이것을 압축시켜 영상 데이터 전송에 따른 병목 현상의 해소에 이용할 수 있다. Retinex 알고리즘의 기본 원리는 입력 영상에 들어있는 배경 성분을 제거하는 것이다.

1. 배경 영상은 입력 영상의 평균적인 영상로 생각할 수 있는데, 이것은 적당한 스케일(필터 사이즈)의 가우시안 필터를 적용하여서 얻을 수 있다. 이 필터를 적용하면 입력 영상에서 필터 사이즈보다 작은 스케일은 무시하는 효과를 준다.
2. 입력 영상의 반사 성분(배경 조명에 무관한)은 입력 영상을 앞서 구한 배경 영상으로 나누면 된다
3. Retinex 출력은 이 반사 성분에 로그 값을 취한 것이다. 로그를 취함으로써 반사 성분이 분포 범위(=dynamic range)를 압축하는 효과를 얻는다.

- 입력 영상: $I(x, y)$ ;
- 가우시안 필터: $G_{\sigma }(x, y)=A\exp [-(x^2+y^2)/2\sigma^2]$
- 배경 영상: $(G_{\sigma}*I)(x, y) = \text{Gaussian convolution}$;

- Retinex 출력:
\begin{align}R(x,y;\sigma)&=\log[\text{반사 성분}] \\ &=\log[\text{입력 성분}/\text{배경 성분}]\\ &=\log [I(x, y)] - \log[ (G_{\sigma} * I)(x,y)]\end{align}

이처럼 하나의 스케일에 대해서 적용하는 경우를 SSR(single-scale retinex) 알고리즘이라고 한다. 컬러 영상의 경우에는 각각의 RGB 채널에 대해 알고리즘을 적용하면 된다. 


Retinex 영상을 구할 때 하나의 스케일이 아니라 다중 스케일에 대해서 적용한 retinex 영상을 적절한 가중치($\omega_\sigma$)를 주어서 더한 결과를 출력 영상으로 사용할 수 있다. 이 경우가 MSR(multi-scale retinex) 알고리즘이다. $$R_{MSR}(x,y) = \sum_{\sigma\in scales} \omega_{\sigma} R_{SSR}(x,y;\sigma) $$

이렇게 얻은 Retinex 출력 영상은 적당한 offset과 stretching을 하여서 픽셀 값이 [0,255] 구간에 있게 조절한다. 이 과정은 Retinex 출력 영상 픽셀의 평균값과 편차를 구하여 해결한다.

컬러 영상의 경우에 Retinex 출력 영상은 전체적으로 그레이화 되는 경향이 있어서 이것을 보완하기 위해서 아래의 추가적인 처리 과정을 더 거친다.  $$ {\tilde R}_{MSR}^{red}(x,y)=\log \left(\frac{C I_{red}}{ I_{red}+I_{green}+I_{blue}} \right) R_{MSR}^{red}(x,y)$$ $$ {\tilde R}_{MSR}^{grren}(x,y)=\log \left(\frac{CI_{green}}{ I_{red}+I_{green}+I_{blue}} \right) R_{MSR}^{green}(x,y)$$ $$ {\tilde R}_{MSR}^{blue}(x,y)=\log \left(\frac{C I_{blue}}{I_{red} +I_{green}+I_{blue}} \right) R_{MSR}^{blue}(x,y)$$

여기서, $C$는 상수이고, $I_{red}, I_{green}, I_{blue}$는 입력 영상의 RGB-채널이다.


*그레이 이미지 처리 결과:


*구현 코드는 다음을 참고: 그레이: http://kipl.tistory.com/33 
                                   컬러: http://blog.naver.com/helloktk/80039132534
* 기술적으로  log를 취하므로 입력 영상에 +1을 더해서 log(0)이 나오는 것을 방지해야 한다.
* convolution 된 영상도 1보다 작은 경우에 1로 만들어야 한다.
* 스케일이 큰 경우에 필터 사이즈가 매우 크므로 효과적인 convolution 알고리즘을 생각해야 한다. 보통 recursive 필터링을 하여서 빠르게 convolution결과를 얻는다.

 

728x90

'Image Recognition' 카테고리의 다른 글

Blur Detection  (0) 2010.05.25
Savitzky-Golay Smoothing Filter  (2) 2010.03.24
Gaussian Mixture Model & KMeans  (4) 2010.01.30
Image Morphing  (0) 2010.01.24
Fant's Algorithm  (0) 2010.01.22
Posted by helloktk
,

각각 200개의 점들로 이루어진 8개의 2차원 가우시안 군집을 무작위로 만들고, 이를 kmeans 알고리즘을 써서 8개로 분할하였다. 아래의 시뮬레이션은 이 정보를 초기 조건으로 하여서 Gaussian Mixture Model (GMM)에 적용한 결과이다. 두 개의 군집에 대해서 kmeans 결과와 GMM의 결과가 서로 많이 차이가 남을 보여준다.

 


코드 추가: 2010.02.23

struct Data2d {
    double x, y ;
    int id ;
    Data2d() { };
    Data2d(double x, double y) : x(x), y(y), id(-1) { }
};
struct Gauss2d {    
    double cov[4];
    double mx, my ; //mean ;
    double mix;
    //
    double nfactor; //1/(2 * Pi * sqrt(det));
    double det;     //det(cov)
    double icov[4];
    void   prepare();
    //
    double pdf(double x, double y);
} ;
void Gauss2d::prepare() {
    // det(cov);
    det = cov[0] * cov[3] - cov[1] * cov[2];
    if (det < 1e-10) {
        AfxMessageBox("not converging");
        return ;
    };
    nfactor = 1. / (2. * MPI * sqrt(det)) ;
    //inv(cov);
    icov[0] =  cov[3] / det;
    icov[1] = -cov[1] / det;
    icov[2] = -cov[2] / det;
    icov[3] =  cov[0] / det;
}
double Gauss2d::pdf(double x, double y) {
    x -= mx ;
    y -= my ;
    double a = x * (icov[0] * x + icov[1] * y) +
               y * (icov[2] * x + icov[3] * y);
    return (nfactor * exp(-0.5 * a));
};
void init_classes(std::vector<Data2d>& data, std::vector<Gauss2d>& classes) {
    /*
    for (int i = 0; i < classes.size(); i++) {
        Gauss2d& cls = classes[i] ;
        cls.cov[0] = 10 + 50 * rand() / double(RAND_MAX);
        cls.cov[1] = 0;
        cls.cov[2] = 0;
        cls.cov[3] = 10 + 50 * rand() / double(RAND_MAX);
        cls.mx = 100 + 300 * rand() / double(RAND_MAX);   
        cls.my = 100 + 300 * rand() / double(RAND_MAX);   
        cls.mix = 1;
    }
    */
    KMeans(data, classes);
    //use kmeans to locate initial positions;
}
void test_step(std::vector<Data2d>& data,
               std::vector<Gauss2d>& classes,
               std::vector<std::vector<double> >& prob_cls) 
{
    //E-step ;
    for (int k = 0; k < classes.size(); k++) {
        Gauss2d& cls = classes[k];
        cls.prepare();
        //
        for (int i = 0; i < data.size(); i++) {
            prob_cls[i][k] = cls.mix * cls.pdf(data[i].x, data[i].y);
        };
    }
    // normalize-->임의의 데이터는 각 어떤 클레스에 속할 활률의 합=1;
    for (int i = 0; i < data.size(); i++) {
        double s = 0; 
        int bc = 0; double bp = 0;  // to determine membership(debug);
        for (int k = 0; k < classes.size(); ++k) {
            s += prob_cls[i][k];
            // find maximum posterior for each data;
            if (bp < prob_cls[i][k]) {
                bp = prob_cls[i][k] ;
                bc = k ;
            };
        }
        data[i].id = bc;
        // normalize to 1;
        for (int k = 0; k < classes.size(); ++k)
            prob_cls[i][k] /= s;
    }
    //M-step;
    for (int k = 0; k < classes.size(); k++) {
        Gauss2d & cls = classes[k];
        //get mean;
        double meanx    = 0;
        double meany    = 0;
        double marginal = 0; 
        for (int i = 0; i < data.size(); i++) {
            meanx    += prob_cls[i][k] * data[i].x ;
            meany    += prob_cls[i][k] * data[i].y ;
            marginal += prob_cls[i][k];
        };
        cls.mx = meanx = meanx / marginal ; 
        cls.my = meany = meany / marginal ;
        // get mixing;
        cls.mix = marginal / classes.size();
        // get stdev;
        double sxx = 0, syy = 0, sxy = 0;
        for (int i = 0; i < data.size(); i++) {
            double dx = data[i].x - meanx ;
            double dy = data[i].y - meany ;
            sxx += prob_cls[i][k] * dx * dx ;
            syy += prob_cls[i][k] * dy * dy ;
            sxy += prob_cls[i][k] * dx * dy ;
        };
        //set covariance;
        cls.cov[0] = sxx / marginal;
        cls.cov[1] = sxy / marginal;
        cls.cov[3] = syy / marginal;
        cls.cov[2] = cls.cov[1]; //symmetric;
    }   
}
void test() {
    int max_iter = 100;
    int nclass = 8;
    int ndata = 500;
    std::vector<Gauss2d> classes(nclass);
    std::vector<Data2d> data(ndata);
    // prepare posterior space;
    std::vector<std::vector<double> > prob_cls;
    for (int i = 0; i < data.size(); ++i) {
        prob_cls.push_back(std::vector<double>(classes.size()));
    } ;
   // generate data...
   ..................................
    //init_classes
    init_classes(data, classes) ;

    int iter = 0;
    do {
        iter++;
        test_step(data, classes, prob_cls);
    } while (iter < max_iter) ;
};
728x90

'Image Recognition' 카테고리의 다른 글

Savitzky-Golay Smoothing Filter  (2) 2010.03.24
Retinex 알고리즘  (11) 2010.02.03
Image Morphing  (0) 2010.01.24
Fant's Algorithm  (0) 2010.01.22
Affine Transformation  (0) 2010.01.20
Posted by helloktk
,

제대로 segmented 된 그레이 영상은 원래의 영상이 나타내고자 하는 전경이 잘 표현이 된 것이다. 이 경우의 원래 영상과 segmented 된 영상은 높은 상관관계를 갖는다. 따라서, 세그먼트를 위한 임계값의 설정 기준으로 이 상관계수를 최대로 하는 임계값을 찾는 것도 좋은 방법 중의 하나가 될 수 있다.

여기서 사용할 상관계수는 원래의 영상(A)과 전경과 배경을 그들의 픽셀 평균값으로 대체한 segmented 된 영상(B) 간의 상관계수를 사용한다. 임계값이 $T$인 경우 세그먼트된 영상 B 

$$B(i,j) = \left\{\begin{array}{ll} m_0, & \text{if}~A(i,j) \le T\\ m_1, &\text{otherwise}\end{array}\right. $$

로 나타난다. 여기서 $m_0$는 배경 픽셀의 평균값이고, $m_1$은 전경 픽셀의 평균값이다. 이 값은 임계값 $T$에 따라 달라진다. 임계값이 높으면 $m_0$는 커지고, 반대로 $m_1$은 작아진다

 

임계값이 $T$일 때 배경 픽셀 비를 $p$, 전경 픽셀 비를 $q(=1- p)$라 하면 segmented된 영상 B는 각 영역에서의 픽셀 값을 평균으로 대체했으므로 원본 영상의 평균과 같다. 또한, 원본 영상의 분산은 임계값에 무관하게 일정한 값을 유지한다. 이를 정리하면,

$$E(A)=E(B)=m=\text{pixel mean}=p m_0 + q m_1$$

$$V(A)=\text{variance} =T\text{-independent} = \text{const}$$

$$V(B)=pm_0^2 + q m_1^2 - m^2 = pq (m_0 - m_1)^2$$

$$E(A,B)= p m_0^2 + q m_1^2 $$

$$E(A,B) - E(A) E(B) = V(B)$$ 이므로, 

\begin{align}\text{Correlation}(A,B) &=\frac{ {E(A,B)-E(A)E(B)} }{\sqrt{V(A)V(B)} } \\ &=\frac{\sqrt{pq(m_0 - m_1)^2 } }{\sqrt{V(A)} }\\ &\propto \sqrt{pq(m_0 -m_1)^2 }\\ &=\sqrt{\text{interclass variance}}\end{align}

, 원래의 그레이 영상 A와 전경과 배경 픽셀을 각각의 평균값으로 대체한 영상간의 상관계수는 전경과 배경 두 클래스 간의 분산이 최대일 때 가장 크게 나타난다. 이 기준은 Otsu 알고리즘에서 사용한 기준과 같다.

 

참고: Otsu Algorithm 구현 예.

728x90

'Image Recognition > Fundamental' 카테고리의 다른 글

Is Pow of 2  (0) 2012.02.13
Fixed-point RGB2Gray  (0) 2012.01.25
Object Orientation  (1) 2010.01.17
Bicubic Interpolation  (1) 2010.01.14
Bezier Curve을 이용한 Histogram Smoothing  (0) 2010.01.10
Posted by helloktk
,

Image Morphing

Image Recognition 2010. 1. 24. 13:50

Beier & Neely Field Morphing Algorithm:

The Beier-Neely algorithm is a feature-based image-morphing method proposed by Beier and Neely in 1992 [3]. The basic idea of this method is to specify the correspondence between source and target images interactively using a set of line-segment pairs. The mapping of pixels from the source image to the target image is defined using these feature line segments.


모핑 결과: 치타의 이미지 영역밖의 픽셀값은 0으로 처리하였다.

 

728x90

'Image Recognition' 카테고리의 다른 글

Retinex 알고리즘  (11) 2010.02.03
Gaussian Mixture Model & KMeans  (4) 2010.01.30
Fant's Algorithm  (0) 2010.01.22
Affine Transformation  (0) 2010.01.20
Color Counting  (0) 2010.01.18
Posted by helloktk
,