文章目錄
- CGO入門和OCR文字識别(非第三方API,有源碼,效果好)實戰
-
- CGO是什麼
-
- 極簡入門
- CGO基礎類型
- 字元串、數組和函數調用
- CGO實戰
-
- 分離Go和C代碼
- 常用cgo編譯指令
- 調用C靜态庫和動态庫
-
- 靜态庫
- 動态庫
- 調用C++動态庫
- CGO的缺陷
- CGO最佳使用場景總結
- CGO案例:在Go中調用動态庫實作OCR文字識别
-
- chineseocr_lite介紹
- 識别效果
- 編譯chineseocr_lite
- 導出c函數
- 環境變量設定
- 運作
- 參考
CGO入門和OCR文字識别(非第三方API,有源碼,效果好)實戰
CGO是什麼
簡單點來講,如果要調用C++,C寫的庫(動态庫,靜态庫),那麼就需要使用Cgo。其他情況下一般用不到,隻需要知道Go能調用C就行了,當然C也可以回調到Go中。
使用Cgo有2種姿勢:
- 直接在go中寫c代碼
- go調用so動态庫(c++要用extern “c”導出)
為了熟悉CGO,我們先介紹第一種方法,直接在Go中寫C代碼。
極簡入門
引用:Command cgo
首先,
通過import “C”導入僞包
(這個包并不真實存在,也不會被Go的compile元件見到,它會在編譯前被CGO工具捕捉到,并做一些代碼的改寫和樁檔案的生成)
然後,Go 就可以使用C的變量和函數了, C.size_t 之類的類型、諸如 C.stdout 之類的變量或諸如 C.putchar 之類的函數。
func main(){
cInt := C.int(1) // 使用C中的int類型
fmt.Println(goInt)
ptr := C.malloc(20) // 調用C中的函數
fmt.Println(ptr) // 列印指針位址
C.free(ptr) // 釋放,需要 #include <stdlib.h>
}
如果“C”的導入緊跟在注釋之前,則
該注釋稱為序言
。例如:
// #include <stdio.h>
/* #include <errno.h> */
import "C"
序言可以包含任何 C 代碼,包括函數和變量聲明和定義
。然後可以從 Go 代碼中引用它們,就好像它們是在包“C”中定義的一樣。可以使用序言中聲明的所有名稱,即使它們以小寫字母開頭。例外:序言中的靜态變量不能從 Go 代碼中引用;靜态函數是允許的。
是以,你可以直接在裡面寫C代碼(注意,C++不行!):
package main
/*
int add(int a,int b){
return a+b;
}
*/
import "C"
import "fmt"
func main() {
a, b := 1, 2
c := C.add(a, b)
}
編譯下,會出現下面的問題( fmt.Println(C.add(1, 2)) 能編譯通過,思考下為什麼? ):
./main.go:20:12: cannot use a (type int) as type _Ctype_int in argument to _Cfunc_add
./main.go:20:12: cannot use b (type int) as type _Ctype_int in argument to _Cfunc_add
為什麼呢?因為C沒有辦法使用Go的類型,必須先轉換成CGO類型才可以,改成這樣就行了:
func main() {
cgoIntA, cgoIntB := C.int(1), C.int(2)
c := C.add(cgoIntA, cgoIntB)
fmt.Println(c)
}
運作後輸出:
CGO基礎類型
就像上面的代碼一樣,
Go沒有辦法直接使用C的東西,必須先轉換成CGO類型
,下面是一個基礎類型對應表。
C類型 | CGO類型 | GO類型 |
---|---|---|
char | C.char | byte |
signed char | C.schar | int8 |
unsigned char | C.uchar | uint8 |
short | C.short | int16 |
unsigned short | C.ushort | uint16 |
int | C.int | int32 |
unsigned int | C.uint | uint32 |
long | C.long | int32 |
unsigned long | C.ulong | uint32 |
long long int | C.longlong | int64 |
unsigned long long int | C.ulonglong | uint64 |
float | C.float | float32 |
double | C.double | float64 |
size_t | C.size_t | uint |
如果直接在C中#include <stdint.h>,則類型關系就比較一緻了,例如:
C類型 | CGO類型 | GO類型 |
---|---|---|
int8_t | C.int8_t | int8 |
int16_t | C.int16_t | int16 |
uint32_t | C.uint32_t | uint32 |
uint64_t | C.uint64_t | uint64 |
字元串、數組和函數調用
那麼,在Go要如何傳遞字元串、位元組數組以及指針?
CGO的C虛拟包提供了以下一組函數,用于Go語言和C語言之間數組和字元串的雙向轉換:
// Go string to C string
// The C string is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CString(string) *C.char
// Go []byte slice to C array
// The C array is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CBytes([]byte) unsafe.Pointer
// C string to Go string
func C.GoString(*C.char) string
// C data with explicit length to Go string
func C.GoStringN(*C.char, C.int) string
// C data with explicit length to Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte
字元串,可以通過C.CString()函數(
别忘記通過free釋放
):
// 通過C.CString,這裡會發生記憶體拷貝,cgo通過malloc重新開辟了一塊空間,使用完需要釋放,否則記憶體洩露
imagePath := C.CString("a.png")
defer C.free(unsafe.Pointer(imagePath))
位元組數組,直接使用go的數組,然後強制轉換即可:
// 隻能使用數組,無法使用切片用作緩沖區給C使用
var buffer [20]byte
// &buffer[0]: 數組在記憶體中是連續存儲的,取首位址
// unsafe.Pointer():轉換為非安全指針,類型是*unsafe.Pointer
// (*C.char)():再強轉一次
cBuffer := (*C.char)(unsafe.Pointer(&buffer[0]))
對應類型的指針,直接使用Cgo類型,然後&取位址即可:
bufferLen := C.int(20)
cPoint := &bufferLen // cPoint在CGO中是*C.int類型,在C中是*int類型。
假如ocr識别函數如下:
有3個參數:
- image_path:訓示了要識别的圖檔路徑。
- out_buffer:識别到的文字輸出到這裡,是一個char位元組數組。
- len:訓示輸出位元組緩沖區大小,調用成功後,值變成字元串長度,便于外界讀取。
在go中調用方式如下:
imagePath := C.CString("a.png")
defer C.free(unsafe.Pointer(imagePath))
var buffer [20]byte
bufferLen := C.int(20)
cInt := C.detect(imagePath, (*C.char)(unsafe.Pointer(&buffer[0])), &bufferLen)
if cInt == 0 {
fmt.Println(string(buffer[0:bufferLen]))
}
CGO實戰
分離Go和C代碼
為了簡化代碼,我們可以把C的代碼放到xxx.h和xxx.c中實作。
有以下結構:
├── hello.c
├── hello.h
└── main.go
hello.h的内容:
#include <stdio.h>
void sayHello(const char* text);
hello.c:
#include "hello.h"
void sayHello(const char* text){
printf("%s", text);
}
main.go中調用hello.h中的函數:
#include "hello.h"
import "C" // 必須放在導入c代碼活頭檔案的注釋後面,否則不生效
func main() {
cStr := C.CString("hello from go")
defer C.free(unsafe.Pointer(cStr))
C.sayHello(cStr)
}
常用cgo編譯指令
如果我們把h和c檔案放到其他目錄,則編譯會報錯:
├── main.go
└── mylib
├── hello.c
└── hello.h
Undefined symbols for architecture x86_64:
"_sayHello", referenced from:
__cgo_7ab15a91ce47_Cfunc_sayHello in _x002.o
(maybe you meant: __cgo_7ab15a91ce47_Cfunc_sayHello)
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
這裡應該可以使用
#cgo預編譯解決
(CFLAGS、CPPFLAGS、CXXFLAGS、FFLAGS和LDFLAGS) :
// #cgo CFLAGS: -DPNG_DEBUG=1 -I ./include
// #cgo LDFLAGS: -L /usr/local/lib -lpng
// #include <png.h>
import "C"
- CFLAGS:-D部分定義了宏PNG_DEBUG,值為1。-I定義了頭檔案包含的檢索目錄
- LDFLAGS:-L指定了連結時庫檔案檢索目錄,-l指定了連結時需要連結png庫
通常實際的工作中遇到要使用cgo的場景,都是調用動态庫的方式,是以這裡沒有繼續往下深究上面的錯誤如何解決了。
調用C靜态庫和動态庫
目錄結構如下:
├── call_shared_lib
│ └── main.go
├── call_static_lib
│ └── main.go
└── mylib
├── hello.c
├── hello.h
├── libhello.a
└── libhello.so
靜态庫
把上面的hello.h 和 hello.c 生成為靜态庫(需要安裝gcc,省略):
# 生成o對象
$ gcc -c hello.c
# 生成靜态庫
$ ar crv libhello.a hello.o
# 檢視裡面包含的内容
# ar -t libhello.a
# 使用靜态庫
#gcc main.c libhello.a -o main
在go中調用:
package main
/*
#cgo CFLAGS: -I ../mylib
#cgo LDFLAGS: -L ../mylib -lhello
#include <stdlib.h>
#include "hello.h"
*/
import "C"
import "unsafe"
func main() {
cStr := C.CString("hello from go")
defer C.free(unsafe.Pointer(cStr))
C.sayHello(cStr)
}
- CFLAGS:C的編譯參數,-I訓示 include路徑
- LDFLAGS: 連結參數,-L 訓示搜尋靜态庫的路徑,-lhello表示連結libhello.a,記住lib這裡不用寫,編譯器會自己補全。
動态庫
生成
# 生成o對象
$ gcc -fPIC -c hello.c
# 生成動态庫
$ gcc -shared -fPIC -o libhello.so hello.o
# 使用動态庫
#gcc main.c -L. -lhello -o main
Go中調用和靜态庫一樣:
package main
// 注意,生成的so檔案一定的得增加lib字首,如libhello.so
// 然後在Go中隻需要寫-lhello(代表libhello.a或libhello.so)即可
// linux下會自動增加lib字首。
/*
#cgo CFLAGS: -I ../mylib
#cgo LDFLAGS: -L ../mylib -lhello
#include <stdlib.h>
#include "hello.h"
*/
import "C"
import "unsafe"
func main() {
cStr := C.CString("hello from go")
defer C.free(unsafe.Pointer(cStr))
C.sayHello(cStr)
}
唯一不同的是,靜态庫需要指定so檔案的搜尋路徑或者
把so動态庫拷貝到/usr/lib
下,在環境變量中配置:
$ export LD_LIBRARY_PATH=../mylib
$ go run main.go
# 也可以在goland中在Run -> Edit Configurations -> Environment
# 配置 LD_LIBRARY_PATH=../mylib ,友善調試
更多關于靜态庫和動态庫的差別:https://segmentfault.com/a/1190000020651578
調用C++動态庫
本質上和調用c動态庫在Go的寫法上是一樣的,隻是需要導出成C風格的即可:
#ifdef __cplusplus
extern "C"
{
#else
// 導出C 命名風格函數,函數名字和定義的一樣,C++因為支援重載,是以導出的函數名被編譯器改變了
#ifdef __cplusplus
}
#endif
然後在go的LDFLAGS中增加 -lstdc++:
CGO的缺陷
cgo is not Go中總結了cgo 的缺點:
- 編譯變慢,實際會使用 c 語言編譯工具,還要處理 c 語言的跨平台問題
- 編譯變得複雜
- 不支援交叉編譯
- 其他很多 go 語言的工具不能使用
- C 與 Go 語言之間的的互相調用繁瑣,是會有
的性能開銷
- C 語言是主導,這時候 go 變得不重要,其實和你用 python 調用 c 一樣
- 部署複雜,不再隻是一個簡單的二進制
這篇文章描述了CGO通過go去調用C性能開銷大的原因:https://blog.csdn.net/u010853261/article/details/108186405
- 必須切換go的協程棧到系統線程的主棧去執行C函數
- 涉及到系統調用以及協程的排程。
- 由于需要同時保留C/C++的運作時,CGO需要在兩個運作時和兩個ABI(抽象二進制接口)之間做翻譯和協調。這就帶來了很大的開銷。
《GO原本》中進一步通過runtime源碼解讀了原因。
是以,使用的時候,自己靈活根據場景取舍吧。
CGO最佳使用場景總結
CGO的一些缺點:
- 記憶體隔離
- C函數執行切換到g0(系統線程)
- 收到GOMAXPROC線程限制
- CGO空調用的性能損耗(50+ns)
- 編譯損耗(CGO其實是有個中間層)
CGO 适合的場景:
-
C 函數是個大計算任務(不在乎CGO調用性能損耗)
-
C 函數調用不頻繁
- C 函數中不存在阻塞IO
- C 函數中不存在建立線程(與go裡面協程排程由潛在可能互相影響)
- 不在乎編譯以及部署的複雜性
更多可以閱讀:
- 【Free Style】CGO: Go與C互操作技術(一):Go調C基本原理
- 【Free Style】CGO: Go與C互操作技術(三):Go調C性能分析及優化
- Go語言使用cgo時的記憶體管理筆記
CGO案例:在Go中調用動态庫實作OCR文字識别
chineseocr_lite介紹
GitHub: https://github.com/DayBreak-u/chineseocr_lite
Star: 7.1 k
介紹:超輕量級中文ocr,支援豎排文字識别, 支援ncnn、mnn、tnn推理 ( dbnet(1.8M) + crnn(2.5M) + anglenet(378KB)) 總模型僅4.7M。
這個開源項目提供了C++、JVM、Android、.Net等實作,隻依賴OpenCV和微軟的Onnx推理架構,
有源碼,經作者實踐,識别效果中等,越小的圖檔越快
。
識别效果
比如識别一個發票号碼,隻需要50ms左右:
複雜的圖檔識别大概500-900ms左右:
表格識别效果一般
是以,适合格式一緻的識别場景。比如
發票的某個位置,身份證,銀行卡
等等。
編譯chineseocr_lite
按照 chineseocr_lite/cpp_projects/OcrLiteOnnx 中的README.md文檔編譯即可,推薦在Linux下,我再windows和Macos沒編譯通過。
然後需要改造成動态庫,我改動的内容有:
- 預設生成動态庫,給ocr_http_server使用
- 去掉jni的支援
- 增加ocr.h,導出c風格函數
導出c函數
ocr.h
/** @file ocr.h
* @brief 封裝給GO調用
* @author teng.qing
* @date 8/13/21
*/
#ifndef ONLINE_BASE_OCRLITEONNX_OCR_H
#define ONLINE_BASE_OCRLITEONNX_OCR_H
#ifdef __cplusplus
extern "C"
{
#else
// c
typedef enum{
false, true
}bool;
#endif
const int kOcrError = 0;
const int kOcrSuccess = 1;
const int kDefaultPadding = 50;
const int kDefaultMaxSideLen = 1024;
const float kDefaultBoxScoreThresh = 0.6f;
const float kDefaultBoxThresh = 0.3f;
const float kDefaultUnClipRatio = 2.0f;
const bool kDefaultDoAngle = true;
const bool kDefaultMostAngle = true;
/**@fn ocr_init
*@brief 初始化OCR
*@param numThread: 線程數量,不超過CPU數量
*@param dbNetPath: dbnet模型路徑
*@param anglePath: 角度識别模型路徑
*@param crnnPath: crnn推理模型路徑
*@param keyPath: keys.txt樣本路徑
*@return <0: error, >0: instance
*/
int ocr_init(int numThread, const char *dbNetPath, const char *anglePath, const char *crnnPath, const char *keyPath);
/**@fn ocr_cleanup
*@brief 清理,退出程式前執行
*/
void ocr_cleanup();
/**@fn ocr_detect
*@brief 識别圖檔
*@param image_path: 圖檔完整路徑,會在同路徑下生成圖檔識别框選效果,便于調試
*@param out_json_result: 識别結果輸出,json格式。
*@param buffer_len: 輸出緩沖區大小
*@param padding: 50
*@param maxSideLen: 1024
*@param boxScoreThresh: 0.6f
*@param boxThresh: 0.3f
*@param unClipRatio: 2.0f
*@param doAngle: true
*@param mostAngle: true
*@return 成功與否
*/
int ocr_detect(const char *image_path, char *out_buffer, int *buffer_len, int padding, int maxSideLen,
float boxScoreThresh, float boxThresh, float unClipRatio, bool doAngle, bool mostAngle);
/**@fn ocr_detect
*@brief 使用預設參數,識别圖檔
*@param image_path: 圖檔完整路徑
*@param out_buffer: 識别結果,json格式。
*@param buffer_len: 輸出緩沖區大小
*@return 成功與否
*/
int ocr_detect2(const char *image_path, char *out_buffer, int *buffer_len);
#ifdef __cplusplus
}
#endif
#endif //ONLINE_BASE_OCRLITEONNX_OCR_H
ocr.cpp
/** @file ocr.h
* @brief
* @author teng.qing
* @date 8/13/21
*/
#include "ocr.h"
#include "OcrLite.h"
#include "omp.h"
#include "json.hpp"
#include <iostream>
#include <sys/stat.h>
using json = nlohmann::json;
static OcrLite *g_ocrLite = nullptr;
inline bool isFileExists(const char *name) {
struct stat buffer{};
return (stat(name, &buffer) == 0);
}
int ocr_init(int numThread, const char *dbNetPath, const char *anglePath, const char *crnnPath, const char *keyPath) {
omp_set_num_threads(numThread); // 并行計算
if (g_ocrLite == nullptr) {
g_ocrLite = new OcrLite();
}
g_ocrLite->setNumThread(numThread);
g_ocrLite->initLogger(
true,//isOutputConsole
false,//isOutputPartImg
true);//isOutputResultImg
g_ocrLite->Logger(
"ocr_init numThread=%d, dbNetPath=%s,anglePath=%s,crnnPath=%s,keyPath=%s \n",
numThread, dbNetPath, anglePath, crnnPath, keyPath);
if (!isFileExists(dbNetPath) || !isFileExists(anglePath) || !isFileExists(crnnPath) || !isFileExists(keyPath)) {
g_ocrLite->Logger("invalid file path.\n");
return kOcrError;
}
g_ocrLite->initModels(dbNetPath, anglePath, crnnPath, keyPath);
return kOcrSuccess;
}
void ocr_cleanup() {
if (g_ocrLite != nullptr) {
delete g_ocrLite;
g_ocrLite = nullptr;
}
}
int ocr_detect(const char *image_path, char *out_buffer, int *buffer_len, int padding, int maxSideLen,
float boxScoreThresh, float boxThresh, float unClipRatio, bool doAngle, bool mostAngle) {
if (g_ocrLite == nullptr) {
return kOcrError;
}
if (!isFileExists(image_path)) {
return kOcrError;
}
g_ocrLite->Logger(
"padding(%d),maxSideLen(%d),boxScoreThresh(%f),boxThresh(%f),unClipRatio(%f),doAngle(%d),mostAngle(%d)\n",
padding, maxSideLen, boxScoreThresh, boxThresh, unClipRatio, doAngle, mostAngle);
OcrResult result = g_ocrLite->detect("", image_path, padding, maxSideLen,
boxScoreThresh, boxThresh, unClipRatio, doAngle, mostAngle);
json root;
root["dbNetTime"] = result.dbNetTime;
root["detectTime"] = result.detectTime;
for (const auto &item : result.textBlocks) {
json textBlock;
for (const auto &boxPoint : item.boxPoint) {
json point;
point["x"] = boxPoint.x;
point["y"] = boxPoint.y;
textBlock["boxPoint"].push_back(point);
}
for (const auto &score : item.charScores) {
textBlock["charScores"].push_back(score);
}
textBlock["text"] = item.text;
textBlock["boxScore"] = item.boxScore;
textBlock["angleIndex"] = item.angleIndex;
textBlock["angleScore"] = item.angleScore;
textBlock["angleTime"] = item.angleTime;
textBlock["crnnTime"] = item.crnnTime;
textBlock["blockTime"] = item.blockTime;
root["textBlocks"].push_back(textBlock);
root["texts"].push_back(item.text);
}
std::string tempJsonStr = root.dump();
if (static_cast<int>(tempJsonStr.length()) > *buffer_len) {
g_ocrLite->Logger("buff_len is too small \n");
return kOcrError;
}
*buffer_len = static_cast<int>(tempJsonStr.length());
::memcpy(out_buffer, tempJsonStr.c_str(), tempJsonStr.length());
return kOcrSuccess;
}
int ocr_detect2(const char *image_path, char *out_buffer, int *buffer_len) {
return ocr_detect(image_path, out_buffer, buffer_len, kDefaultPadding, kDefaultMaxSideLen, kDefaultBoxScoreThresh,
kDefaultBoxThresh, kDefaultUnClipRatio, kDefaultDoAngle, kDefaultMostAngle);
}
ocr_wrapper.go
package ocr
// -I: 配置編譯選項
// -L: 依賴庫路徑
/*
#cgo CFLAGS: -I ../../../OcrLiteOnnx/include
#cgo LDFLAGS: -L ../../../OcrLiteOnnx/lib -lOcrLiteOnnx -lstdc++
#include <stdlib.h>
#include <string.h>
#include "ocr.h"
*/
import "C"
import (
"runtime"
"unsafe"
)
//const kModelDbNet = "dbnet.onnx"
//const kModelAngle = "angle_net.onnx"
//const kModelCRNN = "crnn_lite_lstm.onnx"
//const kModelKeys = "keys.txt"
const kDefaultBufferLen = 10 * 1024
var (
buffer [kDefaultBufferLen]byte
)
func Init(dbNet, angle, crnn, keys string) int {
threadNum := runtime.NumCPU()
cDbNet := C.CString(dbNet) // to c char*
cAngle := C.CString(angle) // to c char*
cCRNN := C.CString(crnn) // to c char*
cKeys := C.CString(keys) // to c char*
ret := C.ocr_init(C.int(threadNum), cDbNet, cAngle, cCRNN, cKeys)
C.free(unsafe.Pointer(cDbNet))
C.free(unsafe.Pointer(cAngle))
C.free(unsafe.Pointer(cCRNN))
C.free(unsafe.Pointer(cKeys))
return int(ret)
}
func Detect(imagePath string) (bool, string) {
resultLen := C.int(kDefaultBufferLen)
// 構造C的緩沖區
cTempBuffer := (*C.char)(unsafe.Pointer(&buffer[0]))
cImagePath := C.CString(imagePath)
defer C.free(unsafe.Pointer(cImagePath))
isSuccess := C.ocr_detect2(cImagePath, cTempBuffer, &resultLen)
return int(isSuccess) == 1, C.GoStringN(cTempBuffer, resultLen)
}
func CleanUp() {
C.ocr_cleanup()
}
環境變量設定
路徑包含庫所在目錄,或者直接把動态庫拷貝到/usr/lib中,推薦後者:
export LD_LIBRARY_PATH=../mylib
運作
效果如下
參考
- 【Free Style】CGO: Go與C互操作技術(一):Go調C基本原理
- 【Free Style】CGO: Go與C互操作技術(三):Go調C性能分析及優化
- Command cgo
- C? Go? Cgo!
- How to use C++ in Go?
- 深入學習CGO
- 如何把Go調用C的性能提升10倍?
- Go語言使用cgo時的記憶體管理筆記
- GO原本:cgo
- CGO 和 CGO 性能之謎
- cgo is not Go
- 深入CGO程式設計(Gopherchina2018)
- how-to-use-c-in-go
- 使用gcc生成靜态庫和動态庫
- 如何使用GCC生成動态庫和靜态庫
- gcc編譯工具生成動态庫和靜态庫之一----介紹