Union-Find 알고리즘을 이용하여 영역 분할 과정이다. 각각의 픽셀에서 4방향으로 연결된 픽셀이 속한 영역 merge 할 수 있는지를 시도한다. merge 조건은 현재 픽셀의 그레이 값과 인접 영역의 평균 그레이 값의 차이가 주어진 임계값보다 작은 경우다.

$$\tt \text{merge 조건: }\quad | 그레이 값- \text{인접 영역 평균값}| < threshold$$

컬러 영상의 경우에는 RGB 채널에 조건을 부여하거나 gray level만 이용해서 판단할 수 있다. root node 수만큼의 분할 영역이 만들어지고, 임계값이 클수록 분할된 각 영역의 사이즈가 커진다.  

gray = 0.2989 * r + 0.5870 * g + 0.1140 * b ;

같은 평균 픽셀값을 가지고 있더라도 4-방향으로 서로 연결된 영역이 아니면 합병되지 못하고 서로 다른 영역으로 남는다. 이를 하나의 영역으로 만들려면 분할된 영역을 다시 검사를 하는 과정이 필요하다. 자세한 과정은 Statistical Region Merge(SRM) 알고리즘(kipl.tistory.com/98)에 잘 구현이 되어 있다.

struct Universe {
    int n;
    std::vector<int> sizes;
    std::vector<int> sums;
    std::vector<int> parent;
    Universe(int n, BYTE *image) {
        this->n = n;
        parent.resize(n, -1);  // all nodes are root;
        sizes.resize(n, 1); 
        sums.resize(n);
        for (int i = 0; i < n; i++)
            sums[i] = image[i];
    }
    int FindCompress (int node) { // find root node;
        if ( parent[node] < 0 ) return node;	 // root-node;
        else  return parent[node] = FindCompress (parent[node]);
        // 찾음과 동시에 depth을 줄인다;   
    }
    int Find(int node) { // find root;
        while (parent[node] >= 0) node = parent[node];
        return node;
    }
    bool Predicate(int root1, int root2, int thresh) {
        double diff = double(sums[root2]) / sizes[root2] 
                      - double(sums[root1]) / sizes[root1];
        return fabs(diff) < thresh;
    }
    void Merge(int root1, int root2) {
        sums[root2]  += sums[root1];
        sizes[root2] += sizes[root1];
        parent[root1] = root2;			 // r2 트리로 합병함;
    }
};
// root 노드는 분할영역의 픽셀 갯수와 픽셀 값의 합을 저장한다.
// 처음 각 픽셀이 root 이고, 픽셀 갯수는 1, 픽셀값은 자신의 픽셀값을 저장;
BOOL UnionFindAverage(BYTE *image, int width, int height, int thresh) {
    Universe *UF = new Universe(width * height, image);
    // 4-connectivity:
    // 첫행; LEFT 픽셀이 속한 영역의 평균값과 차이가 thresh 내이면 left로 합병;
    for ( int x = 1; x < width; x++ ) {
        int left = UF->FindCompress (x - 1);
        int curr = UF->FindCompress (x);
        if (UF->Predicate(curr, left, thresh))
            UF->Merge(curr, left);
    }
    //Flatten(y=0);
    for (int x = 0; x < width; x++) UF->FindCompress(x);
    for ( int y = 1, pos = y * width; y < height; y++ ) {
        // x = 0; TOP 픽셀이 속학 영역과 합병 체크;
        int up   = UF->FindCompress (pos - width);
        int curr = UF->FindCompress (pos);
        if (UF->Predicate(curr, up, thresh))
            UF->Merge(curr, up);
        pos++;
        // TOP-LEFT 픽셀 영역과 합병 체크;
        for ( int x = 1; x < width; x++, pos++ ) {
            int left = UF->FindCompress(pos - 1);
            int curr = UF->FindCompress(pos);
            // left와 합병이 되면 left가 up과 합병이 되는지 확인;
            if (UF->Predicate(curr, left, thresh)) {
                UF->Merge(curr, left);
                curr = left;
            }
            int up = UF->FindCompress (pos - width);
            if ((curr != up) && (UF->Predicate(curr, up, thresh))) 
                UF->Merge(curr, up);
        }
        // Flatten(y>0)
        for (int x = 0; x < width; x++) UF->FindCompress(x + y * width);
    }

    // 평균 이미지 생성;
    for ( int k = width*height; k-->0;) {
        int root = UF->Find(k);
        int avg = int(double(UF->sums[root]) / UF->sizes[root] + 0.5);
        image[k] = avg > 255 ? 255 : avg;
    }
    delete UF;
    return TRUE;
};

