내맘대로 공부기록.

[ C++ ]

[ openCV | C++ ] ( 5 / 6 ) 차선검출. 이미지 전 처리. Window Search 작업.

fwanggus 2021. 9. 5. 20:31

차선 검출을 위한 이미지 전 처리해보기 🛣


⚙️  기본 설명  ⚙️

 

  1. 왜곡 제거(카메라 보정) 👍
  2. Perspective Transform(원본 이미지 ⏩  2D)
  3. Color Filtering(HLS, LAB color space)
  4. 픽셀 값 정규화(feat. 최댓값) 및 이미지 픽셀(HLS 1개, LAB 1개)  합치기.
  5. Window Search
  6. Show Detected Lines and Info.

 


개념  🧐

Window Search 작업부터는 이미지의 픽셀을 계산하고 정보를 획득하는 모듈로 생각하고 진행했습니다. 

먼저, Window Search라는 이름과 같이 창(Window) 또는 박스 형태의 직사각형의 영역을 설정해서 차선의 필셀 위치 정보를 획득할 수 있습니다. ( 여기까지가 온전한 Window Search 작업의 부분입니다. )

이 과정을 통해서 차선에 대한 위치 정보를 가지게 되면, 단일 이미지 프레임 및 연속된 프레임의 인풋 데이터(영상 또는 리얼타임 카메라 인풋)에 대해서 차선의 곡률을 계산할 수 있습니다. 최종적으로는 화면에 해당 데이터를 표시하는 등 부가적인 용도로 사용할 수 있게 됩니다.

 

방법  + DEMO. 🛠🚀

 

순서는 다음과 같습니다.

 

  1. 직사각형 박스를 탐색 영역으로 설정
  2. 높이 방향의 픽셀 수가 가장 많은 컬럼 위치를 탐색의 초기 기준값(폭 방향)으로 설정
  3. 직사각형을 높이 방향으로 전개시켜, 다음 단계의 직사각형을 탐색 및 영역 내의 픽셀 위치 데이터 획득.

이 작업이 무사히 끝이 나면, 차선의 픽셀 위치 데이터를 활용하여 곡률을 다항식으로 근사할 수 있습니다. 최종적으론 근사식을 이용해서 계산된 차선 위치를 화면에 표시할 수 있습니다. 하지만 이 부분은 Window Search의 범위를 벗어나기 때문에 구체적으로 설명하진 않습니다.


1. 직사각형 박스를 탐색 영역으로 설정

아래와 같은 개별의 직사각형 영역을 설정해서, 픽셀 데이터를 획득합니다. 직사각형은 사용자에 따라서 설정하는 크기가 다를 수 있습니다. Rect 클래스를 이용해서 직사각형 영역을 설정해줄 건데요. Rect 인스턴스는 한 점, 폭, 높이를 할당하는 방법으로 생성하였습니다.

좌 : Bird View(undistorted IMG), 우 : Window Search의 직사각형 표시

변수화 작업.

기본적으로, 한 프레임 기준으로 높이 방향의 직사각형 개수를 정해주고 시작합니다. 매 이미지 프레임마다 직사각형을 생성하고, 직사각형의 위치를 업데이트해주는 부분이 있기 때문에 변수화 하여 그 위치를 지정해줍니다.

 

조금 주의할 점은 이미지에서 사용하는 좌표계입니다. 이미지에서 원점은 좌측 상단임을 기억하시고 진행해주면 좋을 것 같습니다. 그 점을 생각 후 win_yHigh라는 변수가 이미지 기준으로 어느 위치에 있을지 상상해보면, 이미지의 가장 아랫부분이 되면, win_yLow는 그 윗단이 되는 것을 이해할 수 있습니다.

 

    ...
	    
    for (int i = 0; i < numWindow; i++)
    {
        win_yHigh = preprocess.rows - (i)*window_height;
        win_yLow = preprocess.rows - (i + 1) * window_height;
        win_xLeft_low = leftX_current - margin;
        win_xRight_low = rightX_current - margin;
        
        ... 
        
        // Do something...
        
     }
     
     ...

 

2. 높이 방향의 픽셀 수가 가장 많은 컬럼 위치를 탐색의 초기 기준값(폭 방향)으로 설정

1번과 순서를 바꿔야 할지 고민을 한 부분입니다. 하지만 Rect 에 대한 기본 정의 및 생성이 수행된 다음에 초기값 설정으로 들어가는 것이 맞다고 판단하여 이렇게 순서를 정했습니다.

 

초기값 세팅은 이미지의 하단 절반만 이용합니다. 차량 입장에서 가장 가까운 부분이기 때문에 하단 절반 이미지에서 초기값을 계산합니다.

