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

发布于:2025-07-04 ⋅ 阅读:(17) ⋅ 点赞:(0)

一、  背景与难点

背景

目前得物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 和 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 {
      // 支持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性能优化实战全揭秘|得物技术

文 / 维克

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

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

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