はじめまして!エンジニアのs.tです!
カメラで麻雀牌を写したらOCRのように自動で牌を認識して計算までしてくれたら便利だなと思ったので、今回OpenCVを使ったiOSの画像処理に挑戦してみようと思います。

1.OpenCVを使う

OpenCVの組み込みについては過去の記事に記載されていますのでこちらを参照してください。

2.映像のリアルタイム処理を行う

わざわざ写真を撮影してそれを画像処理していては、写りが悪くて処理に失敗した場合に時間と手間がかかってしまいます。
そのため今回はAVCaptureVideoDataOutputを使った映像のリアルタイム処理を行います。

仕組みとしてはざっくり書くと以下のようになっています。

出力値であるAVCaptureOutputとしてAVCaptureVideoDataOutputを使用することで、AVCaptureVideoDataOutputSampleBufferDelegateにより背面カメラからの入力映像をCMSampleBufferとしてリアルタイムに取得することができます。
CMSampleBufferを以下のようにUIImageに変換することで画像処理を容易にします。

// AVCaptureVideoDataOutputSampleBufferDelegateのDelegateメソッド
func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) {
let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)!

// イメージバッファのロック
CVPixelBufferLockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))

// 画像情報を取得
let base = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0)!
let bytesPerRow = UInt(CVPixelBufferGetBytesPerRow(imageBuffer))
let width = UInt(CVPixelBufferGetWidth(imageBuffer))
let height = UInt(CVPixelBufferGetHeight(imageBuffer))

// ビットマップコンテキスト作成
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitsPerCompornent = 8
let bitmapInfo = CGBitmapInfo(rawValue: (CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue) as UInt32)
let newContext = CGContext(data: base, width: Int(width), height: Int(height), bitsPerComponent: Int(bitsPerCompornent), bytesPerRow: Int(bytesPerRow), space: colorSpace, bitmapInfo: bitmapInfo.rawValue)! as CGContext

// 画像作成
let imageRef = newContext.makeImage()!
let image = UIImage(cgImage: imageRef, scale: 1.0, orientation: UIImageOrientation.right)

// イメージバッファのアンロック
CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))
}

3.OpenCVで麻雀牌のエッジを抽出する

「2.映像のリアルタイム処理を行う」で取得したUIImageを使って画像内のエッジの検出を行います。

「1.OpenCVを使う」で紹介した記事にも記載されていますが、OpenCVの処理を書くためにはC++のmmファイルが必要です。
このmmファイルで画像処理を行なっていきます。

まずはエッジの検出がしやすいように画像の二値化を行います。

// UIImageをcv::Matに変換
cv::Mat mat;
UIImageToMat(uiImage, mat);

// 二値化用のcv::Mat
cv::Mat grayMat;
cv::cvtColor(mat,grayMat,CV_BGR2GRAY);

// 二値化
cv::threshold(grayMat, grayMat, 200, 255, CV_THRESH_TOZERO_INV );
cv::bitwise_not(grayMat, grayMat);
cv::threshold(grayMat, grayMat, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU);

次にエッジの検出を行います。
今回はエッジによって囲まれたエリアがある程度の面積を有するものだけを抽出します。

// エッジを検出
std::vector<std::vector> contours;
std::vector hierarchy;
cv::findContours(grayMat, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_TC89_L1);
int maxLevel = 0;
for(int i = 0; i < contours.size(); i++){ double area = contourArea(contours[i],false); if(area > 15000) {
cv::drawContours(mat, contours, i, cv::Scalar(255, 0, 0, 255), 3, CV_AA, hierarchy, maxLevel);
}
}

// UIImageに変換
UIImage resultUiImage = MatToUIImage(mat);

出力結果は以下のようになりました。(エッジが赤い線で表示されています。)

以上の結果から得られたエッジ情報を使って、麻雀牌を一つずつ切り出してパターンマッチングで牌の認識を行おうとしましたが、時間が足りなかったので今回はここまでとさせていただきます。

Follow me!

投稿者プロフィール

s.t