有贊零售小票打印圖片二值化方案

 

有贊零售小票打印跨平臺解決方案

 

 

做者:王前、林昊(魚乾)javascript

 

 

 

1、背景

零售商家的平常經營中,小票打印的場景無處不在,顧客的每筆消費都會收到商家打印出的消費小票,這個是顧客的消費憑證,因此小票的內容對顧客和商家都尤其重要。對於有贊零售應用軟件來講,小票打印功能也是必不可少的,諸多業務場景都須要提供相應的小票打印能力。html

  • 打印需求端

 

printRequirement

 

  • 小票業務場景

 

receiptType

 

  • 小票打印機設備類型

 

printerType

 

過去咱們存在的痛點:前端

  1. 每一個端各自實現一套打印流程,方案不統一。致使每次修改都會三端修改,並且 iOS 和 Android 必須依賴發版纔可上線,不具備動態性,並且研發效率比較低。
  2. 打印小票的業務場景比較多,每一個業務都本身實現模板封裝及打印邏輯,模板及邏輯不統一,維護成本大。
  3. 多種小票設備的適配,對於每一個端來講都要適配一遍。

其中最主要的痛點仍是在於第一點,多端的不統一問題。因爲不統一,致使開發和維護的成本成倍級增加。java

針對以上痛點,小票打印技術方案須要解決的三個主要問題:git

  1. iOS 、安卓和網頁端的零售軟件都須要提供小票樣式設置和打印的能力,如何下降小票打印代碼的維護和更新成本。
  2. 如何定製顯示不一樣業務場景的小票內容:不一樣業務場景下的小票信息都不盡相同,好比購物小票和退款小票,商品信息的樣式是同樣的,可是支付信息是不同的,購物小票應當顯示顧客的支付信息,退款小票顯示商家退款信息。
  3. 如何更靈活的適配多種多樣的小票打印機,從鏈接方式上分爲藍牙鏈接和 WIFI 鏈接,從紙張樣式分爲 80mm 和 58mm 兩種寬度。

2、總體解決方案

針對以上三個問題,咱們提出了一個涉及前端、移動端和服務端的跨平臺解決方案,github

  • 架構圖

 

structureChart

 

架構設計的核心在於經過 JS 實現支持跨平臺的小票解析腳本,並具備動態更新的優點;經過服務端下發可編輯的樣式模板實現小票內容的靈活定製;客戶端啓動 JS 執行器執行 JS 小票腳本引擎(如下簡稱:JS 引擎)並負責打印機設備的鏈接管理。ajax

1 、JS 引擎設計

JS 引擎主要能力就是處理小票模版和業務數據,將業務數據整合到模版中(處理不了的交給移動端處理,好比圖片),而後將整合模版數據轉換成打印指令返給移動端。算法

  • 總體處理流程圖

 

jsHandleFlow

 

  • 結構設計
    jsTemplateLayout

    • 小票格式中,打印機是一行一行的輸出。那麼基本輸出佈局單位,咱們定義爲 layout
    • 默認一行有一個內容塊,即一個 layout 裏面有一個 content object
    • 當一行有多列內容的時候,即一個 layout 裏面包含 N 個 content object 。 各自內容塊有 pagerWeight 表明每一個內容的寬度佔比
    • 每一行的後面的是一個佔位符,用數據模型的 key 作佔位

小票 layout 樣式描述:json

jsLayoutDesc

 

content block 內容塊: 後端

jsLayoutContentDesc

 

不一樣類型內容所支持的能力:

jsPower

 

  • 模版編譯

這裏使用了 HandleBars.js 做爲模板編譯的庫。此外,目前還額外提供了部分能力支持。

自定義能力:

jsCustomPower

 

  • 打印機設備適配

主要進行適配指令集解析適配,根據鏈接不一樣設備進行不一樣指令解析。目前已適配設備:365wifi 、 sunmi 、 sprt80 、 sprt58 、 wangpos 、 aclas 、 xprinter 。若是鏈接未適配的設備拋出找不到相應打印機解析器 error。

  • 調用對應打印機的 parser 指令解析流程

 

pastedImage

 

  • 兼容性問題

    • 切紙:支持外部傳入是否須要切紙,防止外部發送打印指令時加入切紙指令後重復切紙問題,默認加切紙指令。
    • 一機多尺寸打印:存在一臺打印機支持兩種紙張打印( 80mm 、 58mm ),這時須要從外部傳入打印尺寸,默認 80mm 。好比,sunmiT1 支持 80mm 和 58mm 打印,默認是 80mm 。
  • 容錯處理

    • 因爲模版解析有必定格式要求,因此一些特殊字符及轉移字符存在數據中會存在解析錯誤。因此 JS 在傳入數據時,作了一層過濾,將 "\\" 、 "\n" 、 "\b" ... 等字符去掉或替換,保證打印。
    • 若是在解析過程當中存在錯誤,將拋出異常給移動端捕獲。

