正品庫拍照PWA應用的實現與性能優化|得物技術

一、? 背景與難點

背景

目前得物ERP主要鑒別流程,是通過鑒別師鑒別提需到倉庫,倉庫庫工去進行商品補圖拍照,現有正品庫59%的人力投入在線下商品借取/歸還業務的操作端,目前,線下借取的方式會占用商品資源,同時在使用用途上,每借出10件會出現1次拍照留檔,因此會有大量的線上閱圖量在日常鑒別和學習中發生;正品庫可通過圖庫搭建,提升圖庫質量,大大節約線下用工和物流成本支出。

但目前庫內存量10~20W件,待進行拍照同步到正品庫中,且目前仍不斷有新品入庫,現有的補圖流程效率約每天30件,難以滿足快速正品庫建立的需要, 主要有以下問題:

※ ?補圖圖片上傳途徑繁瑣

倉端接收到補圖任務后,需使用ERP網頁端完成圖片拍攝&上傳操作,流程繁瑣,操作冗余。

※ ?留檔圖拍攝上傳質量壓縮

新品圖片&補圖圖片上傳ERP后,圖片質量壓縮,部分留檔圖因不清晰需重新拍攝,浪費作業人力。

※ ?鑒別借還操作途徑單一

鑒別借用&歸還只能于PC端操作,不利于鑒別在庫內現場進行借用&歸還。

※ ?正品流轉效率問題

在圖庫建立前有很多鑒別是需要借用到實物的,借用之后的登記、歸還等流程會大大影響流傳效率,同時存在異地倉庫借閱的情況,成本和周期更高。

?優化前后整體方案對比

綜合來說,其實相當于整體的操作都需要在手持設備上完成(包括上傳、拍攝、通知等),這減少了過程操作繁多而導致的效率問題和圖片質量問題。

難點

在Web端上,去實現一個自定義的相機拍攝能力是相對簡單的,實現一個獲取視頻流轉化為圖片的能力也不復雜的。我們的初版應用的拍攝標準是1280x1280的圖片,但鑒別師希望有更高的分辨率,能夠得到原相機一模一樣的拍攝結果,所以必須需要提高分辨率,按照手機原相機的分辨率去加工處理圖片。以倉庫的 iPhoneX 為例:若需分辨率達到超高清范疇的4032 * 3024,庫工需要連續拍攝幾十次甚至上百次的各個模板位的圖片,才能完成一件正品的存檔工作。

綜合難點

※ ?分辨率激增帶來的內存壓力

  1. 內存占用暴增,單個從6.4M左右躍升到48.8M,增長7.6倍。

  2. 超高清分辨率需要更多的GPU內存和計算資源。

  3. 高分辨率與流暢體驗難以兼顧。

※ ?PWA內存分配限制

  1. 多層內存限制:拿iPhoneX為例,從3GB系統內存到300~500MB的實際可用內存,層層削減。若除去一些基礎的開銷(比如js引擎、WebKit開銷等開銷)后則更少,更容易達到系統限制的內存紅線,進而產生卡頓、失敗、被強制回收,降頻等情況。

  2. Webkit嚴格限制,瀏覽器對單個標簽頁內存使用有硬性上限。

※ ?視頻流與圖像處理的資源競爭

  1. 視頻流和圖像處理同時占用大量內存。

  2. GPU資源競爭,視頻解碼和Canvas繪制爭奪GPU資源。

※ ?移動設備性能差異化

  1. 硬件碎片化:不同設備內存和性能差異巨大。

  2. 兼容性問題:需要為不同性能的設備提供不同策略,保障任務的進行。

※ ?瀏覽器內存管理的不可控性

  1. 內存分配不可預測:系統會根據整機的內存壓力動態調整分配。自身web應用無法參與調控。

  2. GC時機不可控:垃圾回收可能在關鍵時刻觸發,影響作業流程。

  3. 進程終止風險:極端情況下瀏覽器自己會終止頁面,reload。

二、實現方案

整體技術實現

我們整體的技術實現基于?WebRTC 和 HTML5 Canvas 以及Web worker

※ ?WebRTC

navigator.mediaDevices.getUserMedia 是 WebRTC API 的一部分,用于訪問用戶設備的攝像頭和麥克風。它可以請求用戶授權以獲取視頻或音頻流,并將實時媒體流綁定到 <video> 標簽上。

※ ?HTML5 的 video

用于顯示攝像頭捕捉到的實時視頻流。

※ ?Canvas

通過 canvas 元素,可以從 <video> 標簽的當前幀中捕獲圖像(拍照),并將其轉換為圖片格式(如 PNG 或 JPEG)。

※ ?WebWorker