하단 절반의 이미지에서 종방향 픽셀 수(row 방향)가 가장 많은 횡방향 위치(cols값)를 직사각형의 초기값 위치로 결정합니다. 여기서 말하는 위치는 직사각형의 중앙 위치를 가리킵니다. 우리는 이미 이전 글에서 이미지 소스로부터 컬러 채널을 필터링 한 후,  0과 1의 픽셀 값만 갖는 이진화(Binary) 형태로 변경해주었습니다.

그렇기 때문에 각 컬럼 별 픽셀의 합을 구한 후, 어떤 컬럼이 가장 많은 빈도로 픽셀 값을 가지고 있는지 확인할 수 있겠죠. 각 컬럼의 픽셀 합을 vector 변수에 집어넣어 최댓값과 그 위치 값(인덱스)을 가져올 수 있습니다.

 

👨🏻‍💻  하단 절반 이미지 획득

Mat halfDownImg(Mat original)
{
    return original(Range(original.rows / 2, original.rows), Range(0, original.cols));
}

 

👨🏻‍💻  이미지의 컬럼 별 픽셀 합을 벡터 변수로 리턴.

vector<int> sumColElm(Mat img_binary)
{
    vector<int> sumArray;
    for (int i = 0; i < img_binary.cols; i++)
    {
        // make one column matrix.
        Mat oneColumn = img_binary.col(i);

        // sum all element and push back on a array.
        int sumResult = sum(oneColumn)[0];
        sumArray.push_back(sumResult);
    }

    return sumArray;
}

 

👨🏻‍💻  왼쪽 차선의 Window 중심 초기값을 계산.(feat. 최댓값 및 위치 계산 함수.)

여기서 아래 함수를 적용하는 전제는, 검출하고자 하는 차선 픽셀이 이미지의 폭 방향 4등분 중 중앙에 위치하고 있는 두 블록 안에 있을 것입니다. 즉, 왼쪽 차선은 1/4 ~ 2/4 부분, 오른쪽 차선은 2/4 ~ 3/4 부분에 각 차선 픽셀이 있을 거다 라고 생각하고 진행했습니다.

 

벡터 변수의 최댓값과 인덱스를 구하기 위해서 max_element 함수를 이용했습니다. 벡터 변수의 포인터 특성을 이용해서 검사하고자 하는 구역을 나눠주고, max_element 함수를 적용해서 초기 위치를 잡아준 내용입니다. 

int getLeftX_base(vector<int> sumArray)
{
    int leftX_base;
    int midPoint = sumArray.size() / 2;
    int qtrPoint = midPoint / 2;

    // get subset of vector range.
    vector<int>::const_iterator begin = sumArray.begin();
    vector<int>::const_iterator last = sumArray.begin() + sumArray.size();
    vector<int> left_qtr(begin + qtrPoint, begin + midPoint);

    // max index and value from a certain array.
    int leftMaxIndex = max_element(left_qtr.begin(), left_qtr.end()) - left_qtr.begin();

    // adjust pixel index for global x width.
    leftX_base = leftMaxIndex + qtrPoint;

    return leftX_base;
}

 

오른쪽 차선 Window의 초기값을 계산하기 위한 함수 작성.

/*상세내용은 가장 아래 깃헙 링크 참고.*/
int getRightX_base(vector<int> sumArray);

 

3. 직사각형을 높이 방향으로 전개시켜, 다음 단계의 직사각형을 탐색 및 영역 내의 픽셀 위치 데이터 획득.

1번에서 작성한 직사각형 요소 값을 세팅한 후 이어지는 내용입니다.

 

👨🏻‍💻  직사각형 인스턴스 생성 : for 루프 내에서 생성.

        ...
        // (make array.)1. Rect object info for specific window.
        Rect curLeftWindow(win_xLeft_low, win_yLow, 2 * margin, window_height);
        Rect curRightWindow(win_xRight_low, win_yLow, 2 * margin, window_height);
        ...

 

👨🏻‍💻  픽셀 값이 0 이 아닌 픽셀의 인덱스(위치)를 이미지로부터 획득.

findNonZero 함수(opencv 내장)로 픽셀 값이 0 이 아닌 픽셀의 인덱스 값을 구해줍니다. (아래 공홈 링크)

    // preprocess는 이진화된 이미지 변수.
    ...
    Mat nonZeroPos;
    findNonZero(preprocess, nonZeroPos);
    ...
 

OpenCV: Operations on arrays

Divides a multi-channel array into several single-channel arrays. The function cv::split splits a multi-channel array into separate single-channel arrays: If you need to extract a single channel or do some other sophisticated channel permutation, use mixCh

docs.opencv.org

 

지금 해준 작업은 이미지 전체에 대해서 0이 아닌 위치를 검사했습니다. 우리가 원하는 인덱스는 차선 부분이기 때문에 직사각형 내부의 것만으로 걸러낼 필요가 있습니다.

 

