2022年8月10日 星期三

Linux 上讓 Flutter 3.0 支援 opencv

opencv 用在影像處理上, 非常方便, 且原始碼用 c++ 寫的, dart 語言透過 ffi 便能與 c 語言相互交流, 近來 Flutter 3.0 支援 linux desktop 也趨成熟, 運用 cmake 結合 c 程式庫, 在 linux 上寫 GUI 程式, 用來處理影像就不再是難事.
1. 先用終端機建立一個專案
    flutter  create  project1
        
2.在專案的 lib 目錄下建立一個 cpp 目錄, 將  c 原始碼放於此, 另外 build 目錄放編譯時的檔案
    cd  project1/lib  && mkdir  cpp  && mkdir  build
       
3. 安裝 opencv-dev 開發程式庫及 cmake,  pkg-config 等工具程式:
    sudo  apt  install   libopencv-dev   cmake   pkg-config
      
4. 在 lib 目錄下建立一個檔案 CMakeLists.txt, 將以下內容存檔:
# lib/CMakeLists.txt
    cmake_minimum_required(VERSION 3.16.3)
    project("native")      
    find_package(PkgConfig  REQUIRED)
    pkg_check_modules(OpenCV   REQUIRED   IMPORTED_TARGET   opencv4)

    include_directories(
        cpp
        ${OpenCV_INCLUDE_DIRS}
    )
    add_library(native  SHARED
        cpp/native.cpp
    )   
    target_link_libraries(native
        ${OpenCV_LIBRARIES}
    )

    
5. 編輯 porject1/lib/cpp/native.h , porject1/lib/cpp/native.cpp 示範呼叫 opencv:
// lib/cpp/native.cpp
#include <vector>
#include "native.h"
using namespace std;
using namespace cv;
extern "C" { // export as C function
    ImgStruct  grayJpeg(unsigned char *bin = nullptr, int size = 0) {// 解碼 bin, 轉成 jpeg 灰階影像   
        static vector<unsigned char> jpg;
        vector<unsigned char>().swap(jpg);
        if (bin == nullptr) return ImgStruct {.size = 0, .data = nullptr};
        imencode(".jpg",
            imdecode(Mat(1, size, CV_8UC1, bin), IMREAD_GRAYSCALE),
            jpg
        );
        return ImgStruct {
            .size = (int) jpg.size(),
            .data = jpg.data()
        };
    }
}

// lib/cpp/native.h
    #ifdef __cplusplus
        extern "C" {
    #endif
    #ifndef Native_H
    #define Native_H
        struct ImgStruct {
            int  size;
            unsigned char *data;
        };

        ImgStruct  grayJpeg(unsigned char*, int);        
    #endif
    #ifdef __cplusplus
        }
    #endif   

6.進入 project1/linux 建好程式庫目錄的聯結:
            cd   porject1/linux  &&  ln  -sf  ../lib  .
   接著編輯  project1/linux/CMakeLists.txt, 拉到最後面, 加入以下文字:   
    add_subdirectory("./lib")
    set(nativeSO "${PROJECT_BINARY_DIR}/lib/libnative.so")
    install(FILES  ${nativeSO}  DESTINATION  ${INSTALL_BUNDLE_LIB_DIR}  COMPONENT Runtime)
     
7. 編輯主程式   project1/lib/main.dart 及繪圖程式 project1/lib/mjpgViewer.dart:
//主程式:  lib/main.dart
    import 'package:flutter/material.dart';
    import 'mjpgViewer.dart';
    void main() {
        WidgetsFlutterBinding.ensureInitialized();
        runApp(MyApp());
    }
    class MyApp extends StatelessWidget {
        const MyApp({Key? key}) : super(key: key);
        @override
        Widget build(BuildContext context) {
            return MaterialApp(
                theme: ThemeData.dark(),
                home: const HomePage(),
                title:"MjpgViewer"
            );
        }
    }
    class HomePage extends StatelessWidget{
        const HomePage({Key? key}) : super(key: key);
        @override
        Widget build(BuildContext context) {
            return Scaffold(
                body: MjpgViewer(MediaQuery.of(context).size)  
            );
        }
    }
   
// 繪圖程式: lib/mjpgViewer.dart
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
import 'dart:ffi' hide Size;
import "package:ffi/ffi.dart";
import 'package:flutter/services.dart';