2 、模板管理服務

小票模板的動態編輯和下發,模版動態配置信息存儲和各業務全量模版存儲,提供移動端動態配置信息接口,拉取業務小票模版接口,各業務方業務數據接口。

  • 總體處理流程圖

 

serverHandleFlow

 

  • 小票基礎模版庫存儲示例

    serverTemplateStored

     

    shopId:店鋪 ID

    business:業務方

    type:打印內容類型

    content:layout 中 content 內容

    sortWeight:排序比重,用於輸出模板 layout 順序

  • 動態設置數據存儲示例

    serverDynamicTemplateStored

     

    shopId:店鋪 ID

    business:業務方

    type:打印內容類型

    params:須要替換填充的內容

  • 接口返回整合後的小票模版 json

{
    "business": "shopping", "shopId": 111111, "id": 321, "version": 0, "layouts": [{ "name": "LOGO", "content": "[{\"content\":\"http://www.test.com/test.jpg\",\"contentType\":\"image\",\"textAlign\":\"center\",\"width\":45}]" },{ "name": "電話", "content": "[{\"content\":\"電話:{{mobile}}\",\"contentType\":\"text\",\"textAlign\":\"left\",\"fontSize\":\"default\",\"pagerWeight\":1}]" },...] } 
12345678910111213

其中相關動態數據後端已經作過整合替換,須要替換的業務數據保留在模板 json 中,等獲取業務數據後由 JS 引擎進行替換。 上面 json 中 http://www.test.com/test.jpg 就是動態整合替換數據,{{mobile}} 是一個須要替換的業務數據。

3 、移動端

移動端除了動態模版配置以外,主要的就是打印流程。移動端只須要關心須要打印什麼業務小票,而後去後端拉取業務小票模版和業務數據,將拉取到的數據傳給 JS 引擎進行預處理,返回模版中處理不了的圖片 url 信息,而後移動端進行下載圖片,進行二值轉換,輸出像素的 16 進制字符串,替換原來模版中的 url ,最後將鏈接的打印機類型和處理後的模版傳給 JS 引擎進行打印指令轉換返回給打印機打印。

  • 動態模版配置

 

nativeDynamicConfi

 

動態配置小票內容,支持 LOGO 、店鋪數據、營銷活動配置等。左側爲在 80mm 和 58mm 上預覽樣式。經過動態配置模版,實現後端接口模版更新,而後能夠實時同步修改打印內容。網頁零售軟件上動態配置內容和移動端同樣。

  • 打印業務流程

 

nativePrintFlow

 

該業務流程,移動端徹底脫離數據,只須要作一些額外能力以及傳輸功能,有效解決了業務數據修改依賴移動端發版的問題。 Android 和 iOS 流程統一。

3、移動端功能設計

1 、動態化

動態化在本解決方案裏是必不可少的一環,實時更新業務數據模板依賴於後端,可是 JS 解析引擎的下發要依靠移動端來實現,爲了及時修復發現的 JS 問題或者快速適配新設備等功能。更新流程圖以下:

 

nativeDynamicJS

 

這裏說明一下,由於可能會出現執行 JS 的過程當中,正在執行本地 JS 文件更新,致使執行 JS 出錯。因此在完成本地更新後會發送一個通知,告知業務方 JS 已更新完成,這時業務方可根據自身需求作邏輯處理,好比從新加載 JS 進行處理業務。

2 、JS 執行器

iOS 使用 JavaScriptCore 框架,Android 使用 J2V8 框架,具體框架的介紹這裏就不說明了。JS 執行器設計包含加載指定 JS 文件,調用 JS 方法,獲取 JS 屬性,JS 異常捕獲。

/** 初始化 JSExecutor @param fileName JS 文件名 @return JSExecutor */ - (instancetype)initWithScriptFile:(NSString *)fileName; /** 加載 JS 文件 @param fileName JS 文件名 */ - (void)loadSriptFile:(NSString *)fileName; /** 執行 JS 方法 @param functionName 方法名 @param args 入參 @return 方法返回值 */ - (JSValue *)runJSFunction:(NSString *)functionName args:(NSArray *)args; /** 獲取 JS 屬性 @param propertyName 屬性名 @return 屬性值 */ - (JSValue *)getJSProperty:(NSString *)propertyName; /** JS 異常捕獲 @param handler 異常捕獲回調 */ - (void)catchExceptionWithHandler:(JSExceptionHandler)handler; 
1234567891011121314151617181920212223242526272829303132333435363738