通過允許在后臺線程中運行腳本,避免阻塞主線程(UI 線程),從而解決復雜計算導致的頁面卡頓問題。

整體架構

整體方案簡要

  1. 在pwa頁面中開啟攝像頭

  2. 獲取視頻流: CameraStreamManager管理相機流,提供video元素

  3. 等待幀穩定

  4. 通過視頻流,創建ImageBitmap

  5. Worker處理: 將ImageBitmap傳遞給Worker進行處理

  6. 策略選擇,根據設備情況做策略選擇

  7. Worker中使用chunked、chunkedConvert等策略分塊處理大圖像

  8. 生成結果: 返回ObjectUrl(內存中的文件或二進制數據)

  9. 更新UI: 更新預覽和上傳隊列

  10. 資源回收

  11. 結束或下一步

其中的實現細節內更多偏向于資源的精細化管理、回收釋放、重試機制、容錯機制等。

最核心的準則是:性能優先,穩定保底

產品使用流程

操作流程里的核心是針對此前在電腦和手機中反復切換拍攝、錄入、上傳等復雜的操作,轉變為在手持設備中一站式完成補圖、拍攝、上傳和通知等。

操作時序

三、性能優化

性能優化思維導圖

為什么需要性能優化

  • 頁面卡頓

  • 低端機型無法順暢拍照

  • 圖片轉化慢,手機熱..

  • 高頻出現圖像轉化失敗

  • 突破內存峰值,系統回收內存降頻等,程序reload

  • ...

首先看下此前的策略中的性能表現,首先我們用的的是超高分辨率的約束配置條件:

