이번 포스팅에서는 Haar 특징 기반의 케스케이드 객체 검출원리에 대해 알아보고 얼굴을 감지하는 특징(feature)을 구현한다.

Cascade Classifier와 얼굴 검출 원리

Haar 객체 검출기를 사용하는 객체 검출은 효과적인 객체 검출 방식으로 Paul Viola 와 Michael Jones의 의해 2001년에 고안되었다. (논문)

해당 논문은 머신러닝 기반의 접근방식으로, Positive 이미지와 Negative 이미지들로부터 케이스케이드 특징을 학습시키고, 다른 이미지내에 있는 객체들을 검출하는데 그것을 사용하게 된다.

Positive 이미지는 검출하고자 하는 객체가 포함된 이미지를 의미합니다. Negative 이미지는 그 반대의 경우를 의미한다.

얼굴 검출을 하기 위해서는 분류기(classifier)를 학습시켜야 하는데, (Postive 및 Negative 둘 다) 많은양의 이미지가 필요하다. 그럴려면 이미지들로부터 특징(feature)들을 추출해야 하는데, 헝가리의 수학자 Haar가 만든 특징 추출 방식을 이용하게 된다.

Haar 특징 추출 방식

위 그림에 보이는 것들은 단지 나선형 커널에 불과하다. 각 특징은 검은색 사각형 픽셀 합에서 흰색 사각형 픽셀 합을 뺀 단일값이다.

각 커널의 가능한 모든 크기와 위치를 이용하여 많은 특징들을 계산하게 된다. 한번 상상해보자, 24*24 윈도우라면 160,000개가 넘는 특징이 생성된다.

아까 언급했듯이 각 특징 계산을 위해서 흰색 사각형과 검은색 사각형에 있는 픽셀들의 합을 찾아야하는데, 이를 해결하기 위해 적분 이미지(integral image)를 도입했다. 이는 이미지가 아무리 크더라도 주어진 픽셀에 대한 계산을 4픽셀만 포함하는 작업으로, 연산량을 획기적으로 줄이게 된다.

원본 이미지를 적분 이미지로 변환

그러나 우리가 계산한 이러한 모든 특징들 대부분은 관련이 없다. 아래의 이미지를 예로 들어보자.

맨 윗 행은 좋은 특징 두가지를 보여준다. 첫 번째로 선택한 특징은 눈의 영역이 코와 뺨의 영역보다 더 어둡다는 속성에 초점을 맞춘다. 선택된 두 번째 특징은 눈이 콧대보다 어둡다는 특성에 의존한다. 그러나 같은 윈도우들을 볼이나 다른 곳에 적용하는 것은 부적절하다. 그렇다면 16만개 이상의 특징 중에서 특징을 어떻게 선택할까? 이 부분은 Adaboost로 해결할 수 있다.

이를 위해, 모든 학습용 이미지에 각각의 모든 특징들을 적용한다. 각 특징에 대해 얼굴(object)을 Positive 및 Negative로 분류하는 최상의 임계값을 찾는다. 분명히 오류 또는 잘 못 분류된 케이스들이 있을 것이다. 얼굴과 얼굴이 포함되지 않은 이미지를 가장 정확하게 분류하는 특징인 최소 오류율을 가진 특징을 선택한다. 처리과정이 말처럼 간단하진 않다. 처음에 각 이미지에 동일한 가중치가 부여하고, 분류 후 잘못 분류된 이미지의 가중치를 증가시킨다. 그런 다음 동일한 프로세스를 다시 수행한다. 새로운 오류율이 계산되고, 새로운 가중치가 또 부여된다. 이 프로세스는 필요한 정확도, 오류율 달성 또는 필요한 특징 수를 찾을 때까지 계속하면 된다.

최종적으로 생선되는 분류기는 이러한 약한 분류기(weak classifier)의 가중 합이다. 그것만으로는 이미지를 분류할 수 없기 때문에 약하다고 불리지만, 다른 것들과 함께 강력한 분류기(strong classifier)를 형성한다. 이 논문은 심지어 200개의 특징이 95%의 정확도를 가지고 탐지할 수 있다고 설명한다. 최종 설정된 분류기에는 약 6000개의 특징들이 포함되었다. 16만개이상의 특징들이 6000개의 특징들로 감소한다고 상상해보자. (개이득)

이제 이미지를 24×24 윈도우로 가져오자. 여기에 6000개의 특징들을 적용하고, 얼굴인지 아닌지 확인해보자. 좀 비효율적이고 시간이 많이 걸리는거 아닌가라고 의심할 수 있다. 사실 그렇다, 그렇기에 Viola와 Jones는 이에 대한 좋은 해결책을 제시했다.