加載 JS 文件方法,能夠加載動態下發的 JS 。邏輯是先判斷本地下發的文件是否存在,若是存在就加載下發 JS ,不然加載 app 中 bundle 裏面的 JS 文件。

- (void)loadSriptFile:(NSString *)fileName{ NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); if (paths.count > 0) { NSString *docDir = [paths objectAtIndex:0]; NSString *docSourcePath = [docDir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.js", fileName]]; NSFileManager *fm = [NSFileManager defaultManager]; if ([fm fileExistsAtPath:docSourcePath]) { NSString *jsString = [NSString stringWithContentsOfFile:docSourcePath encoding:NSUTF8StringEncoding error:nil]; [self.content evaluateScript:jsString]; return; } } NSString *sourcePath = [[YZCommonBundle bundle] pathForResource:fileName ofType:@"js"]; NSAssert(sourcePath, @"can't find jscript file"); NSString *jsString = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil]; [self.content evaluateScript:jsString]; } 
1234567891011121314151617

這時候可能會有人疑問,爲何這裏是直接強制加載本地下發 JS ,而不是對比版本優先加載。這裏主要有兩點緣由:

  • 動態下發 JS 文件,就是爲了補丁或者優化更新,因此通常新版本下發配置不會存在
  • 爲了支持 JS 版本回滾

JS 異常捕獲功能,將異常拋出給業務方,可讓調用者各自實現邏輯處理。

3 、緩存優化

因爲模板和數據都在後端,須要拉取兩次接口進行打印,因此須要提供一套緩存機制來提升打印體驗。因爲業務數據須要實時拉取,因此必須走接口,模板相對於業務數據來講,能夠容許必定的延遲。因此,模板採用本地文件緩存,業務數據採用和業務打印頁面掛鉤的內存緩存,業務數據只須要第一次打印是請求接口,從新打印直接使用。

流程圖:

nativeCacheFlow

 

本緩方案存會存在偶現的模板不一樣步問題,在即將打印時,若是網頁後臺修改了模板,就會出現本次打印模板不是最新的,可是在下一次打印時就會是最新的了。因爲出現的概率比較低,模板也容許有一點延遲,因此不會影響總體流程。

對於離線場景,咱們在 app 中存放一個最小可用模板,專門用於離線下小票打印使用。爲何是最小可用模板,由於離線下,業務數據及一些其餘數據有可能不全,因此最小可用模板能夠保證打印出來的數據準確性。

4 、圖片處理

因爲 JS 引擎是不能解析圖片文件的,因此在最初模板中存在圖片連接時,所有由移動端進行處理,而後進行替換。圖片處理主要就是下載圖片,圖片壓縮,二值圖處理,圖片像素點壓縮(打印指令要求),每一個字節轉換成 16 進制,拼接 16 進制字符串。

  • 下載圖片

採用 SDWebImage 進行下載緩存,建立並行隊列進行多圖片下載,每下載成功一張後回到主線程進行後續的相關處理。全部圖片都處理完成或,回調給 JS 引擎進行指令解析。

  • 圖片壓縮

根據 JS 引擎模板要求的 width(必須是 8 的倍數,後續說明),進行等比例壓縮,轉換成 jpg 格式,過濾掉 alpha 通道。

  • 二值圖處理

遍歷每個像素點,進行 RGB 取值,而後算出 RGB 均值與 255 的比值,根據比值進行取值 0 或 255 。這裏沒有使用直方圖尋找閾值 T 的方式進行處理,是出於性能和時間考慮。

  • 像素點壓縮

因爲打印機指令要求,須要對轉換成二值後的每一個點進行 width 上壓縮,須要將 8 個字節壓縮到 1 個字節,這裏也是爲何圖片壓縮時 width 必須是 8 的倍數的緣由,不然打印出來的圖片會錯位。

 

nativeImageByte

 

  • 16 進制字符串

由於打印機打印圖片接收的是 16 進制字符串,因此須要將處理後的每一個字節轉換成 16 進制字符,而後拼成一個字符串。

5 、實現屢次打印

因爲業務場景須要,須要自動打印多張小票,因此設計了屢次打印邏輯。因爲每次打印都是異步線程中,因此不能夠直接循環打印,這裏使用信號量 dispatch_semaphore_t ,在異步線程中建立和 wait 信號量,每次打印完成回調線程中 signal 信號量,實現屢次打印,保證每次打印依次進行。若是中途打印出錯,則終止後續打印。

dispatch_async(dispatch_get_global_queue(0, 0), ^{ dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); for (int i = 1; i <= printCount; i++) { if (stop) { break; } [self print:template andCompletionBlock:^(State state, NSString *errorStr) { dispatch_async(dispatch_get_main_queue(), ^{ if (errorStr.length > 0 || i == printCount) { if (completion) { completion(state, errorStr); } stop = YES; } dispatch_semaphore_signal(semaphore); }); }]; dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 15*NSEC_PER_SEC)); } }); 
1234567891011121314151617181920

