写点什么

正品库拍照 PWA 应用的实现与性能优化|得物技术

作者:得物技术
  • 2025-07-03
    上海
  • 本文字数:11485 字

    阅读完需:约 38 分钟

正品库拍照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 MB    description: '视频流缓冲区(4帧)'  },   // 处理管道内存  processingPipeline: {    captureBuffer: 46.51,            // 一帧的大小    processingBuffer: 46.51,         // 处理缓冲    encoderBuffer: 46.51 * 0.5,      // 编码缓冲(约半帧)    totalMB: 46.51 * 2.5,           // ≈ 116.28 MB    description: '视频处理管道内存'  },    // 总体内存  total: {    peakMemoryMB: 186.04 + 116.28,  // ≈ 302.32 MB    stableMemoryMB: 186.04 + 93.02, // ≈ 279.06 MB    description: '预估总内存占用'  }};
复制代码

单张图的内存占用

按照上文的视频约束条件,单帧大小:约 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 和 vedioif ((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 {      // 支持img      imageBitmap = 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 性能优化实战全揭秘|得物技术


文 / 维克


关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

用户头像

得物技术

关注

得物APP技术部 2019-11-13 加入

关注微信公众号「得物技术」

评论

发布
暂无评论
正品库拍照PWA应用的实现与性能优化|得物技术_PWA_得物技术_InfoQ写作社区