iT邦幫忙

2026 iThome 鐵人賽

DAY 23
0
Build on Google AI

液態玻璃美學:結合 Gemini 與 Antigravity IDE,打造 120FPS、多視窗比例與網格順延推擠佈局的強大商務運算終端系列 第 23 篇

Day 23:【高階商務計算機】不花一毛 Token 的黑科技——雙方塊 QR Code 與 200% 特化多重 Canvas 二值化

  • 分享至 

  • xImage
  •  

昨天我們開啟了全系列最硬核的篇章,實作了計算機動作列專屬相機掃描入口、按需授權安全調度、串流徹底釋放與賽博龐克風格的霓虹雷射掃描動畫,為實體單據辨識打造了一個安全穩定且兼具高顏值的操作體驗。然而,當我們拿起手機對準台灣最普遍的二聯式電子發票時,一個現實中的工程兩難立刻浮現:如果每一張發票都將整張高解析度照片原封不動傳送到雲端大型語言模型進行視覺辨識,不僅需要等待網路傳輸與伺服器推論,在大批次記帳時更會白白耗費大量寶貴的付費模型配額。

明明台灣電子發票中央已經印製了包含品名、數量與金額的雙二維條碼,為什麼多數工具依然無法在手機前端直接讀取?這是因為實體發票上的條碼面積微小、紙張經常帶有口袋折痕,且熱感應紙表面在室內燈光下極易產生嚴重反光。今天我們實作純前端地端零秒免金鑰解析管線,透過台灣電子發票規格逆向工程、發票下半部特化局部放大與多重高對比黑白二值化濾鏡,讓瀏覽器在不花一毛模型費用的前提下,瞬間精準抽取發票明細。


專案正式上線體驗與開源倉庫

線上體驗 Live Demo:https://410355-collab.github.io/Premium-Business-Calculator/
(強烈建議使用 iPhone Safari 或 Android 手機開啟體驗!)

GitHub 開源倉庫:https://github.com/410355-collab/Premium-Business-Calculator
歡迎提出建議與指導
(開發環境:Google Antigravity IDE + Google Gemini 協同開發)


一、技術痛點:微小條碼辨識率低下與雲端調用浪費

在實作前端電子發票掃描的過程中,傳統開發方案普遍面臨兩大技術短板:

第一個問題是全圖條碼掃描的極低命中率。使用者在拍攝發票時,往往將整張長型發票完整納入鏡頭畫面。此時發票上的雙二維條碼在整張二千萬畫素的照片中,往往僅佔據不到百分之五的像素面積。現代瀏覽器內建的條碼檢測器在處理全景大圖時,極易因條碼像素密度不足、紙張折痕扭曲或環境光源反光,導致條碼定位錨點遺失而直接回傳空陣列,最終被迫降級走向耗時的雲端視覺模型。

第二個問題是缺乏財政部電子發票編碼規格的逆向解析能力。台灣電子發票左側二維碼本質上是一串嚴謹的定長字串與自訂分隔符號組合。前十碼為字軌號碼、隨後七碼為民國年月日,在雙星號分隔符號後才是由冒號串接的各項品名、數量與單價;若未包含商品明細,在特定十六進位區段中則存放著發票總金額。多數通用掃描軟體無法解讀這串專屬格式,只能將混亂的字串直接呈現給使用者,喪失了自動歸檔入帳的自動化價值。

二、架構設計:三重特化畫布與規格逆向解碼

為了達到地端零秒、免聯網且百分之百精確的發票解析,我們建立了多層次圖像增強與語法解碼架構:

三重特化畫布檢測管線:
當圖片傳入檢測器時,系統並非單純只跑一次掃描,而是平行組裝三塊具備不同光學特性的畫布。第一塊維持原圖全景畫布,負責捕捉大尺寸或滿版拍攝的條碼;第二塊鎖定發票下半部百分之六十五的特化區域進行局部裁切,相當於將條碼物理放大超過兩倍,大幅提升條碼黑白模組的像素解析度;第三塊則對放大後的局部區域進行逐像素灰階運算與高對比二值化處理,直接將灰階閾值大於一百二十八的噪點過濾為純白,其餘壓制為純黑,徹底消除環境陰影與紙張反光的干擾。

瀏覽器原生條碼檢測器多重輪詢:
我們直接調用現代瀏覽器底層的原生條碼檢測介面,依序對全景畫布、局部放大畫布與高對比二值化畫布進行極速檢測。由於原生檢測器是由瀏覽器底層以編譯語言直接執行,三塊畫布的連續掃描在現代智慧型手機上僅耗費數十毫秒;只要其中任何一塊畫布成功鎖定二維條碼,管線便立刻中斷並回傳原始字串,完全不需等待遠端網路往返。