4、總結與展望

本方案已經實施,在零售 app 中使用來看,已經知足目前大部分業務場景及需求,後續的開發及維護成本也會大幅度下降,提升了研發效率,接入新業務小票也比較方便。客戶使用上來講,使用體驗和之前沒有較大差異,同時在處理客戶反映的問題來講,也能夠作到快速修改,實時下發等。不過目前還存在一些不足點,好比說圖片打印的功能,還不能徹底知足全部圖片都作到完美打印,畢竟圖片處理考慮到性能體驗方面;還有模板後續能夠增長版本號,這樣在模板存在異常時也能夠回滾或兼容處理等;再者就是緩存優化能夠後續進一步優化體驗,好比加入模板推送,本地緩存優化等。

參考連接

 

 

 

 

 

 

1、背景

小票打印是零售商家的基礎功能,在小票信息中,必然會存在一些相關店鋪的信息。好比,logo 、店鋪二維碼等。對於商家來講,上傳 logo 及店鋪二維碼時,基本都是彩圖,可是小票打印機基本都是隻支持黑白二值圖打印。爲了商家的服務體驗,咱們沒有對商家上傳的圖片進行要求,商家能夠根據實際狀況上傳本身的個性化圖片,所以就須要咱們對商家的圖片進行二值圖處理後進行打印。

此次文章是對《有贊零售小票打印跨平臺解決方案》中的圖片的二值圖處理部分的解決方案的說明。

2、圖像二值化處理流程

圖像二值化就是將圖像上的像素點的灰度值(若是是 RGB 彩圖,則須要先將像素點的 RGB 轉成灰度值)設置爲 0 或 255 ,也就是將整個圖像呈現出明顯的黑白效果的過程。

其中劃分 0 和 255 的中間閾值 T 是二值化的核心,一個準確的閾值能夠獲得一個較好的二值圖。

 

二值化總體流程圖:

 

從上面的流程圖中能夠看出,獲取灰度圖和計算閾值 T 是二值化的核心步驟。

3、之前的解決方案

之前使用的方案是,首先將圖像處理成灰度圖,而後再基於 OTSU(大津法、最大類間方差法)算法求出分割 0 和 255 的閾值 T ,而後根據 T 對灰度值進行二值化處理,獲得二值圖像。

咱們的全部算法都有使用 C 語言實現,目的爲了跨平臺通用性。

流程圖:

 

灰度算法:

對於 RGB 彩色轉灰度,有一個很著名的公式:

 

Gray = R * 0.299 + G * 0.587 + B * 0.114

 

這種算法叫作 Luminosity,也就是亮度算法。目前這種算法是最經常使用的,裏面的三個數據都是經驗值或者說是實驗值。由來請參見 wiki 。

然而實際應用時,你們都但願避免低速的浮點運算,爲了提升效率將上述公式變造成整數運算和移位運算。這裏將採用移位運算公式:

Gray = (R * 38 + G * 75 + B * 15) >> 7

 

若是想了解具體由來,能夠自行了解,這裏不作過多解釋。

具體實現算法以下:

/**
 獲取灰度圖

 @param bit_map 圖像像素數組地址( ARGB 格式)
 @param width 圖像寬
 @param height 圖像高
 @return 灰度圖像素數組地址
 */
int * gray_image(int *bit_map, int width, int height) {  
    double pixel_total = width * height; // 像素總數
    if (pixel_total == 0) return NULL;
    // 灰度像素點存儲
    int *gray_pixels = (int *)malloc(pixel_total * sizeof(int));
    memset(gray_pixels, 0, pixel_total * sizeof(int));
    int *p = bit_map;
    for (u_int i = 0; i < pixel_total; i++, p++) {
        // 分離三原色及透明度
        u_char alpha = ((*p & 0xFF000000) >> 24);
        u_char red = ((*p & 0xFF0000) >> 16);
        u_char green = ((*p & 0x00FF00) >> 8);
        u_char blue = (*p & 0x0000FF);

        u_char gray = (red*38 + green*75 + blue*15) >> 7;
        if (alpha == 0 && gray == 0) {
            gray = 0xFF;
        }
        gray_pixels[i] = gray;
    }
    return gray_pixels;
}

