阅读视图

发现新文章,点击刷新页面。

正品库拍照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 的一部分,用于访问用户设备的摄像头和麦克风。它可以请求用户授权以获取视频或音频流,并将实时媒体流绑定到 标签上。

※  HTML5 的 video

用于显示摄像头捕捉到的实时视频流。

※  Canvas

通过 canvas 元素,可以从 标签的当前帧中捕获图像(拍照),并将其转换为图片格式(如 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: {        min1280,        ideal4032,        max4032      },      height: {        min720,        ideal3024,        max3024      },      frameRate: {        ideal30, // 适当降低可以降低视频缓冲区的内存占用,我们先按照这样的场景来看。        min15      },      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,        00, 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(00, chunkConfig.size, chunkConfig.size);       tempCtx.drawImage(      imageData,      x, y, chunkWidth, chunkHeight,      00, chunkWidth, chunkHeight    );        finalCtx.drawImage(      tempCanvas,      00, 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(00, chunkConfig.size, chunkConfig.size);      tempCtx.drawImage(      imageData,      x, y, chunkWidth, chunkHeight,      00, chunkWidth, chunkHeight    );        finalCtx.drawImage(      tempCanvas,      00, 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, height00, 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,            00, 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性能优化实战全揭秘|得物技术

文 / 维克

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

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

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

❌