네이버 블로그 이전
statistical region merge 알고리즘을 적용한 결과

 
 
 
 
728x90

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

Octree Quantization  (0) 2021.01.12
Median-Cut 컬러 양자화  (0) 2021.01.12
Multilevel Otsu Thresholding  (0) 2021.01.09
Binary Image에서 Convex Hull  (0) 2021.01.06
Kuwahara Filter  (2) 2020.12.28
Posted by helloktk
,

Otsu 알고리즘은 이미지를 이진화시키는데 기준이 되는 값을 통계적인 방법을 이용해서 결정한다. 같은 클래스(전경/배경)에 속한 픽셀의 그레이 값은 유사한 값을 가져야 하므로 클래스 내에서 픽셀 값의 분산은 되도록이면 작게 나오도록 threshold 값이 결정되어야 한다. 또 잘 분리가 되었다는 것은 클래스 간의 거리가 되도록이면 멀리 떨어져 있다는 의미이므로 클래스 사이의 분산 값은 커야 함을 의미한다. 이 두 가지 요구조건은 동일한 결과를 줌을 수학적으로 보일 수 있다.

이미지의 이진화는 전경과 배경을 분리하는 작업이므로 클래스의 개수가 2개, 즉, threshold 값이 1개만 필요하다. 그러나 일반적으로 주어진 이미지의 픽셀 값을 임의의 개수의 클래스로 분리할 수도 있다. 아래의 코드는 주어진 이미지의 histogram을 Otsu의 아이디어를 이용해서 nclass개의 클래스로 분리하는 알고리즘을 재귀적으로 구현한 것이다. 영상에서 얻은 히스토그램을 사용하여 도수를 계산할 수 있는 0차 cumulative histogram($\tt ch$)과 평균을 계산할 수 있는 1차 culmuative histogram($\tt cxh$)을 입력으로 사용한다. 

$$ {\tt thresholds}= \text {argmax} \left( \sigma^2_B = \sum_{j=0}^{nclass-1} \omega_j m_j^2 \right)$$

 

* Otsu 알고리즘을 이용한 이미지 이진화 코드: kipl.tistory.com/17

* 좋은 결과를 얻으려면 히스토그램에 적당한 필터를 적용해서 smooth하게 만드는 과정이 필요하다.

// 0 <= start < n;
double histo_partition(int nclass, double cxh[], int ch[], int n, int start, int th[]) {
    if (nclass < 1) return 0;
    if (nclass == 1) {
        int ws; double ms;
        if (start == 0) {
            ws = ch[n - 1];
            ms = cxh[n - 1] / ws;
        } else {
            ws = ch[n - 1] - ch[start - 1];             // start부터 끝까지 돗수;
            ms = (cxh[n - 1] - cxh[start - 1]) / ws;    // start부터 끝까지 평균값;
        }
        th[0] = n - 1;
        return ws * ms * ms;                            // weighted mean;
    }

    double gain_max = -1;
    int *tt = new int [nclass - 1];
    for (int j = start; j < n; j++) {
        int wj; double mj;
        if (start == 0) {
            wj = ch[j]; 
            mj = cxh[j];                    //mj = cxh[j] / wj;
        }
        else {
            wj = ch[j] - ch[start - 1];     //start부터 j까지 돗수;
            mj = (cxh[j] - cxh[start - 1]); //mj = (cxh[j] - cxh[start - 1]) / wj;
        }
        if (wj == 0) continue;
        mj /= wj;                           //start부터 j까지 평균;
        double gain = wj * mj * mj + histo_partition(nclass - 1, cxh, ch, n, j + 1, tt);
        if (gain > gain_max) {
            th[0] = j;
            for (int k = nclass - 1; k > 0; k--) th[k] = tt[k - 1];
            gain_max = gain;
        }
    }
    delete [] tt;
    return gain_max;
};

trimodal 분포의 분리;

class0: 0~th[0]

class1: (th[0]+1)~th[1],

class2: (th[1]+1)~th[2]=255