typedef  CharPtr = Pointer<Uint8>;
class ImgStruct extends Struct {
  @Int32() external int length;
  external CharPtr data;
  Uint8List asTypedList() => data.asTypedList(length); // method to get data
}


    class MjpgViewer extends StatefulWidget {
        final Size bodySize;
        MjpgViewer(Size? size): bodySize = size ?? Size(400, 400);
        @override _mjpgViewerState createState() => _mjpgViewerState();
    }
    class _mjpgViewerState extends State<MjpgViewer> {
        ui.Image? assetsImage;
        @override  void  initState( ) {
            super .initState( );
            final libso = DynamicLibrary.open("libnative.so");// load share library
            
            final grayJpeg = libso.lookupFunction<ImgStruct Function(CharPtr, Int),
               
ImgStruct Function (CharPtr, int)
            >("grayJpeg");


            rootBundle.load("assets/test.jpg").then((ByteData bytes) {
                final Uint8List u8List = bytes.buffer.asUint8List();
                final int len = u8List.lengthInBytes;
                final CharPtr jpg = malloc.allocate<Uint8>(len);//預先分配動態記憶空間         
                jpg.asTypedList(len).setAll(0, u8List);// 因無法獲得 u8List 的指標, 只能複製後再傳過去, 從 u8List 位置 0 開始, 複製到底.
                final ImgStruct  img = grayJpeg(jpg, len);
                decodeImageFromList(img.asTypedList( )).then(
                  (_uiImg) => setState(()=> assetsImage = _uiImg)
                );
                malloc.free(jpg);// 動態分配的空間, 不用時必須釋放掉
            });
        }
        @override Widget build(BuildContext context) => CustomPaint(
            painter: CavasDraw(assetsImage),
            size: widget.bodySize,
        );
    }
    class CavasDraw extends CustomPainter {
        final ui.Image? background;
        CavasDraw(this.background);
        @override bool shouldRepaint(CavasDraw old) => true;
        @override void paint(Canvas canvas, Size size){
            final bg = background;   
            if (bg != null) {
                final pen = Paint();
                pen.strokeWidth = 1;
                pen.style = PaintingStyle.stroke;
                final src = Rect.fromLTWH(0, 0, bg.width.toDouble(), bg.height.toDouble());
                final dst = Rect.fromLTWH(0, 0, size.width, size.height);
                canvas.drawImageRect(bg, src, dst, pen);
            }
        }
    }

// project1/pubspec.yaml
name: project1
description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1

environment:
  sdk: ">=2.17.6 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  ffi: ^2.0.1
 
flutter:
  assets:
    - assets/test.jpg
  uses-material-design: true

最後進入專案 project1 的目錄, 建個子目錄 assets, 將 test.jpg 放進該目錄內, 執行:
    cd project1  &&  flutter  pub get  &&  flutter  run  --no-version-check

後記:

1. 整個專案目錄結構
project1/
    pubspec.yaml
    assets/
        test.jpg
    lib/
        main.dart
        mjpgViewer.dart
        CMakeLists.txt
        CMakeLists.android

        cpp/
            native.cpp
            native.h
    linux/
        CMakeLists.txt
        lib  -> ../lib
    android/
        lib/
            CMakeLists.txt  -> ../../lib/CMakeLists.android
            cpp  -> ../../lib/cpp

        app/
           build.gradle

2. 參考之前文章 https://masontseng.blogspot.com/search?q=opencv 用 OpenCV jni 來編譯能在 Android 上跑的 share library, 稍微修改 CMakeLists.txt, 將它取名為 CMakeLists.android
# CMakeLists.android
    cmake_minimum_required(VERSION 3.16.3)
    project("native")   
    set(OpenCV_DIR  /home/mint/Downloads/OpenCV-android-sdk/sdk/native/jni)
    find_package(OpenCV  REQUIRED)

    include_directories(
        cpp
        ${OpenCV_INCLUDE_DIRS}
    )
    add_library(native SHARED
        cpp/native.cpp
    )
    target_link_libraries(native
        -ljnigraphics
        ${OpenCV_LIBS}
    )

 但 cmake 只認得 CMakeLists.txt, 因此用 ln -sf 符號聯結的方式來解決檔名的問題, 也不需複製.
        cd Project1/android &&  mkdir  lib  &&  cd  lib
        ln   -sf   ../../lib/cpp   .
        ln   -sf   ../../lib/CMakeLists.android    CMakeLists.txt
 
最後編輯 project1/android/app/build.gradle, 加入 externalNativeBuild, 將 CMakeLists.txt 路徑設定好就可以了:
 android {
    //...
    externalNativeBuild {
        cmake {
            path "../lib/CMakeLists.txt"
        }
    }

}
p.s. 編譯成 Android 可以用的 share library, 參考文章:   https://levelup.gitconnected.com/port-an-existing-c-c-app-to-flutter-with-dart-ffi-8dc401a69fd7

沒有留言:

張貼留言

使用 pcie 轉接器連接 nvme SSD

之前 AM4 主機板使用 pcie ssd, 但主機板故障了沒辦法上網, 只好翻出以前買的 FM2 舊主機板, 想辦法讓老主機復活, 但舊主機板沒有 nvme 的界面, 因此上網買了 pcie 轉接器用來連接 nvme ssd, 遺憾的是 grub2 bootloader 無法識...