一、  背景与难点

背景

目前得物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;类型名占用存…