該算法中,主要是爲了統一各平臺的兼容性,入參要求傳入 ARGB 格式的 bitmap 。爲何使用 int 而不是用 unsigned int,是由於在 java 中沒有無符號數據類型,使用 int 具備通用性。

OTSU 算法:

OTSU 算法也稱最大類間差法,有時也稱之爲大津算法,由大津於 1979 年提出,被認爲是圖像分割中閾值選取的最佳算法,計算簡單,不受圖像亮度和對比度的影響,所以在數字圖像處理上獲得了普遍的應用。它是按圖像的灰度特性,將圖像分紅背景和前景兩部分。因方差是灰度分佈均勻性的一種度量,背景和前景之間的類間方差越大,說明構成圖像的兩部分的差異越大,當部分前景錯分爲背景或部分背景錯分爲前景都會致使兩部分差異變小。所以,使類間方差最大的分割意味着錯分機率最小。

原理:

對於圖像 I ( x , y ) ,前景(即目標)和背景的分割閾值記做 T ,屬於前景的像素點數佔整幅圖像的比例記爲 ω0 ,其平均灰度 μ0 ;背景像素點數佔整幅圖像的比例爲 ω1 ,其平均灰度爲 μ1 。圖像的總平均灰度記爲 μ ,類間方差記爲 g 。

假設圖像的背景較暗,而且圖像的大小爲 M × N ,圖像中像素的灰度值小於閾值 T 的像素個數記做 N0 ,像素灰度大於等於閾值 T 的像素個數記做 N1 ,則有:

ω0 = N0 / M × N                         (1)
ω1 = N1 / M × N                         (2)
N0 + N1 = M × N                         (3)
ω0 + ω1 = 1                             (4)
μ = ω0 * μ0 + ω1 * μ1                   (5)
g = ω0 * (μ0 - μ)^2 + ω1 * (μ1 - μ)^2   (6)

將式 (5) 代入式 (6) ,獲得等價公式:

 

g = ω0 * ω1 * (μ0 - μ1)^2           (7)

公式 (7) 就是類間方差計算公式,採用遍歷的方法獲得使類間方差 g 最大的閾值 T ,即爲所求。

由於 OTSU 算法求閾值的基礎是灰度直方圖數據,因此使用 OTSU 算法的前兩步:

一、獲取原圖像的灰度圖

二、灰度直方統計

這裏須要屢次對圖像進行遍歷處理,若是每一步都單獨處理,會增長很多遍歷次數,因此這裏作了步驟整合處理,減小沒必要要的遍歷,提升性能。

具體實現算法以下:

/**
 OTSU 算法獲取二值圖

 @param bit_map 圖像像素數組地址( ARGB 格式)
 @param width 圖像寬
 @param height 圖像高
 @param T 存儲計算得出的閾值
 @return 二值圖像素數組地址
 */
int * binary_image_with_otsu_threshold_alg(int *bit_map, int width, int height, int *T) {

    double pixel_total = width * height; // 像素總數
    if (pixel_total == 0) return NULL;

    unsigned long sum1 = 0;  // 總灰度值
    unsigned long sumB = 0;  // 背景總灰度值
    double wB = 0.0;        // 背景像素點比例
    double wF = 0.0;        // 前景像素點比例
    double mB = 0.0;        // 背景平均灰度值
    double mF = 0.0;        // 前景平均灰度值
    double max_g = 0.0;     // 最大類間方差
    double g = 0.0;         // 類間方差
    u_char threshold = 0;    // 閾值
    double histogram[256] = {0}; // 灰度直方圖,下標是灰度值,保存內容是灰度值對應的像素點總數

    // 獲取灰度直方圖和總灰度
    int *gray_pixels = (int *)malloc(pixel_total * sizeof(int));
    memset(gray_pixels, 0, pixel_total * sizeof(int));
    int *p = bit_map;
    for (u_int i = 0; i < pixel_total; i++, p++) {
        // 分離三原色及透明度
        u_char alpha = ((*p & 0xFF000000) >> 24);
        u_char red = ((*p & 0xFF0000) >> 16);
        u_char green = ((*p & 0x00FF00) >> 8);
        u_char blue = (*p & 0x0000FF);

        u_char gray = (red*38 + green*75 + blue*15) >> 7;
        if (alpha == 0 && gray == 0) {
            gray = 0xFF;
        }
        gray_pixels[i] = gray;

        // 計算灰度直方圖分佈,Histogram 數組下標是灰度值,保存內容是灰度值對應像素點數
        histogram[gray]++;
        sum1 += gray;
    }

    // OTSU 算法
    for (u_int i = 0; i < 256; i++)
    {
        wB = wB + histogram[i]; // 這裏不算比例,減小運算,不會影響求 T
        wF = pixel_total - wB;
        if (wB == 0 || wF == 0)
        {
            continue;
        }
        sumB = sumB + i * histogram[i];
        mB = sumB / wB;
        mF = (sum1 - sumB) / wF;
        g = wB * wF * (mB - mF) * (mB - mF);
        if (g >= max_g)
        {
            threshold = i;
            max_g = g;
        }
    }

    for (u_int i = 0; i < pixel_total; i++) {
        gray_pixels[i] = gray_pixels[i] <= threshold ? 0xFF000000:0xFFFFFFFF;
    }

    if (T) {
        *T = threshold;    // OTSU 算法閾值
    }

    return gray_pixels;
}