台灣電子發票規格逆向工程解析器:
解析器接收到解碼字串後,首先執行民國年轉西元年的日期校正運算,提取十碼發票字軌與七碼購買日期。隨後定位雙星號標記,以每三個欄位為一組將品名、數量與單價精確切片,並即刻導入語意分類器為每項商品打上生活分類標籤;若發票左側未記載細項商品,解析器自動攔截第二十九至三十七位元組的十六進位字串,將其逆向還原為十進位的正確總金額,完成雙重保險的資料封裝。

三、核心代碼精華:ai-scan.js

以下為多重路徑二維碼檢測、局部畫布二值化增強與台灣發票規格逆向解析的關鍵實作:

// 台灣電子發票規格逆向解析器
function parseTaiwanEInvoiceQR(qrText) {
    if (!qrText) return null;
    try {
        let dateStr = null;
        let invNum = null;

        // 1. 民國年轉西元年日期解析 (位置 10~16, 7碼 YYYMMDD)
        if (qrText.length >= 17) {
            invNum = qrText.substring(0, 10);
            const rocYear = parseInt(qrText.substring(10, 13), 10);
            const month = qrText.substring(13, 15);
            const day = qrText.substring(15, 17);
            if (rocYear > 0 && month && day) {
                const year = rocYear + 1911;
                dateStr = `${year}-${month}-${day}`;
            }
        }

        // 2. 尋找雙星號 (**) 分隔符號:解構商品明細品項
        const starPos = qrText.indexOf('**');
        let items = [];

        if (starPos !== -1) {
            const itemsStr = qrText.substring(starPos + 2);
            const parts = itemsStr.split(':');
            // 規範:每 3 個欄位為一組 [品名, 數量, 單價]
            for (let i = 0; i + 2 < parts.length; i += 3) {
                const name = parts[i].trim();
                const count = parseInt(parts[i + 1], 10) || 1;
                const price = parseInt(parts[i + 2], 10) || 0;
                if (name) {
                    items.push({
                        name: name,
                        count: count,
                        price: price,
                        category: inferCategoryFromName(name)
                    });
                }
            }
        }

        // 3. 備援方案:若未記載細項,提取 29~37 欄位之十六進位總金額
        if (items.length === 0 && qrText.length >= 37) {
            const hexTotal = qrText.substring(29, 37);
            const totalAmount = parseInt(hexTotal, 16);
            if (!isNaN(totalAmount) && totalAmount > 0) {
                const itemName = invNum ? `電子發票 (${invNum})` : "電子發票金額";
                items.push({
                    name: itemName,
                    count: 1,
                    price: totalAmount,
                    category: "shopping"
                });
            }
        }

        if (items.length > 0) {
            return {
                store_name: "電子發票 (QR Code 優先精準解析)",
                date: dateStr,
                items: items
            };
        }
    } catch (e) {
        console.warn('QR Code 發票解析失敗:', e);
    }
    return null;
}

// 高能力多重路徑 QR Code 檢測器(地端 0 秒免 Key 解析)
async function detectQRCodeMultiPass(base64DataUrl) {
    return new Promise((resolve) => {
        const img = new Image();
        img.onload = async () => {
            const w = img.naturalWidth || img.width;
            const h = img.naturalHeight || img.height;
            if (!w || !h) return resolve(null);

            const canvasesToTry = [];

            // 路徑 1:原圖全圖畫布
            const canvasFull = document.createElement('canvas');
            canvasFull.width = w;
            canvasFull.height = h;
            const ctxFull = canvasFull.getContext('2d');
            ctxFull.drawImage(img, 0, 0);
            canvasesToTry.push(canvasFull);

            // 路徑 2:發票下半部 65% 區域特化放大畫布
            const cropY = Math.floor(h * 0.35);
            const cropH = h - cropY;
            const canvasBottom = document.createElement('canvas');
            canvasBottom.width = w;
            canvasBottom.height = cropH;
            const ctxBottom = canvasBottom.getContext('2d');
            ctxBottom.drawImage(img, 0, cropY, w, cropH, 0, 0, w, cropH);
            canvasesToTry.push(canvasBottom);

            // 路徑 3:高對比反差強化黑白二值化畫布
            try {
                const canvasContrast = document.createElement('canvas');
                canvasContrast.width = w;
                canvasContrast.height = cropH;
                const ctxContrast = canvasContrast.getContext('2d');
                ctxContrast.drawImage(img, 0, cropY, w, cropH, 0, 0, w, cropH);
                const imgData = ctxContrast.getImageData(0, 0, w, cropH);
                const data = imgData.data;

                // 逐像素灰階運算與二值化過濾
                for (let i = 0; i < data.length; i += 4) {
                    const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
                    const val = avg > 128 ? 255 : 0;
                    data[i] = val;
                    data[i + 1] = val;
                    data[i + 2] = val;
                }
                ctxContrast.putImageData(imgData, 0, 0);
                canvasesToTry.push(canvasContrast);
            } catch (e) {
                console.warn('二值化 Canvas 建立跳過:', e);
            }

            // 調用原生 BarcodeDetector 進行多重圖像區域掃描
            if ('BarcodeDetector' in window) {
                try {
                    const detector = new BarcodeDetector({ formats: ['qr_code'] });
                    for (const targetCanvas of canvasesToTry) {
                        const barcodes = await detector.detect(targetCanvas);
                        for (const barcode of barcodes) {
                            const parsed = parseTaiwanEInvoiceQR(barcode.rawValue);
                            if (parsed) {
                                return resolve(parsed);
                            }
                        }
                    }
                } catch (e) {
                    console.warn('BarcodeDetector 檢測失敗:', e);
                }
            }

            resolve(null);
        };
        img.onerror = () => resolve(null);
        img.src = base64DataUrl;
    });
}