인덱스 값을 획득했다면, 각 직사각형 안에 위치하고 있는 픽셀 들을 검사하고, 왼쪽 그리고 오른쪽 차선에 대해서 새로운 벡터 변수에 그 정보들을 저장해 줄 겁니다. 이렇게 하는 이유는 각 차선에 대해 다항식 근사를 수행하는데, 그때 각 차선에 대한 위치 데이터가 필요하기 때문입니다. 쉽게 말해 근사 작업에 필요한 x, y 좌표를 획득하기 위함이라고 생각하시면 될 것 같네요.

 

👨🏻‍💻  각 직사각형 별 크기가 0 이 아닌 픽셀 인덱스(위치)를 획득.

(이미지 전체로부터 직사각형 내부의 것으로 필터링)

 

아래의 winowBox는 현재의 직사각형 인스턴스가 됩니다. 해당 정보를 이용하여 검색할 범위를 다시 지정합니다.

vector<Point> getIndexArray_onWindow(Mat nonZeroPos, Rect windowBox)
{

    vector<Point> windowPoint_IDX;
    
    // define boundary window.
    int xLow = windowBox.x;
    int xHigh = xLow + windowBox.width;
    int yLow = windowBox.y;
    int yHigh = yLow + windowBox.height;
    ...

 

nonZeroPos 변수로부터 각 픽셀이 현재 직사각형 내부에 있는지 검사를 진행하고, 벡터 변수에 추가합니다.

    ...
    
    // Put a certain Point that is on the window boundary.
    for (int i = 0; i < nonZeroPos.rows; i++)
    {
        int nonZeroX = nonZeroPos.at<Point>(i).x;
        int nonZeroY = nonZeroPos.at<Point>(i).y;

        // initializing Point variable.
        Point onePt = {0, 0};
        onePt = {nonZeroX, nonZeroY};

        // When non-zero position is on spcified Window Area.
        if (nonZeroX >= xLow && nonZeroX < xHigh && nonZeroY >= yLow && nonZeroY < yHigh)
        {
            windowPoint_IDX.push_back(onePt);
        }
    }
    
    return windowPoint_IDX;
}

 

👨🏻‍💻  다음 직사각형으로 전개(위치 업데이트.)

위에서 이미지의 가장 아래에 위치하는 첫 번째 직사각형을 생성했습니다. 그 후 높이 방향으로 직사각형을 전개시켜 전체 차선에 대한 정보를 획득해야 합니다. 다음 직사각형의 위치를 정하기 위한 방법으로 이전 직사각형에서 획득한 픽셀 개수를 이용합니다. 즉, 이전 직사각형에 획득한 픽셀의 폭 방향 인덱스 값(X좌표)을 평균하여 다음 직사각형의 초기값으로 사용합니다. 단, 이전 직사각형에서 획득한 픽셀 수가 너무 적으면 평균한 값이 전혀 다른 위치를 가리킬 가능성이 있기 때문에, 최소한의 픽셀 수를 조건으로 걸어둘 필요가 있을 것 같네요. 

최소 픽셀 수 값은 이미지에서 윈도우를 몇 개로 가져갈지와도 상관이 있을 것 같습니다. 그리고 윈도우 갯수는 크면 클수록 더 정확한 차선을 찾아내겠지만 그만큼 계산에 대한 비용이 커지기 때문에 어떤 부분을 중요시 할지는 따져봐야 할 부분일 것 같습니다.

 

아래에서 pntIndexArrary는 현재 윈도우 내부에 있는 포인트 인덱스 정보를 갖는 변수입니다.

void reCenterCurrentPos(vector<Point> pntIndexArray, int *currentXPos)
{
    // check the length of pointIndexArray is bigger than minNumPix or not.
    if (pntIndexArray.size() > minNumPix)
    {
        // re-center window center x coordinates with mean values for the array.
        *currentXPos = mean_vectorArray(pntIndexArray);
    }
    else
    {
        // no need to process
    }
}

 

Window Search를 진행하면, 최종적으로 연속된 직사각형 내부에 위치하고 있는 왼쪽 그리고 오른쪽 차선에 대한 인덱스 데이터를 획득할 수 있으며 다음 단계(다항식 근사 등)에서 사용할 수 있습니다. 이 부분은 단일 이미지뿐만 아니라 동영상 그리고 카메라 입력에도 연속적으로 동작할 수 있습니다.

 

Window Search.

깃헙 링크 🔗 

void winSearchImg(Mat preprocess,
                  int numWindow,
                  vector<int> xBase,
                  vector<vector<Rect>> **rectWindowInfo,
                  vector<vector<Point>> *leftPixelContainer,
                  vector<vector<Point>> *rightPixelContainer);

 

반응형