const?videoConstraints?=?useRef({video: {facingMode:?'environment',width: {min:?1280,ideal:?4032,max:?4032},height: {min:?720,ideal:?3024,max:?3024},frameRate: {ideal:?30, // 適當降低可以降低視頻緩沖區的內存占用,我們先按照這樣的場景來看。min:?15},advanced: [{?focusMode:?"continuous"?},]}?as?MediaTrackConstraints,
});

如果單獨拍攝一張圖內存,粗略計算為如下(主要以iPhoneX的情況做解析):

// 視頻流約束
const?iphoneXStreamConfig = {width:?4032,height:?3024,frameRate:?24,format:?'RGBA'?// 4字節/像素
};// 單幀內存計算
const?frameMemoryCalculation = {// 單幀大小pixelCount:?4032?*?3024, ? ? ? ? ? ? ? ? ? ?// = 12,192,768 像素bytesPerFrame:?4032?*?3024?*?4, ? ? ? ? ? ??// = 48,771,072 字節mbPerFrame: (4032?*?3024?*?4) / (1024?*?1024),?// ≈ 46.51 MB
};// 實際運行時內存占用
const?runtimeMemoryUsage = {// 視頻流緩沖區 (至少3-4幀)streamBuffer: {frameCount:?4,totalBytes:?48771072?*?4, ? ? ? ?// ≈ 186.04 MBdescription:?'視頻流緩沖區(4幀)'},// 處理管道內存processingPipeline: {captureBuffer:?46.51, ? ? ? ? ? ?// 一幀的大小processingBuffer:?46.51, ? ? ? ??// 處理緩沖encoderBuffer:?46.51?*?0.5, ? ? ?// 編碼緩沖(約半幀)totalMB:?46.51?*?2.5, ? ? ? ? ??// ≈ 116.28 MBdescription:?'視頻處理管道內存'},// 總體內存total: {peakMemoryMB:?186.04?+?116.28, ?// ≈ 302.32 MBstableMemoryMB:?186.04?+?93.02,?// ≈ 279.06 MBdescription:?'預估總內存占用'}
};

單張圖的內存占用

按照上文的視頻約束條件,單幀大小:約 46.51MB,實際單張內存需要76.7M左右(15 + 15 + 46.5 + 0.2 「objectURL引用」),三五張圖大概就會達到內存限制紅線,這樣的內存占用對移動設備來說太大了,實際上,在項目上線初期,業務使用也反饋:拍照幾張手機發熱嚴重,頁面經常卡死。

PWA相機應用內存占用情況

在移動端中,特別是ios,內存限制是動態的,依賴多個因素,如:設備物理內存總量,設備當前可用內存,后臺的軟件運行情況。上文可以看出至少有300M是固定支出的,還需增加一些WebRtc視頻幀緩沖累積的占用、瀏覽器內存緩存解碼幀的堆積。

在iPhone的WeKit的內核瀏覽器下,官方內存限制雖是1.5G,實際上可能在是800-1200M左右,在實際的測試場景下,甚至還要低很多。

拍攝過程內存變化

秒數是為了更直觀的觀察區分內存數據的變化。

有些并不能立即回收canvas對象,需要等之前的二進制blob文件被回收后才可進行,這無疑是在慢慢增加內存的壓力。

內存壓力趨勢分析

基于上文的單獨內存占用和相機應用的內存占用(按照1.5G的分配),可以粗略分析出:

這些大部分都是官方的數據計算和累積,在實際操作中,如果操作過快,差不多會在第三、四張時開始出現問題了。因為變量比較多,比如充電或發熱情況;而連續作業時候的情況又各不同,但是整體規律是差不多的。上文分析的是5張開始危險,實際情況則是第三張就已經出現問題了。

不僅如此,在拍攝作業流程中,還有CPU的熱節流風險,如內存85%使用率超過30秒,cpu會降頻至70%或更低的性能。

這其中的主要消耗是:視頻流處理(35-45%) + Canvas處理(25-35%) ?及4032×3024這類大分辨率導致的計算密集型操作。

做了哪些優化

  • canvas主線程繪制更改為離屏渲染繪制

  • 視頻流管理、前置設備參數預熱

  • 分辨率管理

  • 引入Webworker線程單獨繪制

  • 優化設備檢測策略

  • 異步上傳管理

  • 產品兜底,頁面reload,緩存歷史數據

  • 內存分配模型

方案選擇與實現

實現原相機拍攝的最初的一版,是通過把canvas內容轉為base64后,同步上傳圖片,最初通過一些低端機的測試情況來看,最主要的問題是圖片比較大,生成的base64的code自然也比較大,在數據體積上會增大33%左右。 因為是移動設備,這么大的圖片上傳的速度又相對緩慢,導致操作的過程需要等待和加載。

在這樣的場景下為什么要異步上傳呢,如果拍攝的快些,頁面會變得很卡頓。由于大量的字符串涌入到頁面中,再加上cavans轉化這么大的image到base64 code又會比較消耗內存,所以整體有丟幀卡頓的表現。進而考慮替換為blobUrl。

toDataURL 和 toBlob對比

如上所示,我們最終選擇了性能更好的canvas to Blob并使用二進制的形式。

更快的回顯

更快的轉化

更小的內存占用

在運用了 Blob 后, 通過埋點等操作,頁面渲染和流暢度雖然有所緩解,但會在比較高頻的情況下出現圖片轉化失敗,而且也是間隔性的,如上文所示,我們根據渲染和一些實際案例分析過后,發現問題還是存在于內存峰值和CPU資源。

canvas.convertToBlob失敗主要是因為內存的限制問題,特別是在處理大圖像時。編碼同一圖像可能在資源充足時成功,資源緊張時失敗,這也就解釋了為什么是間隔性的出現轉化失敗。

因為有大量的繪制需在主線程完成,但由于JS的單線程問題,嚴重影響了頁面的操作和后續的渲染, 使得庫工的作業流程被迫等待。因此,我們引入了WebWorker以及OffscreenCanvas,開啟新線程專一用來做繪制。當然Webworker中的內存的管理也是比較復雜的,同樣會占據大量內存,也有數據通信成本,但是相較于用戶體驗,我們不得不做一定程度的平衡和取舍。

Web Worker + OffscreenCanvas 架構

  • 主線程不阻塞:圖像處理在Worker中進行,UI保持響應

  • 更好的性能:OffscreenCanvas在獨立線程中渲染

  • 內存隔離:Worker獨立內存空間,避免主線程內存壓力

好處就是可以多張并發,降低內存泄漏風險,劣勢是開發復雜度增加,調試困難, 數據傳輸開銷(ImageBitmap需要轉移所有權)。

相機資源的動態管理與釋放

我們知道每個機器的分辨率與他們對WebRtc相關能力的支持是不同的。比如iPhoneX 的最大分辨率支持是:4032 * 3024,其他的機器則會不同,所以固定的分辨率配置是行不通的,需要在進入相機后檢查設備支持情況等。以及視頻通道的保留操作和暫時性暫停,也對操作流程產生著很大積極影響。在繼續服用的場景下僅暫停數據傳輸,保持活躍連接,在下一張拍攝的時候復用連接,而非重新進行初始化、連接和檢查等操作。

ImageBitmap 直接創建策略

在繪制中,如果 imageData 是普通的 Image 或 Canvas,每次 drawImage 都可能涉及格式轉換和內存拷貝,無疑增大了內存支出。引入 ImageBitmap,因其是專門為高性能圖像作處理設計,數據存儲在 GPU 內存中,最重要的是:它支持內存的復制轉義,可以交到Webworker中去處理,可以在主線程和 Worker 之間零拷貝傳輸,在worker中直接使用,無需解碼。

直接從視頻流創建ImageBitmap,跳過Canvas中間步驟。

...
let?imageBitmap:?ImageBitmap?|?null?=?null;
// 判斷是否為視頻元素,如果是則嘗試直接創建ImageBitmap
// 支持img 和 vedio
if?((source?instanceof?HTMLVideoElement?|| source?instanceof?HTMLImageElement) && supportsImageBitmap) {try?{console.log('嘗試直接從視頻元素創建ImageBitmap');// 直接從視頻元素創建ImageBitmap,跳過Canvas中間步驟if?(source?instanceof?HTMLVideoElement) {imageBitmap =?await?createImageBitmap(source,0,?0, sourceWidth, sourceHeight);}?else?{// 支持imgimageBitmap =?await?createImageBitmap(source);}console.log('直接創建ImageBitmap成功!!');}?catch?(directError) {console.warn('這直接從視頻創建ImageBitmap失敗,回退到Canvas:', directError);// 失敗后將通過下面的Canvas方式創建imageBitmap =?null;}}...

createImageBitmap 實際上是:

  • 創建一個位圖引用

  • 可能直接使用視頻解碼器的輸出緩沖區

  • 在支持的平臺上,直接使用GPU內存中的紋理

  • 最重要的是:不涉及實際的像素繪制操作、高效的跨線程傳輸(支持通過結構化克隆算法高效傳輸避免了序列化/反序列化開銷,能高效傳送到Worker)

※ ?綜合表現

  • 性能最優: 避免Canvas繪制的中間步驟。

  • 內存效率: 直接從視頻幀創建位圖,占用更低。

  • 硬件加速: 可利用GPU加速。

Worker中的圖像處理策略

在web端,主線程和Worker間的數據傳輸有三種方式,結構化克隆和Transferable對象,ShareArrayBuffer(共享內存訪問,支持度有問題),整體上使用Transferable對象的形式,可降低內存消耗。接下來,我們簡單介紹這里用到的兩種執行策略。

※ ?chunked策略(chunked processing分塊處理)

主要源于內存控制,避免圖像過大導致的內存溢出。將大圖像分割成多個小塊,使用一個小的臨時畫布逐塊處理后繪制到最終畫布,通過"分而治之"的策略顯著降低內存峰值使用,避免大圖像處理時的內存溢出問題。

劣勢是處理時間增加,算法復雜度高。

chunked策略流程示意

class?ChunkedProcessStrategy?extends?ImageProcessStrategy?{readonly?name =?'chunked';protected?async?doProcess(imageData:?ImageBitmap,?options:?ProcessOptions):?Promise<Blob> {const?{ width, height, quality } = options;const?optimalChunkSize =?ResourceManager.calculateOptimalChunkSize(width, height);const?chunkConfig:?ChunkConfig?= {size: optimalChunkSize,cols:?Math.ceil(width / optimalChunkSize),rows:?Math.ceil(height / optimalChunkSize),};const?{?canvas: finalCanvas,?ctx: finalCtx } =?ResourceManager.createCanvas(width, height);const?{?canvas: tempCanvas,?ctx: tempCtx } =?ResourceManager.createCanvas(optimalChunkSize, optimalChunkSize);try?{for?(let?row =?0; row < chunkConfig.rows; row++) {for?(let?col =?0; col < chunkConfig.cols; col++) {await?this.processChunk(imageData,tempCanvas,tempCtx,finalCtx,row,col,chunkConfig,width,height);await?new?Promise(resolve?=>?setTimeout(resolve,?0));}}return?await?finalCanvas.convertToBlob({type:?'image/jpeg',quality,});}?finally?{ResourceManager.releaseResources(tempCanvas, tempCtx);ResourceManager.releaseResources(finalCanvas, finalCtx);}}private?async?processChunk(imageData:?ImageBitmap,tempCanvas:?OffscreenCanvas,tempCtx:?OffscreenCanvasRenderingContext2D,finalCtx:?OffscreenCanvasRenderingContext2D,row:?number,col:?number,chunkConfig:?ChunkConfig,width:?number,height:?number):?Promise<void> {const?x = col * chunkConfig.size;const?y = row * chunkConfig.size;const?chunkWidth =?Math.min(chunkConfig.size, width - x);const?chunkHeight =?Math.min(chunkConfig.size, height - y);tempCtx.clearRect(0,?0, chunkConfig.size, chunkConfig.size);tempCtx.drawImage(imageData,x, y, chunkWidth, chunkHeight,0,?0, chunkWidth, chunkHeight);finalCtx.drawImage(tempCanvas,0,?0, chunkWidth, chunkHeight,x, y, chunkWidth, chunkHeight);}
}...

主要針對中等性能的機型,適用于直接轉化可能失敗的情形。

※ ?chunkedConvert策略(分塊處理轉化)

將大圖像分塊后,每塊獨立轉換為壓縮的Blob存儲,最后再將所有Blob重新解碼,同時合并到最終畫布,通過"分塊壓縮存儲 + 最終合并"的策略實現極致的內存控制,但代價是處理時間翻倍,屬于時間換內存的策略。

chunkedConvert策略流程示意

// 分塊轉化 最終返回
class?ChunkedProcessStrategy?extends?ImageProcessStrategy?{readonly?name =?'chunked';protected?async?doProcess(imageData:?ImageBitmap,?options:?ProcessOptions):?Promise<Blob> {const?{ width, height, quality } = options;const?optimalChunkSize =?ResourceManager.calculateOptimalChunkSize(width, height);const?chunkConfig:?ChunkConfig?= {size: optimalChunkSize,cols:?Math.ceil(width / optimalChunkSize),rows:?Math.ceil(height / optimalChunkSize),};const?{?canvas: finalCanvas,?ctx: finalCtx } =?ResourceManager.createCanvas(width, height);const?{?canvas: tempCanvas,?ctx: tempCtx } =?ResourceManager.createCanvas(optimalChunkSize, optimalChunkSize);try?{for?(let?row =?0; row < chunkConfig.rows; row++) {for?(let?col =?0; col < chunkConfig.cols; col++) {await?this.processChunk(imageData,tempCanvas,tempCtx,finalCtx,row,col,chunkConfig,width,height);// 給GC機會await?new?Promise(resolve?=>?setTimeout(resolve,?0));}}return?await?finalCanvas.convertToBlob({type:?'image/jpeg',quality,});}?finally?{ResourceManager.releaseResources(tempCanvas, tempCtx);ResourceManager.releaseResources(finalCanvas, finalCtx);}}private?async?processChunk(imageData:?ImageBitmap,tempCanvas:?OffscreenCanvas,tempCtx:?OffscreenCanvasRenderingContext2D,finalCtx:?OffscreenCanvasRenderingContext2D,row:?number,col:?number,chunkConfig:?ChunkConfig,width:?number,height:?number):?Promise<void> {const?x = col * chunkConfig.size;const?y = row * chunkConfig.size;const?chunkWidth =?Math.min(chunkConfig.size, width - x);const?chunkHeight =?Math.min(chunkConfig.size, height - y);tempCtx.clearRect(0,?0, chunkConfig.size, chunkConfig.size);tempCtx.drawImage(imageData,x, y, chunkWidth, chunkHeight,0,?0, chunkWidth, chunkHeight);finalCtx.drawImage(tempCanvas,0,?0, chunkWidth, chunkHeight,x, y, chunkWidth, chunkHeight);}
}...
...class?ChunkedConvertStrategy?extends?ImageProcessStrategy?{readonly?name =?'chunkedConvert';protected?async?doProcess(imageData:?ImageBitmap,?options:?ProcessOptions):?Promise<Blob> {const?{ width, height, quality } = options;const?config =?WorkerConfig.getInstance();const?chunks:?Array<{blob:?Blob;x:?number;y:?number;width:?number;height:?number;}> = [];// 分塊處理for?(let?y =?0; y < height; y += config.chunkSize) {for?(let?x =?0; x < width; x += config.chunkSize) {const?chunkWidth =?Math.min(config.chunkSize, width - x);const?chunkHeight =?Math.min(config.chunkSize, height - y);const?chunk =?await?this.processSingleChunk(imageData, x, y, chunkWidth, chunkHeight, quality);chunks.push({ ...chunk, x, y,?width: chunkWidth,?height: chunkHeight });await?new?Promise(resolve?=>?setTimeout(resolve,?0));}}// 合并塊return?chunks.length?===?1?? chunks[0].blob?:?await?this.mergeChunks(chunks, width, height, quality);}private?async?processSingleChunk(imageData:?ImageBitmap,x:?number,y:?number,width:?number,height:?number,quality:?number):?Promise<{?blob:?Blob?}> {const?{ canvas, ctx } =?ResourceManager.createCanvas(width, height);try?{ctx.drawImage(imageData, x, y, width, height,?0,?0, width, height);const?blob =?await?canvas.convertToBlob({type:?'image/jpeg',quality,});return?{ blob };}?finally?{ResourceManager.releaseResources(canvas, ctx);}}private?async?mergeChunks(chunks:?Array<{?blob:?Blob;?x:?number;?y:?number;?width:?number;?height:?number?}>,width:?number,height:?number,quality:?number):?Promise<Blob> {const?{?canvas: finalCanvas,?ctx: finalCtx } =?ResourceManager.createCanvas(width, height);try?{for?(const?chunk?of?chunks) {const?imgBitmap =?await?createImageBitmap(chunk.blob);try?{finalCtx.drawImage(imgBitmap,0,?0, chunk.width, chunk.height,chunk.x, chunk.y, chunk.width, chunk.height);}?finally?{imgBitmap.close();}await?new?Promise(resolve?=>?setTimeout(resolve,?0));}return?await?finalCanvas.convertToBlob({type:?'image/jpeg',quality,});}?finally?{ResourceManager.releaseResources(finalCanvas, finalCtx);}}
}

會有更小的峰值,適配與更低端的機型和極大圖像。不會內存溢出,但是也會降低轉化效率。在可用與效率方面,選擇了可用。

其中整體方案里還有一些其他的策略,如Direct直接轉化、邊轉化邊繪制等,會根據不同的機型進行選擇。目前,重點保障低端機型,因為中高端機器在使用過程中沒有性能上的卡點。

優化后對比

首先,我們明確了這幾個主要策略:

  • Web Worker架構 - 主線程內存壓力分散

  • ImageBitmap直接傳輸 - 減少內存拷貝

  • 繪制分塊處理 - 降低內存峰值

  • 資源管理優化 - Canvas復用和及時釋放

最重要策略:增加很多管理器和優化方式降低內存的峰值,即那一瞬間的值。

同時,將可以在后臺做轉化和運算的操作,投入到web worker中去做,降低主線程的內存壓力。

優化后單圖內存占用情況

優化后PWA相機應用內存占用

優化后的效果

※ ?內存優化結果

  1. 單張圖片處理峰值減少33% - 從123.2MB降至82.2MB。

  2. 單張圖片持久占用減少61% - 從76.7MB降至30.2MB。

  3. PWA應用整體內存優化16-26% - 根據圖片數量不同。

  4. 內存壓力等級顯著降低,如從3-4張開始有明顯警示壓力,到操作快速秒級拍攝速率時才出現(實際操作過程中大概10-15秒一張,因需要擺放和根據模版與提醒進行拍攝)。

※ ?用戶體驗

  • 最終在高清圖片的繪制作業流程中,由原來的3張圖告警到一次性可以拍攝50張圖的情況,大大降低了失敗風險。提升了作業的流暢度。

  • 用戶體驗改善,消除UI阻塞,響應時間減半。

四、業務結果

通過幾輪的策略優化,整個pwa應用已可以相對順暢、高效的繪制原相機標準的正品圖,已完全達到鑒別師高清圖的要求,同時不會有操作流的中斷。

  • 目前日均的拍攝件數提升?330%,達成預期目標。

  • 將每件的人力投入成本降低?41.18%

  • 目前通過PWA項目快速搭建了圖庫項目,Q2拍照數據占比72.5%,預期后面比例會逐步升高,圖庫流轉效率提高到了20%,超出業務預期。

五、規劃和展望

在技術的實現上,許多時候要去做用空間換時間或用時間換空間的策略方案,本質上還是根據我們當前的業務場景和訴求,追求當下收益。有些時候可能不止局限在實現上,需要從實際需求出發,不應該只停留在工具的層面,而深入到業務里剖析挖掘其潛在的業務價值,做更深遠的思考,從工具思維轉向價值發現與傳遞的方向上。

未來我們還會思考:

  1. 前置對設備的綜合能力評估,更精細化的拆分低、中、高端設備和適配策略,收集更多的實際處理時間和內存峰值、CPU 性能指標等,用于不斷優化策略選擇算法。

  2. 根據類目做區分(比如鞋服、奢品),這些在鑒別的時候圖片質量有不同的品質要求的分類。后續可能會進行更加具有定制化屬性的方案,針對鑒別打標,針對當前業務中圖片拍攝重試場景下的AI圖像識別,針對重復拍攝場景做優化,進一步提高效率。

  3. 針對目前 10 到 15 秒的拍攝時間,能進一步壓縮問題,思考更加智能的拍攝能力。根據設備的真實情況,或基于色溫分析的光線評估,提高圖像質量和降低重復率。基于正品特征進行構圖優化,在設備上做實時拍攝指導,不只以單一模板和示例進行人工檢查,而是進一步標準化,降低人力參與度。

  4. 針對于商研側業務和前置拍照流程,將拍照H5的方案也納入采賣商品入庫流程,同時支持鑒別師對于圖庫的驗收,加快圖庫的驗收入庫效率,縮短庫內的拍照數據積壓周期。

往期回顧

1.匯金資損防控體系建設及實踐 | 得物技術

2.一致性框架:供應鏈分布式事務問題解決方案|得物技術

3.Redis 是單線程模型?|得物技術

4.得物社區活動:組件化的演進與實踐

5.從CPU冒煙到絲滑體驗:算法SRE性能優化實戰全揭秘|得物技術

文 / 維克

關注得物技術,每周更新技術干貨

要是覺得文章對你有幫助的話,歡迎評論轉發點贊~

未經得物技術許可嚴禁轉載,否則依法追究法律責任。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/87663.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/87663.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/87663.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

如何使用python識別出文件夾中全是圖片合成的的PDF,并將其移動到指定文件夾

引言 在現代數字化工作流程中&#xff0c;無論是為機器學習模型處理數據&#xff0c;還是進行數字歸檔&#xff0c;區分原生文本 PDF&#xff08;例如&#xff0c;由文字處理器生成的報告&#xff09;和基于圖像的 PDF&#xff08;例如&#xff0c;掃描的發票、檔案文件&#…

淘系怎么做?

首先&#xff0c;要明確一點就是&#xff0c;補單不是“刷/單”&#xff0c;補單是為了給買家營造一個良好的購物氛圍&#xff0c;畢竟再好的產品沒有排名、沒有權重&#xff0c;買家根本都沒有機會看到你的產品&#xff0c;而且只有讓淘寶感覺的產品有扶持必要它才會給你對應的…

網安系列【6】之[特殊字符] SQL注入揭秘:從入門到防御實戰指南

文章目錄一 真實案例二 SQL注入三 為什么危害堪比核彈&#xff1f;四 深入解剖攻擊原理&#x1f3af; 4.1&#xff1a;探測SQL漏洞的存在&#x1f3af; 4.2&#xff1a;數據庫信息探測&#x1f3af; 4.3&#xff1a;數據庫信息探測&#x1f3af; 4.4&#xff1a;數據庫信息進一…

Windows內核并發優化

Windows內核并發優化通過多層次技術手段提升多核環境下的系統性能&#xff0c;以下是關鍵技術實現方案&#xff1a; 一、內核鎖機制優化? 精細化鎖策略? 采用自旋鎖&#xff08;Spinlock&#xff09;替代信號量處理短臨界區&#xff0c;減少線程切換開銷 對共享資源實施讀…

【數據結構】 排序算法

【數據結構】 排序算法 一、排序1.1 排序是什么&#xff1f;1.2 排序的應用1.3 常見排序算法二、常見排序算法的實現2.1 插入排序2.1.1 直接插入排序2.1.2 希爾排序2.2 選擇排序2.2.1 直接選擇排序2.2.1.1 方法12.2.1.1 方法22.2.2 堆排序&#xff08;數組形式&#xff09;2.3 …

NumPy-核心函數np.matmul()深入解析

NumPy-核心函數np.matmul深入解析 一、矩陣乘法的本質與np.matmul()的設計目標1. 數學定義&#xff1a;從二維到多維的擴展2. 設計目標 二、np.matmul()核心語法與參數解析函數簽名核心特性 三、多維場景下的核心運算邏輯1. 二維矩陣乘法&#xff1a;基礎用法2. 一維向量與二維…

突破政務文檔理解瓶頸:基于多模態大模型的智能解析系統詳解

重磅推薦專欄&#xff1a; 《大模型AIGC》 《課程大綱》 《知識星球》 本專欄致力于探索和討論當今最前沿的技術趨勢和應用領域&#xff0c;包括但不限于ChatGPT、DeepSeek、Stable Diffusion等。我們將深入研究大型模型的開發和應用&#xff0c;以及與之相關的人工智能生成內容…

深入探討支持向量機(SVM)在乳腺癌X光片分類中的應用及實現

?? 博主簡介:CSDN博客專家、CSDN平臺優質創作者,高級開發工程師,數學專業,10年以上C/C++, C#, Java等多種編程語言開發經驗,擁有高級工程師證書;擅長C/C++、C#等開發語言,熟悉Java常用開發技術,能熟練應用常用數據庫SQL server,Oracle,mysql,postgresql等進行開發應用…

九、K8s污點和容忍

九、K8s污點和容忍 文章目錄九、K8s污點和容忍1、污點&#xff08;Taint&#xff09;和容忍&#xff08;Toleration&#xff09;1.1 什么是污點&#xff08;Taint&#xff09;&#xff1f;1.2 什么是容忍&#xff08;Toleration&#xff09;&#xff1f;1.3 污點的影響效果&…

基于開源AI智能名片鏈動2+1模式S2B2C商城小程序的超級文化符號構建路徑研究

摘要&#xff1a;在數字技術重構文化傳播生態的背景下&#xff0c;超級文化符號的塑造已突破傳統IP運營框架。本文以開源AI智能名片鏈動21模式與S2B2C商城小程序的融合創新為切入點&#xff0c;結合"嶼光生活"體驗館、快手燒烤攤主等典型案例&#xff0c;提出"技…

QT 日志 - qInstallMessageHandler將qDebug()打印內容輸出到文件

在編程開發中&#xff0c;日志功能至關重要&#xff0c;對于在開發期間或者是程序上線后&#xff0c;都有助于排查問題&#xff1b; 對于C/C和QT方向&#xff0c;日志庫有log4cpp、plog、log4qt等&#xff0c;本篇文章將使用qt自帶的日志方式去實現。 定義日志函數&#xff1a…

記錄一下seata啟動403問題

1.現象&#xff1a;啟動報錯可能是403&#xff0c;或是是密碼錯誤一般是nacos加了認證&#xff0c;seata配置nacos賬號密碼的時候就啟動不了。可能是密碼錯誤&#xff0c;最有可能是seata版本太低導致的。1.4.2以及一下的版本應該都有這個問題2.問題密碼不能有特殊符號如&#…

【STM32實踐篇】:GPIO 詳解

文章目錄GPIO 基本結構GPIO 工作模式GPIO 基本結構 右邊的紅框是I/O引腳&#xff0c;這個I/O引腳就是我們可以看到的芯片實物的引腳&#xff0c;其他部分都是GPIO的內部結構。 保護二極管 上方二極管用于防過壓保護&#xff0c;當I/O引腳電壓高于 V_DD 二極管導通壓降?時&…

#include

關于 C 中的 include <>和 include “” 這兩種形式&#xff0c;區別其實是關于“搜索路徑”和“優先級”的。讓我詳細為你講解。 1. 簡單區別總結 #include <header>&#xff1a;告訴編譯器去“系統標準目錄”或“預定義的標準路徑”中查找頭文件&#xff08;比如…

永磁同步電機參數辨識算法--帶遺忘因子的遞推最小二乘法辨識

一、原理介紹之前已經介紹了遞推最小二乘法進行電氣參數辨識&#xff0c;在實時參數辨識中&#xff0c;協方差矩陣P和增益矩陣K是用于更新參數估計的重要工具&#xff0c;而系統參數變化時&#xff0c;P、K矩陣會逐漸減小&#xff0c;導致數據飽和。數據飽和與參數遲滯是實時參…

JVM 知識點

一、JVM 概述JVM&#xff08;Java Virtual Machine&#xff09;即 Java 虛擬機&#xff0c;它是 Java 編程語言的核心組件之一&#xff0c;負責執行 Java 程序。JVM 使得 Java 程序可以實現“一次編寫&#xff0c;到處運行”的特性&#xff0c;因為它提供了一個抽象的運行環境&…

windows裝機

1、制作啟動盤 2、制作啟動盤 啟動盤中含有WinPE系統和ISO 3、從U盤啟動&#xff0c;加載ISO 4、執行ISO中的setup安裝win10 5、之后從C盤啟動進入win10系統 6、安裝“華為電腦管家”,安裝驅動 華為電腦管家官方下載-筆記本驅動更新 | 華為官網 7、下載安裝必要軟件 https://…

提示技術系列(13)——ReAct

什么是提示技術&#xff1f; 提示技術是實現提示工程目標的具體技術手段&#xff0c;是提示工程中的“工具庫”。 什么又是提示工程&#xff1f; 提示工程是指通過設計、優化和迭代輸入到大語言模型&#xff08;LLM&#xff09;的提示&#xff08;Prompt&#xff09;&#xff…

【SVO】klt與極限搜索塊匹配findEpipolarMatchDirect

Matcher::findEpipolarMatchDirect 函數邏輯與原理分析 核心目標&#xff1a; 在極線上搜索參考幀特征點 ref_ftr 在當前幀 cur_frame 中的最佳匹配點&#xff0c;并通過三角化計算深度。 關鍵步驟解析&#xff1a; 1. 極線端點計算&#xff1a; const BearingVector A T_…

C 語言基礎入門:基本數據類型與運算符詳解

一、基本數據類型C 語言提供了豐富的基本數據類型&#xff0c;用于存儲不同類型的數據&#xff0c;主要包括整數類型、浮點類型和布爾類型。1. 整數類型整數類型用于存儲整數&#xff0c;根據是否帶符號以及占用存儲空間的不同&#xff0c;可進一步細分&#xff1a;類型名占用存…