四、深度調校剖析:零成本優先權與影像處理效能

在結合地端圖像運算與遠端大模型的雙軌架構中,我們進行了縝密的調度優化:

零成本最高優先權攔截:
在整個辨識管線的最前線,系統將地端檢測器設定為絕對的第一順位。只要使用者拍攝的照片符合台灣電子發票規範且條碼成功被解碼,管線便立刻將解析結果組裝為標準卡片並直接呈現,完全不會向遠端發起任何網路請求。這意味著使用者在國內日常的大多數消費記帳中,享受的是零秒延遲且完全免費的極致體驗。

逐像素二值化的陣列運算優化:
在前端執行圖像像素遍歷時,若演算法效率不佳,很容易造成主執行緒短暫凍結。我們在操作影像資料陣列時,採用了一維連續陣列直接運算,將紅綠藍三原色取平均值後立即與閾值比對,原地寫回同一記憶體區塊。這樣的極簡就地處理避免了反覆分配記憶體的垃圾回收開銷,即使在入門級手機上也能在數十毫秒內完成整張畫布的高對比過濾。

雙重容錯與十六進位金額兜底:
在實際消費場景中,許多小額店家或特定收銀機印製的發票,其左側條碼並不包含完整的商品明細清單。若解析器只依賴雙星號判斷,就會判定失敗。我們嚴格參照台灣財政部技術規範,在明細缺失的情境下自動啟用第二道防線,精確截取第二十九至三十七位元組並以十六進位基底轉換為數值,確保發票總金額依然能被一分不差地精準提取。

五、總結

今天我們成功實作了台灣電子發票規格逆向工程、發票特化局部放大與多重黑白二值化圖像增強演算法。打破了發票辨識必須全面依賴雲端大模型的昂貴枷鎖,讓計算機在手機瀏覽器本機端實現了零秒、零延遲、免金鑰且不花一毛模型費用的強大辨識力。

然而,當我們在手機端拍攝長條型發票或細微收據時,新的設備相容性難題接踵而至:為什麼在手機上我們更需要直接呼叫系統相機進行近距離對焦?而在筆電或桌機上又該如何實現畫面變焦與縮放?拍攝超高畫質照片時,又該如何避免手機記憶體瞬間被撐爆?明天我們將為大家深入剖析跨裝置相機調用與畫布記憶體防爆保護!

明日預告

拿著手機拍長發票常常對焦模糊,拍了高畫質大圖又讓手機瀏覽器直接閃退?而在沒有後置鏡頭的筆電前,又該如何對準細微文字?明天我們將揭開手機原生相機調用、桌機視訊變焦與前端記憶體防爆的精采實踐,敬請期待!


上一篇
Day 22:【高階商務計算機】首頁相機入口、視訊串流安全釋放與高科技雷射掃描動畫
下一篇
Day 24:【高階商務計算機】記憶體又爆了?看高階商務計算機如何完美消化「原生相機直拍 + 長發票」
系列文
液態玻璃美學:結合 Gemini 與 Antigravity IDE,打造 120FPS、多視窗比例與網格順延推擠佈局的強大商務運算終端 共 28 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言