測試執行時間數據:

iPhone 6: imageSize:260, 260; OTSU 使用時間:0.005254; 5次異步處理使用時間:0.029240

iPhone 6: imageSize:620, 284; OTSU 使用時間:0.029476; 5次異步處理使用時間:0.050313

iPhone 6: imageSize:2560,1440; OTSU 使用時間:0.200595; 5次異步處理使用時間:0.684509

通過測試,該算法處理時間都是毫秒級別的,並且通常咱們的圖片大小都不大,因此性能沒問題。

處理後的效果:

通過 OTSU 算法處理過的二值圖基本能夠知足大部分商家 logo 。

 

不過對於實際場景來講還有些不足,好比商家的 logo 顏色差異比較大的時候,可能打印出來的圖片會和商家意願的不太一致。好比以下 logo :

 

上面 logo 對於算法來講,黃色的灰度值比閾值小,因此二值化變成了白色,可是對於商家來講,logo 上紅色框內信息缺失了一部分,可能不能知足商家需求。

  • 存在問題總結
    • 算法單一,對於不一樣圖片處理結果可能與預期不一致
    • 每次打印都對圖片進行處理,沒有緩存機制

 

4、新的解決方案

針對之前使用的方案中存在的兩個問題,新的方案中加入了具體優化。

4.1 問題一 (算法單一,對於不一樣圖片處理結果可能與預期不一致)

加入多算法求閾值 T ,而後根據每一個算法得出的二值圖和原圖的灰度圖進行對比,相識度比較高的做爲最優閾值 T 。

流程圖:

 

整個流程當中會並行三個算法進行二值圖處理,同時獲取二值圖的圖片指紋 hashCode ,與原圖圖片指紋 hashCode 進行對比,獲取與原圖最爲相近的二值圖做爲最優二值圖。

其中的OTSU算法上面已經說明,此次針對平均灰度算法和雙峯平均值算法進行解析。

平均灰度算法:

平均灰度算法其實很簡單,就是將圖片灰度處理後,求一下灰度圖的平均灰度。假設總灰度爲 sum ,總像素點爲 pixel_total ,則閾值 T :

 

T = sum / pixel_total

 

具體實現算法以下:

/**
 平均灰度算法獲取二值圖

 @param bit_map 圖像像素數組地址( ARGB 格式)
 @param width 圖像寬
 @param height 圖像高
 @param T 存儲計算得出的閾值
 @return 二值圖像素數組地址
 */
int * binary_image_with_average_gray_threshold_alg(int *bit_map, int width, int height, int *T) {

    double pixel_total = width * height; // 像素總數
    if (pixel_total == 0) return NULL;

    unsigned long sum = 0;  // 總灰度
    u_char threshold = 0;    // 閾值


    int *gray_pixels = (int *)malloc(pixel_total * sizeof(int));
    memset(gray_pixels, 0, pixel_total * sizeof(int));
    int *p = bit_map;
    for (u_int i = 0; i < pixel_total; i++, p++) {
        // 分離三原色及透明度
        u_char alpha = ((*p & 0xFF000000) >> 24);
        u_char red = ((*p & 0xFF0000) >> 16);
        u_char green = ((*p & 0x00FF00) >> 8);
        u_char blue = (*p & 0x0000FF);

        u_char gray = (red*38 + green*75 + blue*15) >> 7;
        if (alpha == 0 && gray == 0) {
            gray = 0xFF;
        }
        gray_pixels[i] = gray;
        sum += gray;
    }
    // 計算平均灰度
    threshold = sum / pixel_total;

    for (u_int i = 0; i < pixel_total; i++) {
        gray_pixels[i] = gray_pixels[i] <= threshold ? 0xFF000000:0xFFFFFFFF;
    }

    if (T) {
        *T = threshold;
    }

    return gray_pixels;
}

 

雙峯平均值算法:

