Closed10

FlutterでOpenCVを使えるか

SwiftでOpenCVを使う

Swift(ContenView.swift)

struct ContentView: View {
    private let grayImage = OpenCVManager.gray(UIImage(named: "icon"))!
    var body: some View {
        VStack {
            Image("icon")
            Image(uiImage: grayImage)
        }
    }
}

Objective-C++(OpenCVManager.mm)


#ifdef __cplusplus
#import <opencv2/opencv.hpp>
#import <opencv2/highgui.hpp>
#import <opencv2/imgcodecs/ios.h>
#import "OpenCVManager.h"
#endif
#import <Foundation/Foundation.h>

@implementation OpenCVManager : NSObject

+ (UIImage*)gray:(UIImage*)image {
   cv::Mat img_Mat;
   UIImageToMat(image, img_Mat);
   cv::cvtColor(img_Mat, img_Mat, cv::COLOR_BGR2GRAY);
   return MatToUIImage(img_Mat);
}

@end

ヘッダファイル(UseOpenCVwithSwift-Bridging-Header.h)


#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "OpenCVManager.h"

@interface OpenCVManager : NSObject

+ (UIImage *)gray:(UIImage *)image;

@end

ヘッダファイル(OpenCVManager.h)

何も変更しないよ。(自動で作られたコードのみ)

#ifndef OpenCVManager_h
#define OpenCVManager_h


#endif /* OpenCVManager_h */

実行結果

大量の警告文

CocoapodでOpenCVを導入した場合、大量の警告文が表示される。この事例は多く、公式の対応を待つしかなさそうかな?

https://answers.opencv.org/question/133898/documentation-warnings-w-xcode/

参考文献

https://qiita.com/john-rocky/items/0ac8ffcbf9b8f4227fd5

UInt8List ⇆ UInt8 及び画像変換の技術調査

MethodChannelは、現状MessageCodecしか対応していない。
そのため、

以上のバイトデータを画像に変換し、OpenCVの処理をしなければならない。また、Unit8List符号なし8bit固定長の整数値型のため、符号なし8bit整数値型UInt8で処理した方がデータ変換でエラーは減ると思う。(SInt32,Sint64,Float64の情報がなく不安という理由もあり)

Dart側

https://cbtdev.net/dart-image-library/#toc9

Image -> Unit8List

// Dart Imageのバイトデータ
Uint8List bytes = image.getBytes()

// v = List<int> を Uint8List に変換
Uint8List bytes = Uint8List.fromList(v);

https://flutteragency.com/how-to-read-bytes-of-a-local-image-file-in-flutter/
https://newbedev.com/how-to-read-bytes-of-a-local-image-file-in-dart-flutter

v = List<int> -> Image

// v = List<int> を Dart Image に変換
imgLib.Image image = imageLib.Image.fromBytes(width, height, v);

Flutter Widgetでの画像表示

// Dart Image を List<int> に変換
List<int> _imageBytes = imgLib.encodeJpg(image);

// Flutter の Image ウィジェットを以下のように書きます
Image.memory(_imageBytes),

Swift側

UIImage -> Data

// pngに変換する
image.pngData()

// jpegに変換する
// compressionQualityには0~1の範囲で圧縮率を指定する
image.jpegData(compressionQuality: 1)

https://qiita.com/shocho0101/items/1cabe4aeb5edc9181328

Data -> UInt8 に変換

https://qiita.com/haru15komekome/items/c8d9c3a1a11f0813acdb

UInt8 -> UIImage に変換

let data = NSData(bytes: [UInt8], length: Int)
let image = UIImage(data: data)

https://stackoverflow.com/questions/33259989/converting-uint8-to-image
https://developer.apple.com/documentation/foundation/nsdata/1412793-init

SwiftでUIImage ⇆ UInt8の相互変換

コード全体

import SwiftUI

struct ContentView: View {
    let image = UIImage(named: "image")!
    @State var change = UIImage(named: "icon")!

    var body: some View {
        VStack {
            Image(uiImage: image)
            Image(uiImage: change)
        }
        .onAppear(perform: {
            // UIImage -> (JPEG)Data
            let jpegData = image.jpegData(compressionQuality: 1)!
            // Data -> Array<UInt8>
            let arrayUInt8 = jpegData.encodedHexadecimals!
            // Array<UInt8> -> Data
            let data = Data(arrayUInt8)
            // Data -> UIImage
            change = UIImage(data: data)!
        })
    }
}

extension Data {
    /// Data型をUInt8の配列に変換
    var encodedHexadecimals: [UInt8]? {
        let responseValues = self.withUnsafeBytes({ (pointer: UnsafeRawBufferPointer) -> [UInt8] in
            let unsafeBufferPointer = pointer.bindMemory(to: UInt8.self)
            let unsafePointer = unsafeBufferPointer.baseAddress!
            return [UInt8](UnsafeBufferPointer(start: unsafePointer, count: self.count))
        })
        return responseValues
    }
}