th[0]=103, th[1]=172 (th[2]=255)
th[0]=88, th[1]=176, th[2]=255

더보기
// recursive histo-partition 테스트;
// 0--t[0];--t[1];...;--t[nclass-2];t[nclass-1]=255=n-1;
// nclass일 때 threshold 값은 0---(nclss-2)까지;
double GetThreshValues(int hist[], int n, int nclass, int th[]) {
    if (nclass < 1) nclass = 1;
    // preparing for 0-th and 1-th cumulative histograms;
    int *ch = new int [n];          // cdf;
    double *cxh = new double [n];   //1-th cdf;
    ch[0] = hist[0];
    cxh[0] = 0; // = 0 * hist[0]
    for (int i = 1; i < n; i++) {
        ch[i] = ch[i - 1] + hist[i] ;
        cxh[i] = cxh[i - 1] + i * hist[i];
    }
    // nclass=1인 경우도 histo_partition()내에서 처리할 수 있게 만들었다.
    double var_b = histo_partition(nclass, cxh, ch, n, 0, th);
    delete [] ch;
    delete [] cxh;
    return var_b;
}
728x90

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

Median-Cut 컬러 양자화  (0) 2021.01.12
Union-Find 알고리즘을 이용한 영역분할  (0) 2021.01.11
Binary Image에서 Convex Hull  (0) 2021.01.06
Kuwahara Filter  (2) 2020.12.28
Moving Average을 이용한 Thresholding  (0) 2020.11.26
Posted by helloktk
,

책상에 세워둔 연필이 넘어져서 바닥에 닿는데 걸리는 시간은 어떻게 될까? 대략 경험적으로 보면 연필 길이가 길수록 넘어지는데 시간이 오래 걸린다. 왜 그럴까? 넘어지는데 걸리는 시간은 연필의 운동을 결정하는 물리량과 관련 있는데, 우선 크기와 관련 있는 길이$(L)$가 있고, 넘어지려면 토크를 작용해야 하는데 이는 중력(가속도: $g$)과 관련이 있다.(마찰/수직 항력은 회전축에 걸리므로 무관하다). 물론 질량에도 의존할 수 있는데, 중력 가속도, 길이, 질량의 물리량으로 시간의 단위를 만들 수 있는 조합은 $\text{const} \times\sqrt { L /g}$ 밖에 없다. 즉, 넘어지는데 걸리는 시간은 길이에 제곱근에 비례한다. 

구체적으로 얼마나 걸리는지 계산을 시도해 보자. 연필이 넘어지면 수직과 이루는 각 $\theta$가 증가한다: $0\rightarrow \pi/2$. 뉴턴 방정식을 이용하면 $\theta$가 만족해야 할 미분방정식을 얻을 수 있지만, 실질적으로 일하는 힘이 중력밖에 없으므로 역학적 에너지가 보존된다는 사실을 이용하는 것이 좀 더 수월하다. 연필을 균일한 막대로 근사하면 넘어지는 과정은 연필심에 대한 회전운동이다. 역학적 에너지 보존식을 쓰면

$$\frac {1}{2} I_{tip} \Big(\frac {d\theta}{dt}\Big)^2 + \frac {1}{2} MgL \cos \theta = \text {const} = \frac {1}{2} MgL.$$ 여기서 각속도를 구하면

$$ \\ \frac{d\theta}{dt} = \sqrt { \frac {3g}{L}  (1- \cos \theta ) } = \sqrt {\frac {6g}{L}} \sin \frac {\theta}{2},$$

이고, 적분하여 수직 상태에서 바닥에 도달하는데 걸리는 시간을 구하면,

$$T= \sqrt { \frac {L}{6g}} \int_0^{\pi/2} \frac {d \theta }{\sin \frac {\theta}{2} } \rightarrow \infty.$$

기대(?)와는 다르지만 이 결과는 구체적으로 계산하지 않더라도 예상할 수 있다. 왜냐면 완전히 똑바로 서 있으면 회전의 시작에 필요한 토크를 생성하는 힘이 없기 때문이다. 연필에는 중력이 작용하고 끝에 마찰력이나 수직 항력이 있긴 하지만 토크를 만들지는 못한다. 따라서 연필을 넘어뜨리기 위해서는 처음에 약간의 충격을 주던지(속도 제공) 아니면 약간 기울인 상태에서 시작해야 한다. 처음 $\theta_0$의 각도에서 시작하였다면 걸리는 시간은 위 적분식에서

