WallStudio

技術ブログや創作ブログに届かない雑記です

喋る同人誌が作りたい!⑧ iOS版+OpenCV リアルタイムカメラ処理

iOSAndroidのCamera2 APIに比べてかなりシンプルに利用できました。

// UIに描き戻すだけ
class AVCaptureViewController: UIViewController{
    
    @IBOutlet weak var matchPreviewView: UIImageView!
    
    let avCapture = AVCapture()
    let openCv = OpenCv()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        avCapture.delegate = self
    }
    
    func capture(image: UIImage) {
        matchPreviewView.image = openCv.filter(image)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

// カメラに関するクラス
class AVCapture:NSObject, 
        AVCaptureVideoDataOutputSampleBufferDelegate {
    
    var captureSession: AVCaptureSession!
    var delegate: AVCaptureViewController?

    var counter = 0 //更に処理を少なくする
    
    override init(){
        super.init()
        
        captureSession = AVCaptureSession()
        
        // 解像度
        captureSession.sessionPreset = AVCaptureSessionPreset640x480
        
        let videoDevice = AVCaptureDevice.defaultDevice(
                withMediaType: AVMediaTypeVideo) // カメラ
        videoDevice?.activeVideoMinFrameDuration 
                = CMTimeMake(1, 30)// 1/30秒 (1秒間に30フレーム)
        
        let videoInput = try! AVCaptureDeviceInput.init(device: videoDevice)
        captureSession.addInput(videoInput)
        
        let videoDataOutput = AVCaptureVideoDataOutput()
        videoDataOutput.setSampleBufferDelegate(
                self, queue: DispatchQueue.main)
        // ピクセルフォーマット(32bit BGRA)
        videoDataOutput.videoSettings 
                = [kCVPixelBufferPixelFormatTypeKey 
                as AnyHashable : Int(kCVPixelFormatType_32BGRA)]
        // 処理中の場合は、フレームを破棄する
        videoDataOutput.alwaysDiscardsLateVideoFrames = false 
        captureSession.addOutput(videoDataOutput)
        DispatchQueue.global(qos: .userInitiated).async {
            self.captureSession.startRunning()
        }
    }
    
    // 新しいキャプチャの追加で呼ばれる(1/30秒に1回)
    func captureOutput(_ captureOutput: AVCaptureOutput!, 
            didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, 
            from connection: AVCaptureConnection!) {
        if (counter % 5) == 0 {
            let image = imageFromSampleBuffer(sampleBuffer: sampleBuffer)
            delegate?.capture(image: image)
        }
        counter += 1
    }
    
    func imageFromSampleBuffer(sampleBuffer :CMSampleBuffer) 
            -> UIImage {
        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))
        return image
    }
}

という感じでカメラ映像を取得するだけならこんな感じです…が、それをOpenCVで加工・分析していかなければなりません。それがどうせSwiftラッパーがあるだろうと思っていたのですが、ないんですよこれが…つまり、Objective-CまたはC++で書くしかないのですわよ。

// C++
using namespace std;
using namespace cv;

class FeaturedImage{
    Ptr<AKAZE> akaze;
    Mat mat;
public:
    Ptr<vector<cv::KeyPoint>> keyPoints;
    Ptr<Mat> descriptors;
    FeaturedImage(const cv::Mat& image){
        akaze = cv::AKAZE::create();
        mat = image;
        cvtColor(mat, mat, CV_BGR2GRAY);
        // サイズが大きいとなぜかUIImageが全て描画されなくなってしまう
        resize(mat, mat, cv::Size(200, 100));
        keyPoints = new vector<KeyPoint>();
        descriptors = new Mat();
        akaze->detectAndCompute(mat, noArray(), *keyPoints, *descriptors);
        drawKeypoints(mat, *keyPoints, mat);
    }
    FeaturedImage(const NSString& path){
        
    }
    ~FeaturedImage(){
        mat.release();
        keyPoints.release();
        descriptors.release();
        akaze.release();
    }
    cv::Mat getMat(){
        return mat;
    }
};

// Objective-C
@implementation OpenCv : NSObject
- (id) init {
    return self;
}

-(UIImage *)Filter:(UIImage *)image{
    // 方向を修正
    UIGraphicsBeginImageContext(image.size);
    [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
    image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    
    //UIImageをcv::Matに変換
    cv::Mat mat;
    UIImageToMat(image, mat);
    FeaturedImage fi(mat);
    return MatToUIImage(fi.getMat());
}
@end

つらみ(´・ω・`)

加えてSSではちゃんと特徴点をマークした画像が表示されていますが、AKAZEに掛ける画像のサイズを200x200ぐらい以上にしたとたん、UIに画像が描画されなくなってしまいます。デバッガ上では正しく処理されていく様は確認できるのですが、なぜか UIImageView.image = processedImage が反映されません。メモリリークしているんじゃないかと思っているのですが、プロファイラでは確認できず、詳細な調査が必要そうで先が思いやられます。