본문 바로가기
인공지능/컴퓨터비전

[OpenCV/악보인식] 광학 음악 인식 기반 자동 편곡 시스템 - 4

by 이민훈 2021. 5. 27.

인식 과정 1 - 객체 검출

오선까지 제거되고 나면 본 인식 과정이 남아있습니다.

 

먼저 findContours 함수로 영상 내 모든 객체를 검출할 수 있습니다.

 

    // 악보 영상 내 모든 객체를 찾음
    vector<vector<Point>> contours;
    findContours(sheetMusic, contours, RETR_LIST, CHAIN_APPROX_NONE);
    vector<RectInfo> objectInfo;
    RectInfo rectInfo;
    cvtColor(sheetMusic, sheetMusic, COLOR_GRAY2RGB);
    for (int i = 0; i < contours.size(); i++) {
        Rect rect = boundingRect(contours[i]);
        // j는 몇 번째 오선 영역인지를 나타냄
        for (int j = 0; j < arrayLength / 5; j++) {
            // 객체의 중간 y 좌표
            double yCenter = (rect.y + rect.y + rect.height) / 2;
            double errorW = rect.width / cols;
            double errorH = rect.height / rows;
            double topPixel, botPixel;
            // 객체의 가로, 세로 길이가 음표 조건에 맞는지 검사
            if ((rect.width >= weighted(20) && rect.height >= weighted(50)) || (rect.width >= weighted(20) && rect.height <= weighted(25) && rect.height >= weighted(15))) {
                topPixel = staffsInfo[j * 5] - weighted(40);
                botPixel = staffsInfo[(j + 1) * 5 - 1] + weighted(40);
            }
            // 아니라면 쉼표로 일단 판단
            else {
                topPixel = staffsInfo[j * 5] - weighted(10);
                botPixel = staffsInfo[(j + 1) * 5 - 1] + weighted(10);
            }
            // 오선 영역 안에 들어오고 일정 크기보다 작아야 음표, 쉼표로 판단
            if (rect.width >= weighted(3) && yCenter >=  topPixel && yCenter <= botPixel && errorW < 0.3 && errorH < 0.3) {
                rectInfo = {j, rect};
                objectInfo.push_back(rectInfo);
                rectangle(sheetMusic, rect, Scalar(255, 0, 0), 3);
            }
        }
    }

 

검출된 객체들

 

인식 과정 2 - 인접 객체 병합 및 중복 제거

이때 객체 하나가 여러 개로 분할되어 검출되는 것을 볼 수 있는데,

 

인접한 객체들을 병합하는 과정이 필요합니다.

 

병합하는 과정에서 생긴 중복 객체들도 제거해줍니다.

 

    // 분할된 객체들을 합침
    for (int i = 0; i < objectInfo.size(); i++) {
        for (int j = 0; j < objectInfo.size(); j++) {
            Rect r1 = objectInfo[i].rect;
            Rect r2 = objectInfo[j].rect;
            objectInfo[i].rect = mergeRect(r1, r2);
        }
    }
    for (int i = 0; i < objectInfo.size(); i++) {
        for (int j = 0; j < objectInfo.size(); j++) {
            Rect r1 = objectInfo[i].rect;
            Rect r2 = objectInfo[j].rect;
            objectInfo[i].rect = mergeRect(r1, r2);
        }
    }

    // 중복 제거
    vector<RectInfo> objectInfoEx;
    int overlap = 0;
    for (int i = 0; i < objectInfo.size(); i++) {
        if (objectInfo[i].rect.width >= weighted(15) && objectInfo[i].rect.height >= weighted(10)) {
            if (objectInfoEx.size() == 0) {
                objectInfoEx.push_back(objectInfo[i]);
            } else {
                for (int j = 0; j < objectInfoEx.size(); j++) {
                    if (objectInfo[i].rect == objectInfoEx[j].rect) {
                        overlap++;
                    }
                }
                if (overlap == 0) {
                    objectInfoEx.push_back(objectInfo[i]);
                }
                overlap = 0;
            }
        }
    }

 

병합된 객체들

 

여기까지 왔다면 이제 모든 객체를 검출하고 해당 객체가 음표인지, 쉼표인지, 음표라면 몇 분 음표인지 등

 

인식 과정만이 남아있는 상태라고 할 수 있습니다.

 

딥러닝과 같은 비교적 최근 핫해진 방법을 통해 해당 과정을 진행할 수도 있고,

 

템플릿 매칭 등의 고전적인 방법을 사용할 수도 있습니다.

 

 

저는 해당 졸작을 진행할 당시에 인공지능의 '인'자도 모르는 학부생이었기에,

 

인식하고자 하는 객체의 구조적 특징을 모두 파악하여 일일이 휴리스틱 하게 알고리즘을 구현하였습니다.

 

코딩을 한 저조차도 몇 개월이 지난 지금 코드를 제대로 보기가 힘들 정도로 복잡한데,

 

인식 과정에 대한 코드는 빼고 어떤 식으로 구현을 하였는지 정도만 포스팅하겠습니다.

댓글