$$ T = \sqrt{\frac {2L}{3g} }\log \frac { \tan \frac {\pi}{8}}{\tan \frac {\theta_0}{4} }.$$

$\theta_0\rightarrow 0$이면 시간은 $T\sim -\sqrt { \frac {2L}{3g}} \log \theta_0 \rightarrow \infty$이고, 유한할 때는 길이의 제곱근에 비례함도 확인할 수 있다.

youtu.be/oowAdPjDY5M

 

728x90
Posted by helloktk
,

야구 배트로 공을 칠 때 공을 맞추는 지점을 잘 선택하면 배트를 잡는 위치(회전축)의 움직임이 거의 없게(즉, 손이 충격을 안 받게) 만들 수 있다. 이는 배트가 힘을 받았을 때 운동이 질량중심과 같은 속도로 움직이는 병진 운동과 질량중심에 대한 회전운동의 합으로 표현되는 점을 고려하면 쉽게 이해할 수 있다. 배트의 각 지점의 실제 속도는 질량중심의 속도와 회전운동에 의한 속도의 벡터 합이므로 손잡이 부분에서 0인 조건을 만족되면 가능하게 된다. 이 경우 배트의 운동은 손잡이 위치에 대한 순수 회전운동으로 표현할 수 있다. 그리고 공을 맞추는 지점을 center of percussion(COP)이라고 한다(손잡이 위치에 대한 상대적 위치임)

공이 배트에 준 힘을 $F$라면 (손이 준 힘이 없는 조건에서)

$$\text {cm-병진}:  ~F = M a,$$

$$\text {cm-회전}: ~Fb = I_{cm} \alpha. $$

손잡이 지점의 가속도가 없는 조건을 쓰면

$$ a_{손잡이}= a- \alpha p = 0\quad \rightarrow ~ \frac {F}{M} = \frac {Fb} {I_{cm}} p \quad \therefore b =  \frac {I_{cm}}{pM}.$$

$b$와 $p$는 정확히 대칭적 역할을 한다. 배트의 COP 지점을 손으로 잡고 원래 손잡이 위치에 공을 맞추어도 손에 충격이 오지 않는다.

배트의 회전관성을 직접 구하지 않고 COP을 알아낼 수 있는 방법은 없을까? 배트의 손잡이를 회전축으로 만들어 배트를 살짝 흔들면 주기가 일정한 진동 운동을 한다(물리진자, COP를 회전축으로 만들어도 같은 주기를 가진다). 그리고 주기는 다음과 같이 주어진다: 

$$ T ={2\pi} \sqrt{ \frac{I_{cm} + M p^2 }{ Mgp}} = 2\pi \sqrt{\frac{b+p}{g}} .$$  

이 표현은 길이가 $\ell=b+p$인 단순진자의 주기와 같다. 따라서 단순진자의 길이를 바꾸어 가면서 배트와 같은 주기를 같게 되는 경우를 찾으면  $b+p$ 값을 구할 수 있다. (추가로 배트의 질량중심은 균형을 이용하면 쉽게 찾을 수 있다.)

https://youtu.be/Dw3UpKQVhVY

728x90
Posted by helloktk
,

동일한 비커에 같은 양의 물을 채운 후 한쪽 비커에는 바닥에 연결된 줄로 탁구공을 완전히 잠기도록 만들었고, 다른 비커에는 스탠드와 줄을 이용해서 쇠공이 바닥에 닿지 않고 물속에 완전히 잠겨 있게 만들었다. 이 두 비커를 그대로 양팔 저울 위에 올리면 어느 쪽으로 기울까? 단, 탁구공과 쇠공의 부피는 같다. (이 문제는 googling을 하면 찾아볼 수 있을 정도로 유명하다)

1. 수평을 유지: 탁구공과 쇠공이 밀어낸 물의 부피가 같아 비커 바닥이 받는 압력도 동일하게 증가하므로

2. 탁구공 쪽: 쇠공은 스탠드가 지탱하여 영향이 없지만 탁구공의 무게는 결국 바닥이 받기 때문에

3. 쇠공 쪽: 부력은 같지만 탁구공에 연결된 줄은 위로 당겨 바닥을 누르는 힘이 감소시키므로

728x90
Posted by helloktk
,