此方法實用於具備明顯雙峯直方圖的圖像,其尋找雙峯的谷底做爲閾值,可是該方法不必定能得到閾值,對於那些具備平坦的直方圖或單峯圖像,該方法不合適。該函數的實現是一個迭代的過程,每次處理前對直方圖數據進行判斷,看其是否已是一個雙峯的直方圖,若是不是,則對直方圖數據進行半徑爲 1(窗口大小爲 3 )的平滑,若是迭代了必定的數量好比 1000 次後仍未得到一個雙峯的直方圖,則函數執行失敗,如成功得到,則最終閾值取雙峯的平均值做爲閾值。所以實現該算法應有的步驟:

一、獲取原圖像的灰度圖

二、灰度直方統計

三、平滑直方圖

四、求雙峯平均值做爲閾值 T

其中第三步平滑直方圖的過程是一個迭代過程,具體流程圖:

 

具體實現算法以下:

// 判斷是不是雙峯直方圖
int is_double_peak(double *histogram) {  
    // 判斷直方圖是存在雙峯
    int peak_count = 0;
    for (int i = 1; i < 255; i++) {
        if (histogram[i - 1] < histogram[i] && histogram[i + 1] < histogram[i]) {
            peak_count++;
            if (peak_count > 2) return 0;
        }
    }
    return peak_count == 2;
}

/**
 雙峯平均值算法獲取二值圖

 @param bit_map 圖像像素數組地址( ARGB 格式)
 @param width 圖像寬
 @param height 圖像高
 @param T 存儲計算得出的閾值
 @return 二值圖像素數組地址
 */
int * binary_image_with_average_peak_threshold_alg(int *bit_map, int width, int height, int *T) {  
    double pixel_total = width * height; // 像素總數
    if (pixel_total == 0) return NULL;

    // 灰度直方圖,下標是灰度值,保存內容是灰度值對應的像素點總數
    double histogram1[256] = {0};
    double histogram2[256] = {0}; // 求均值的過程會破壞前面的數據,所以須要兩份數據
    u_char threshold = 0;    // 閾值

    // 獲取灰度直方圖
    int *gray_pixels = (int *)malloc(pixel_total * sizeof(int));
    memset(gray_pixels, 0, pixel_total * sizeof(int));
    int *p = bit_map;
    for (u_int i = 0; i < pixel_total; i++, p++) {
        // 分離三原色及透明度
        u_char alpha = ((*p & 0xFF000000) >> 24);
        u_char red = ((*p & 0xFF0000) >> 16);
        u_char green = ((*p & 0x00FF00) >> 8);
        u_char blue = (*p & 0x0000FF);

        u_char gray = (red*38 + green*75 + blue*15) >> 7;
        if (alpha == 0 && gray == 0) {
            gray = 0xFF;
        }
        gray_pixels[i] = gray;

        // 計算灰度直方圖分佈,Histogram數組下標是灰度值,保存內容是灰度值對應像素點數
        histogram1[gray]++;
        histogram2[gray]++;
    }

    // 若是不是雙峯,則經過三點求均值來平滑直方圖
    int times = 0;
    while (!is_double_peak(histogram2)) {
        times++;
        if (times > 1000) {                // 這裏使用 1000 次,考慮到過屢次循環可能會存在性能問題
            return NULL;                          // 彷佛直方圖沒法平滑爲雙峯的,返回錯誤代碼
        }
        histogram2[0] = (histogram1[0] + histogram1[0] + histogram1[1]) / 3;                   // 第一點
        for (int i = 1; i < 255; i++) {
            histogram2[i] = (histogram1[i - 1] + histogram1[i] + histogram1[i + 1]) / 3;       // 中間的點
        }
        histogram2[255] = (histogram1[254] + histogram1[255] + histogram1[255]) / 3;           // 最後一點
        memcpy(histogram1, histogram2, 256 * sizeof(double));                                  // 備份數據,爲下一次迭代作準備
    }

    // 求閾值T
    int peak[2] = {0};
    for (int i = 1, y = 0; i < 255; i++) {
        if (histogram2[i - 1] < histogram2[i] && histogram2[i + 1] < histogram2[i]) {
            peak[y++] = i;
        }
    }
    threshold = (peak[0] + peak[1]) / 2;

    for (u_int i = 0; i < pixel_total; i++) {
        gray_pixels[i] = gray_pixels[i] <= threshold ? 0xFF000000:0xFFFFFFFF;
    }

    if (T) {
        *T = threshold;
    }

    return gray_pixels;
}

 

測試執行時間數據:

iPhone 6: imageSize:260, 260; average_peak 使用時間:0.035254

iPhone 6: imageSize:800, 800; average_peak 使用時間:0.101282