実行結果

  • 上側 : 変換前
  • 下側 : 変換後

課題

  • UIImage ⇆ UInt8 変換すると画質は少し落ちる
  • データ容量がまだ大きい
    • 本開発では画像変換に加え、OpenCVの処理をリアルタイムで実施する
    • 非同期処理で結果をFlutter(Dart)側に送信する必要がある
    • ホスト側とクライアント側どちらも画像の圧縮方法をもう少し考える必要がある
  • Array型での通信ができるかどうか
    • Uint8List UInt8で通信処理をしようとしていた
      • 8bitの画像変換は無理
    • ドキュメントには、List ⇆ Arrayしか書いていない
      • 謎が多い
        • Listの形式は?
        • Arrayの形式は?
        • 要素数は?

画像

変換する画像


https://www.pakutaso.com/20160452095post-7493.html

比較画像

解説

画像をPNG/JPEGのData型に変換

let pngData = image.pngData()!
let jpegData = image.jpegData(compressionQuality: 1)!
  • PNG : 665368 bytes
  • JPEG : 185349 bytes

圧縮率が高く、Data容量が小さいJPEG形式を採用する

(JPEG)Data -> UInt8の配列に変換

let arrayUInt8 = jpegData.encodedHexadecimals!
  • データ型 : Array<UInt8>
  • 要素数 : 185349

UInt8の配列 -> Data

let data = Data(arrayUInt8)
  • データ容量 : 185349 bytes

Data -> UIImage

change = UIImage(data: data)!

参考文献

https://glassonion.hatenablog.com/entry/2017/01/20/003716
https://qiita.com/shocho0101/items/1cabe4aeb5edc9181328
https://qiita.com/haru15komekome/items/c8d9c3a1a11f0813acdb

パッケージの干渉

パッケージが干渉する例

ダメなimport文

import 'package:flutter/material.dart';
import 'package:image/image.dart' as pkgImg;

エラーメッセージ

lib/main.dart:60:13: Error: 'Image' is imported from both               
'package:flutter/src/widgets/image.dart' and 'package:image/src/image.dart'.

Imageは両方から読み取られています。

解決策

import 'package:flutter/material.dart';
import 'package:image/image.dart' as pkgImg;

asでパッケージを別名で呼び出す。

Flutter(Dart)で画像のエンコード/デコード

標準ライブラリだと未対応のため(?)、image外部ライブラリで行う。

エンコード

import 'dart:typed_data';
import 'package:image/image.dart' as pkgImg;

...

final pathImage = File('/Users/ryo/github/UseOpenCVwithFlutter/MethodChannel/ImageConversion/check_image_conversion/image/icon.jpeg');
// JPEP Decode : List of bytes -> Jpeg Image
pkgImg.Image image = pkgImg.decodeJpg(pathImage.readAsBytesSync());
// JPEG Encode : Image -> List<Int>
List<int> jpg = pkgImg.JpegEncoder().encodeImage(image);
// List<Int> -> Unit8List
Uint8List bytesList = Uint8List.fromList(jpg);

デコード

// Unit8List -> Image
 _image = Image.memory(bytesList);

参考文献

https://api.dart.dev/stable/2.13.4/dart-typed_data/Uint8List/Uint8List.fromList.html
https://pub.dev/packages/image

Image ⇆ Unit8List 変換し、画像を表示するサンプルプログラム

コード

import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:image/image.dart' as pkgImg;

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  void _convertImage() {
    final pathImage = File('/Users/ryo/github/UseOpenCVwithFlutter/MethodChannel/ImageConversion/check_image_conversion/image/icon.jpeg');

    // JPEP Decode : List of bytes -> Jpeg Image
    pkgImg.Image image = pkgImg.decodeJpg(pathImage.readAsBytesSync());
    // JPEG Encode : Image -> List<Int>
    List<int> jpg = pkgImg.JpegEncoder().encodeImage(image);
    // List<Int> -> Unit8List
    Uint8List bytesList = Uint8List.fromList(jpg);
    setState(() {
      // Unit8List -> Image
      _image = Image.memory(bytesList);
    });
  }

  Image _image = Image.asset('image/sample.jpg');

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Image.asset('image/sample.jpg'),
            _image,
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _convertImage,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

実行結果

変更前

変更後

Uint8Listを非同期で相互通信は負担が大きすぎる

エラーコード

Could not cast value of type 'FlutterStandardTypedData' (0x10561bdd8) to 'NSArray' (0x10561c090).
Lost connection to device.

エラーの内容は、通信処理でメモリを大量消費する。その結果、メモリ不足でOSがアプリを強制終了する。

