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
# 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
沒有留言:
張貼留言