通過測試,該算法在圖片比較小的時候,還算能夠,若是圖片比較大會存在較大性能消耗,並且根據圖片色彩分佈不一樣也可能形成屢次循環平滑,也會影響性能。對於 logo 來講,咱們處理的時候作了壓縮,通常都是很大,因此處理時間也在能夠接受返回內,並且進行處理和對比時,是在異步線程中,不會影響主流程。

圖片指紋 hashCode :

圖片指紋 hashCode ,能夠理解爲圖片的惟一標識。一個簡單的圖片指紋生成步驟須要如下幾步:

一、圖片縮小尺寸通常縮小到 8 * 8 ,一共 64 個像素點。

二、將縮小的圖片轉換成灰度圖。

三、計算灰度圖的平均灰度。

四、灰度圖的每一個像素點的灰度與平均灰度比較。大於平均灰度,記爲 1 ;小於平均灰度,記爲 0。

五、計算哈希值,第 4 步的結果能夠構成一個 64 爲的整數,這個 64 位的整數就是該圖片的指紋 hashCode 。

六、對比不一樣圖片生成的指紋 hashCode ,計算兩個 hashCode 的 64 位中有多少位不同,即「漢明距離」,差別越少圖片約相近。

因爲使用該算法生成的圖片指紋具備差別性比較大,由於對於 logo 來講處理後的二值圖壓縮到 8 * 8 後的類似性很大,因此使用 8 * 8 生成 hashCode 偏差性比較大,通過試驗,確實如此。因此,在此基礎上,對上述中的 一、五、6 步進行了改良,改良後的這幾步爲:

一、圖片縮小尺寸可自定義(必須是整數),可是最小像素數要爲 64 個,也就是 width * height >= 64 。建議爲 64 的倍數,爲了減小偏差。

五、哈希值不是一個 64 位的整數,而是一個存儲 64 位整數的數組,數組的長度就是像素點數量對 64 的倍數(取最大的整數倍)。這樣每生成一個 64 位的 hashCode 就加入到數組中,該數組就是圖片指紋。

六、對比不一樣指紋時,遍歷數組,對每個 64 爲整數進行對比不一樣位數,最終結果爲,每個 64 位整數的不一樣位數總和。

在咱們對商家 logo 測試實踐中發現,採用 128 * 128 的壓縮,能夠獲得比較滿意的結果。

最優算法爲 OTSU 算法例子:

 

 

 

最優算法爲平均灰度算法例子:

 

最優算法爲雙峯均值算法例子:

 

實際實驗中,發現真是中選擇雙峯均值的機率比較低,也就是絕大多數的 logo 都是在 OTSU 和平均灰度兩個算法之間選擇的。因此,後續能夠考慮加入選擇統計,若是雙峯均值機率確實特別低且結果與其餘兩種差很少大,那就能夠去掉該方法。

 

4.2 問題二 (每次打印都對圖片進行處理,沒有緩存機制)

加入緩存機制,通常店鋪的 logo 和店鋪二維碼都是固定的,不多會更換,因此,在進入店鋪和修改店鋪二維碼時能夠對其進行預處理,並緩存處理後的圖片打印指令,後續打印時直接拿緩存使用便可。

因爲緩存的內容是處理後的打印指令字符串,因此使用 NSUserDefaults 進行存儲。

 

緩存策略流程圖:

 

這裏面爲何只有修改店鋪二維碼,而沒有店鋪 logo ?由於在咱們 app 中,logo 是不可修改的,只能在 pc 後臺修改,而登陸店鋪後,本地就能夠直接拿到店鋪信息;店鋪二維碼是在小票模板設置裏自行上傳的圖片,因此商家在 app 中是能夠自行修改店鋪二維碼的。

打印時圖片處理流程圖:

 

 

在新流程中,若是緩存中沒有查到,則會走老方案去處理圖片。緣由是考慮到,這時候是商家實時打印小票,若是選用新方案處理,恐怕時間會加長,使用戶體驗下降。老方案已經在線上跑了好久,因此使用老的方案處理也問題不大。

5、將來指望與規劃

在後續規劃中加入幾點優化:

  • 添加新流程處理統計,對商家 logo 和店鋪二維碼處理後的最優算法進行統計,爲後續優化作數據準備。
  • 處理後的結果若是商家不滿意,商家能夠自主選擇處理二值圖的閾值 T ,達到滿意爲止。
  • 圖片更新不及時問題,PC 後臺修改了圖片沒法及時更新本地緩存。
  • 圖片精細化處理,針對二維碼能夠採用分塊處理算法。

其中第二點,商家自主選擇閾值 T ,預覽效果以下:

 

 

 

 

參考連接

相關文章
相關標籤/搜索