ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

用Canvas实现Overlay相机:实时叠加文字贴纸与导出

用Canvas实现Overlay相机:实时叠加文字贴纸与导出 在相机和图像处理项目里overlay 是一个出现频率很高的概念。很多手机相机中的水印、滤镜、AR 贴纸本质上都是在原始画面上叠加了一层或多层 overlay 内容。项目代号“不乖^_overlay”就是围绕 overlay 相机场景做的一个小实验在摄像头实时画面之上叠加自定义文字、时间戳和图片贴纸并将合成后的画面导出为图片。这篇文章会从 overlay 的基本含义讲起然后用一个浏览器就能跑通的 Web 示例带你把“摄像头采集 - 画面预览 - overlay 绘制 - 合成导出”这条链路完整实现一遍。1. 先搞清楚 overlay 在不同技术栈里分别指什么1.1 图像与相机场景中的 overlay 是什么在图像处理、相机和视频编辑领域overlay 通常指“覆盖层”。它叠加在原始视频帧或图片上方并且不影响原始内容的数据结构。常见的相机水印、时间戳、滤镜蒙层、人脸贴纸、游戏 HUD都属于 overlay 的应用范畴。overlay 的技术本质是“多图层合成”。每一层都有自己的透明度、位置、尺寸和绘制内容合成时按照 z 轴顺序从底层到顶层逐层绘制。以 Canvas 为例先绘制摄像头视频帧再在视频帧上绘制半透明遮罩、文字、贴纸最终得到一张合成后的画面。“overlay 相机”这个热词指的就是带有叠加能力的相机。它不只是拍照而是在拍照或预览过程中把额外信息叠加到画面上。直播工具里的弹幕、短视频里的贴纸、监控录像里的时间戳背后都是同一套逻辑。1.2 网络、容器与 UI 领域中的 overlay 对照overlay 并不是相机领域独有的术语。在计算机技术的不同层面overlay 有几种常见含义但它们都遵循“在已有基础之上再覆盖一层”的核心思想。领域overlay 的含义典型应用图像与视频在视频帧或图片上叠加文字、贴纸、滤镜层相机水印、AR 贴纸、直播字幕Docker 存储overlayfs 把多个目录层合并成一个可读写视图镜像分层、容器可写层、镜像缓存容器网络overlay 网络在宿主机网络之上再建立一个虚拟网络Docker Swarm、Kubernetes Pod 通信前端界面浮层覆盖在页面内容之上弹窗、Toast、抽屉、图片预览遮罩嵌入式系统overlay 分区覆盖只读系统分区保存运行时修改Android 系统升级、路由器固件配置在开始写代码之前先明确这个对照关系可以避免后面搜索资料时把不同的 overlay 概念混在一起。这篇文章只讨论“图像与视频 overlay”并以后端叠加的思路把相机预览和 Canvas 合成串起来。1.3 为什么需要单独研究 overlay 相机效果如果你只是需要“打开相机拍一张照片”不需要关心 overlay。但一旦需要在视频流上叠加动态信息就会遇到几个独立于拍照功能的问题摄像头画面由系统硬件和浏览器内核控制不能直接在视频流像素上画东西。需要把“实时视频显示”和“overlay 图层”放在同一个坐标系里并保证没有错位。导出图片时必须把摄像头帧和 overlay 图层合并成一张位图而不是截图 overlay 层或截屏原始视频。不同设备的分辨率、摄像头方向、安全上下文和 Surface 限制各不相同需要做兼容处理。换句话说overlay 相机不是“给照片加个水印”而是“在每一帧画面上维持一个可变的覆盖层”。理解了这一点再去看代码会清晰很多。2. 准备一个小型 Web 工程承担 overlay 相机实验环境2.1 技术选型为什么用 getUserMedia Canvas 而不是原生 App实现 overlay 相机的技术路线很多。原生 Android 可以用 CameraX GraphicOverlayiOS 可以用 AVFoundation Metal 图层Unity 和 Flutter 也都有各自的相机插件。这里选择浏览器方案原因很直接浏览器内置navigator.mediaDevices.getUserMedia()不需要安装 SDK。Canvas 的drawImage()可以把视频帧绘制为静态图片支持逐帧合成。只要一个 HTML 文件和一个本地 HTTP 服务就能在 Chrome、Edge、Safari 上直接预览。摄像头权限由浏览器统一管理代码可以专注于 overlay 绘制逻辑。这个实验适合学习、原型验证和前端相机功能开发。如果要做正式 App思路仍然可以迁移到 Android 的 SurfaceView 或 iOS 的 Core Animation 图层上。2.2 工程结构和依赖这个项目不依赖任何第三方库所有功能都由浏览器原生 API 完成。建议创建一个干净的目录比如bu-guai-overlay并在里面放一个index.html。bu-guai-overlay/ └── index.html代码会写到这个文件里。为了提升可维护性也可以拆成style.css和main.js但单文件版本更适合第一次跑通流程。如果需要把贴纸图片放在本地可以把图片放在同目录下的assets文件夹里。本文示例使用 Canvas 绘制的简单图形代替贴纸图片这样读者不需要准备额外素材。2.3 浏览器兼容性与安全上下文检查清单getUserMedia不是所有环境下都能直接运行。在开始写功能之前先按下面的清单检查浏览器环境检查项要求说明HTTPS 或 localhost必须浏览器只允许安全上下文使用摄像头和麦克风Chrome / Edge / Firefox推荐Safari 支持 getUserMedia但部分参数可能忽略系统摄像头权限已授权Windows / macOS 需要在系统设置中允许浏览器访问摄像头本机有摄像头至少一个外接摄像头也可以本地服务任意静态文件服务直接双击打开文件可能被浏览器拦截需要注意file://协议下的getUserMedia在很多浏览器中不可用。因此即使只有一个 HTML 文件也建议启动一个本地 HTTP 服务例如python3 -m http.server 8080。3. 实现“不乖^_overlay”的最小可运行版本3.1 页面布局视频预览、画布、控制按钮页面需要三个核心元素video显示摄像头原始画面一个隐藏或半隐藏的canvas用于合成 overlay以及按钮用来启动摄像头和保存图片。为了让实验更直观我会把 Canvas 放在视频预览上方并让两者尺寸一致。这样用户看到的就是带 overlay 的实时预览效果。实际开发中也可以让 Canvas 完全覆盖在video上并把最终合成结果单独导出。HTML 结构如下!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title不乖^_overlay Overlay Camera/title style body { font-family: system-ui, sans-serif; max-width: 720px; margin: 40px auto; padding: 0 16px; background: #f6f8fb; color: #222; } .camera-box { position: relative; width: 100%; aspect-ratio: 4 / 3; border-radius: 16px; overflow: hidden; background: #000; box-shadow: 0 12px 30px rgba(0, 0, 0, 0.15); } video, canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; } canvas { z-index: 10; } .controls { display: flex; gap: 12px; margin: 20px 0; flex-wrap: wrap; } button { padding: 10px 18px; border: 0; border-radius: 10px; background: #2f6fed; color: #fff; font-size: 15px; cursor: pointer; } button:disabled { background: #a0aab8; cursor: not-allowed; } .overlay-config { margin: 20px 0; padding: 16px; background: #fff; border-radius: 12px; border: 1px solid #e3e8ef; } .overlay-config label { display: block; margin: 8px 0 4px; font-weight: 600; } .overlay-config input { width: 100%; padding: 8px 10px; border: 1px solid #d0d7e2; border-radius: 8px; box-sizing: border-box; } .error-box { margin: 16px 0; padding: 12px 16px; border-radius: 10px; background: #fdecea; color: #b42318; white-space: pre-wrap; display: none; } /style /head body h1不乖^_overlay Overlay Camera Demo/h1 p在摄像头实时画面上叠加文字、时间戳和图形贴纸。/p div classcamera-box video idcamera autoplay playsinline muted/video canvas idoverlayCanvas/canvas /div div classcontrols button idstartBtn打开摄像头/button button idcaptureBtn disabled导出合成图片/button /div div classoverlay-config label foroverlayText叠加文字/label input idoverlayText typetext value不乖^_overlay | 相机实验 / label forshowTimeCheck input idshowTimeCheck typecheckbox checked / 显示时间戳 /label /div div iderrorBox classerror-box/div script srcmain.js/script /body /html这段 HTML 完成了三件事使用aspect-ratio: 4 / 3固定相机预览区域比例。让video和canvas绝对定位canvas 的z-index更高用于显示 overlay。准备按钮和配置项方便后续控制摄像头和导出图片。3.2 获取摄像头流并回显到 video 标签在main.js中第一步是获取摄像头流。这里用video标签来承载原始画面因为它不需要手动绘制视频帧浏览器会自动渲染。const video document.getElementById(camera); const canvas document.getElementById(overlayCanvas); const ctx canvas.getContext(2d); const startBtn document.getElementById(startBtn); const captureBtn document.getElementById(captureBtn); const overlayTextInput document.getElementById(overlayText); const showTimeCheck document.getElementById(showTimeCheck); const errorBox document.getElementById(errorBox); let localStream null; let animationId null; async function startCamera() { try { localStream await navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 1280 }, height: { ideal: 720 }, facingMode: user }, audio: false }); video.srcObject localStream; await video.play(); startBtn.disabled true; captureBtn.disabled false; // 根据视频尺寸初始化 canvas video.addEventListener(loadedmetadata, () { resizeCanvas(); startOverlayLoop(); }, { once: true }); } catch (err) { showError(摄像头打开失败 err.message); } } function resizeCanvas() { const box document.querySelector(.camera-box); const rect box.getBoundingClientRect(); canvas.width rect.width; canvas.height rect.height; }这里的getUserMedia约束使用ideal而不是固定值表示“优先尝试 1280x720如果没有再自动降级”。这种写法比直接指定width: 1280更能兼容不同摄像头。video.play()返回 Promise在部分浏览器中如果不await后续绘制可能会因为视频首帧未就绪而出现短暂黑屏。3.3 用 Canvas 合成 overlay 层并定时刷新overlay 相机最关键的部分是回环绘制。每次从requestAnimationFrame回调中把视频当前帧绘制到 Canvas 上然后在上面绘制文字、时间戳和贴纸。function startOverlayLoop() { stopOverlayLoop(); const draw () { drawOverlay(); animationId requestAnimationFrame(draw); }; animationId requestAnimationFrame(draw); } function stopOverlayLoop() { if (animationId) { cancelAnimationFrame(animationId); animationId null; } } function drawOverlay() { const { videoWidth, videoHeight } video; if (!videoWidth || !videoHeight) return; // 让视频画面按 4:3 居中裁剪到画布中 const rect canvas.getBoundingClientRect(); const targetW rect.width; const targetH rect.height; const videoRatio videoWidth / videoHeight; const targetRatio targetW / targetH; let drawW targetW; let drawH targetH; let offsetX 0; let offsetY 0; if (videoRatio targetRatio) { drawH targetH; drawW targetH * videoRatio; offsetX (targetW - drawW) / 2; } else { drawW targetW; drawH targetW / videoRatio; offsetY (targetH - drawH) / 2; } // 1. 绘制视频帧 ctx.clearRect(0, 0, targetW, targetH); ctx.save(); ctx.translate(offsetX drawW / 2, offsetY drawH / 2); ctx.scale(-1, 1); // 前置摄像头镜像 ctx.translate(-(offsetX drawW / 2), -(offsetY drawH / 2)); ctx.drawImage(video, offsetX, offsetY, drawW, drawH); ctx.restore(); // 2. 绘制半透明遮罩模拟滤镜 ctx.fillStyle rgba(255, 180, 0, 0.08); ctx.fillRect(0, 0, targetW, targetH); // 3. 绘制顶部渐变色条 const gradient ctx.createLinearGradient(0, 0, 0, 80); gradient.addColorStop(0, rgba(0, 0, 0, 0.45)); gradient.addColorStop(1, rgba(0, 0, 0, 0)); ctx.fillStyle gradient; ctx.fillRect(0, 0, targetW, 80); // 4. 绘制图片贴纸这里用一个圆形图形代替 const stickerX targetW - 70; const stickerY targetH - 70; ctx.save(); ctx.shadowColor rgba(0, 0, 0, 0.3); ctx.shadowBlur 10; ctx.beginPath(); ctx.arc(stickerX, stickerY, 26, 0, Math.PI * 2); ctx.fillStyle #ff6b6b; ctx.fill(); ctx.strokeStyle #fff; ctx.lineWidth 4; ctx.stroke(); ctx.restore(); // 5. 绘制自定义文字 const overlayText overlayTextInput.value || 不乖^_overlay; ctx.font 600 22px system-ui, sans-serif; ctx.textAlign left; ctx.textBaseline top; ctx.fillStyle #ffffff; ctx.shadowColor rgba(0, 0, 0, 0.5); ctx.shadowBlur 4; ctx.fillText(overlayText, 16, 16); ctx.shadowBlur 0; // 6. 绘制时间戳 if (showTimeCheck.checked) { const now new Date(); const timeStr now.toLocaleString(zh-CN, { hour12: false }); ctx.font 500 14px system-ui, sans-serif; ctx.textAlign right; ctx.fillStyle rgba(255, 255, 255, 0.85); ctx.fillText(timeStr, targetW - 16, targetH - 24); } }这段代码有六个绘制步骤顺序不能随意调换视频帧在最底层是整张合成图的背景。半透明遮罩用来模拟滤镜不会完全遮挡人脸。渐变色条让顶部文字在亮背景下仍然清晰。贴纸绘制在遮罩之上、文字之前。自定义文字和日志内容要保证在画面左上角或右下角避免遮挡主体。时间戳放在右下角并带阴影提高可读性。使用ctx.save()和ctx.restore()可以保证镜像、阴影等状态不会污染后续绘制。3.4 拍照导出带 overlay 的图片导出功能是 overlay 相机和普通截图的最大区别。我们要把当前视频帧和所有 overlay 内容合并到同一个 Canvas 上然后调用canvas.toDataURL()生成图片。captureBtn.addEventListener(click, () { // 先绘制一次确保最新状态被合成 drawOverlay(); const dataUrl canvas.toDataURL(image/png); const link document.createElement(a); link.download overlay-capture- Date.now() .png; link.href dataUrl; link.click(); });点击导出按钮后浏览器会下载一张 PNG 图片。这张图片上的内容与预览画面完全一致因为预览本来就是在 Canvas 上实时绘制的。要特别注意如果需要导出与物理摄像头分辨率一致的原始大图就不能只使用预览 Canvas 的尺寸。应该在内存中创建另一个 Canvas尺寸取video.videoWidth和video.videoHeight再重新绘制视频帧和 overlay。预览用低分辨率 Canvas 可以减少性能开销导出用高分辨率 Canvas 可以保留画面细节。4. 关键代码逐段拆解约束、绘制、合成、导出4.1 getUserMedia 的约束参数与常见设备问题getUserMedia的video参数是最容易踩坑的地方。它可以是一个布尔值也可以是一个包含约束的对象。约束参数作用示例值错误使用表现width期望的视频宽度{ ideal: 1280 }固定值过大会导致设备切换失败height期望的视频高度{ ideal: 720 }同上facingMode前置或后置摄像头user/environment不支持的设备会忽略frameRate期望帧率{ ideal: 30 }过高帧率会占用大量 CPUaspectRatio宽高比{ ideal: 1.7777 }与 CSS 宽高不一致时出现裁剪在桌面浏览器上facingMode通常不能切换前后摄像头因为很多电脑只有一个摄像头。想切换摄像头需要使用mediaDevices.enumerateDevices()列出所有视频设备再用deviceId精确指定。4.2 Canvas 绘制叠加层的坐标计算摄像头视频的原始分辨率和 CSS 显示尺寸几乎不可能完全一致。例如摄像头输出是 1280x720但 Canvas 预览区域是 640x480如果直接drawImage(video, 0, 0, canvas.width, canvas.height)画面会被拉伸。正确的做法是计算“覆盖裁剪”区域。先把视频等比缩放使画面铺满整个画布然后裁剪掉多余部分。前面drawOverlay()里的videoRatio和targetRatio比较就是在处理这个问题。if (videoRatio targetRatio) { // 视频更宽需要裁剪左右两边 drawH targetH; drawW targetH * videoRatio; offsetX (targetW - drawW) / 2; } else { // 视频更高需要裁剪上下两边 drawW targetW; drawH targetW / videoRatio; offsetY (targetH - drawH) / 2; }这种写法和 CSS 的object-fit: cover效果一致。预览 Canvas 和视频层使用同样的坐标逻辑overlay 就不会错位。4.3 文字、时间戳、图片贴纸的绘制顺序Overlay 并不是越多越好绘制的先后顺序直接影响最终效果。先绘制滤镜遮罩再绘制文字文字才能保持在滤镜上方。贴纸可以盖住文字也可以被文字盖住按业务需求决定。阴影效果使用ctx.shadowBlur但每绘制完一部分就把shadowBlur归零否则后续绘制也会带上阴影。文字要用textBaseline top或alphabetic并且用ctx.measureText()检查宽度防止中文长文本溢出屏幕。如果要在 overlay 中加载外部图片贴纸需要使用Image对象const img new Image(); img.src ./assets/sticker.png; img.onload () { ctx.drawImage(img, stickerX - 26, stickerY - 26, 52, 52); };但要注意图片加载是异步的。在requestAnimationFrame循环里不能每次都重新加载图片应该在初始化时加载一次然后在绘制函数中直接使用。4.4 镜像翻转与分辨率对齐前置摄像头拍出来的画面如果不做镜像处理用户会看到自己的画面是反的。很多相机 App 都会开启镜像预览但保存的照片往往会撤销镜像保留真实方向。在 Canvas 中做镜像最简单的方式是使用坐标变换ctx.save(); ctx.translate(targetW, 0); ctx.scale(-1, 1); ctx.drawImage(video, 0, 0, targetW, targetH); ctx.restore();这段代码先把坐标系水平翻转再绘制视频帧。注意这里使用translate(targetW, 0)再scale(-1, 1)相当于以画面中线为轴做水平翻转。导出图片时是否要镜像取决于业务需求。如果只是给自己的视频加水印通常保留镜像更自然如果是给其他平台提供的素材一般不希望镜像。可以把镜像逻辑抽成一个参数在启动摄像头时配置。5. 运行验证与效果检查5.1 本地启动 HTTPS 或 localhost 环境在项目目录下执行python3 -m http.server 8080然后打开浏览器访问http://localhost:8080浏览器会弹出摄像头权限请求点击允许即可。如果你使用 127.0.0.1 或 localhost 访问同属于安全上下文不需要额外配置 HTTPS 证书。如果要在手机上测试需要让手机和电脑处于同一局域网并通过https://访问。因为非 localhost 环境必须使用 HTTPS才能调用摄像头。5.2 授权摄像头并观察预览点击“打开摄像头”按钮后应当看到以下现象按钮变成禁用状态说明摄像头流已经打开。video标签显示摄像头原始画面。canvas覆盖在 video 上方显示滤镜、文字、时间戳和圆形贴纸。修改“叠加文字”输入框内容预览中的文字会实时变化。如果点击按钮后没有反应先查看控制台报错再检查摄像头权限和地址是否为 localhost。5.3 导出图片与预期效果点击“导出合成图片”按钮浏览器会自动下载 PNG 文件。用图片查看器打开后应看到图片内容是摄像头画面加 overlay。图片没有全屏黑边画面没有被拉伸。文字清晰贴纸位置在右下角。时间戳显示当前时间精确到秒。如果导出的图片是空白的大概率是 Canvas 的width和height为 0。这通常发生在loadedmetadata事件尚未触发时调用了导出。5.4 用表格记录验证清单验证项预期结果通过标准摄像头授权浏览器弹出权限请求允许后画面出现在 video 中叠加文字输入框内容显示在画面左上角文本不溢出无文字阴影残留时间戳画面右下角显示当前时间时间随系统时间变化贴纸右下角有圆形图形图形边缘清晰阴影正常镜像预览前置摄像头预览为镜像画面画面左右翻转导出图片下载 PNG 文件图片包含摄像头画面和所有 overlay 内容关闭摄像头页面无摄像头占用关闭标签页后摄像头指示灯熄灭6. 常见问题排查摄像头、权限、黑屏、导出异常6.1 摄像头无法打开现象点击按钮后页面报错NotAllowedError或NotFoundError。可能原因浏览器没有摄像头权限。当前页面不是 HTTPS 或 localhost。摄像头被其他应用占用。系统设置中禁止浏览器使用摄像头。排查路径打开浏览器控制台查看具体错误名称。换成 Chrome 或 Edge 再试。在地址栏输入about://settings/content/camera检查权限。关闭其他正在占用摄像头的应用。检查系统隐私设置允许浏览器访问摄像头。6.2 画面黑屏或只有网线图标现象video标签没有画面或者画面显示一张灰色图片。可能原因video.play()没有被调用成功。摄像头流对象没有赋值给srcObject。CSS 层叠顺序错误Canvas 覆盖了视频画面并绘制了黑底。视频画面绘制没有开始Canvas 是透明的但背景也是黑色的。解决方案在video.srcObject stream后调用await video.play()。检查 Canvas 的drawImage是否在videoWidth大于 0 后才执行。临时设置canvas.style.opacity 0如果能看到摄像头说明 Canvas 绘制逻辑有误。检查 CSS 中object-fit和 Canvas 的尺寸是否匹配。6.3 导出图片为空白或没有 overlay现象点击导出后图片是黑色背景或者只有摄像头画面但没有文字贴纸。可能原因canvas.width和canvas.height为 0导致所有绘制不生效。导出前没有调用drawOverlay()Canvas 中还是上次的旧画面。Canvas 被 CSS 设置了宽高但canvas.width的像素属性未同步。浏览器从 Canvas 导出图片时可能因为图片跨域污染了画布。解决方案在loadedmetadata或setTimeout后初始化 Canvas 尺寸。导出前手动调用一次drawOverlay()。使用canvas.width rect.width * devicePixelRatio时注意要在 draw 时同步使用这个比例。6.4 移动端兼容性差异现象手机上摄像头打不开或者画面方向不对。原因移动端浏览器对facingMode处理不一致。iOS Safari 对 Canvas 的toDataURL()导出有尺寸限制。手机摄像头方向传感器不同部分竖屏画面会被旋转。处理建议优先使用playsinline属性避免 iOS 视频全屏播放。如果facingMode无效使用enumerateDevices()手动选择设备。导出超长截图时分块导出或采用canvas.toBlob()减少内存压力。7. 从 Demo 到正式项目性能、安全与工程化建议7.1 高性能叠加层的实现路径当前 Demo 使用 Canvas 逐帧绘制在 720p 分辨率和 30 帧率下CPU 占用会明显上升。如果要在正式产品中使用建议从几个方向优化。性能瓶颈优化方案每帧绘制大尺寸 Canvas预览画布缩小到显示尺寸导出时再生成大图高频修改 Canvas 状态避免每帧改变填充颜色、阴影、字体预创建固定贴纸视频帧绘制开销高使用 WebGL 做 GPU 合成或结合 CSS transform 处理固定图层导出图片卡顿导出操作在用户点击时执行不在预览循环中调用多路 overlay把静态水印放到 CSS 层动态内容才用 Canvas使用requestAnimationFrame已经比setTimeout优化很多但仍要避免在函数里创建大量临时对象。7.2 安全与隐私合规建议相机类功能涉及用户隐私在真实项目中不能只写代码还要处理合规问题。必须在用户主动操作后调用getUserMedia不要页面加载就自动打开摄像头。摄像头使用结束后及时调用track.stop()释放资源。如果要把图片上传到服务器需要明确告知用户用途并获得授权。不要在日志中输出视频帧或包含人脸信息的图片。如果项目开源不要内置未经授权的品牌贴纸、卡通形象或商标素材。停止摄像头流的代码function stopCamera() { if (localStream) { localStream.getTracks().forEach((track) track.stop()); video.srcObject null; localStream null; } stopOverlayLoop(); startBtn.disabled false; captureBtn.disabled true; }7.3 扩展方向人脸贴纸、美颜、OCR、实时滤镜“overlay 相机”的扩展空间非常大。如果你已经跑通这个 Demo接下来可以朝这些方向深入。加入 MediaPipe Face Mesh 或 TensorFlow.js 的人脸关键点检测把贴纸固定到人脸指定位置。使用 CSS filter 或 WebGL 着色器实现实时美颜、磨皮、美白。接入 Tesseract.js在视频画面上识别文字并高亮显示。用 Canvas 实现多种滤镜模板比如复古、黑白、逆光。把合成结果通过 WebSocket 或 WebRTC 推流到直播服务。这些扩展都会围绕同一个核心把实时视频帧作为底层把计算后的内容绘制为 overlay 层。理解了“不乖^_overlay”这个最小链路你已经掌握了实现 overlay 相机的基础能力。接下来要做的就是在真实项目里根据业务需求不断调整绘制顺序、图层结构、分辨率和性能策略。建议先保持现有代码运行稳定再逐步加入人脸关键点检测和滤镜模板一次只加到一条链路避免把性能问题和绘制逻辑混在一起排查。
返回列表