생각해보면 이미지에서 대부분은 얼굴이 아닌 영역이다. 따라서 윈도우에서 얼굴 영역이 아닌 부분을 확인해보는 것이다. 얼굴영역이 아니라면 폐기해버리고 더 이상 작업을 수행하지 않으면 된다. 얼굴이 있을 수 있는 영역에 집중하면 된다. 이렇게 하면 얼굴이 있을만한 영역을 확인하는데 더 많은 시간을 할애할 수 있다.

이를 위해 Viola와 Jones는 Cascade of Classifiers 개념을 도입했고, 6000개 특징을 하나의 윈도우에 모두 적용하는 대신 특징들을 분류기의 여러 단계로 그룹화하여 하나씩 적용한다. 일반적으로 처음 몇 단계에는 매우 적은 수의 특징들만 포함된다. 윈도우가 첫 번째 단계에서 실패하면 더 이상 진행하지 않고 폐기해버린다. 나머지 특징들은 고려하지 않는다. 만약 통과한다면 두 번째 단계를 적용하고 프로세스를 계속 진행한다. 모든 단계를 통과하는 윈도우가 얼굴 영역이 되는 것이다.

위 논문에서의 얼굴 검출기는 처음 5단계에서 (순서대로) 1, 10, 25, 25 및 50개의 특징을 갖고 38단계에서 6000개 이상의 특징을 갖는다. (위 이미지의 특징 2개는 실제로 Adaboost에서 가장 좋은 특징 두가지를 가져온 것이다). 저자에 따르면 6000개 이상의 특징 중 평균 10개 특징이 윈도우 일부 영역에 사용된다고 한다.

얼굴 검출 예제코드

CascasdeClassifier 객체 생성

val cascadeClassifier = CascadeClassifier(모델 파일명)

// 또는 

val cascadeClassifier = CascadeClassifier().apply {
    load(모델 파일명)
}

모델 학습

OpenCV에서는 학습하기 위한 방법이나 학습된 모델들을 제공하고 있다.

OpenCV를 활용한 컴퓨터 비전과 딥러닝

멀티스케일 객체 검출 함수

detectMultiScale(
    Mat image, 
    MatOfRect objects, 
    double scaleFactor, 
    int minNeighbors,
    int flags, 
    Size minSize, 
    Size maxSize
)
  • image: 입력 영상(회색조 이미지; CV_8U)
  • objects: 검출된 객체의 사강형 정보
  • scaleFactor: 영상 축소 비율. 기본값 1.1
  • minNeighbors: 얼마나 많은 이웃 사각형이 검출되어야 최종 검출 영역으로 설정할지를 지정. 기본값은 3
  • flags: (현재) 사용되지 않음
  • minSize: 최소 객체 크기
  • maxSize: 최대 객체 크기

최종 코드

val image by remember {
        mutableStateOf(
            Utils.loadResource(context, R.drawable.lenna)
                .also { Imgproc.cvtColor(it, it, Imgproc.COLOR_BGR2RGB) })
    }
    val faceModelName = "haarcascade_frontalface_alt.xml"
    val eyesModelName = "haarcascade_eye.xml"
    val faceModelFile = copyToCacheDir(context, faceModelName)
    val eyesModelFile = copyToCacheDir(context, eyesModelName)
    val faceCascade = CascadeClassifier().apply {
        load(faceModelFile.absolutePath)
    }
    val eyesCascade = CascadeClassifier().apply {
        load(eyesModelFile.absolutePath)
    }

    val grayImage = Mat().apply {
        Imgproc.cvtColor(image, this, Imgproc.COLOR_RGB2GRAY)
        Imgproc.equalizeHist(this, this)
    }

    val dstImage = Mat().apply {
        Core.copyTo(image, this, Mat())
    }
    // -- Detect faces
    val faces = MatOfRect()
    faceCascade.detectMultiScale(grayImage, faces)
    val listOfFaces: List<Rect> = faces.toList()
    for (face in listOfFaces) {
        val center = Point(
            (face.x + face.width / 2).toDouble(),
            (face.y + face.height / 2).toDouble()
        )
        Imgproc.ellipse(
            dstImage, center, Size((face.width / 2).toDouble(), (face.height / 2).toDouble()), 0.0, 0.0, 360.0,
            RED
        )
        val faceROI: Mat = grayImage.submat(face)
        // -- In each face, detect eyes
        val eyes = MatOfRect()
        eyesCascade.detectMultiScale(faceROI, eyes)
        val listOfEyes: List<Rect> = eyes.toList()
        for (eye in listOfEyes) {
            val eyeCenter = Point(
                (face.x + eye.x + eye.width / 2).toDouble(),
                (face.y + eye.y + eye.height / 2).toDouble()
            )
            val radius = ((eye.width + eye.height) * 0.25).roundToInt()
            Imgproc.circle(dstImage, eyeCenter, radius, BLUE, 4)
        }
    }

카테고리: OpenCV

0개의 댓글

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.