☰
Windows-universal-samples 摄像头人脸检测示例:用 MediaCapture 与 Face Detection effect 构建完整相机应用
2026/9/25 3:03:46 网站建设 项目流程
  • 示例工程

【免费下载链接】Windows-universal-samples

API samples for the Universal Windows Platform.

项目地址:https://gitcode.com/gh_mirrors/wi/Windows-universal-samples
点击查看免费下载

archived/CameraFaceDetection是 Windows-universal-samples 仓库中一个基于 JavaScript(WinJS)的归档相机示例,它展示了如何使用Windows.Media.Capture命名空间编写一个端到端的相机应用:从MediaCapture对象的生命周期管理、前置摄像头获取、预览镜像,到设备旋转处理与拍照方向元数据,再到调用 Windows 内置的 Face Detection effect 并在预览上叠加人脸边界框。读完本文,你将掌握该示例的完整架构、人脸检测效果的创建/配置/注销全流程,以及预览坐标到 UI 坐标换算的关键算法,并能在 Visual Studio 中构建运行它(或在Samples/CameraFaceDetection下查看其维护中的 C++/C# 对应版本)。

示例定位与仓库内文件结构

该示例基于仓库中的基础相机示例 CameraStarterKit,在其之上叠加了人脸检测功能。示例说明中强调,它实现了贴近真实相机应用体验的自定义 UI,因此面向开发者的提示信息都输出到调试控制台而非界面弹窗。

归档示例位于archived/CameraFaceDetection,仅保留 JavaScript(WinJS)版本,核心文件包括:

  • camera.js:全部相机与人脸检测逻辑(约 942 行);
  • camera.html:预览<video>元素、人脸叠加<canvas>以及拍照/录像/人脸检测三个WinJS.UI.AppBarCommand按钮;
  • Package.appxmanifest:包清单,声明了picturesLibrary、webcam、microphone三个能力,目标最低系统版本为10.0.14393.0;
  • CameraFaceDetection.sln 与 CameraFaceDetection.jsproj:Visual Studio 解决方案与 JS 项目文件。

需要说明的是:这是归档(archived)示例,仓库中仍在维护的 C++/C#/VB 版本位于 Samples/CameraFaceDetection,两者架构一致,下文会指出对应实现的位置,便于交叉对照。

示例要解决的八个核心问题

原文档列出了该示例演示的全部能力,这也是理解整个代码结构的骨架:

  1. 管理 MediaCapture 对象:贯穿应用生命周期与导航事件(初始化、清理、最小化/恢复);
  2. 获取设备特定侧的摄像头:示例优先获取前置摄像头(Panel.front);
  3. 启动与停止预览:包括对前置摄像头的镜像处理;
  4. 拍摄普通照片并保存到文件:计入设备方向的旋转元数据;
  5. 处理旋转事件:同时处理设备在空间中的转动和页面在屏幕上的方向变化,并修正预览流旋转;
  6. 处理 MediaCapture 的 Failed 事件:出错时清理 MediaCapture 实例;
  7. 管理 Face Detection effect:创建、配置、启用/停用、注册事件、清理;
  8. 渲染人脸边界框:作为叠加层绘制在相机预览上,且要考虑镜像与旋转。

MediaCapture 生命周期管理

应用启动时(app.onactivated)调用initializeCameraAsync(),恢复挂起时(resuming事件)同样重新初始化;oncheckpoint(即将挂起)与onunload(关闭)时调用cleanupCameraAsync()。这一成对管理在 camera.js 中实现:

oMediaCapture = new Capture.MediaCapture(); // 注册错误与录制时长上限通知 oMediaCapture.addEventListener("recordlimitationexceeded", mediaCapture_recordLimitationExceeded); oMediaCapture.addEventListener("failed", mediaCapture_failed); var settings = new Capture.MediaCaptureInitializationSettings(); settings.videoDeviceId = camera.id; settings.streamingCaptureMode = Capture.StreamingCaptureMode.audioAndVideo; return oMediaCapture.initializeAsync(settings) .then(function () { isInitialized = true; startPreview(); updateCaptureControls(); }, function (error) { console.log(error.message); });

初始化流程为:找到摄像头 → 创建MediaCapture→ 注册failed与recordlimitationexceeded事件 → 通过MediaCaptureInitializationSettings指定videoDeviceId和streamingCaptureMode(音频+视频)→initializeAsync成功后启动预览。

清理逻辑(camera.js)按序处理:若正在录像则先stopRecordingAsync()保存录像;若存在人脸检测效果则先cleanUpFaceDetectionEffectAsync();停止预览;WinJS.Promise.join等所有任务完成后解除事件监听并调用oMediaCapture.close()。这种"先停流、再摘效果、最后关设备"的顺序是避免底层媒体管道报脏的关键。

维护中的 C++ 版本同样实现了CleanupCameraAsync中先清理人脸检测效果再停止预览的顺序,见 MainPage.xaml.cpp。

按面板位置选择摄像头并决定镜像策略

示例优先获取前置摄像头,实现于findCameraDeviceByPanelAsync()(camera.js):

return DeviceInformation.findAllAsync(DeviceClass.videoCapture) .then(function (devices) { devices.forEach(function (cameraDeviceInfo) { if (cameraDeviceInfo.enclosureLocation != null && cameraDeviceInfo.enclosureLocation.panel === panel) { deviceInfo = cameraDeviceInfo; return; } }); // 找不到匹配面板时,退化为返回列表中的第一个设备 if (!deviceInfo && devices.length > 0) { deviceInfo = devices.getAt(0); } return deviceInfo; });

拿到设备后还要判断它是否为内置摄像头:若enclosureLocation为空或面板为unknown,则视为外接摄像头(externalCamera = true)——外接摄像头不随设备转动,因此拍照时不需要附加设备旋转;只有内置前摄像头才开启预览镜像(mirroringPreview)。这个标志后续会贯穿预览镜像、按钮旋转修正和人脸框镜像三处逻辑。

预览启动、旋转校正与镜像

startPreview()(camera.js)先通过DisplayRequest.requestActive()阻止屏幕休眠,再把MediaCapture通过URL.createObjectURL绑定到<video>元素并播放;若需镜像则对预览元素应用scale(-1, 1)变换。playing事件触发后读取预览流的编码属性并调用setPreviewRotationAsync()。

旋转校正是相机应用中容易出错的部分。示例维护两套方向状态:oDeviceOrientation(SimpleOrientationSensor报告的设备在空间中的朝向)和oDisplayOrientation(DisplayInformation报告的页面在屏幕上的朝向)。setPreviewRotationAsync()将显示方向换算成角度,注意预览被镜像时角度要取反:

var rotationDegrees = convertDisplayOrientationToDegrees(oDisplayOrientation); // 预览镜像时需要反转旋转方向 if (mirroringPreview) { rotationDegrees = (360 - rotationDegrees) % 360; } var props = oMediaCapture.videoDeviceController .getMediaStreamProperties(Capture.MediaStreamType.videoPreview); props.properties.insert(RotationKey, rotationDegrees); return oMediaCapture.setEncodingPropertiesAsync(Capture.MediaStreamType.videoPreview, props, null);

其中RotationKey是媒体属性 GUID"C380465D-2271-428C-9B83-ECEA3B4A85C1"(即MF_MT_VIDEO_ROTATION),把旋转以元数据形式写入预览流,可保证渲染与取帧时宽高比一致。displayInformation_orientationChanged事件(页面旋转,例如AutoRotationPreferences无法生效时)会再次调用该方法,并同步更新按钮角度。

setupUiAsync()(camera.js)还做了两件体验相关的设置:用DisplayInformation.autoRotationPreferences = landscape锁定页面方向,避免CaptureElement随页面旋转;以及把拍照/录像保存目录定位到"图片"库(KnownLibraryId.pictures),不可用时回退到ApplicationData.current.localFolder。

设备方向的处理还有一个值得注意的细节(camera.js):当传感器报告faceup/facedown(设备平行于地面)时保留上一次的方向值,这样用户可以先摆好方向再把相机对准桌面文件(FaceUp)或天花板(FaceDown)拍照。getCameraOrientation()(camera.js)则集中处理了两类修正:竖屏优先(portrait-first)设备上传感器天然有 90° 安装偏移,需要整体偏移一次方向;前置镜像情况下 90°/270° 互换。这些换算的 C++ 版本可对照 MainPage.xaml.cpp 中的GetCameraOrientation相关实现。

Face Detection effect 的创建、配置与事件

这是示例区别于基础相机应用的核心部分。人脸检测以"视频效果"的形式插入到预览流中,创建流程在createFaceDetectionEffectAsync()(camera.js):

function createFaceDetectionEffectAsync() { // 创建效果定义,包含初始化设置 var definition = new Windows.Media.Core.FaceDetectionEffectDefinition(); // 为保证预览流畅,不阻塞(同步等待)输入样本 definition.synchronousDetectionEnabled = false; // 本场景下检测速度优先于精度 definition.detectionMode = Windows.Media.Core.FaceDetectionMode.highPerformance; // 将效果添加到预览流 return oMediaCapture.addVideoEffectAsync(definition, Capture.MediaStreamType.videoPreview) .then(function (extension) { oFaceDetectionEffect = extension; // 注册人脸检测事件 oFaceDetectionEffect.addEventListener("facedetected", faceDetectionEffect_FaceDetected); // 选择最短的检测事件间隔 oFaceDetectionEffect.desiredDetectionInterval = 33; // 毫秒 // 开始检测人脸 oFaceDetectionEffect.enabled = true; }); }

三个配置参数的取舍在注释中交代得很清楚,可归纳为:

参数取值含义
synchronousDetectionEnabledfalse异步检测,不延迟预览帧到达,保证预览流畅
detectionModehighPerformance速度优先于精度
desiredDetectionInterval33(毫秒,约 30fps)检测事件的最小间隔

C++ 版本中同样的逻辑可见于 MainPage.xaml.cpp,其中DesiredDetectionInterval是TimeSpan类型,33 毫秒写作duration.Duration = 330000(单位为 100 纳秒),阅读源码时注意这个单位差异。

效果的停用与清理在cleanUpFaceDetectionEffectAsync()(camera.js)中按相反顺序进行:enabled = false→ 移除facedetected事件 → 清空画布上残留的方框 →oMediaCapture.removeEffectAsync(effect)→ 将成员变量置空。效果只加在videoPreview流上,因此不影响拍照与录像的输出。UI 上,faceDetectionButton_tapped()根据效果是否存在/是否启用,在"创建"与"清理"两个动作间切换(camera.js),并通过updateCaptureControls()同步按钮图标(contact/contact2)。

人脸边界框叠加层:坐标换算与镜像

FaceDetected事件回调拿到的是预览分辨率坐标系下的faceBox,而绘制目标是一个覆盖整个窗口的<canvas>(camera.html 中的#facesCanvas),两者分辨率不同,且预览控件可能因缩放模式(Uniform)出现 letterbox(黑边),因此需要两步换算。

第一步,getPreviewStreamRectInControl()(camera.js)计算预览流在控件内实际占用的矩形:横屏方向下宽高直接取流分辨率,竖屏方向下先交换宽高;再比较窗口宽高比与流宽高比,决定 letterbox 出现在左右还是上下,并据此求出x/y/width/height。

第二步,convertPreviewToUiRectangle()(camera.js)按线性比例把方框从流坐标映射到 UI 坐标:

// 将预览流坐标缩放为窗口坐标 result.width = (faceBoxInPreviewCoordinates.width / streamWidth) * previewInUI.width; result.height = (faceBoxInPreviewCoordinates.height / streamHeight) * previewInUI.height; result.x = (faceBoxInPreviewCoordinates.x / streamWidth) * previewInUI.width; result.y = (faceBoxInPreviewCoordinates.y / streamHeight) * previewInUI.height;

highlightDetectedFaces()(camera.js)在每次事件到达时把画布拉伸到窗口尺寸、平移预览内容区域的偏移、清屏后逐个描边:线宽 3 像素,第一张人脸用蓝色#0000FF高亮、其余用#00BFFF;若预览正在镜像,画布整体再应用scale(-1, 1),使方框与镜像后的预览对齐。这就是原文档第 8 点"考虑镜像和旋转渲染人脸边界框"的完整实现。

拍照与录像中的方向元数据

拍照流程(camera.js)先经capturePhotoToStreamAsync把 JPEG 拍到内存流,再用reencodeAndSavePhotoAsync()重编码写文件——重编码的目的一直是写入方向元数据:

var properties = new Imaging.BitmapPropertySet(); properties.insert("System.Photo.Orientation", new Imaging.BitmapTypedValue(orientation, Windows.Foundation.PropertyType.uint16)); return bitmapEncoder.bitmapProperties.setPropertiesAsync(properties);

其中orientation由convertOrientationToPhotoOrientation(getCameraOrientation())计算,把SimpleOrientation映射为PhotoOrientation的normal/rotate90/rotate180/rotate270,设备与显示两层修正(外接摄像头、portrait-first 偏移、前置镜像取反)都已在getCameraOrientation()中集中完成。

录像侧(startRecordingAsync())则把旋转角度直接写进 MP4 编码 profile:

var rotationAngle = 360 - convertDeviceOrientationToDegrees(getCameraOrientation()); var encodingProfile = MediaProperties.MediaEncodingProfile .createMp4(VideoEncodingQuality.auto); encodingProfile.video.properties.insert(RotationKey, rotationAngle); return oMediaCapture.startRecordToStorageFileAsync(encodingProfile, file);

另外两处体验细节:拍照按钮在拍照瞬间会依据mediaCaptureSettings.concurrentRecordAndPhotoSupported决定录像按钮是否仍可点击(部分相机不支持边录边拍);手机设备上还注册了Windows.Phone.UI.Input.HardwareButtons.CameraPressed事件,使物理相机键也能触发拍照。应用最小化(通过SystemMediaTransportControls的 mute 状态推断)时调用cleanupCameraAsync(),恢复时重新初始化——这是把 MediaCapture 管理与导航/前后台事件挂钩的具体做法。

构建与运行

系统要求(沿用原文档):

  • 硬件:摄像头(Camera)
  • 客户端:Windows 10
  • 服务器:Windows Server 2016 Technical Preview
  • 手机:Windows 10 Phone

构建步骤:

  1. 若下载的是整个样本集 ZIP,务必解压全部文件(不要只解压目标示例文件夹),否则会丢失共享依赖;
  2. 启动 Visual Studio(原文档基于 Visual Studio 2017),选择File>Open>Project/Solution;
  3. 在解压目录的对应语言子文件夹下双击.sln文件——本示例为archived/CameraFaceDetection/js/CameraFaceDetection.sln;
  4. 按 Ctrl+Shift+B,或选择Build>Build Solution。

运行步骤:

  • 仅部署:选择Build>Deploy Solution;
  • 部署并运行:按 F5(调试运行)或 Ctrl+F5(不调试运行),对应菜单为Debug>Start Debugging/Start Without Debugging。

由于该示例依赖真实摄像头与方向传感器,建议在带前置摄像头的 Windows 10 桌面设备或模拟器(启用相机模拟)上运行;若findCameraDeviceByPanelAsync找不到前置摄像头,它会退化为使用列表中第一个相机,此时镜像逻辑不生效。

关联资源

  • 基础相机示例(本示例的蓝本):archived/CameraStarterKit
  • 维护中的多语言版本(C++/C#/VB,C++ 人脸检测实现见 MainPage.xaml.cpp):Samples/CameraFaceDetection
  • 仓库根目录说明:README.md

从源码结构看,归档 JS 版与Samples/CameraFaceDetection的 C++ 版在关键路径上完全同构:FindCameraDeviceByPanelAsync、CreateFaceDetectionEffectAsync、ConvertPreviewToUiRectangle等函数一一对应,因此本文基于 JS 版源码的坐标换算与效果管理分析,同样适用于阅读 C++ 实现。

  • 示例工程

【免费下载链接】Windows-universal-samples

API samples for the Universal Windows Platform.

项目地址:https://gitcode.com/gh_mirrors/wi/Windows-universal-samples
点击查看免费下载
上一篇:螺旋矩阵 II 四边界收缩解法详解:以 LeetCode-Book 的 lc_59 实现为例
下一篇:从0到1构建Windows包仓库:GitHub_Trending/wi/winget-pkgs私有部署指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询