전처리 과정 3 - 수평 히스토그램을 사용해 오선을 제거
그레이스케일 및 영상 이진화를 끝내고 오선 영역 밖의 데이터를 모두 제거한 후
마지막 하나의 전처리 과정을 더 수행해야 합니다.
바로 오선을 제거하는 것인데, 오선 제거는 악보를 인식하는데 있어 상당히 중요한 과제입니다.
악보 영상의 구성요소(박자, 음표, 쉼표 등)들을 수월하기 인식하기 위해서 오선을 제거합니다.
오선을 제거하는데는 수평 히스토그램을 사용합니다.
악보 영상에서 오선을 제거할 때 오선 영역 위에 존재하는 객체들이 변형되지 않게
오선의 위아래로 픽셀이 존재하지 않을 때만 제거합니다.
// ============================================================================================
// 전처리 과정 3 - 수평 히스토그램을 사용해 오선을 제거함
// ============================================================================================
JNIEXPORT jintArray JNICALL Java_com_example_practiceopencv_SheetFragment_preprocess3(
JNIEnv *env,
jobject thiz,
jlong sheetMusicAddr) {
// 로그 태그 및 악보 영상 레퍼런스 변수 생성
const char* LOGTAG = "preprocess3";
Mat &sheetMusic = *(Mat *) sheetMusicAddr;
// 악보 영상 행, 열 길이
int rows = sheetMusic.rows, cols = sheetMusic.cols;
// 그레이스케일 및 이진화
cvtColor(sheetMusic, sheetMusic, COLOR_RGB2GRAY);
threshold(sheetMusic, sheetMusic, 127, 255, THRESH_BINARY_INV | THRESH_OTSU);
// 오선 좌표와 길이를 담을 벡터를 생성
vector<int> staffsLocation;
vector<int> staffsHeight;
uchar* pixelData = sheetMusic.data;
double histogram = 0;
// 영상의 각 행을 탐색하며..
for (int y = 0; y < rows; y++) {
// 검은색 픽셀 개수만큼 변수를 증가시키고
for (int x = 0; x < cols; x++) {
if (pixelData[y * cols + x] != 255) {
histogram++;
}
}
// 픽셀의 개수가 행 길이의 50% 이상이라면 오선으로 판단한다.
if (histogram / cols > 0.5) {
// 벡터에 오선 좌표가 없다면..
if (staffsLocation.size() == 0) {
staffsLocation.push_back(y);
staffsHeight.push_back(0);
}
// 새로 발견된 오선 좌표가 이전 좌표와 1보다 크게 차이 나면 새로운 오선임
else if (abs(staffsLocation.back() - y) > 1) {
staffsLocation.push_back(y);
staffsHeight.push_back(0);
}
// 아니라면 같은 오선으로 오선 좌표와 오선 길이를 업데이트함
else {
staffsLocation[staffsLocation.size() - 1] = y;
staffsHeight[staffsHeight.size() - 1] += 1;
}
// 오선을 확대해보면 1픽셀로만 이루어진 게 아니므로 좌표와 길이를 각각 유지
}
histogram = 0;
}
// 오선 영역 제거
const int SIZE = staffsLocation.size();
pixelData = sheetMusic.data;
for (int x = 0; x < cols; x++) {
for (int i = 0; i < SIZE; i++) {
// 오선 최하단 좌표에서 오선 길이를 빼면 최상단 좌표가 나옴
int topPixel = staffsLocation[i] - staffsHeight[i];
int botPixel = staffsLocation[i];
// 최상단 좌표 위와 최하단 좌표 아래에 아무 픽셀이 없다면
if (pixelData[(topPixel - 1) * cols + x] != 0 && pixelData[(botPixel + 1) * cols + x] != 0) {
// 해당 좌표의 픽셀은 지운다.
for (int j = 0; j < staffsHeight[i] + 1; j++) {
int removePixel = staffsLocation[i] - j;
pixelData[removePixel * cols + x] = 255;
}
// 위, 아래 좌표를 확인하지 않으면 음표나 쉼표 등의 객체들이 지워질 수 있으므로 픽셀을 확인 후 지움
}
}
}
// 오선의 좌표들을 리턴 해주기 위해 JintArray로 변환
jintArray intArray = (*env).NewIntArray(SIZE);
for (int i = 0; i < SIZE; i++) {
jint element = staffsLocation[i];
(*env).SetIntArrayRegion(intArray, i, 1, &element);
}
LOGI("Function End :: %s", LOGTAG);
return intArray;
}
전처리 과정 4 - 악보 영상에 가중치를 곱해줌(객체들이 항상 비슷한 크기를 갖게 하기 위함)
또한 오선을 제거하는 과정에서 오선 간의 간격을 구할 수 있는데
이를 사용해 악보 영상의 비율을 항상 동일하게 조정할 수 있습니다.
// ============================================================================================
// 전처리 과정 4 - 악보 영상에 가중치를 곱해줌(객체들이 항상 비슷한 크기를 갖게 하기 위함)
// ============================================================================================
JNIEXPORT jintArray JNICALL Java_com_example_practiceopencv_SheetFragment_preprocess4(
JNIEnv *env,
jobject thiz,
jlong sheetMusicAddr,
jintArray staffsInfoArray) {
// 로그 태그 및 악보 영상 레퍼런스 변수 생성
const char* LOGTAG = "preprocess4";
Mat &sheetMusic = *(Mat *) sheetMusicAddr;
// 해당 함수를 호출한 자바 클래스에서 avgDistance라는 변수를 참조함
jclass cls = (*env).GetObjectClass(thiz);
jfieldID fid = (*env).GetFieldID(cls, "avgDistance", "D");
// 전처리 과정 3에서 반환했던 오선 좌표 정보를 불러옴
const int arrayLength = (*env).GetArrayLength(staffsInfoArray);
jint staffsInfo[arrayLength];
(*env).GetIntArrayRegion(staffsInfoArray, 0, arrayLength, staffsInfo);
// 악보 영상 행, 열 길이
int rows = sheetMusic.rows, cols = sheetMusic.cols;
// 오선 좌표들 간의 거리를 모두 더한 후 나눠 평균을 구함
double avgDistance = 0.00;
for (int i = 0; i < arrayLength / 5; i++) {
for (int j = 0; j < 4; j++) {
avgDistance += abs(staffsInfo[i * 5 + j] - staffsInfo[i * 5 + j + 1]);
}
}
avgDistance /= (arrayLength - arrayLength / 5);
// 가중치를 구해 영상에 곱해줌(오선 간격은 항상 20픽셀이 되도록)
const double weightStd = 20.0;
double weight = weightStd / avgDistance;
double newWidth = sheetMusic.cols * weight;
double newHeight = sheetMusic.rows * weight;
LOGI("%s :: weight : %s", LOGTAG, to_string(weight).data());
LOGI("%s :: Original width : %s, Original height : %s", LOGTAG, to_string(cols).data(), to_string(rows).data());
LOGI("%s :: Weighted width : %s, Weighted height : %s", LOGTAG, to_string(newWidth).data(), to_string(newHeight).data());
resize(sheetMusic, sheetMusic, Size(newWidth, newHeight));
// 오선 평균 거리와 오선 좌표 정보도 업데이트해 줌
LOGI("%s :: Original avgDistance : %s", LOGTAG, to_string(avgDistance).data());
avgDistance *= weight;
LOGI("%s :: Weighted avgDistance : %s", LOGTAG, to_string(avgDistance).data());
for (int i = 0; i < arrayLength; i++) {
staffsInfo[i] *= weight;
}
// 업데이트된 오선 정보를 리턴 해주기 위해 JintArray로 변환
jintArray intArray = (*env).NewIntArray(arrayLength);
for (int i = 0; i < arrayLength; i++) {
jint element = staffsInfo[i];
(*env).SetIntArrayRegion(intArray, i, 1, &element);
}
LOGI("Function End :: %s", LOGTAG);
(*env).SetDoubleField(thiz, fid, avgDistance);
return intArray;
};
'인공지능 > 컴퓨터비전' 카테고리의 다른 글
[OpenCV/Python] 악보 인식(디지털 악보 인식) - 1 (0) | 2021.08.04 |
---|---|
[OpenCV/악보인식] 광학 음악 인식 기반 자동 편곡 시스템 - 5 (2) | 2021.05.27 |
[OpenCV/악보인식] 광학 음악 인식 기반 자동 편곡 시스템 - 4 (0) | 2021.05.27 |
[OpenCV/악보인식] 광학 음악 인식 기반 자동 편곡 시스템 - 2 (0) | 2021.05.21 |
[OpenCV/악보인식] 광학 음악 인식 기반 자동 편곡 시스템 - 1 (0) | 2021.05.21 |
댓글