コード

Dart

Future<void> _getMethodNumber() async {
    String methodText;
    final pathImage = File('/Users/ryo/github/UseOpenCVwithFlutter/MethodChannel/ImageConversion/connect_image/images/icon.jpeg');
    // JPEP Decode : List of bytes -> Jpeg Image
    pkgImg.Image image = pkgImg.decodeJpg(pathImage.readAsBytesSync());
    // JPEG Encode : Image -> List<Int>
    List<int> jpg = pkgImg.JpegEncoder().encodeImage(image);
    // List<Int> -> Unit8List
    Uint8List bytesList = Uint8List.fromList(jpg);

    try {
      int result = await platform.invokeMethod('getMethodText', bytesList);
      methodText = '$result';
    } on PlatformException catch (e) {
      methodText = "Failed to get : '${e.message}'";
    }
  }

参考文献

https://bleepcoder.com/ja/flutter/363896806/lost-connection-to-device

MethodChannelで画像データをコールバックする

ImageBase64にエンコードし、画像データをDart⇆Swfit間で非同期処理させる。

コード

Dart

import 'dart:convert';
import 'dart:io' show File;
import 'dart:typed_data' show Uint8List;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image/image.dart' as pkgImg;

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  static const platform = const MethodChannel('samples.flutter.dev/image');
  Image _image = Image.asset('images/sample.jpg');

  Future<void> _convertImage() async {
    try {
      final pathImage = File('/Users/ryo/github/UseOpenCVwithFlutter/MethodChannel/ImageConversion/connect_image/images/icon.jpeg');
      // JPEP Decode : List of bytes -> Jpeg Image
      pkgImg.Image image = pkgImg.decodeJpg(pathImage.readAsBytesSync());
      // JPEG Encode : Image -> List<int>
      List<int> encodeJPEG = pkgImg.JpegEncoder().encodeImage(image);
      // base64 Encode : List<int> -> String(base64)
      String base64 = base64Encode(encodeJPEG);

      String result = await platform.invokeMethod('getBase64', base64);
      
      // base64 Decode : String(base64) -> Unit8List
      Uint8List bytes = base64Decode(result);

      setState(() {
        _image = Image.memory(bytes);
      });
    } on PlatformException catch (e) {
      print(e);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Material(
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            Image.asset('images/sample.jpg'),
            ElevatedButton(
              child: Text('Convert Image'),
              onPressed: _convertImage,
            ),
            _image,
          ],
        ),
      ),
    );
  }
}

Swift

import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
      
    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let methodChannel = FlutterMethodChannel(name: "samples.flutter.dev/image",
                                              binaryMessenger: controller.binaryMessenger)
                                     
    methodChannel.setMethodCallHandler(
      {
        [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in

        guard call.method == "getBase64" else {
          result(FlutterMethodNotImplemented)
          return
        }

        let parameters = call.arguments 
        result(parameters)
      }
    )
        
    GeneratedPluginRegistrant.register(with: self)   
    return super.application(application, didFinishLaunchingWithOptions: launchOptions) 
  } 
}

実行結果


懸念点

final pathImage = File('/Users/ryo/github/UseOpenCVwithFlutter/MethodChannel/ImageConversion/connect_image/images/icon.jpeg');
// JPEP Decode : List of bytes -> Jpeg Image
pkgImg.Image image = pkgImg.decodeJpg(pathImage.readAsBytesSync());
// JPEG Encode : Image -> List<int>
List<int> encodeJPEG = pkgImg.JpegEncoder().encodeImage(image);
// base64 Encode : List<int> -> String(base64)
String base64 = base64Encode(encodeJPEG);

この部分を非同期で処理させた方が、パフォーマンスあがるかな?
非同期処理 = 別スレットで処理
という感じで、ザックリしか理解していないから非同期処理をちゃんと勉強してから判断しよう。

参考文献

https://ja.wikipedia.org/wiki/Base64
https://api.dart.dev/stable/2.13.4/dart-convert/base64Encode.html
https://api.dart.dev/stable/2.13.4/dart-convert/base64Decode.html

OpenCVをCocoapodsでインストールするとエラーを吐く

The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.

というエラーを吐き、pod updateや'pod install'で解決出来ない時

https://dolphinetech.com/flutter/error-running-pod-install/

'OpenCVManager'というインターフェースの宣言を見つけることができない...


懸念点(問題の考えられる発生理由)

  • FlutterでCocoapod経由だと多くのエラーがある
  • Flutter/Debug.xcofig & Flutter/Release.xconfig がライブラリの導入後自動で編集されていた
  • Objective-C追加時、ヘッダーファイルを自動で追加するか聞かれなかった(ヘッダーファイルは手動で追加した)
このスクラップは2021/11/17にクローズされました
ログインするとコメントできます