比较提交
22
次代码提交
| 作者 | SHA1 | 提交日期 | |
|---|---|---|---|
|
|
5793e3de49 | ||
|
|
9bebf68e3f | ||
|
|
ad54726b17 | ||
|
|
16525d62e5 | ||
|
|
1008651f6e | ||
|
|
a6551c334f | ||
|
|
abe97ed0cb | ||
|
|
ec22f1b2cf | ||
|
|
2a856c669a | ||
|
|
302dc20c0a | ||
|
|
e03e460a9b | ||
|
|
764994a1c4 | ||
|
|
77d386825c | ||
|
|
22cd96a772 | ||
|
|
71a583e306 | ||
|
|
d561537eed | ||
|
|
dd04a471e1 | ||
|
|
75a16835f2 | ||
|
|
2e59185b32 | ||
|
|
0310ca83ba | ||
|
|
e2803e727f | ||
|
|
f71826b64f |
没有与搜索条件相匹配的文件
@@ -1,28 +1,38 @@
|
||||
# PaintFCP FxPlug4
|
||||
# 大鱼人像美化 · beatygril
|
||||
|
||||
独立 FxPlug4 滤镜程序源码工程。修改前的 `beatygril` 是 Apple Tileable Remote Brightness 模板;在此基础上实现新的滤镜。第三方参考 APP、二进制和资源不进入仓库。
|
||||
基于本仓库的 FxPlug4 初始工程制作的人像美化滤镜,支持 Motion 和 Final Cut Pro。源码使用 Objective-C、Vision、Core Image 与 Metal。
|
||||
|
||||
## 工程
|
||||
功能包括磨皮、肤色、细节、牙齿美白、眼睛提亮、九类脸型调整、画面选脸、三目标分析跟踪扩展和蒙版预览。原版 MSE18V2 的静态分析依据、近似实现与尚未对齐的部分见 [分析说明](docs/analysis/REA-分析与复刻说明.md)。本版尚未完成原版逐像素与复杂运动跟踪对照。
|
||||
|
||||
- `beatygril/beatygril.xcodeproj`:Xcode 工程,`Wrapper Application` 与 `XPC Service` 两个 target。
|
||||
- `beatygril/beatygril/Plugin`:Objective-C `FxTileableEffect` 实现、Metal GPU shader 和设备缓存。
|
||||
- `beatygril/beatygril/Wrapper Application`:插件外层 APP 和沙盒 entitlement。
|
||||
- 原始参数:Brightness,默认 1.0,参数范围 0–100,滑块范围 0–10;RGB 乘亮度、Alpha 保持。
|
||||
## 使用
|
||||
|
||||
## 本机依赖与构建入口
|
||||
正式安装路径为 `/Library/Plug-Ins/FxPlug/beatygril.app`。Motion 滤镜分类为「大鱼」,滤镜名「大鱼人像美化」。Final Cut Pro 在「效果 → 大鱼 → 大鱼人像美化」中使用配套 Motion 效果,模板目录必须同时保留 `.moef` 与 `Drop Zone.tiff`。美化会随当前帧的人脸自动更新,不需要先点分析;默认脸型跟随主要人脸,开启多人高速模式后对多脸变形。可按需调中频磨皮、肤色与脸型;「只处理选中人脸」用于画面重选目标。三个目标的向前/向后分析属于可选扩展,数据可随工程保存。
|
||||
|
||||
盘点环境:Xcode 27.1(27A9275),FxPlug.framework 4.3.4、PluginManager.framework 1.7.4;附加 SDK 为 `/Library/Developer/SDKs/FxPlug.sdk`。SDK 内含开发 headers,运行 framework 位于 `/Library/Developer/Frameworks`。项目初始最低部署目标为 macOS 27.2。
|
||||
## 开发和恢复
|
||||
|
||||
需要 Xcode 27.1、FxPlug SDK 4.3.4 与 PluginManager 1.7.4;最低运行系统 macOS 13。正式产物为 arm64/x86_64 通用版本。
|
||||
|
||||
```sh
|
||||
xcodebuild -list -project beatygril/beatygril.xcodeproj
|
||||
xcodebuild -project beatygril/beatygril.xcodeproj \
|
||||
-scheme 'Wrapper Application' -configuration Release \
|
||||
-derivedDataPath /tmp/paintfcp-build \
|
||||
CODE_SIGN_STYLE=Manual \
|
||||
CODE_SIGN_IDENTITY='Developer ID Application: zhong luo (B3W25FX5H5)' \
|
||||
DEVELOPMENT_TEAM=B3W25FX5H5 build
|
||||
./scripts/test.sh
|
||||
PAINTFCP_HOST_ASAN=1 ./scripts/test-host.sh
|
||||
PAINTFCP_OSC_ASAN=1 ./scripts/test-osc.sh
|
||||
xcodebuild -project beatygril/beatygril.xcodeproj -scheme 'Wrapper Application' -configuration Release -derivedDataPath /tmp/paintfcp-build -destination 'generic/platform=macOS' build
|
||||
```
|
||||
|
||||
初始工程复制并签名 FxPlug 与 PluginManager 两个 framework;正式交付必须依次完成 Release 构建、内到外签名、Apple 公证 Accepted、票据粘附与验证、唯一正式路径安装及宿主发现验证。初始基线仅核验工程可读取和依赖,不代表已完成构建或交付。
|
||||
工程从 `/Library/Developer/SDKs/FxPlug.sdk` 读取头文件与链接信息,从 `/Library/Developer/Frameworks` 嵌入可运行的 Apple 框架。构建默认使用团队 B3W25FX5H5 的 Developer ID Application 身份;正式交付使用 `python3 scripts/deliver.py` 完成公证、粘附和安装;安装前先关闭 Motion 与 FCP,脚本在检测到宿主仍运行时保留原安装版并停止。原生验收完成后继续脚本的清理阶段,清除临时注册与 APP 副本。
|
||||
|
||||
参考 [RECOVERY.md](RECOVERY.md) 恢复源码与依赖。签名证书、私钥和公证凭据均由本机钥匙串管理,不随源码保存。
|
||||
私有源码仓库:`https://dayu.media/git/dayu/paintfcp-fxplug.git`。恢复入口见 [RECOVERY.md](RECOVERY.md),当前验证记录见 [验证记录](docs/验证记录.md)。原始模板保存在 `baseline-20261021` 标签。
|
||||
|
||||
## 1.0.5 瘦脸修复
|
||||
|
||||
瘦脸现固定使用Vision revision 3的76点配置,以17组轮廓到鼻部/瞳孔锚点的局部逆向采样替代脸框水平挤压,包含原版尺寸增益与滑块强度映射。完整点阵贯通实时稳定与离线存储;旧分析记录没有完整76点时跳过瘦脸,重新分析可补齐。其他脸型项仍使用原有近似实现,不能将本次对齐扩张为全部脸型逐像素等价。
|
||||
|
||||
离屏验证新增224项:含48组脸部投影/尺寸/歪头与独立17-pin数值场对照、数据恢复和缺点保护;完整回归604项通过。Motion/FCP界面由用户自测。可运行 `python3 scripts/deliver.py --wait-for-hosts --user-tests`,签名公证后只读等待宿主自然退出,安装验证与清理无需操作宿主界面。
|
||||
|
||||
## 1.0.6 下巴与图像坐标修复
|
||||
|
||||
下巴改为76点中68→38、67→37、66→36的三组局部位移,正值上收、负值延伸,保留原版正负不同系数及尺寸增益。Core Image/IOSurface输入输出移除按TOP_LEFT额外翻转,和原版图像链对齐。修复依据与离屏复现见[下巴与坐标分析](docs/analysis/下巴与坐标分析.md)。七类其余脸型项仍是近似实现。
|
||||
|
||||
旧版分析记录无法区分是否包含额外翻转,新版保留原字符串但暂用实时跟随;重新分析即可写入新坐标标记并恢复离线跟踪。最终Motion/FCP画面由用户自行测试。
|
||||
|
||||
1.0.6(8)已完成正式签名、Apple公证、票据、安装和模板同步。904项完整回归及独立远程恢复核验通过;最终宿主视觉由用户自测。交付回执在`docs/verification/v1.0.6/`,恢复标签为`v1.0.6`。
|
||||
+42
-2
@@ -3,7 +3,7 @@
|
||||
- 私有远程:`https://dayu.media/git/dayu/paintfcp-fxplug.git`
|
||||
- 本地项目根:`/Users/dayu/Documents/PaintFCP`
|
||||
- 工作分支:`main`
|
||||
- 修改前版本标签:`baseline-20261021`(完整 SHA 在后续恢复核验记录补充)
|
||||
- 修改前版本标签:`baseline-20261021`,完整 SHA:`1746f38398588e2367abb37a5c2b31cb64042429`
|
||||
- 无 Git LFS 或子模块;源码及必要 Xcode 工程资源均入库。
|
||||
- `/Applications/MSE18V2.app` 是只读参考目标,不属于本项目资源,不随仓库保存。
|
||||
|
||||
@@ -12,9 +12,49 @@
|
||||
```sh
|
||||
git clone https://dayu.media/git/dayu/paintfcp-fxplug.git
|
||||
cd paintfcp-fxplug
|
||||
git switch --detach baseline-20261021
|
||||
git switch --detach v1.0.6
|
||||
```
|
||||
|
||||
需要继续编辑时,基于该提交创建新分支。安装 Xcode 与 Apple FxPlug 4 SDK;盘点版本和构建入口见 README.md。SDK/framework 为 Apple 外部开发依赖,应重新安装匹配版本。证书、私钥和公证凭据需从独立加密备份恢复或重新申请;Git 不包含它们。
|
||||
|
||||
恢复承诺仅覆盖已成功推送且核验的提交与仓库内资源。大鱼服务当前只有手动服务端备份,不能据此承诺本机与服务器同时丢失后的恢复。
|
||||
|
||||
## 首次远端恢复核验(2026-10-21)
|
||||
|
||||
已在本任务创建的独立临时目录从远端克隆 `baseline-20261021`,核验 HEAD 为 `1746f38398588e2367abb37a5c2b31cb64042429`,并与本地该提交的 23 个受跟踪文件逐字节比对一致;恢复目录可通过 `xcodebuild -list` 读取两个 target / scheme。远端 `main` 与标签解引用 SHA 均回读一致。临时恢复目录已清理。
|
||||
|
||||
初始模板存在行尾空白,修改前基线保留原样。本次核验未执行初始 APP 编译、签名、公证、安装或宿主加载;这些将由最终实现阶段单独验证。
|
||||
|
||||
## 1.0.4 恢复入口
|
||||
|
||||
最终交付标签为 `v1.0.4`,工作分支为 `main`;本次实际编译和独立恢复测试的代码节点为完整 SHA `e03e460a9b3e5ad489aad24095485ba10ceb987d`。最终标签还包含验证文档。需要精确恢复已测试源码时可执行 `git switch --detach e03e460a9b3e5ad489aad24095485ba10ceb987d`。两者都保留初始模板及全部中间提交。
|
||||
|
||||
依赖:Xcode27.1、FxPlug SDK4.3.4、PluginManager1.7.4,系统最低macOS13;Metal与Vision由系统提供。SDK头文件/链接存根安装到 `/Library/Developer/SDKs/FxPlug.sdk/Library/Frameworks`,可执行运行框架在 `/Library/Developer/Frameworks`;不要把SDK链接存根当运行框架嵌入APP。两者属于需重新安装的Apple外部依赖,仓库不保存证书/私钥或登录凭据。
|
||||
|
||||
测试与通用Release构建命令见README。关闭Motion和FCP后,执行 `python3 scripts/deliver.py` 完成正式签名、公证、粘附、唯一目录安装与验收后的副本清理;签名团队为B3W25FX5H5,公证使用登录钥匙串profile `codex-dayu-notary-B3W25FX5H5`。新机器需要自行恢复凭据或重新签发,不要把凭据加入Git。
|
||||
|
||||
FCP模板从 `Templates/大鱼人像美化/` 恢复,将 `.moef` 和 `Drop Zone.tiff` 一起放入 `~/Movies/Motion Templates.localized/Effects.localized/大鱼/大鱼人像美化/`;只放 `.moef` 会缺少标准效果源输入资源。正式APP仅安装在 `/Library/Plug-Ins/FxPlug/beatygril.app`,不是从工作树或构建目录运行。没有LFS或子模块,必要工程资源、NASA测试人像及配套模板资源均在Git中。
|
||||
|
||||
用户用于宿主验收的外置卷视频及build内的验收工程/截图属于测试素材与可重建产物,不属于产品构建依赖,也没有远程上传。原MSE18V2 APP与原版MS模板仅为只读参考,不作为运行依赖。远端恢复记录及实际宿主测试范围见 `docs/验证记录.md`。
|
||||
|
||||
## 瘦脸与侧脸问题的分析节点
|
||||
|
||||
`v1.0.4`之后的分析提交包含`docs/analysis/脸型变形与转脸分析.md`、`slim-pose-report.json`、REA证据包及独立Vision离线检测工具`scripts/inspect-face-geometry.m`。产品源码与安装版没有因该分析改变;已查明的问题仍存在于1.0.4。复现检测工具的依赖与命令见分析文档,用户视频、完整人脸坐标和帧图保留本地,不是产品恢复依赖。
|
||||
|
||||
## 1.0.5 当前恢复入口
|
||||
|
||||
当前发布标签为 `v1.0.5`,APP版本1.0.5、build7。签名构建的完整源码SHA为 `abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964`;最终标签还包含交付脚本恢复修正及验证记录,产品源码与该SHA一致。该节点已从独立远端克隆恢复,69文件及9项必要资源完整、389项核心测试通过。主工作区另通过604项完整回归及同一用户视频11帧的264项瘦脸ASan场景。
|
||||
|
||||
从最终标签恢复后,关闭Motion/FCP,运行 `python3 scripts/deliver.py --wait-for-hosts --user-tests` 可完成签名公证安装、只读宿主退出等待、静态发现及副本清理;它不会操作宿主界面。若上次因非产品错误在Accepted且stapled后停止,可加 `--resume-prepared`,脚本必须重新核对签名CDHash、版本、票据及产品源码未变才会继续。缺少原staging或回执时应完整重新构建,不能伪造回执。
|
||||
|
||||
1.0.5瘦脸需要完整76点;旧离线记录缺该字段时保留其余功能并跳过瘦脸。关闭使用跟踪数据可使用当前帧自动76点,或重新分析生成新记录。没有引入原版metallib或CoreML模型,运行只依赖本产品及Apple系统框架。最终安装、配套模板和扫描范围见验证记录。
|
||||
|
||||
## 1.0.6 恢复入口
|
||||
|
||||
本次签名构建源码SHA为`ad54726b17fabd11f46bc6121e664eac38960a23`,APP版本1.0.6、build8;最终发布标签为`v1.0.6`。恢复该SHA可重建相同产品源码,最终标签另包含交付文档与回执。Release恢复仍用上节同一构建/公证命令、固定Bundle ID和正式安装路径;用户自测模式不会操作Motion/FCP界面。
|
||||
|
||||
新增下巴三pin与图像适配轴修复;新分析档案使用`hostCoordinateSpace=iosurface-ci-y-up-v1`。旧未标记档案保留原字符串但回退实时跟随,重新分析后才能用于离线重放,不能直接补字段伪装成新档案。七类剩余脸型项及组合顺序仍有近似边界。
|
||||
|
||||
`ad54726b17fabd11f46bc6121e664eac38960a23`已独立从上述远程克隆,87文件的byte/blob/mode与14项资源一致,strict/full fsck和559项核心测试通过,无LFS、子模块或APP产物。临时恢复目录已清理,完整报告见`docs/verification/v1.0.6/recovery-ad54726.json`。
|
||||
|
||||
1.0.6已完成正式签名、公证(Accepted ab0271c8-0c4b-4b10-9474-cebe9b55385b)、票据粘附、唯一正式目录安装和配套模板同步;临时APP/旧备份APP/公证ZIP已清理。完整回执及有界副本扫描见`docs/verification/v1.0.6/installed-verification.json`。未操作Motion/FCP界面,最终视觉验收保留为用户自测。
|
||||
二进制文件未显示。
@@ -0,0 +1,502 @@
|
||||
{
|
||||
"version": "1.0.2",
|
||||
"pluginUUID": "FB14B0E6-59B2-4836-916F-CD0C559E9241",
|
||||
"filterObject": "510000005",
|
||||
"visiblePublished": 46,
|
||||
"parameters": [
|
||||
{
|
||||
"id": 70,
|
||||
"name": "启用磨皮",
|
||||
"group": 100,
|
||||
"channel": "./100/70",
|
||||
"kind": "bool",
|
||||
"default": 1,
|
||||
"value": 1,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "磨皮强度",
|
||||
"group": 100,
|
||||
"channel": "./100/1",
|
||||
"kind": "float",
|
||||
"default": 1,
|
||||
"value": 1,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "中频磨皮",
|
||||
"group": 100,
|
||||
"channel": "./100/12",
|
||||
"kind": "float",
|
||||
"default": 0,
|
||||
"value": 0.5,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "磨皮半径",
|
||||
"group": 100,
|
||||
"channel": "./100/3",
|
||||
"kind": "float",
|
||||
"default": 8,
|
||||
"value": 8,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"name": "保护五官",
|
||||
"group": 100,
|
||||
"channel": "./100/30",
|
||||
"kind": "bool",
|
||||
"default": 1,
|
||||
"value": 1,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 71,
|
||||
"name": "启用肤色",
|
||||
"group": 101,
|
||||
"channel": "./101/71",
|
||||
"kind": "bool",
|
||||
"default": 1,
|
||||
"value": 1,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 42,
|
||||
"name": "肤色风格",
|
||||
"group": 101,
|
||||
"channel": "./101/42",
|
||||
"kind": "popup",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "肤色提亮",
|
||||
"group": 101,
|
||||
"channel": "./101/2",
|
||||
"kind": "float",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "肤色色相",
|
||||
"group": 101,
|
||||
"channel": "./101/14",
|
||||
"kind": "float",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "肤色饱和度",
|
||||
"group": 101,
|
||||
"channel": "./101/15",
|
||||
"kind": "float",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"name": "均匀肤色",
|
||||
"group": 101,
|
||||
"channel": "./101/41",
|
||||
"kind": "float",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"name": "牙齿美白",
|
||||
"group": 101,
|
||||
"channel": "./101/31",
|
||||
"kind": "float",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"name": "眼睛提亮",
|
||||
"group": 101,
|
||||
"channel": "./101/32",
|
||||
"kind": "float",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 73,
|
||||
"name": "启用细节",
|
||||
"group": 102,
|
||||
"channel": "./102/73",
|
||||
"kind": "bool",
|
||||
"default": 1,
|
||||
"value": 1,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": "高频细节平滑",
|
||||
"group": 102,
|
||||
"channel": "./102/17",
|
||||
"kind": "float",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "锐化",
|
||||
"group": 102,
|
||||
"channel": "./102/4",
|
||||
"kind": "float",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 72,
|
||||
"name": "启用脸型",
|
||||
"group": 103,
|
||||
"channel": "./103/72",
|
||||
"kind": "bool",
|
||||
"default": 1,
|
||||
"value": 1,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "瘦脸",
|
||||
"group": 103,
|
||||
"channel": "./103/18",
|
||||
"kind": "float",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 33,
|
||||
"name": "大眼",
|
||||
"group": 103,
|
||||
"channel": "./103/33",
|
||||
"kind": "float",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"name": "短脸",
|
||||
"group": 103,
|
||||
"channel": "./103/22",
|
||||
"kind": "float",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"name": "窄脸",
|
||||
"group": 103,
|
||||
"channel": "./103/23",
|
||||
"kind": "float",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"name": "下巴",
|
||||
"group": 103,
|
||||
"channel": "./103/24",
|
||||
"kind": "float",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"name": "V 脸",
|
||||
"group": 103,
|
||||
"channel": "./103/25",
|
||||
"kind": "float",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"name": "下颌",
|
||||
"group": 103,
|
||||
"channel": "./103/26",
|
||||
"kind": "float",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"name": "颧骨",
|
||||
"group": 103,
|
||||
"channel": "./103/27",
|
||||
"kind": "float",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"name": "小头",
|
||||
"group": 103,
|
||||
"channel": "./103/28",
|
||||
"kind": "float",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"name": "小头范围",
|
||||
"group": 103,
|
||||
"channel": "./103/29",
|
||||
"kind": "float",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"name": "只处理选中人脸",
|
||||
"group": 104,
|
||||
"channel": "./104/20",
|
||||
"kind": "bool",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"name": "选脸位置",
|
||||
"group": 104,
|
||||
"channel": "./104/21",
|
||||
"kind": "point",
|
||||
"default": 0.5,
|
||||
"value": 0.5,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 39,
|
||||
"name": "多人高速模式",
|
||||
"group": 104,
|
||||
"channel": "./104/39",
|
||||
"kind": "bool",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 40,
|
||||
"name": "防跳脸范围",
|
||||
"group": 104,
|
||||
"channel": "./104/40",
|
||||
"kind": "float",
|
||||
"default": 1.8,
|
||||
"value": 1.8,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 45,
|
||||
"name": "使用跟踪数据",
|
||||
"group": 104,
|
||||
"channel": "./104/45",
|
||||
"kind": "bool",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 46,
|
||||
"name": "跟踪目标",
|
||||
"group": 104,
|
||||
"channel": "./104/46",
|
||||
"kind": "popup",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 50,
|
||||
"name": "向前分析",
|
||||
"group": 104,
|
||||
"channel": "./104/50",
|
||||
"kind": "button",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 51,
|
||||
"name": "向后分析",
|
||||
"group": 104,
|
||||
"channel": "./104/51",
|
||||
"kind": "button",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 52,
|
||||
"name": "清除当前目标",
|
||||
"group": 104,
|
||||
"channel": "./104/52",
|
||||
"kind": "button",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 53,
|
||||
"name": "跟踪状态",
|
||||
"group": 104,
|
||||
"channel": "./104/53",
|
||||
"kind": "status",
|
||||
"default": "选择画面中的人脸后开始分析",
|
||||
"value": "选择画面中的人脸后开始分析",
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"name": "显示人脸框",
|
||||
"group": 104,
|
||||
"channel": "./104/19",
|
||||
"kind": "bool",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"name": "显示跟踪框",
|
||||
"group": 104,
|
||||
"channel": "./104/35",
|
||||
"kind": "bool",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "蒙版阈值",
|
||||
"group": 105,
|
||||
"channel": "./105/7",
|
||||
"kind": "float",
|
||||
"default": 148,
|
||||
"value": 148,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "蒙版柔度",
|
||||
"group": 105,
|
||||
"channel": "./105/8",
|
||||
"kind": "float",
|
||||
"default": 0.6,
|
||||
"value": 0.6,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "边缘保护",
|
||||
"group": 105,
|
||||
"channel": "./105/9",
|
||||
"kind": "float",
|
||||
"default": 0.4,
|
||||
"value": 0.4,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "预览蒙版",
|
||||
"group": 105,
|
||||
"channel": "./105/10",
|
||||
"kind": "bool",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 43,
|
||||
"name": "反转蒙版",
|
||||
"group": 105,
|
||||
"channel": "./105/43",
|
||||
"kind": "bool",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 44,
|
||||
"name": "蒙版通道",
|
||||
"group": 105,
|
||||
"channel": "./105/44",
|
||||
"kind": "popup",
|
||||
"default": 0,
|
||||
"value": 0,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "整体混合",
|
||||
"group": 105,
|
||||
"channel": "./105/5",
|
||||
"kind": "float",
|
||||
"default": 1,
|
||||
"value": 1,
|
||||
"published": true
|
||||
},
|
||||
{
|
||||
"id": 60,
|
||||
"name": "分析数据",
|
||||
"channel": "./60",
|
||||
"kind": "hidden-string",
|
||||
"value": "",
|
||||
"published": false
|
||||
},
|
||||
{
|
||||
"id": 61,
|
||||
"name": "分析数据",
|
||||
"channel": "./61",
|
||||
"kind": "hidden-string",
|
||||
"value": "",
|
||||
"published": false
|
||||
},
|
||||
{
|
||||
"id": 62,
|
||||
"name": "分析数据",
|
||||
"channel": "./62",
|
||||
"kind": "hidden-string",
|
||||
"value": "",
|
||||
"published": false
|
||||
}
|
||||
],
|
||||
"effectSource": {
|
||||
"object": "510000004",
|
||||
"sourceMediaChannel": "./1/324/300",
|
||||
"clip": "510000006",
|
||||
"footage": "510000007",
|
||||
"type": 3,
|
||||
"dropZone": 1,
|
||||
"path": "Drop Zone.tiff",
|
||||
"resourceBundled": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
# 大鱼人像美化 FCP 效果模板
|
||||
|
||||
版本:1.0.6。滤镜 UUID:`FB14B0E6-59B2-4836-916F-CD0C559E9241`。模板必须与已安装的大鱼人像美化 FxPlug APP 配套使用。
|
||||
|
||||
## 默认数值
|
||||
|
||||
模板首次应用时,“中频磨皮”为 **50%(0.5)**;直接在 Motion 添加裸 FxPlug 滤镜时为 **0%(0)**。模板保存了 `default=0 / value=0.5`,保留裸插件的重置基准;其他功能参数与插件默认一致。整体混合为100%,脸型、肤色提亮、锐化、牙齿和眼睛提亮初始为0,跟踪初始关闭。此模板不预置人脸身份或跟踪记录。
|
||||
|
||||
## 参数与输入
|
||||
|
||||
- 保留磨皮、肤色、细节、脸型、人脸选择与跟踪、蒙版与输出六个原生 FxPlug 参数组,发布46项可见功能参数,包括前向/后向分析及清除按钮。
|
||||
- 目标1–3的隐藏分析数据以参数60/61/62保留在滤镜节点中,不发布成用户控件;分析完成后由插件写入宿主参数并随工程保存。
|
||||
- 选脸位置发布为原生二维参数,并启用滤镜的“发布 OSC”宿主通道。跟踪按钮、OSC与工程关闭重开后的数据恢复须由实际 FCP 验证。
|
||||
- 画面只含一个标准 Effect Source(Type=3、Drop Zone=1),其 Source Media 指向 clip `510000006`,clip 位于 footage `510000007`。`pathURL=Drop Zone.tiff` 为模板同目录内的相对引用,标准1200×1200占位图已随模板附带,没有外部安装目录依赖、示例人像、旧滤镜或新动画。FCP时间线片段的实际替换已在独立验收项目中确认正常出图。
|
||||
- 作者与网站为两个原生空绑定展示菜单,不改变渲染。
|
||||
|
||||
建议模板安装位置:`~/Movies/Motion Templates.localized/Effects.localized/大鱼/大鱼人像美化/`。APP正式安装与模板安装、Motion/FCP原生验收由主任务处理。
|
||||
|
||||
## 结构来源与静态验证
|
||||
|
||||
Apple项目、群组、Effect Source和滤镜节点骨架来源于用户自有“大鱼去黑.moef”;仅保留这些原生结构,已替换滤镜UUID、名称、版本和全部功能参数。FxPlug组/字符串/点参数的原生结构交叉参照自有Number与Zoom and Pan样本;作者/网站空绑定来自ae-to-motion技能的原生版权样本。
|
||||
|
||||
`PARAMETERS.json`逐项记录参数ID、组、发布通道及初始值;`VALIDATION.json`记录本次静态核验。48个发布路径(46功能+2信息)均能反查实际对象和通道,实体ID无冲突,三个隐藏数据通道未发布;必要占位媒体以相对路径随包附带,保存选择状态为0个内容对象且焦点为项目。技能`motion_selection.py check`与`audit_motion.py --require-no-selection`通过。
|
||||
|
||||
结构核验之外,已用实际素材在FCP和Motion完成加载、画面及提亮回归测试。详细范围及尚未覆盖的边界见 [验证记录](../../docs/验证记录.md)。
|
||||
|
||||
## 前版输入链错误与本次修复
|
||||
|
||||
前版生成稿(SHA256 `3b7984e11a92487834ab67121fd3814bf13c22b77a70851298efa84206f91478`)将效果源的 Source Media 设为0,并删除了原生clip/footage。用户在FCP应用后实测整幅黑屏。这是模板装配错误;保留Type=3本身并不能证明FCP输入替换链完整。此前XML解析、发布路径和无缺媒体检查通过,也没有覆盖真实FCP画面输入,不能当作该模板已经可用的证据。
|
||||
|
||||
黑屏修复恢复标准效果源clip/footage和Source Media关联,并随包加入标准`Drop Zone.tiff`,保留49个插件参数、48个发布入口与Type=3/Drop Zone=1。1.0.4同时包含独立的提亮蒙版与选脸坐标修复。该占位图从Apple Motion的Ozone.framework标准资源复制,实际运行只引用本模板内的相对副本。图像SHA256为 `a7f8e3a2a431dbb3ce9e777040bb55e701cfe9ab9632e8cbb10359732bc8692e`,420128字节。
|
||||
|
||||
本次已经静态核验Source Media→clip→相对文件链存在、文件字节一致、48项发布仍解析、隐藏数据未发布和保存选择状态;修复后已在FCP独立验收项目新增实例,确认输入替换、实际人像和提亮画面正常;Mix=0逐字节直通由宿主适配器IOSurface回归测试覆盖,未将其混称为FCP截图逐像素核验。
|
||||
|
||||
1.0.5将瘦脸改为76点驱动的17组局部位移,供用户在Motion/FCP自测最大瘦脸与转脸。旧跟踪记录若没有全点阵,需重新分析后才有瘦脸,或关闭“使用跟踪数据”采用当前帧自动检测。
|
||||
|
||||
1.0.6移除Core Image/IOSurface链按宿主原点重复翻转,修正下巴三组局部位移及正负方向。旧版未标记坐标空间的分析记录保留但不再重放,自动回退实时跟随;需要离线记录时重新分析当前目标。
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"version": "1.0.6",
|
||||
"sha256": "30ca8edf36801264c5de2c753c40b720aba8052633620713a12401098fcd2409",
|
||||
"filterUUID": "FB14B0E6-59B2-4836-916F-CD0C559E9241",
|
||||
"publishedFunctionalControls": 46,
|
||||
"informationalControls": 2,
|
||||
"hiddenTrackingSlots": 3,
|
||||
"publishedPathsResolved": 48,
|
||||
"sourceType": 3,
|
||||
"sourceMedia": "510000006",
|
||||
"clip": "510000006",
|
||||
"footage": "510000007",
|
||||
"sourceResource": {
|
||||
"relativePath": "Drop Zone.tiff",
|
||||
"bytes": 420128,
|
||||
"sha256": "a7f8e3a2a431dbb3ce9e777040bb55e701cfe9ab9632e8cbb10359732bc8692e",
|
||||
"origin": "Apple Motion Ozone.framework standard placeholder; copied into this template for relative reference"
|
||||
},
|
||||
"inputChainRepair": {
|
||||
"previousTemplateSHA256": "3b7984e11a92487834ab67121fd3814bf13c22b77a70851298efa84206f91478",
|
||||
"previousSourceMedia": 0,
|
||||
"previousFootageCount": 0,
|
||||
"problem": "Previous generated template removed the standard source clip and could not establish the native effect-source input chain. XML/reference checks did not validate FCP timeline replacement.",
|
||||
"change": "Restored the user-owned reference template clip/footage skeleton with new unique IDs; restored the Source Media link; bundled standard Drop Zone.tiff; filter, publications and Type=3/Drop Zone=1 preserved."
|
||||
},
|
||||
"hostValidation": {
|
||||
"date": "2026-10-21",
|
||||
"FCP": "Restored input-chain template shows supplied real video; effect parameters and forward analysis responded in independent QA project.",
|
||||
"Motion": "1.0.4 installed formal app loaded the real-video QA project and rendered brightness/geometry with a primary box at different playhead positions; offline tracking disabled.",
|
||||
"scope": "1.0.6 coordinate/chin repair native visual QA is reserved for user self-testing; no Motion/FCP UI operations. Historical host tests remain scoped to their versions.",
|
||||
"version105": "Formal 1.0.5 (7) signed/notarized/stapled/installed and PluginKit formal path verified. User closed hosts before replacement; no Motion/FCP UI operated. Native visual testing pending user.",
|
||||
"version106": "Formal 1.0.6 (8) universal Release signed/notarized/stapled/installed, identity/Gatekeeper/formal PluginKit path verified. User closed hosts; companion moef and TIFF synchronized and byte-verified. Generated outer APPs and notarization ZIP removed. No Motion/FCP UI operations; native visual QA pending user."
|
||||
},
|
||||
"slimValidation": {
|
||||
"offlineFullChecks": 604,
|
||||
"slimChecks": 224,
|
||||
"extraRealVideoASanChecks": 264,
|
||||
"sampledVideoFrames": 11,
|
||||
"maxUVOracleError": 1.09828022e-05
|
||||
},
|
||||
"coordinateChinValidation": {
|
||||
"offlineFullChecks": 904,
|
||||
"thinChinChecks": 394,
|
||||
"hostAdapterChecksASan": 247,
|
||||
"oscChecksASan": 98,
|
||||
"extraRealVideoASanChecks": 494,
|
||||
"sampledVideoFrames": 11,
|
||||
"independentAnatomicalChecks": 12,
|
||||
"originCombinations": 16,
|
||||
"allOriginTagsPixelIdentical": true,
|
||||
"maxByteErrorVsIndependentCIReference": 2,
|
||||
"maxUVOracleError": 1.09828022e-05,
|
||||
"nativeVisualQA": "pending user"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE ozxmlscene>
|
||||
<ozml version="5.13">
|
||||
<displayversion>5.6.7</displayversion>
|
||||
<factory id="1" uuid="46c844a813d311d8a438000a95af9f7e">
|
||||
<description>Channel</description>
|
||||
<manufacturer>Apple</manufacturer>
|
||||
<version>1</version>
|
||||
</factory>
|
||||
<factory id="2" uuid="65cb4dc9d4504fa281921f5f751fba06">
|
||||
<description>Widget</description>
|
||||
<manufacturer>Apple</manufacturer>
|
||||
<version>1</version>
|
||||
</factory>
|
||||
<factory id="3" uuid="66fc0d6af6a911d6a7a7000393670732">
|
||||
<description>Image</description>
|
||||
<manufacturer>Apple</manufacturer>
|
||||
<version>1</version>
|
||||
</factory>
|
||||
<factory id="4" uuid="7d468273c013498e9806a0d7bc32fddf">
|
||||
<description>Project</description>
|
||||
<manufacturer>Apple</manufacturer>
|
||||
<version>1</version>
|
||||
</factory>
|
||||
<factory id="5" uuid="dbca752470fd11d7980100039389b702">
|
||||
<description>Channel</description>
|
||||
<manufacturer>Apple</manufacturer>
|
||||
<version>1</version>
|
||||
</factory>
|
||||
<factory id="7" uuid="402246385249454b90e08b2ee1b6bf06">
|
||||
<description>绑定</description>
|
||||
<manufacturer>Apple</manufacturer>
|
||||
<version>1</version>
|
||||
</factory>
|
||||
<factory id="8" uuid="e35c74f01d5042f6b468eb530169bfb7">
|
||||
<description>小组件</description>
|
||||
<manufacturer>Apple</manufacturer>
|
||||
<version>1</version>
|
||||
</factory>
|
||||
<factory id="6" uuid="deca4859b16011d7a12d0003936f6f92">
|
||||
<description>ProPlugin Filter</description>
|
||||
<manufacturer>Apple</manufacturer>
|
||||
<version>1</version>
|
||||
</factory>
|
||||
<template>
|
||||
<flags>1</flags>
|
||||
</template>
|
||||
<build />
|
||||
<description>大鱼人像美化 1.0.6:人像磨皮、肤色、脸型、五官保护和三目标人脸跟踪。模板中频磨皮默认为50%。</description>
|
||||
<scene>
|
||||
<sceneSettings>
|
||||
<width>1920</width>
|
||||
<height>1080</height>
|
||||
<duration>300</duration>
|
||||
<shouldOverrideFCDuration>0</shouldOverrideFCDuration>
|
||||
<frameRate>30</frameRate>
|
||||
<NTSC>0</NTSC>
|
||||
<pixelAspectRatio>1</pixelAspectRatio>
|
||||
<workingGamut>0</workingGamut>
|
||||
<viewGamut>-1</viewGamut>
|
||||
<optimizeForDisplay>0</optimizeForDisplay>
|
||||
<backgroundColor red="0" green="0" blue="0" alpha="1" />
|
||||
<audioChannels>2</audioChannels>
|
||||
<audioBitsPerSample>32</audioBitsPerSample>
|
||||
<fieldRenderingMode>0</fieldRenderingMode>
|
||||
<motionBlurSamples>8</motionBlurSamples>
|
||||
<motionBlurDuration>1</motionBlurDuration>
|
||||
<sharpScaling>0</sharpScaling>
|
||||
<startTimecode>0</startTimecode>
|
||||
<presetPath />
|
||||
<backgroundMode>0</backgroundMode>
|
||||
<reflectionRecursionLimit>2</reflectionRecursionLimit>
|
||||
<glyphOSCMode>0</glyphOSCMode>
|
||||
<animateFlag>0</animateFlag>
|
||||
<parameterColorSpaceID>3</parameterColorSpaceID>
|
||||
<savePreviewMovie>0</savePreviewMovie>
|
||||
<Object3DEnvironments>100</Object3DEnvironments>
|
||||
<DRTSupport>1</DRTSupport>
|
||||
<onHDRDisplay>0</onHDRDisplay>
|
||||
</sceneSettings>
|
||||
<publishSettings>
|
||||
<version>2</version>
|
||||
<target object="510000011" channel="./2/100" name="FCP 版本作者→" />
|
||||
<target object="510000012" channel="./2/100" name="更多模板浏览器访问→" />
|
||||
<target object="510000005" channel="./100/70" name="启用磨皮" />
|
||||
<target object="510000005" channel="./100/1" name="磨皮强度" />
|
||||
<target object="510000005" channel="./100/12" name="中频磨皮" />
|
||||
<target object="510000005" channel="./100/3" name="磨皮半径" />
|
||||
<target object="510000005" channel="./100/30" name="保护五官" />
|
||||
<target object="510000005" channel="./101/71" name="启用肤色" />
|
||||
<target object="510000005" channel="./101/42" name="肤色风格" />
|
||||
<target object="510000005" channel="./101/2" name="肤色提亮" />
|
||||
<target object="510000005" channel="./101/14" name="肤色色相" />
|
||||
<target object="510000005" channel="./101/15" name="肤色饱和度" />
|
||||
<target object="510000005" channel="./101/41" name="均匀肤色" />
|
||||
<target object="510000005" channel="./101/31" name="牙齿美白" />
|
||||
<target object="510000005" channel="./101/32" name="眼睛提亮" />
|
||||
<target object="510000005" channel="./102/73" name="启用细节" />
|
||||
<target object="510000005" channel="./102/17" name="高频细节平滑" />
|
||||
<target object="510000005" channel="./102/4" name="锐化" />
|
||||
<target object="510000005" channel="./103/72" name="启用脸型" />
|
||||
<target object="510000005" channel="./103/18" name="瘦脸" />
|
||||
<target object="510000005" channel="./103/33" name="大眼" />
|
||||
<target object="510000005" channel="./103/22" name="短脸" />
|
||||
<target object="510000005" channel="./103/23" name="窄脸" />
|
||||
<target object="510000005" channel="./103/24" name="下巴" />
|
||||
<target object="510000005" channel="./103/25" name="V 脸" />
|
||||
<target object="510000005" channel="./103/26" name="下颌" />
|
||||
<target object="510000005" channel="./103/27" name="颧骨" />
|
||||
<target object="510000005" channel="./103/28" name="小头" />
|
||||
<target object="510000005" channel="./103/29" name="小头范围" />
|
||||
<target object="510000005" channel="./104/20" name="只处理选中人脸" />
|
||||
<target object="510000005" channel="./104/21" name="选脸位置" />
|
||||
<target object="510000005" channel="./104/39" name="多人高速模式" />
|
||||
<target object="510000005" channel="./104/40" name="防跳脸范围" />
|
||||
<target object="510000005" channel="./104/45" name="使用跟踪数据" />
|
||||
<target object="510000005" channel="./104/46" name="跟踪目标" />
|
||||
<target object="510000005" channel="./104/50" name="向前分析" />
|
||||
<target object="510000005" channel="./104/51" name="向后分析" />
|
||||
<target object="510000005" channel="./104/52" name="清除当前目标" />
|
||||
<target object="510000005" channel="./104/53" name="跟踪状态" />
|
||||
<target object="510000005" channel="./104/19" name="显示人脸框" />
|
||||
<target object="510000005" channel="./104/35" name="显示跟踪框" />
|
||||
<target object="510000005" channel="./105/7" name="蒙版阈值" />
|
||||
<target object="510000005" channel="./105/8" name="蒙版柔度" />
|
||||
<target object="510000005" channel="./105/9" name="边缘保护" />
|
||||
<target object="510000005" channel="./105/10" name="预览蒙版" />
|
||||
<target object="510000005" channel="./105/43" name="反转蒙版" />
|
||||
<target object="510000005" channel="./105/44" name="蒙版通道" />
|
||||
<target object="510000005" channel="./105/5" name="整体混合" />
|
||||
</publishSettings>
|
||||
<currentFrame>0 1 1 0</currentFrame>
|
||||
<currentObject>510000001</currentObject>
|
||||
<activeLayer>510000003</activeLayer>
|
||||
<timeRange offset="0 1 1 0" duration="10 1 1 0" />
|
||||
<playRange offset="0 1 1 0" duration="10 1 1 0" />
|
||||
<flags>1</flags>
|
||||
<audioTracks>0</audioTracks>
|
||||
<timemarkerset />
|
||||
<guideset />
|
||||
<curvesets selected="1" />
|
||||
<scenenode name="项目" id="510000001" factoryID="4" version="5">
|
||||
<scenenode name="Widget" id="510000002" factoryID="2" version="5">
|
||||
<flags>0</flags>
|
||||
<timing in="0 1 1 0" out="-4004 120000 1 0" offset="0 1 1 0" />
|
||||
<foldFlags>0</foldFlags>
|
||||
<baseFlags>16</baseFlags>
|
||||
<parameter name="Properties" id="1" flags="8589938704" />
|
||||
<parameter name="Object" id="2" flags="8589938704">
|
||||
<parameter name="Options" id="103" flags="8589938688" />
|
||||
<parameter name="Hidden" id="102" flags="8589934608" default="0" value="1" />
|
||||
<parameter name="Snapshots" id="101" flags="8589938706" />
|
||||
<parameter name="Widget" id="100" flags="8589934608" default="1.7777777777777777" value="1.7777777777777777" />
|
||||
</parameter>
|
||||
</scenenode>
|
||||
<flags>0</flags>
|
||||
<timing in="0 1 1 0" out="-4004 120000 1 0" offset="0 1 1 0" />
|
||||
<foldFlags>0</foldFlags>
|
||||
<baseFlags>268435473</baseFlags>
|
||||
<parameter name="Properties" id="1" flags="8589938704">
|
||||
<parameter name="HDR White Level" id="103" default="0.95" value="0.95" />
|
||||
</parameter>
|
||||
<parameter name="Object" id="2" flags="8589938705" />
|
||||
</scenenode>
|
||||
<scenenode name="绑定" id="510000010" factoryID="7" version="5">
|
||||
<scenenode name="弹出式菜单 1" id="510000012" factoryID="8" version="5">
|
||||
<affecting>2</affecting>
|
||||
<snapshot>3</snapshot>
|
||||
<flags>0</flags>
|
||||
<timing in="0 1 1 0" out="-2560 153600 1 0" offset="0 1 1 0" />
|
||||
<foldFlags>0</foldFlags>
|
||||
<baseFlags>16</baseFlags>
|
||||
<parameter name="属性" id="1" flags="8589938704" />
|
||||
<parameter name="对象" id="2" flags="8589938704">
|
||||
<parameter name="选项" id="103" flags="8589938688">
|
||||
<parameter name="初始值" id="106" flags="68736319504" default="0" value="0" />
|
||||
</parameter>
|
||||
<parameter name="Hidden" id="102" flags="8606711824" default="0" value="3" />
|
||||
<parameter name="快照" id="101" flags="8589938706">
|
||||
<parameter name="快照" id="3" flags="4112">
|
||||
<parameter name="值" id="1" flags="16" default="0" value="2" />
|
||||
<parameter name="插值" id="2" flags="65552" default="1" value="1" />
|
||||
</parameter>
|
||||
</parameter>
|
||||
<parameter name="弹出式菜单" id="100" flags="16842769" default="2" value="2">
|
||||
<entry name="dayu.media" tag="2" />
|
||||
</parameter>
|
||||
</parameter>
|
||||
</scenenode>
|
||||
<scenenode name="弹出式菜单" id="510000011" factoryID="8" version="5">
|
||||
<affecting>2</affecting>
|
||||
<snapshot>3</snapshot>
|
||||
<flags>0</flags>
|
||||
<timing in="0 1 1 0" out="-2560 153600 1 0" offset="0 1 1 0" />
|
||||
<foldFlags>0</foldFlags>
|
||||
<baseFlags>16</baseFlags>
|
||||
<parameter name="属性" id="1" flags="8589938704" />
|
||||
<parameter name="对象" id="2" flags="8589938704">
|
||||
<parameter name="选项" id="103" flags="8589938688">
|
||||
<parameter name="初始值" id="106" flags="68736319504" default="0" value="0" />
|
||||
</parameter>
|
||||
<parameter name="Hidden" id="102" flags="8606711824" default="0" value="3" />
|
||||
<parameter name="快照" id="101" flags="8589938706">
|
||||
<parameter name="快照" id="3" flags="4112">
|
||||
<parameter name="值" id="1" flags="16" default="0" value="2" />
|
||||
<parameter name="插值" id="2" flags="65552" default="1" value="1" />
|
||||
</parameter>
|
||||
</parameter>
|
||||
<parameter name="弹出式菜单" id="100" flags="16842768" default="2" value="2">
|
||||
<entry name="大鱼先森Media" tag="2" />
|
||||
</parameter>
|
||||
</parameter>
|
||||
</scenenode>
|
||||
<flags>0</flags>
|
||||
<timing in="0 1 1 0" out="-2560 153600 1 0" offset="0 1 1 0" />
|
||||
<foldFlags>0</foldFlags>
|
||||
<baseFlags>16</baseFlags>
|
||||
<parameter name="属性" id="1" flags="8589938704" />
|
||||
<parameter name="对象" id="2" flags="8589938704" />
|
||||
</scenenode>
|
||||
<layer name="大鱼人像美化" id="510000003">
|
||||
<scenenode name="Effect Source" id="510000004" factoryID="3" version="5">
|
||||
<validTracks>1</validTracks>
|
||||
<aspectRatio>1</aspectRatio>
|
||||
<flags>0</flags>
|
||||
<timing in="0 1 1 0" out="299 30 1 0" offset="0 1 1 0" />
|
||||
<foldFlags>16385</foldFlags>
|
||||
<baseFlags>524304</baseFlags>
|
||||
<parameter name="Properties" id="1" flags="8589938704">
|
||||
<parameter name="Cinematic" id="344" flags="8589938706">
|
||||
<foldFlags>131076</foldFlags>
|
||||
<parameter name="Depth of Field" id="346" flags="8589934608" default="1.3999999999999999" value="2" />
|
||||
<parameter name="Focus Point" id="348" flags="8589938706">
|
||||
<foldFlags>15</foldFlags>
|
||||
</parameter>
|
||||
</parameter>
|
||||
<parameter name="Lighting" id="230" flags="8589938706">
|
||||
<foldFlags>15</foldFlags>
|
||||
</parameter>
|
||||
<parameter name="Shadows" id="234" flags="8589938706">
|
||||
<foldFlags>15</foldFlags>
|
||||
</parameter>
|
||||
<parameter name="Reflection" id="223" flags="8589971474">
|
||||
<foldFlags>131087</foldFlags>
|
||||
</parameter>
|
||||
<parameter name="Color" id="243" flags="8589938688">
|
||||
<parameter name="Conversion Version" id="245" flags="8589934610" default="1" value="2" />
|
||||
<parameter name="Conversion Type" id="247" flags="8590032912" default="2" value="2" />
|
||||
</parameter>
|
||||
<parameter name="Media" id="324" flags="8589938704">
|
||||
<foldFlags>4</foldFlags>
|
||||
<parameter name="Source Media" id="300" flags="12901744656" default="510000006" value="510000006" />
|
||||
<parameter name="Source Media" id="325" flags="8590000146" />
|
||||
</parameter>
|
||||
<parameter name="Page Number" id="301" flags="8589934610" default="1" value="1" />
|
||||
<parameter name="Retime Value" id="304" flags="8590066066">
|
||||
<curve type="1" default="1" value="1" round="0" retimingExtrapolation="1">
|
||||
<numberOfKeypoints>2</numberOfKeypoints>
|
||||
<keypoint interpolation="1" flags="0">
|
||||
<time>0 1 1 0</time>
|
||||
<value>1</value>
|
||||
</keypoint>
|
||||
<keypoint interpolation="1" flags="0">
|
||||
<time>10 1 1 0</time>
|
||||
<value>301</value>
|
||||
</keypoint>
|
||||
</curve>
|
||||
</parameter>
|
||||
<parameter name="Retime Value Cache" id="319" flags="8590065810">
|
||||
<curve type="1" default="1" value="1">
|
||||
<numberOfKeypoints>2</numberOfKeypoints>
|
||||
<keypoint interpolation="1" flags="128">
|
||||
<time>0 1 1 0</time>
|
||||
<value>1</value>
|
||||
</keypoint>
|
||||
<keypoint interpolation="1" flags="128">
|
||||
<time>10 1 1 0</time>
|
||||
<value>301</value>
|
||||
</keypoint>
|
||||
</curve>
|
||||
</parameter>
|
||||
<parameter name="Duration Cache" id="320" flags="8589934610" default="0" value="300" />
|
||||
</parameter>
|
||||
<parameter name="Object" id="2" flags="8589938704">
|
||||
<parameter name="Drop Zone" id="311" flags="8589934738" default="0" value="1" />
|
||||
<parameter name="Type" id="321" flags="8590000146" default="0" value="3" />
|
||||
<parameter name="Pan" id="326" flags="77309415442">
|
||||
<foldFlags>15</foldFlags>
|
||||
</parameter>
|
||||
<parameter name="Scale" id="327" flags="77309415442">
|
||||
<foldFlags>15</foldFlags>
|
||||
</parameter>
|
||||
<parameter name="Fill Opaque" id="328" flags="8589934608" default="0" value="0" />
|
||||
<parameter name="Fill Color" id="329" flags="8589938704">
|
||||
<foldFlags>15</foldFlags>
|
||||
</parameter>
|
||||
<parameter name="Clear" id="315" flags="8606777360" default="0" value="0" />
|
||||
<parameter name="Width" id="313" flags="8589934610" default="1" value="1920" />
|
||||
<parameter name="Height" id="314" flags="8589934610" default="1" value="1080" />
|
||||
</parameter>
|
||||
<filter name="大鱼人像美化" id="510000005" factoryID="6" pluginUUID="FB14B0E6-59B2-4836-916F-CD0C559E9241" pluginVersion="1.0.6" pluginName="大鱼人像美化" pluginDynamicParams="0">
|
||||
<timing in="0 1 1 0" out="299 30 1 0" offset="0 1 1 0" />
|
||||
<baseFlags>8589934608</baseFlags>
|
||||
<parameter name="磨皮" id="100" flags="8589938704">
|
||||
<parameter name="启用磨皮" id="70" flags="12901679120" default="1" value="1" />
|
||||
<parameter name="磨皮强度" id="1" flags="12901679120" default="1" value="1" />
|
||||
<parameter name="中频磨皮" id="12" flags="12901679120" default="0" value="0.5" />
|
||||
<parameter name="磨皮半径" id="3" flags="12901679120" default="8" value="8" />
|
||||
<parameter name="保护五官" id="30" flags="12901679120" default="1" value="1" />
|
||||
</parameter>
|
||||
<parameter name="肤色" id="101" flags="8589938704">
|
||||
<parameter name="启用肤色" id="71" flags="12901679120" default="1" value="1" />
|
||||
<parameter name="肤色风格" id="42" flags="12901744656" default="0" value="0" />
|
||||
<parameter name="肤色提亮" id="2" flags="12901679120" default="0" value="0" />
|
||||
<parameter name="肤色色相" id="14" flags="12901679120" default="0" value="0" />
|
||||
<parameter name="肤色饱和度" id="15" flags="12901679120" default="0" value="0" />
|
||||
<parameter name="均匀肤色" id="41" flags="12901679120" default="0" value="0" />
|
||||
<parameter name="牙齿美白" id="31" flags="12901679120" default="0" value="0" />
|
||||
<parameter name="眼睛提亮" id="32" flags="12901679120" default="0" value="0" />
|
||||
</parameter>
|
||||
<parameter name="细节" id="102" flags="8589938704">
|
||||
<parameter name="启用细节" id="73" flags="12901679120" default="1" value="1" />
|
||||
<parameter name="高频细节平滑" id="17" flags="12901679120" default="0" value="0" />
|
||||
<parameter name="锐化" id="4" flags="12901679120" default="0" value="0" />
|
||||
</parameter>
|
||||
<parameter name="脸型" id="103" flags="8589938704">
|
||||
<parameter name="启用脸型" id="72" flags="12901679120" default="1" value="1" />
|
||||
<parameter name="瘦脸" id="18" flags="12901679120" default="0" value="0" />
|
||||
<parameter name="大眼" id="33" flags="12901679120" default="0" value="0" />
|
||||
<parameter name="短脸" id="22" flags="12901679120" default="0" value="0" />
|
||||
<parameter name="窄脸" id="23" flags="12901679120" default="0" value="0" />
|
||||
<parameter name="下巴" id="24" flags="12901679120" default="0" value="0" />
|
||||
<parameter name="V 脸" id="25" flags="12901679120" default="0" value="0" />
|
||||
<parameter name="下颌" id="26" flags="12901679120" default="0" value="0" />
|
||||
<parameter name="颧骨" id="27" flags="12901679120" default="0" value="0" />
|
||||
<parameter name="小头" id="28" flags="12901679120" default="0" value="0" />
|
||||
<parameter name="小头范围" id="29" flags="12901679120" default="0" value="0" />
|
||||
</parameter>
|
||||
<parameter name="人脸选择与跟踪" id="104" flags="8589938704">
|
||||
<parameter name="只处理选中人脸" id="20" flags="12901679120" default="0" value="0" />
|
||||
<parameter name="选脸位置" id="21" flags="12901683200">
|
||||
<foldFlags>15</foldFlags>
|
||||
<parameter name="X" id="1" flags="12901679120" default="0.5" value="0.5" />
|
||||
<parameter name="Y" id="2" flags="12901679120" default="0.5" value="0.5" />
|
||||
</parameter>
|
||||
<parameter name="多人高速模式" id="39" flags="12901679120" default="0" value="0" />
|
||||
<parameter name="防跳脸范围" id="40" flags="12901679120" default="1.8" value="1.8" />
|
||||
<parameter name="使用跟踪数据" id="45" flags="12901679120" default="0" value="0" />
|
||||
<parameter name="跟踪目标" id="46" flags="12901744656" default="0" value="0" />
|
||||
<parameter name="向前分析" id="50" />
|
||||
<parameter name="向后分析" id="51" />
|
||||
<parameter name="清除当前目标" id="52" />
|
||||
<parameter name="跟踪状态" id="53" flags="12889161760">
|
||||
<text>选择画面中的人脸后开始分析</text>
|
||||
</parameter>
|
||||
<parameter name="显示人脸框" id="19" flags="12901679120" default="0" value="0" />
|
||||
<parameter name="显示跟踪框" id="35" flags="12901679120" default="0" value="0" />
|
||||
</parameter>
|
||||
<parameter name="蒙版与输出" id="105" flags="8589938704">
|
||||
<parameter name="蒙版阈值" id="7" flags="12901679120" default="148" value="148" />
|
||||
<parameter name="蒙版柔度" id="8" flags="12901679120" default="0.6" value="0.6" />
|
||||
<parameter name="边缘保护" id="9" flags="12901679120" default="0.4" value="0.4" />
|
||||
<parameter name="预览蒙版" id="10" flags="12901679120" default="0" value="0" />
|
||||
<parameter name="反转蒙版" id="43" flags="12901679120" default="0" value="0" />
|
||||
<parameter name="蒙版通道" id="44" flags="12901744656" default="0" value="0" />
|
||||
<parameter name="整体混合" id="5" flags="12901679120" default="1" value="1" />
|
||||
</parameter>
|
||||
<parameter name="分析数据" id="60" flags="12905938976">
|
||||
<text />
|
||||
</parameter>
|
||||
<parameter name="分析数据" id="61" flags="12905938976">
|
||||
<text />
|
||||
</parameter>
|
||||
<parameter name="分析数据" id="62" flags="12905938976">
|
||||
<text />
|
||||
</parameter>
|
||||
<parameter name="混合" id="10001" flags="12884901888" default="1" value="1" />
|
||||
<parameter name="Flip" id="10002" flags="12889161760" default="0" value="0" />
|
||||
<parameter name="Input Points" id="10003" flags="12889161760" default="1" value="1" />
|
||||
<parameter name="发布 OSC" id="10005" flags="12901679104" default="0" value="1" />
|
||||
</filter>
|
||||
</scenenode>
|
||||
<aspectRatio>1</aspectRatio>
|
||||
<flags>0</flags>
|
||||
<timing in="0 1 1 0" out="299 30 1 0" offset="0 1 1 0" />
|
||||
<foldFlags>0</foldFlags>
|
||||
<baseFlags>524304</baseFlags>
|
||||
<parameter name="Properties" id="1" flags="8589938704">
|
||||
<parameter name="Lighting" id="230" flags="8589938706">
|
||||
<foldFlags>15</foldFlags>
|
||||
</parameter>
|
||||
<parameter name="Shadows" id="234" flags="8589938706">
|
||||
<foldFlags>15</foldFlags>
|
||||
</parameter>
|
||||
<parameter name="Reflection" id="223" flags="8589971474">
|
||||
<foldFlags>131087</foldFlags>
|
||||
</parameter>
|
||||
</parameter>
|
||||
<parameter name="Object" id="2" flags="8589938704">
|
||||
<parameter name="Type" id="307" flags="12901679120" default="0" value="0" />
|
||||
<parameter name="Fixed Width" id="302" flags="12901679124" default="1920" value="1920" />
|
||||
<parameter name="Fixed Height" id="303" flags="12901679124" default="1080" value="1080" />
|
||||
<parameter name="Flatten" id="311" flags="8589934610" default="0" value="0" />
|
||||
<parameter name="Layer Order" id="305" flags="8589934610" default="0" value="0" />
|
||||
<parameter name="Aperture Width" id="312" flags="12884901906" default="1920" value="1920" />
|
||||
<parameter name="Aperture Height" id="313" flags="12884901906" default="1080" value="1080" />
|
||||
<parameter name="New Fixed Res Behavior" id="315" flags="8594194480" default="1" value="0" />
|
||||
</parameter>
|
||||
</layer>
|
||||
<footage name="效果源媒体" id="510000007">
|
||||
<clip name="Effect Source" id="510000006">
|
||||
<pathURL>Drop Zone.tiff</pathURL>
|
||||
<missingWidth>1200</missingWidth>
|
||||
<missingHeight>1200</missingHeight>
|
||||
<missingDuration>0.033333333333333333</missingDuration>
|
||||
<creationDuration>1</creationDuration>
|
||||
<mediaID />
|
||||
<flags>0</flags>
|
||||
<timing in="0 1 1 0" out="0 120000 1 0" offset="0 1 1 0" />
|
||||
<foldFlags>0</foldFlags>
|
||||
<baseFlags>524304</baseFlags>
|
||||
<parameter name="Properties" id="1" flags="8589938704" />
|
||||
<parameter name="Object" id="2" flags="8589938704">
|
||||
<parameter name="Pixel Aspect Ratio" id="104" flags="12884901888" default="1" value="1" />
|
||||
<parameter name="Frame Rate" id="107" flags="8589934592" default="0" value="30" />
|
||||
<parameter name="Fixed Width" id="114" flags="12884901888" default="600" value="600" />
|
||||
<parameter name="Fixed Height" id="115" flags="12884901888" default="600" value="600" />
|
||||
<parameter name="Use Background Color" id="116" flags="8589934594" default="0" value="0" />
|
||||
<parameter name="Background Color" id="117" flags="8589971458">
|
||||
<foldFlags>15</foldFlags>
|
||||
<parameter name="Red" id="1" flags="8589967376" default="1" value="1" />
|
||||
<parameter name="Green" id="2" flags="8589967376" default="1" value="1" />
|
||||
<parameter name="Blue" id="3" flags="8589967376" default="1" value="1" />
|
||||
</parameter>
|
||||
<parameter name="Missing Is Still" id="128" flags="8589934610" default="0" value="1" />
|
||||
</parameter>
|
||||
</clip>
|
||||
<flags>0</flags>
|
||||
<timing in="0 1 1 0" out="0 120000 1 0" offset="0 1 1 0" />
|
||||
<foldFlags>0</foldFlags>
|
||||
<baseFlags>524304</baseFlags>
|
||||
<parameter name="Properties" id="1" flags="8589938704" />
|
||||
<parameter name="Object" id="2" flags="8589938704" />
|
||||
</footage>
|
||||
</scene>
|
||||
</ozml>
|
||||
@@ -63,10 +63,11 @@
|
||||
"/Localized: Plugin/InfoPlist.strings",
|
||||
"/Localized: Plugin/Localizable.strings",
|
||||
Plugin/beatygrilPlugIn.m,
|
||||
Plugin/PGBeautyRenderer.m,
|
||||
Plugin/PGFaceProcessor.m,
|
||||
Plugin/PGFaceSelectOSC.m,
|
||||
Plugin/Info.plist,
|
||||
Plugin/main.m,
|
||||
Plugin/MetalDeviceCache.m,
|
||||
Plugin/TileableRemoteBrightness.metal,
|
||||
"Wrapper Application/Info.plist",
|
||||
);
|
||||
target = 6945FC8A3088B7220013E108 /* Wrapper Application */;
|
||||
@@ -248,6 +249,7 @@
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
6945FC933088B7220013E108 /* Copy and Code Sign FxPlug.framework */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
name = "Copy and Code Sign FxPlug.framework";
|
||||
shellPath = /bin/sh;
|
||||
shellScript = (
|
||||
@@ -260,6 +262,7 @@
|
||||
};
|
||||
6945FC943088B7220013E108 /* Copy and Code Sign PluginManager.framework */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
name = "Copy and Code Sign PluginManager.framework";
|
||||
shellPath = /bin/sh;
|
||||
shellScript = (
|
||||
@@ -303,7 +306,10 @@
|
||||
6945FCCD3088B7220013E108 /* Debug configuration for PBXNativeTarget "XPC Service" */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
||||
CODE_SIGN_IDENTITY = "Developer ID Application";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
DEVELOPMENT_TEAM = B3W25FX5H5;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_PRECOMPILE_PREFIX_HEADER = YES;
|
||||
@@ -313,7 +319,7 @@
|
||||
"@loader_path/../Frameworks",
|
||||
);
|
||||
MACH_O_TYPE = mh_execute;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.dayu.beatygril.fxplug;
|
||||
PRODUCT_NAME = "beatygril $(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "beatygril/Plugin/XPC Service-Bridging-Header.h";
|
||||
WRAPPER_EXTENSION = pluginkit;
|
||||
@@ -323,7 +329,10 @@
|
||||
6945FCCE3088B7220013E108 /* Release configuration for PBXNativeTarget "XPC Service" */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
||||
CODE_SIGN_IDENTITY = "Developer ID Application";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
DEVELOPMENT_TEAM = B3W25FX5H5;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_PRECOMPILE_PREFIX_HEADER = YES;
|
||||
@@ -333,7 +342,7 @@
|
||||
"@loader_path/../Frameworks",
|
||||
);
|
||||
MACH_O_TYPE = mh_execute;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.dayu.beatygril.fxplug;
|
||||
PRODUCT_NAME = "beatygril $(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "beatygril/Plugin/XPC Service-Bridging-Header.h";
|
||||
WRAPPER_EXTENSION = pluginkit;
|
||||
@@ -344,12 +353,14 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "beatygril/Wrapper Application/SandboxEntitlements.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = B3W25FX5H5;
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
||||
CODE_SIGN_IDENTITY = "Developer ID Application";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
DEVELOPMENT_TEAM = B3W25FX5H5;
|
||||
GCC_PRECOMPILE_PREFIX_HEADER = YES;
|
||||
INFOPLIST_FILE = "beatygril/Wrapper Application/Info.plist";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.dayu.beatygril;
|
||||
PRODUCT_NAME = beatygril;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "beatygril/Wrapper Application/Wrapper Application-Bridging-Header.h";
|
||||
};
|
||||
@@ -359,12 +370,14 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "beatygril/Wrapper Application/SandboxEntitlements.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = B3W25FX5H5;
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
||||
CODE_SIGN_IDENTITY = "Developer ID Application";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
DEVELOPMENT_TEAM = B3W25FX5H5;
|
||||
GCC_PRECOMPILE_PREFIX_HEADER = YES;
|
||||
INFOPLIST_FILE = "beatygril/Wrapper Application/Info.plist";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.dayu.beatygril;
|
||||
PRODUCT_NAME = beatygril;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "beatygril/Wrapper Application/Wrapper Application-Bridging-Header.h";
|
||||
};
|
||||
@@ -374,13 +387,15 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ADDITIONAL_SDKS = /Library/Developer/SDKs/FxPlug.sdk;
|
||||
ARCHS = "arm64 x86_64";
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = NO;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
@@ -411,7 +426,7 @@
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
/Library/Frameworks,
|
||||
/Library/Developer/SDKs/FxPlug.sdk/Library/Frameworks,
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -430,10 +445,9 @@
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 27.2;
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_VERSION = 5;
|
||||
};
|
||||
@@ -443,13 +457,15 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ADDITIONAL_SDKS = /Library/Developer/SDKs/FxPlug.sdk;
|
||||
ARCHS = "arm64 x86_64";
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = NO;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
@@ -480,7 +496,7 @@
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
/Library/Frameworks,
|
||||
/Library/Developer/SDKs/FxPlug.sdk/Library/Frameworks,
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -493,7 +509,7 @@
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 27.2;
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = macosx;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>beatygrilXPCService</string>
|
||||
<string>大鱼人像美化</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -17,9 +17,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>1.0.6</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>8</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string></string>
|
||||
<key>PlugInKit</key>
|
||||
@@ -48,7 +48,7 @@
|
||||
<array>
|
||||
<dict>
|
||||
<key>groupName</key>
|
||||
<string>beatygril Group</string>
|
||||
<string>大鱼</string>
|
||||
<key>uuid</key>
|
||||
<string>0461C6B0-B8B7-4DE8-B882-03C635553ECB</string>
|
||||
</dict>
|
||||
@@ -59,19 +59,40 @@
|
||||
<key>className</key>
|
||||
<string>beatygrilPlugIn</string>
|
||||
<key>displayName</key>
|
||||
<string>beatygril::FxPlug Name</string>
|
||||
<string>大鱼人像美化</string>
|
||||
<key>group</key>
|
||||
<string>0461C6B0-B8B7-4DE8-B882-03C635553ECB</string>
|
||||
<key>infoString</key>
|
||||
<string>beatygril::FxPlug Description</string>
|
||||
<string>人像磨皮、肤色、脸型和人脸跟踪</string>
|
||||
<key>protocolNames</key>
|
||||
<array>
|
||||
<string>FxFilter</string>
|
||||
<string>FxAnalyzer</string>
|
||||
</array>
|
||||
<key>uuid</key>
|
||||
<string>FB14B0E6-59B2-4836-916F-CD0C559E9241</string>
|
||||
<key>version</key>
|
||||
<string>1.0</string>
|
||||
<string>1.0.6</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>className</key>
|
||||
<string>PGFaceSelectOSC</string>
|
||||
<key>displayName</key>
|
||||
<string>人脸选择</string>
|
||||
<key>group</key>
|
||||
<string>0461C6B0-B8B7-4DE8-B882-03C635553ECB</string>
|
||||
<key>uuid</key>
|
||||
<string>E0E432C8-3D71-4ABA-B006-E7F519E06C87</string>
|
||||
<key>version</key>
|
||||
<string>1.0.6</string>
|
||||
<key>protocolNames</key>
|
||||
<array>
|
||||
<string>FxOnScreenControl</string>
|
||||
</array>
|
||||
<key>supportedPlugins</key>
|
||||
<array>
|
||||
<string>FB14B0E6-59B2-4836-916F-CD0C559E9241</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>XPCService</key>
|
||||
@@ -88,5 +109,7 @@
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>13.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,28 +0,0 @@
|
||||
//
|
||||
// MetalDeviceCache.h
|
||||
// beatygril
|
||||
//
|
||||
// Created by dayu on 2026/10/21.
|
||||
//
|
||||
|
||||
#import <Metal/Metal.h>
|
||||
#import <FxPlug/FxPlugSDK.h>
|
||||
|
||||
@class MetalDeviceCacheItem;
|
||||
|
||||
@interface MetalDeviceCache : NSObject
|
||||
{
|
||||
NSMutableArray<MetalDeviceCacheItem*>* deviceCaches;
|
||||
}
|
||||
|
||||
+ (MetalDeviceCache*)deviceCache;
|
||||
+ (MTLPixelFormat)MTLPixelFormatForImageTile:(FxImageTile*)imageTile;
|
||||
|
||||
- (id<MTLDevice>)deviceWithRegistryID:(uint64_t)registryID;
|
||||
- (id<MTLRenderPipelineState>)pipelineStateWithRegistryID:(uint64_t)registryID
|
||||
pixelFormat:(MTLPixelFormat)pixFormat;
|
||||
- (id<MTLCommandQueue>)commandQueueWithRegistryID:(uint64_t)registryID
|
||||
pixelFormat:(MTLPixelFormat)pixFormat;
|
||||
- (void)returnCommandQueueToCache:(id<MTLCommandQueue>)commandQueue;
|
||||
|
||||
@end
|
||||
@@ -1,339 +0,0 @@
|
||||
//
|
||||
// MetalDeviceCache.m
|
||||
// beatygril
|
||||
//
|
||||
// Created by dayu on 2026/10/21.
|
||||
//
|
||||
|
||||
#import "MetalDeviceCache.h"
|
||||
|
||||
const NSUInteger kMaxCommandQueues = 5;
|
||||
static NSString* kKey_InUse = @"InUse";
|
||||
static NSString* kKey_CommandQueue = @"CommandQueue";
|
||||
|
||||
static MetalDeviceCache* gDeviceCache = nil;
|
||||
|
||||
@interface MetalDeviceCacheItem : NSObject
|
||||
|
||||
@property (readonly) id<MTLDevice> gpuDevice;
|
||||
@property (readonly) id<MTLRenderPipelineState> pipelineState;
|
||||
@property (retain) NSMutableArray<NSMutableDictionary*>* commandQueueCache;
|
||||
@property (readonly) NSLock* commandQueueCacheLock;
|
||||
@property (readonly) MTLPixelFormat pixelFormat;
|
||||
|
||||
- (instancetype)initWithDevice:(id<MTLDevice>)device
|
||||
pixelFormat:(MTLPixelFormat)pixFormat;
|
||||
- (id<MTLCommandQueue>)getNextFreeCommandQueue;
|
||||
- (void)returnCommandQueue:(id<MTLCommandQueue>)commandQueue;
|
||||
- (BOOL)containsCommandQueue:(id<MTLCommandQueue>)commandQueue;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MetalDeviceCacheItem
|
||||
|
||||
- (instancetype)initWithDevice:(id<MTLDevice>)device
|
||||
pixelFormat:(MTLPixelFormat)pixFormat;
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self != nil)
|
||||
{
|
||||
_gpuDevice = [device retain];
|
||||
|
||||
_commandQueueCache = [[NSMutableArray alloc] initWithCapacity:kMaxCommandQueues];
|
||||
for (NSUInteger i = 0; (_commandQueueCache != nil) && (i < kMaxCommandQueues); i++)
|
||||
{
|
||||
NSMutableDictionary* commandDict = [NSMutableDictionary dictionary];
|
||||
[commandDict setObject:[NSNumber numberWithBool:NO]
|
||||
forKey:kKey_InUse];
|
||||
|
||||
id<MTLCommandQueue> commandQueue = [_gpuDevice newCommandQueue];
|
||||
[commandDict setObject:commandQueue
|
||||
forKey:kKey_CommandQueue];
|
||||
|
||||
[_commandQueueCache addObject:commandDict];
|
||||
}
|
||||
|
||||
// Load all the shader files with a .metal file extension in the project
|
||||
id<MTLLibrary> defaultLibrary = [[_gpuDevice newDefaultLibrary] autorelease];
|
||||
|
||||
// Load the vertex function from the library
|
||||
id<MTLFunction> vertexFunction = [[defaultLibrary newFunctionWithName:@"vertexShader"] autorelease];
|
||||
|
||||
// Load the fragment function from the library
|
||||
id<MTLFunction> fragmentFunction = [[defaultLibrary newFunctionWithName:@"fragmentShader"] autorelease];
|
||||
|
||||
// Configure a pipeline descriptor that is used to create a pipeline state
|
||||
MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[[MTLRenderPipelineDescriptor alloc] init] autorelease];
|
||||
pipelineStateDescriptor.label = @"Simple Pipeline";
|
||||
pipelineStateDescriptor.vertexFunction = vertexFunction;
|
||||
pipelineStateDescriptor.fragmentFunction = fragmentFunction;
|
||||
pipelineStateDescriptor.colorAttachments[0].pixelFormat = pixFormat;
|
||||
_pixelFormat = pixFormat;
|
||||
|
||||
NSError* error = nil;
|
||||
_pipelineState = [_gpuDevice newRenderPipelineStateWithDescriptor:pipelineStateDescriptor
|
||||
error:&error];
|
||||
if (error != nil)
|
||||
{
|
||||
NSLog (@"Error generating brightness pipeline state: %@", error);
|
||||
}
|
||||
|
||||
if (_commandQueueCache != nil)
|
||||
{
|
||||
_commandQueueCacheLock = [[NSLock alloc] init];
|
||||
}
|
||||
|
||||
if ((_gpuDevice == nil) || (_commandQueueCache == nil) || (_commandQueueCacheLock == nil) ||
|
||||
(_pipelineState == nil))
|
||||
{
|
||||
[self release];
|
||||
self = nil;
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[_gpuDevice release];
|
||||
[_commandQueueCache release];
|
||||
[_commandQueueCacheLock release];
|
||||
[_pipelineState release];
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (id<MTLCommandQueue>)getNextFreeCommandQueue
|
||||
{
|
||||
id<MTLCommandQueue> result = nil;
|
||||
|
||||
[_commandQueueCacheLock lock];
|
||||
NSUInteger index = 0;
|
||||
while ((result == nil) && (index < kMaxCommandQueues))
|
||||
{
|
||||
NSMutableDictionary* nextCommandQueue = [_commandQueueCache objectAtIndex:index];
|
||||
NSNumber* inUse = [nextCommandQueue objectForKey:kKey_InUse];
|
||||
if (![inUse boolValue])
|
||||
{
|
||||
[nextCommandQueue setObject:[NSNumber numberWithBool:YES]
|
||||
forKey:kKey_InUse];
|
||||
result = [nextCommandQueue objectForKey:kKey_CommandQueue];
|
||||
}
|
||||
index++;
|
||||
}
|
||||
[_commandQueueCacheLock unlock];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)returnCommandQueue:(id<MTLCommandQueue>)commandQueue
|
||||
{
|
||||
[_commandQueueCacheLock lock];
|
||||
|
||||
BOOL found = false;
|
||||
NSUInteger index = 0;
|
||||
while ((!found) && (index < kMaxCommandQueues))
|
||||
{
|
||||
NSMutableDictionary* nextCommandQueuDict = [_commandQueueCache objectAtIndex:index];
|
||||
id<MTLCommandQueue> nextCommandQueue = [nextCommandQueuDict objectForKey:kKey_CommandQueue];
|
||||
if (nextCommandQueue == commandQueue)
|
||||
{
|
||||
found = YES;
|
||||
[nextCommandQueuDict setObject:[NSNumber numberWithBool:NO]
|
||||
forKey:kKey_InUse];
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
[_commandQueueCacheLock unlock];
|
||||
}
|
||||
|
||||
- (BOOL)containsCommandQueue:(id<MTLCommandQueue>)commandQueue
|
||||
{
|
||||
BOOL found = NO;
|
||||
NSUInteger index = 0;
|
||||
while ((!found) && (index < kMaxCommandQueues))
|
||||
{
|
||||
NSMutableDictionary* nextCommandQueuDict = [_commandQueueCache objectAtIndex:index];
|
||||
id<MTLCommandQueue> nextCommandQueue = [nextCommandQueuDict objectForKey:kKey_CommandQueue];
|
||||
if (nextCommandQueue == commandQueue)
|
||||
{
|
||||
found = YES;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MetalDeviceCache
|
||||
|
||||
+ (MTLPixelFormat)MTLPixelFormatForImageTile:(FxImageTile*)imageTile
|
||||
{
|
||||
MTLPixelFormat result = MTLPixelFormatRGBA16Float;
|
||||
|
||||
switch (imageTile.ioSurface.pixelFormat)
|
||||
{
|
||||
case kCVPixelFormatType_128RGBAFloat:
|
||||
result = MTLPixelFormatRGBA32Float;
|
||||
break;
|
||||
|
||||
case kCVPixelFormatType_32BGRA:
|
||||
result = MTLPixelFormatBGRA8Unorm;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (MetalDeviceCache*)deviceCache;
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
gDeviceCache = [[MetalDeviceCache alloc] init];
|
||||
});
|
||||
|
||||
return gDeviceCache;
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self != nil)
|
||||
{
|
||||
NSArray<id<MTLDevice>>* devices = MTLCopyAllDevices();
|
||||
|
||||
deviceCaches = [[NSMutableArray alloc] initWithCapacity:devices.count];
|
||||
|
||||
for (id<MTLDevice> nextDevice in devices)
|
||||
{
|
||||
MetalDeviceCacheItem* newCacheItem = [[[MetalDeviceCacheItem alloc] initWithDevice:nextDevice
|
||||
pixelFormat:MTLPixelFormatRGBA16Float]
|
||||
autorelease];
|
||||
[deviceCaches addObject:newCacheItem];
|
||||
}
|
||||
|
||||
[devices release];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[deviceCaches release];
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (id<MTLDevice>)deviceWithRegistryID:(uint64_t)registryID
|
||||
{
|
||||
for (MetalDeviceCacheItem* nextCacheItem in deviceCaches)
|
||||
{
|
||||
if (nextCacheItem.gpuDevice.registryID == registryID)
|
||||
{
|
||||
return nextCacheItem.gpuDevice;
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (id<MTLRenderPipelineState>)pipelineStateWithRegistryID:(uint64_t)registryID
|
||||
pixelFormat:(MTLPixelFormat)pixFormat
|
||||
{
|
||||
for (MetalDeviceCacheItem* nextCacheItem in deviceCaches)
|
||||
{
|
||||
if ((nextCacheItem.gpuDevice.registryID == registryID) &&
|
||||
(nextCacheItem.pixelFormat == pixFormat))
|
||||
{
|
||||
return nextCacheItem.pipelineState;
|
||||
}
|
||||
}
|
||||
// Didn't find one, so create one with the right settings
|
||||
NSArray<id<MTLDevice>>* devices = MTLCopyAllDevices();
|
||||
id<MTLDevice> device = nil;
|
||||
for (id<MTLDevice> nextDevice in devices)
|
||||
{
|
||||
if (nextDevice.registryID == registryID)
|
||||
{
|
||||
device = nextDevice;
|
||||
}
|
||||
}
|
||||
|
||||
id<MTLRenderPipelineState> result = nil;
|
||||
if (device != nil)
|
||||
{
|
||||
MetalDeviceCacheItem* newCacheItem = [[[MetalDeviceCacheItem alloc] initWithDevice:device
|
||||
pixelFormat:pixFormat]
|
||||
autorelease];
|
||||
if (newCacheItem != nil)
|
||||
{
|
||||
[deviceCaches addObject:newCacheItem];
|
||||
result = newCacheItem.pipelineState;
|
||||
}
|
||||
}
|
||||
[devices release];
|
||||
return result;
|
||||
}
|
||||
|
||||
- (id<MTLCommandQueue>)commandQueueWithRegistryID:(uint64_t)registryID
|
||||
pixelFormat:(MTLPixelFormat)pixFormat;
|
||||
{
|
||||
for (MetalDeviceCacheItem* nextCacheItem in deviceCaches)
|
||||
{
|
||||
if ((nextCacheItem.gpuDevice.registryID == registryID) &&
|
||||
(nextCacheItem.pixelFormat == pixFormat))
|
||||
{
|
||||
return [nextCacheItem getNextFreeCommandQueue];
|
||||
}
|
||||
}
|
||||
|
||||
// Didn't find one, so create one with the right settings
|
||||
NSArray<id<MTLDevice>>* devices = MTLCopyAllDevices();
|
||||
id<MTLDevice> device = nil;
|
||||
for (id<MTLDevice> nextDevice in devices)
|
||||
{
|
||||
if (nextDevice.registryID == registryID)
|
||||
{
|
||||
device = nextDevice;
|
||||
}
|
||||
}
|
||||
|
||||
id<MTLCommandQueue> result = nil;
|
||||
if (device != nil)
|
||||
{
|
||||
MetalDeviceCacheItem* newCacheItem = [[[MetalDeviceCacheItem alloc] initWithDevice:device
|
||||
pixelFormat:pixFormat]
|
||||
autorelease];
|
||||
if (newCacheItem != nil)
|
||||
{
|
||||
[deviceCaches addObject:newCacheItem];
|
||||
result = [newCacheItem getNextFreeCommandQueue];
|
||||
}
|
||||
}
|
||||
[devices release];
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)returnCommandQueueToCache:(id<MTLCommandQueue>)commandQueue;
|
||||
{
|
||||
for (MetalDeviceCacheItem* nextCacheItem in deviceCaches)
|
||||
{
|
||||
if ([nextCacheItem containsCommandQueue:commandQueue])
|
||||
{
|
||||
[nextCacheItem returnCommandQueue:commandQueue];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,27 @@
|
||||
#import <CoreImage/CoreImage.h>
|
||||
#import "PGBeautyTypes.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// Stateless, lazy Core Image graph. All masks use red=coverage and image coordinates.
|
||||
/// Caller owns face detection, geometric warping, tracking, and the CIContext/Metal device.
|
||||
/// showMask enables debugChannel 0=skin, 1=teeth, 2=eyes. invertMask affects each preview.
|
||||
/// If the caller blends geometry plus beauty against the original, pass settings.mix=1.
|
||||
@interface PGBeautyRenderer : NSObject
|
||||
- (nullable CIImage *)processImage:(CIImage *)image
|
||||
settings:(PGBeautySettings)settings
|
||||
skinMask:(nullable CIImage *)skinMask
|
||||
teethMask:(nullable CIImage *)teethMask
|
||||
eyeMask:(nullable CIImage *)eyeMask
|
||||
error:(NSError * _Nullable * _Nullable)error;
|
||||
/// Face-based callers supply uncut coverage for smoothly blended color changes.
|
||||
- (nullable CIImage *)processImage:(CIImage *)image
|
||||
settings:(PGBeautySettings)settings
|
||||
skinMask:(nullable CIImage *)skinMask
|
||||
colorMask:(nullable CIImage *)colorMask
|
||||
teethMask:(nullable CIImage *)teethMask
|
||||
eyeMask:(nullable CIImage *)eyeMask
|
||||
error:(NSError * _Nullable * _Nullable)error;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,266 @@
|
||||
#import "PGBeautyRenderer.h"
|
||||
#import <math.h>
|
||||
|
||||
// Original implementation reconstructed from observed processing stages, not vendor
|
||||
// resources. The learned segmentation model and LUTs are deliberately not embedded.
|
||||
static float PGClamp(float v, float lo, float hi) { return isfinite(v) ? fmaxf(lo, fminf(hi, v)) : lo; }
|
||||
static CIImage *PGBlur(CIImage *image, CGFloat radius, CGRect extent) {
|
||||
return [[[image imageByClampingToExtent] imageByApplyingFilter:@"CIGaussianBlur"
|
||||
withInputParameters:@{kCIInputRadiusKey:@(radius)}] imageByCroppingToRect:extent];
|
||||
}
|
||||
static CIColorKernel *PGKernel(NSString *body) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
return [CIColorKernel kernelWithString:body];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
static CIImage *PGApply(CIColorKernel *kernel, CGRect extent, NSArray *arguments) {
|
||||
return [kernel applyWithExtent:extent arguments:arguments];
|
||||
}
|
||||
static NSDictionary<NSString *, CIColorKernel *> *PGKernels(void) {
|
||||
static NSDictionary *kernels;
|
||||
static dispatch_once_t once;
|
||||
dispatch_once(&once, ^{
|
||||
NSMutableDictionary *k = [NSMutableDictionary dictionary];
|
||||
// The fallback mask observes the shipped G/B overlay > Gaussian high-pass >
|
||||
// repeated hard-light stages. Semantic skin support is independently estimated.
|
||||
k[@"overlay"] = PGKernel(@
|
||||
"kernel vec4 pgGB(__sample p) { vec4 q=unpremultiply(p);"
|
||||
"float g=clamp(q.g*0.5,0.0,1.0); float b=clamp(q.b*0.5,0.0,1.0);"
|
||||
"float v=2.0*g*b; return vec4(v,v,v,1.0); }");
|
||||
k[@"candidate"] = PGKernel(@
|
||||
"kernel vec4 pgCandidate(__sample p,__sample g,__sample b) {"
|
||||
"vec3 c=clamp(unpremultiply(p).rgb,0.0,1.0);"
|
||||
"float y=dot(c,vec3(0.299,0.587,0.114));"
|
||||
"float cb=(c.b-y)*0.564+0.5; float cr=(c.r-y)*0.713+0.5;"
|
||||
"float chroma=exp(-pow((cb-0.415)/0.115,2.0)-pow((cr-0.565)/0.135,2.0));"
|
||||
"float h=clamp(g.r-b.r+0.5,0.0,1.0);"
|
||||
"h=mix(2.0*h*h,1.0-2.0*(1.0-h)*(1.0-h),step(0.5,h));"
|
||||
"h=mix(2.0*h*h,1.0-2.0*(1.0-h)*(1.0-h),step(0.5,h));"
|
||||
"h=mix(2.0*h*h,1.0-2.0*(1.0-h)*(1.0-h),step(0.5,h));"
|
||||
"float band=clamp((h-0.29411765)*2.86516854,0.0,1.0);"
|
||||
"float edges=1.0-smoothstep(0.06,0.26,abs(band-0.59));"
|
||||
"float value=clamp(chroma*smoothstep(0.025,0.15,y)*(0.8+0.2*edges),0.0,1.0);"
|
||||
"return vec4(value,value,value,1.0); }");
|
||||
k[@"mask"] = PGKernel(@
|
||||
"kernel vec4 pgMask(__sample raw,__sample feather,__sample p,__sample b,__sample eyes,__sample teeth,"
|
||||
"float threshold,float softness,float edge,float protection,float invert) {"
|
||||
"float m=clamp(mix(raw.r,feather.r,softness),0.0,1.0);"
|
||||
"float width=0.025+0.24*softness;"
|
||||
"m=smoothstep(max(0.0,threshold-width),min(1.0,threshold+width),m);"
|
||||
"m=mix(m,1.0-m,invert);"
|
||||
"vec3 c=unpremultiply(p).rgb; vec3 l=unpremultiply(b).rgb;"
|
||||
"float structure=length(c-l);"
|
||||
"m*=1.0-edge*smoothstep(0.025,0.19,structure);"
|
||||
"float features=clamp(max(eyes.r,teeth.r),0.0,1.0);"
|
||||
"m*=1.0-protection*features;"
|
||||
"return vec4(m,m,m,1.0); }");
|
||||
// Independent frequency separation: texture and structure bands are compressed
|
||||
// in luminance, with feature/edge attenuation, keeping chroma and alpha stable.
|
||||
k[@"detail"] = PGKernel(@
|
||||
"kernel vec4 pgDetail(__sample p,__sample fine,__sample broad,float strength,float protect) {"
|
||||
"vec4 src=unpremultiply(p); vec3 c=src.rgb;"
|
||||
"vec3 t=unpremultiply(fine).rgb; vec3 b=unpremultiply(broad).rgb;"
|
||||
"vec3 w=vec3(0.299,0.587,0.114);"
|
||||
"float y=dot(c,w); float yf=dot(t,w); float yb=dot(b,w);"
|
||||
"float f=y-yf; float mid=yf-yb; float structure=length(c-b);"
|
||||
"float middle=smoothstep(0.04,0.22,y)*(1.0-smoothstep(0.82,0.98,y));"
|
||||
"float texture=smoothstep(0.001,0.010,abs(f));"
|
||||
"float edge=1.0-smoothstep(0.08,0.26,structure);"
|
||||
"float dark=(1.0-smoothstep(0.16,0.42,y))*smoothstep(0.0015,0.018,abs(f)+0.35*abs(y-yb));"
|
||||
"float light=smoothstep(0.70,0.96,y)*smoothstep(0.0025,0.024,abs(f));"
|
||||
"float ridge=smoothstep(0.10,0.30,structure)*smoothstep(0.002,0.020,abs(f)+0.85*abs(y-yb));"
|
||||
"float feature=max(dark*0.96,max(light*0.52,ridge*0.84));"
|
||||
"float gain=clamp(strength*middle*mix(0.56,0.88,texture),0.0,1.05);"
|
||||
"gain*=1.0-protect*feature*mix(0.42,0.82,texture);"
|
||||
"float delta=-gain*(f*mix(0.18,0.76,edge)+mid*mix(0.08,0.30,edge));"
|
||||
"float target=clamp(y+delta,0.0,1.0); float ratio=clamp(target/max(y,0.001),0.82,1.18);"
|
||||
"vec3 outColor=c*mix(1.0,ratio,0.88);"
|
||||
"outColor+=vec3((target-dot(outColor,w))*0.08);"
|
||||
"return premultiply(vec4(clamp(outColor,0.0,1.0),src.a)); }");
|
||||
k[@"maskedMix"] = PGKernel(@
|
||||
"kernel vec4 pgMaskedMix(__sample source,__sample effect,__sample mask,float amount) {"
|
||||
"vec4 s=unpremultiply(source); vec4 e=unpremultiply(effect);"
|
||||
"return premultiply(vec4(mix(s.rgb,e.rgb,clamp(mask.r*amount,0.0,1.0)),s.a)); }");
|
||||
k[@"colorCoverage"] = PGKernel(@
|
||||
"kernel vec4 pgColorCoverage(__sample coverage,__sample image,float invert) {"
|
||||
"vec3 c=unpremultiply(image).rgb; float y=dot(c,vec3(0.299,0.587,0.114));"
|
||||
"float cb=(c.b-y)*0.564+0.5; float cr=(c.r-y)*0.713+0.5;"
|
||||
"float likelihood=exp(-pow((cb-0.415)/0.115,2.0)-pow((cr-0.565)/0.135,2.0));"
|
||||
"float v=clamp(coverage.r,0.0,1.0)*smoothstep(0.05,0.60,likelihood);"
|
||||
"v=mix(v,1.0-v,invert); return vec4(v,v,v,1.0); }");
|
||||
k[@"even"] = PGKernel(@
|
||||
"kernel vec4 pgEven(__sample source,__sample broad,float amount) {"
|
||||
"vec4 s=unpremultiply(source); vec3 b=unpremultiply(broad).rgb;"
|
||||
"vec3 w=vec3(0.299,0.587,0.114); float y=dot(s.rgb,w); float yb=dot(b,w);"
|
||||
"vec3 baseChroma=s.rgb-vec3(y); vec3 smoothChroma=b-vec3(yb);"
|
||||
"float guard=1.0-smoothstep(0.12,0.35,length(s.rgb-b));"
|
||||
"vec3 c=vec3(y)+mix(baseChroma,smoothChroma,amount*0.78*guard);"
|
||||
"return premultiply(vec4(clamp(c,0.0,1.0),s.a)); }");
|
||||
// These six looks are newly authored color transforms, not the shipped LUTs.
|
||||
k[@"preset"] = PGKernel(@
|
||||
"kernel vec4 pgPreset(__sample source,float preset) {"
|
||||
"vec4 s=unpremultiply(source); vec3 c=s.rgb; float y=dot(c,vec3(0.299,0.587,0.114));"
|
||||
"if(preset<1.5){c=mix(vec3(y),c,0.91);c+=vec3(0.038,0.027,0.012)*(1.0-y);}"
|
||||
"else if(preset<2.5){c=mix(vec3(y),c,0.89);c+=vec3(0.003,0.020,0.038)*(1.0-y);}"
|
||||
"else if(preset<3.5){c=mix(vec3(y),c,1.04);c+=vec3(0.037,0.011,-0.018)*(1.0-y);}"
|
||||
"else if(preset<4.5){c=mix(vec3(y),c,0.94);c+=vec3(0.032,0.001,0.018)*(1.0-y);}"
|
||||
"else if(preset<5.5){c=pow(max(c,vec3(0.0)),vec3(1.08));c+=vec3(0.024,0.003,-0.018)*(1.0-y);}"
|
||||
"else{c=pow(max(c,vec3(0.0)),vec3(1.20));c=mix(vec3(dot(c,vec3(0.299,0.587,0.114))),c,1.06);"
|
||||
"c+=vec3(0.030,0.007,-0.022)*(1.0-y);}"
|
||||
"return premultiply(vec4(clamp(c,0.0,1.0),s.a)); }");
|
||||
k[@"teeth"] = PGKernel(@
|
||||
"kernel vec4 pgTeeth(__sample source,__sample mask,float amount) {"
|
||||
"vec4 s=unpremultiply(source);vec3 c=clamp(s.rgb,0.0,1.0);"
|
||||
"float y=dot(c,vec3(0.299,0.587,0.114));float hi=max(c.r,max(c.g,c.b));float lo=min(c.r,min(c.g,c.b));"
|
||||
"float sat=(hi-lo)/max(hi,0.0001);"
|
||||
"float gate=smoothstep(0.26,0.56,max(y,hi*0.86))*(1.0-smoothstep(0.52,0.86,sat));"
|
||||
"float weight=clamp(pow(max(0.0,smoothstep(0.08,0.92,mask.r)*gate*amount),0.90)*(1.02+0.18*amount),0.0,0.88);"
|
||||
"float yellow=max(0.0,(c.r+c.g)*0.5-c.b);"
|
||||
"vec3 neutral=mix(c,vec3(y),0.16+0.22*amount);"
|
||||
"neutral+=yellow*vec3(-0.070*amount,-0.028*amount,0.18+0.28*amount);"
|
||||
"neutral+=(vec3(1.0)-neutral)*(0.045+0.155*amount);"
|
||||
"return premultiply(vec4(mix(c,clamp(neutral,0.0,1.0),weight),s.a)); }");
|
||||
k[@"eyes"] = PGKernel(@
|
||||
"kernel vec4 pgEyes(__sample source,__sample mask,float amount) {"
|
||||
"vec4 s=unpremultiply(source);float y=dot(s.rgb,vec3(0.299,0.587,0.114));"
|
||||
"float weight=clamp(mask.r,0.0,1.0)*amount*smoothstep(0.08,0.32,y);"
|
||||
"vec3 c=s.rgb+(vec3(1.0)-s.rgb)*(0.18*weight);"
|
||||
"return premultiply(vec4(clamp(c,0.0,1.0),s.a)); }");
|
||||
k[@"debug"] = PGKernel(@
|
||||
"kernel vec4 pgDebug(__sample source,__sample mask,float invert) {"
|
||||
"vec4 s=unpremultiply(source);float m=clamp(mask.r,0.0,1.0);"
|
||||
"m=mix(m,1.0-m,invert);return premultiply(vec4(vec3(m),s.a)); }");
|
||||
kernels = [k copy];
|
||||
});
|
||||
return kernels;
|
||||
}
|
||||
|
||||
@implementation PGBeautyRenderer
|
||||
- (CIImage *)processImage:(CIImage *)image settings:(PGBeautySettings)s
|
||||
skinMask:(CIImage *)skinMask teethMask:(CIImage *)teethMask
|
||||
eyeMask:(CIImage *)eyeMask error:(NSError **)error {
|
||||
return [self processImage:image settings:s skinMask:skinMask colorMask:nil teethMask:teethMask eyeMask:eyeMask error:error];
|
||||
}
|
||||
- (CIImage *)processImage:(CIImage *)image settings:(PGBeautySettings)s
|
||||
skinMask:(CIImage *)skinMask colorMask:(CIImage *)colorMask
|
||||
teethMask:(CIImage *)teethMask eyeMask:(CIImage *)eyeMask error:(NSError **)error {
|
||||
if (error) *error = nil;
|
||||
CGRect extent = image.extent;
|
||||
if (!image || CGRectIsEmpty(extent) || CGRectIsInfinite(extent) ||
|
||||
!isfinite(extent.origin.x) || !isfinite(extent.origin.y) ||
|
||||
!isfinite(extent.size.width) || !isfinite(extent.size.height)) {
|
||||
if (error) *error = [NSError errorWithDomain:@"PGBeautyRenderer" code:1
|
||||
userInfo:@{NSLocalizedDescriptionKey:@"The source image needs a finite nonempty extent."}];
|
||||
return nil;
|
||||
}
|
||||
float mix = PGClamp(s.mix, 0, 1);
|
||||
if (mix == 0 && !s.showMask) return image;
|
||||
NSDictionary *k = PGKernels();
|
||||
if (k.count != 11) {
|
||||
if (error) *error = [NSError errorWithDomain:@"PGBeautyRenderer" code:2
|
||||
userInfo:@{NSLocalizedDescriptionKey:@"The beauty image kernels could not be compiled."}];
|
||||
return nil;
|
||||
}
|
||||
@try {
|
||||
float amount = PGClamp(s.amount,0,1);
|
||||
float local = PGClamp(s.midtoneDetail,0,2)*0.5f;
|
||||
float global = PGClamp(s.highPassDetail,0,1);
|
||||
float brightness = PGClamp(s.skinBrightness,0,1);
|
||||
float hue = PGClamp(s.skinHue,-30,30);
|
||||
float saturation = PGClamp(s.skinSaturation,-100,100);
|
||||
float even = PGClamp(s.skinEvenTone,0,1);
|
||||
float sharpness = PGClamp(s.sharpness,0,2);
|
||||
float teeth = PGClamp(s.teethWhitening,0,1);
|
||||
float eyes = PGClamp(s.eyeBrightening,0,1);
|
||||
BOOL needsSkin = local*amount>0 || brightness>0 || hue!=0 || saturation!=0 || even>0 ||
|
||||
(s.skinTonePreset>0 && s.skinTonePreset<=6) || (s.showMask && s.debugChannel==0);
|
||||
CIImage *black = [[CIImage imageWithColor:[CIColor colorWithRed:0 green:0 blue:0 alpha:1]] imageByCroppingToRect:extent];
|
||||
CIImage *teethInput = teethMask ? [teethMask imageByCroppingToRect:extent] : black;
|
||||
CIImage *eyeInput = eyeMask ? [eyeMask imageByCroppingToRect:extent] : black;
|
||||
CIImage *raw = skinMask ? [skinMask imageByCroppingToRect:extent] : black;
|
||||
CIImage *mask = black;
|
||||
CGFloat radius = PGClamp(s.radius,1,40);
|
||||
if (needsSkin) {
|
||||
if (!skinMask) {
|
||||
CIImage *gb = PGApply(k[@"overlay"],extent,@[image]);
|
||||
raw = PGApply(k[@"candidate"],extent,@[image,gb,PGBlur(gb,radius,extent)]);
|
||||
}
|
||||
float soft = PGClamp(s.maskSoftness,0,1);
|
||||
float threshold = PGClamp(s.maskThreshold,0,255)/255.f;
|
||||
mask = PGApply(k[@"mask"],extent,@[raw,PGBlur(raw,0.35+soft*radius*0.35,extent),
|
||||
image,PGBlur(image,1.5,extent),eyeInput,teethInput,
|
||||
@(threshold),@(soft),@(PGClamp(s.maskEdgeProtect,0,1)),
|
||||
@(s.featureProtection ? 1 : 0),@(s.invertMask ? 1 : 0)]);
|
||||
}
|
||||
if (s.showMask) {
|
||||
// Popup values select coverage source; changing the popup alone must
|
||||
// never enable a preview. Feature masks retain their supplied coverage.
|
||||
CIImage *preview = s.debugChannel==1 ? teethInput : (s.debugChannel==2 ? eyeInput : mask);
|
||||
float invert = s.debugChannel==1 || s.debugChannel==2 ? (s.invertMask ? 1 : 0) : 0;
|
||||
return PGApply(k[@"debug"],extent,@[image,preview,@(invert)]);
|
||||
}
|
||||
// Detail protection may contain small eye, brow, nose and lip cutouts.
|
||||
// Exposure and color must not reuse those holes or sharpen a face ellipse
|
||||
// with the detail threshold; that creates visible rings and islands.
|
||||
CIImage *toneMask=mask;
|
||||
BOOL changesColor=brightness>0 || hue!=0 || saturation!=0 || even>0 ||
|
||||
(s.skinTonePreset>0 && s.skinTonePreset<=6);
|
||||
if(colorMask && changesColor) {
|
||||
CGFloat feather=MAX(3,MIN(extent.size.width,extent.size.height)*0.03);
|
||||
CIImage *support=[colorMask imageByCroppingToRect:extent];
|
||||
toneMask=PGApply(k[@"colorCoverage"],extent,@[PGBlur(support,feather,extent),
|
||||
PGBlur(image,MAX(1,feather*0.15),extent),@(s.invertMask?1:0)]);
|
||||
}
|
||||
CIImage *result = image;
|
||||
if (local*amount>0) {
|
||||
// The shipped helper derives both blur scales from the detail strength.
|
||||
CGFloat fine = fmin(7.4,2.275+3.15*pow(local,0.72)+0.95*local);
|
||||
CGFloat broad = fmin(16,(2.55+0.45*local)*fine+1.1+1.3*local);
|
||||
float compression = 0.12+2.4*powf(local,2.15)+1.25*local;
|
||||
CIImage *detail = PGApply(k[@"detail"],extent,@[result,PGBlur(result,fine,extent),
|
||||
PGBlur(result,broad,extent),@(compression),@(s.featureProtection ? 1 : 0)]);
|
||||
result = PGApply(k[@"maskedMix"],extent,@[result,detail,mask,@(amount)]);
|
||||
}
|
||||
CIImage *color = result;
|
||||
if (brightness>0) {
|
||||
float shoulder = PGClamp((brightness-0.56f)/0.44f,0,1);
|
||||
float gain = 0.88f*powf(brightness,1.24f)+0.12f*shoulder*shoulder*(3-2*shoulder);
|
||||
color = [color imageByApplyingFilter:@"CIExposureAdjust" withInputParameters:@{kCIInputEVKey:@(gain*0.95f)}];
|
||||
}
|
||||
if (hue!=0) color = [color imageByApplyingFilter:@"CIHueAdjust" withInputParameters:@{kCIInputAngleKey:@(hue*M_PI/180.0)}];
|
||||
if (saturation!=0) color = [color imageByApplyingFilter:@"CIColorControls" withInputParameters:@{kCIInputSaturationKey:@(1+saturation/100.f)}];
|
||||
if (even>0) color = PGApply(k[@"even"],extent,@[color,PGBlur(color,radius*1.8,extent),@(even)]);
|
||||
if (color!=result) result = PGApply(k[@"maskedMix"],extent,@[result,color,toneMask,@1]);
|
||||
if (sharpness*amount>0) result = [result imageByApplyingFilter:@"CISharpenLuminance"
|
||||
withInputParameters:@{kCIInputSharpnessKey:@(sharpness*amount)}];
|
||||
if (s.skinTonePreset>0 && s.skinTonePreset<=6) {
|
||||
CIImage *preset = PGApply(k[@"preset"],extent,@[result,@(s.skinTonePreset)]);
|
||||
result = PGApply(k[@"maskedMix"],extent,@[result,preset,toneMask,@1]);
|
||||
}
|
||||
if (global>0) {
|
||||
CGFloat fine = fmin(7.4,2.275+3.15*pow(global,0.72)+0.95*global);
|
||||
CGFloat broad = fmin(16,(2.55+0.45*global)*fine+1.1+1.3*global);
|
||||
float compression = 0.12+2.4*powf(global,2.15)+1.25*global;
|
||||
result = PGApply(k[@"detail"],extent,@[result,PGBlur(result,fine,extent),
|
||||
PGBlur(result,broad,extent),@(compression),@0]);
|
||||
}
|
||||
if (teeth>0 && teethMask) result = PGApply(k[@"teeth"],extent,@[result,teethInput,@(teeth)]);
|
||||
if (eyes>0 && eyeMask) result = PGApply(k[@"eyes"],extent,@[result,eyeInput,@(eyes)]);
|
||||
if (mix<1) {
|
||||
CIImage *white = [[CIImage imageWithColor:[CIColor colorWithRed:1 green:1 blue:1 alpha:1]] imageByCroppingToRect:extent];
|
||||
result = PGApply(k[@"maskedMix"],extent,@[image,result,white,@(mix)]);
|
||||
}
|
||||
if (!result) {
|
||||
if (error) *error = [NSError errorWithDomain:@"PGBeautyRenderer" code:3
|
||||
userInfo:@{NSLocalizedDescriptionKey:@"Core Image could not construct the beauty render graph."}];
|
||||
return nil;
|
||||
}
|
||||
return [result imageByCroppingToRect:extent];
|
||||
} @catch (NSException *exception) {
|
||||
if (error) *error = [NSError errorWithDomain:@"PGBeautyRenderer" code:4
|
||||
userInfo:@{NSLocalizedDescriptionKey:exception.reason ?: @"Invalid Core Image render state."}];
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
@end
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
// Persisted FxPlug render state. Append fields and bump version for migrations.
|
||||
typedef struct {
|
||||
uint32_t version;
|
||||
float amount, skinBrightness, skinHue, skinSaturation, skinEvenTone;
|
||||
float midtoneDetail, highPassDetail, teethWhitening, eyeBrightening;
|
||||
float radius, sharpness, mix, maskThreshold, maskSoftness, maskEdgeProtect;
|
||||
float faceThin, faceBigEye, faceShort, faceNarrow, faceChin, faceV;
|
||||
float faceJaw, faceCheekbone, faceSmallHead, faceSmallHeadRange;
|
||||
float faceJumpGuardRange, selectionX, selectionY;
|
||||
uint32_t featureProtection, skinTonePreset, showMask, invertMask, debugChannel;
|
||||
uint32_t showFaceBoxes, showTrackBox, singleFace, fastMultiFace, trackingEnabled, targetIndex;
|
||||
} PGBeautySettings;
|
||||
|
||||
static inline PGBeautySettings PGBeautyDefaults(void) {
|
||||
PGBeautySettings s = {0};
|
||||
s.version = 1;
|
||||
s.amount = 1.0f;
|
||||
s.radius = 8.0f;
|
||||
s.mix = 1.0f;
|
||||
s.maskThreshold = 148.0f;
|
||||
s.maskSoftness = 0.6f;
|
||||
s.maskEdgeProtect = 0.4f;
|
||||
s.featureProtection = 1;
|
||||
s.faceJumpGuardRange = 1.8f;
|
||||
s.selectionX = s.selectionY = 0.5f;
|
||||
return s;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreImage/CoreImage.h>
|
||||
#import <CoreMedia/CoreMedia.h>
|
||||
#import <Vision/Vision.h>
|
||||
#import "PGBeautyTypes.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface PGFaceFrame : NSObject
|
||||
/// Vision observations in normalized, bottom-left image coordinates. Restored
|
||||
/// observations retain their bounding box; render geometry is stored separately.
|
||||
@property (nonatomic, copy, readonly) NSArray<VNFaceObservation *> *faces;
|
||||
/// Stable, normalized control geometry used for image and mask deformation.
|
||||
/// Default realtime processing deforms one primary face while faces/masks may
|
||||
/// cover all current detections. Explicit fast-multi mode deforms every face.
|
||||
@property (nonatomic, copy, readonly) NSArray<NSDictionary *> *deformationGeometry;
|
||||
@property (nonatomic, strong, readonly) CIImage *skinMask;
|
||||
/// Continuous face support before eye/nose/lip detail-protection cutouts.
|
||||
@property (nonatomic, strong, readonly) CIImage *colorMask;
|
||||
@property (nonatomic, strong, readonly) CIImage *teethMask;
|
||||
@property (nonatomic, strong, readonly) CIImage *eyeMask;
|
||||
@property (nonatomic, strong, readonly) CIImage *image;
|
||||
/// Transparent face/target outlines, for compositing after beauty processing.
|
||||
@property (nonatomic, strong, readonly) CIImage *overlayImage;
|
||||
@end
|
||||
|
||||
@interface PGFaceProcessor : NSObject
|
||||
- (nullable PGFaceFrame *)processImage:(CIImage *)image
|
||||
settings:(PGBeautySettings)settings
|
||||
time:(CMTime)time
|
||||
error:(NSError * _Nullable * _Nullable)error;
|
||||
- (BOOL)analyzeImage:(CIImage *)image
|
||||
settings:(PGBeautySettings)settings
|
||||
time:(CMTime)time
|
||||
error:(NSError * _Nullable * _Nullable)error;
|
||||
- (void)resetTracking;
|
||||
/// Targets are zero-based (0, 1, 2). Data is a versioned property-list archive,
|
||||
/// independent of Vision implementation classes. Only analysis writes these
|
||||
/// persisted frames; realtime geometry has a separate bounded, in-memory cache.
|
||||
- (nullable NSData *)trackingDataForTarget:(NSUInteger)target;
|
||||
- (void)setTrackingData:(nullable NSData *)data forTarget:(NSUInteger)target;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,704 @@
|
||||
// The source kernels remain available on the supported macOS 12+ range. This
|
||||
// keeps the processor self contained instead of requiring a separate metallib.
|
||||
#define CI_SILENCE_GL_DEPRECATION 1
|
||||
#import "PGFaceProcessor.h"
|
||||
#include <math.h>
|
||||
#include <float.h>
|
||||
|
||||
static NSString * const PGFaceErrorDomain = @"media.dayu.PaintFCP.Face";
|
||||
// Contour origin / internal anchor, in the reference's recovered pin order.
|
||||
// The unused middle entry of each reference triplet is intentionally omitted.
|
||||
static const uint8_t PGThinPins[17][2] = {
|
||||
{75,6},{59,13},{74,47},{60,47},{73,47},{61,47},
|
||||
{72,48},{62,48},{71,48},{63,48},{70,49},{64,49},
|
||||
{69,49},{65,49},{68,52},{66,52},{67,47}
|
||||
};
|
||||
static const uint8_t PGChinPins[3][2] = {{68,38},{67,37},{66,36}};
|
||||
|
||||
@interface PGFaceFrame ()
|
||||
@property (nonatomic, copy, readwrite) NSArray<VNFaceObservation *> *faces;
|
||||
@property (nonatomic, copy, readwrite) NSArray<NSDictionary *> *deformationGeometry;
|
||||
@property (nonatomic, strong, readwrite) CIImage *skinMask;
|
||||
@property (nonatomic, strong, readwrite) CIImage *colorMask;
|
||||
@property (nonatomic, strong, readwrite) CIImage *teethMask;
|
||||
@property (nonatomic, strong, readwrite) CIImage *eyeMask;
|
||||
@property (nonatomic, strong, readwrite) CIImage *image;
|
||||
@property (nonatomic, strong, readwrite) CIImage *overlayImage;
|
||||
@end
|
||||
@implementation PGFaceFrame
|
||||
@end
|
||||
|
||||
// All geometry in stored records is normalized to the full source image, with
|
||||
// Y increasing upward. Pixel size, render scale and tile order are not persisted.
|
||||
static NSString *PGTimeKey(CMTime time) {
|
||||
if (!CMTIME_IS_NUMERIC(time) || time.timescale <= 0) return nil;
|
||||
int64_t a = time.value, b = time.timescale;
|
||||
// Unsigned magnitude avoids overflow for the minimum signed integer.
|
||||
uint64_t u = a < 0 ? (uint64_t)(-(a + 1)) + 1 : (uint64_t)a;
|
||||
uint64_t v = (uint64_t)b;
|
||||
while (v) { uint64_t r = u % v; u = v; v = r; }
|
||||
int64_t divisor = (int64_t)(u ?: 1);
|
||||
return [NSString stringWithFormat:@"%lld/%lld/%lld", (long long)(a / divisor),
|
||||
(long long)(b / divisor), (long long)time.epoch];
|
||||
}
|
||||
|
||||
static float PGClamp(float value, float low, float high) {
|
||||
return isfinite(value) ? fminf(high, fmaxf(low, value)) : 0;
|
||||
}
|
||||
static NSArray *PGRectArray(CGRect rect) {
|
||||
return @[@(rect.origin.x), @(rect.origin.y), @(rect.size.width), @(rect.size.height)];
|
||||
}
|
||||
static CGRect PGArrayRect(NSArray *array) {
|
||||
if (![array isKindOfClass:NSArray.class] || array.count != 4) return CGRectZero;
|
||||
for (id value in array) if (![value isKindOfClass:NSNumber.class] || !isfinite([value doubleValue])) return CGRectZero;
|
||||
return CGRectMake([array[0] doubleValue], [array[1] doubleValue], [array[2] doubleValue], [array[3] doubleValue]);
|
||||
}
|
||||
static BOOL PGValidRect(CGRect rect) {
|
||||
return rect.size.width > 0.001 && rect.size.height > 0.001 &&
|
||||
rect.origin.x >= -0.1 && rect.origin.y >= -0.1 && CGRectGetMaxX(rect) <= 1.1 && CGRectGetMaxY(rect) <= 1.1;
|
||||
}
|
||||
static CGPoint PGCenter(CGRect rect) { return CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect)); }
|
||||
static NSArray *PGRegion(VNFaceLandmarkRegion2D *region, CGRect box) {
|
||||
if (!region || region.pointCount == 0) return @[];
|
||||
NSMutableArray *points = [NSMutableArray arrayWithCapacity:region.pointCount];
|
||||
const CGPoint *p = region.normalizedPoints;
|
||||
if (!p) return @[];
|
||||
for (NSUInteger i = 0; i < MIN(region.pointCount, 256); i++) {
|
||||
if (isfinite(p[i].x) && isfinite(p[i].y)) {
|
||||
[points addObject:@[@(box.origin.x + p[i].x * box.size.width),
|
||||
@(box.origin.y + p[i].y * box.size.height)]];
|
||||
}
|
||||
}
|
||||
return points;
|
||||
}
|
||||
// This optional field has a fixed topology. Never remove an invalid point and
|
||||
// shift the remaining indices: the local pins refer to exact Vision 76 indices.
|
||||
static NSArray *PGSanitizedPoints76(id value) {
|
||||
if (![value isKindOfClass:NSArray.class] || [value count] != 76) return nil;
|
||||
NSMutableArray *points = [NSMutableArray arrayWithCapacity:76];
|
||||
for (id point in value) {
|
||||
if (![point isKindOfClass:NSArray.class] || [point count] != 2) return nil;
|
||||
for (id coordinate in point) {
|
||||
if (![coordinate isKindOfClass:NSNumber.class] || !isfinite([coordinate doubleValue]) || fabs([coordinate doubleValue]) > 2) return nil;
|
||||
}
|
||||
[points addObject:[point copy]];
|
||||
}
|
||||
return [points copy];
|
||||
}
|
||||
static NSArray *PGFullPoints76(VNFaceLandmarkRegion2D *region, CGRect box) {
|
||||
if (region.pointCount != 76 || !region.normalizedPoints) return nil;
|
||||
const CGPoint *raw = region.normalizedPoints;
|
||||
NSMutableArray *points = [NSMutableArray arrayWithCapacity:76];
|
||||
for (NSUInteger i=0; i<76; i++) {
|
||||
if (!isfinite(raw[i].x) || !isfinite(raw[i].y)) return nil;
|
||||
[points addObject:@[@(box.origin.x+raw[i].x*box.size.width),
|
||||
@(box.origin.y+raw[i].y*box.size.height)]];
|
||||
}
|
||||
return PGSanitizedPoints76(points);
|
||||
}
|
||||
static NSDictionary *PGGeometry(VNFaceObservation *observation) {
|
||||
CGRect box = observation.boundingBox;
|
||||
VNFaceLandmarks2D *lm = observation.landmarks;
|
||||
NSMutableDictionary *geometry = [@{@"box": PGRectArray(box),
|
||||
@"leftEye": PGRegion(lm.leftEye, box), @"rightEye": PGRegion(lm.rightEye, box),
|
||||
@"leftBrow": PGRegion(lm.leftEyebrow, box), @"rightBrow": PGRegion(lm.rightEyebrow, box),
|
||||
@"nose": PGRegion(lm.nose, box), @"lips": PGRegion(lm.outerLips, box),
|
||||
@"mouth": PGRegion(lm.innerLips, box), @"contour": PGRegion(lm.faceContour, box)} mutableCopy];
|
||||
NSArray *points = PGFullPoints76(lm.allPoints, box);
|
||||
if (points) geometry[@"points76"] = points;
|
||||
return [geometry copy];
|
||||
}
|
||||
static NSDictionary *PGSanitizedGeometry(id value) {
|
||||
if (![value isKindOfClass:NSDictionary.class]) return nil;
|
||||
CGRect box = PGArrayRect(value[@"box"]);
|
||||
if (!PGValidRect(box)) return nil;
|
||||
NSMutableDictionary *result = [NSMutableDictionary dictionaryWithObject:PGRectArray(box) forKey:@"box"];
|
||||
for (NSString *name in @[@"leftEye", @"rightEye", @"leftBrow", @"rightBrow", @"nose", @"lips", @"mouth", @"contour"]) {
|
||||
id raw = value[name];
|
||||
if (![raw isKindOfClass:NSArray.class] || [raw count] > 256) return nil;
|
||||
NSMutableArray *points = [NSMutableArray array];
|
||||
for (id point in raw) {
|
||||
if (![point isKindOfClass:NSArray.class] || [point count] != 2) return nil;
|
||||
for (id coordinate in point) if (![coordinate isKindOfClass:NSNumber.class] || !isfinite([coordinate doubleValue]) || fabs([coordinate doubleValue]) > 2) return nil;
|
||||
[points addObject:[point copy]];
|
||||
}
|
||||
result[name] = points;
|
||||
}
|
||||
// Old archives and partially detected faces remain usable by the other
|
||||
// controls. Missing or malformed full topology disables local thin/chin pins.
|
||||
NSArray *points76 = PGSanitizedPoints76(value[@"points76"]);
|
||||
if (points76) result[@"points76"] = points76;
|
||||
return [result copy];
|
||||
}
|
||||
static CGRect PGRegionBounds(NSDictionary *geometry, NSString *name, CGRect fallback) {
|
||||
NSArray *points = geometry[name];
|
||||
if (points.count == 0) return fallback;
|
||||
CGFloat minX = 2, minY = 2, maxX = -1, maxY = -1;
|
||||
for (NSArray *point in points) {
|
||||
CGFloat x = [point[0] doubleValue], y = [point[1] doubleValue];
|
||||
minX = MIN(minX, x); minY = MIN(minY, y); maxX = MAX(maxX, x); maxY = MAX(maxY, y);
|
||||
}
|
||||
return CGRectMake(minX, minY, MAX(0.001, maxX - minX), MAX(0.001, maxY - minY));
|
||||
}
|
||||
static CGRect PGInFace(CGRect face, CGFloat x, CGFloat y, CGFloat w, CGFloat h) {
|
||||
return CGRectMake(face.origin.x + x * face.size.width, face.origin.y + y * face.size.height,
|
||||
w * face.size.width, h * face.size.height);
|
||||
}
|
||||
static CGRect PGToPixels(CGRect normalized, CGRect extent) {
|
||||
return CGRectMake(extent.origin.x + normalized.origin.x * extent.size.width,
|
||||
extent.origin.y + normalized.origin.y * extent.size.height,
|
||||
normalized.size.width * extent.size.width, normalized.size.height * extent.size.height);
|
||||
}
|
||||
static CIVector *PGVector(CGPoint point) { return [CIVector vectorWithX:point.x Y:point.y]; }
|
||||
static CIImage *PGBlack(CGRect extent) {
|
||||
return [[CIImage imageWithColor:[CIColor colorWithRed:0 green:0 blue:0 alpha:1]] imageByCroppingToRect:extent];
|
||||
}
|
||||
static CIImage *PGTransparent(CGRect extent) {
|
||||
return [[CIImage imageWithColor:[CIColor colorWithRed:0 green:0 blue:0 alpha:0]] imageByCroppingToRect:extent];
|
||||
}
|
||||
static CIImage *PGMaximum(CIImage *a, CIImage *b) {
|
||||
return [a imageByApplyingFilter:@"CIMaximumCompositing" withInputParameters:@{kCIInputBackgroundImageKey:b}];
|
||||
}
|
||||
|
||||
static CMTime PGRecordTime(NSDictionary *record) {
|
||||
return CMTimeMakeWithEpoch([record[@"value"] longLongValue], [record[@"scale"] intValue], [record[@"epoch"] longLongValue]);
|
||||
}
|
||||
static double PGSeedDistance(NSDictionary *record, PGBeautySettings settings) {
|
||||
return hypot([record[@"seedX"] doubleValue]-settings.selectionX, [record[@"seedY"] doubleValue]-settings.selectionY);
|
||||
}
|
||||
static NSArray<NSString *> *PGGeometryRegions(void) {
|
||||
return @[@"leftEye",@"rightEye",@"leftBrow",@"rightBrow",@"nose",@"lips",@"mouth",@"contour"];
|
||||
}
|
||||
static CGPoint PGRemapPoint(NSArray *point, CGRect old, CGRect current) {
|
||||
return CGPointMake(current.origin.x+([point[0] doubleValue]-old.origin.x)*current.size.width/old.size.width,
|
||||
current.origin.y+([point[1] doubleValue]-old.origin.y)*current.size.height/old.size.height);
|
||||
}
|
||||
static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previous, double dt, BOOL stabilizePoints76) {
|
||||
CGRect old=PGArrayRect(previous[@"box"]),box=PGArrayRect(current[@"box"]);
|
||||
if(!PGValidRect(old) || !PGValidRect(box))return current;
|
||||
// Independent stabilization: translate/scale old landmarks into the current
|
||||
// detection box first, so normal head motion does not accumulate box lag.
|
||||
double weight=.9*fmin(1,fmax(0,dt/.03));
|
||||
CGPoint oldLE=PGCenter(PGRegionBounds(previous,@"leftEye",PGInFace(old,.16,.53,.25,.15)));
|
||||
CGPoint oldRE=PGCenter(PGRegionBounds(previous,@"rightEye",PGInFace(old,.59,.53,.25,.15)));
|
||||
CGPoint newLE=PGCenter(PGRegionBounds(current,@"leftEye",PGInFace(box,.16,.53,.25,.15)));
|
||||
CGPoint newRE=PGCenter(PGRegionBounds(current,@"rightEye",PGInFace(box,.59,.53,.25,.15)));
|
||||
double ox=oldRE.x-oldLE.x,oy=oldRE.y-oldLE.y,nx=newRE.x-newLE.x,ny=newRE.y-newLE.y;
|
||||
double oldLength=hypot(ox,oy),newLength=hypot(nx,ny);
|
||||
BOOL axisHold=oldLength>MAX(.003,.055*MIN(old.size.height,box.size.height)) &&
|
||||
newLength>MAX(.003,.055*MIN(old.size.height,box.size.height)) &&
|
||||
(ox*nx+oy*ny)/(oldLength*newLength)<-.18;
|
||||
NSMutableDictionary *stable=[NSMutableDictionary dictionaryWithObject:current[@"box"] forKey:@"box"];
|
||||
for(NSString *name in PGGeometryRegions()) {
|
||||
NSArray *a=previous[name],*b=current[name];
|
||||
if(!a.count || (!axisHold && a.count!=b.count)) {stable[name]=b;continue;}
|
||||
NSMutableArray *points=[NSMutableArray arrayWithCapacity:a.count];
|
||||
for(NSUInteger i=0;i<a.count;i++) {
|
||||
CGPoint p=PGRemapPoint(a[i],old,box);
|
||||
if(!axisHold) {p.x+=( [b[i][0] doubleValue]-p.x)*weight;p.y+=([b[i][1] doubleValue]-p.y)*weight;}
|
||||
[points addObject:@[@(p.x),@(p.y)]];
|
||||
}
|
||||
stable[name]=[points copy];
|
||||
}
|
||||
NSArray *current76 = PGSanitizedPoints76(current[@"points76"]);
|
||||
NSArray *previous76 = PGSanitizedPoints76(previous[@"points76"]);
|
||||
if (current76) {
|
||||
if (stabilizePoints76 && previous76) {
|
||||
NSMutableArray *points = [NSMutableArray arrayWithCapacity:76];
|
||||
for (NSUInteger i=0; i<76; i++) {
|
||||
CGPoint p = PGRemapPoint(previous76[i], old, box);
|
||||
if (!axisHold) {
|
||||
p.x += ([current76[i][0] doubleValue]-p.x)*weight;
|
||||
p.y += ([current76[i][1] doubleValue]-p.y)*weight;
|
||||
}
|
||||
[points addObject:@[@(p.x),@(p.y)]];
|
||||
}
|
||||
stable[@"points76"] = [points copy];
|
||||
} else {
|
||||
// Reacquired detections and changes from/to incomplete topology do
|
||||
// not borrow a held face's old projected pose or invent missing points.
|
||||
stable[@"points76"] = current76;
|
||||
}
|
||||
}
|
||||
return [stable copy];
|
||||
}
|
||||
|
||||
// This live selector is deliberately independent of _targets. Its past-only
|
||||
// .35s reference window is a documented project policy, not a recovered exact
|
||||
// vendor formula. Continuous held outputs advance time, so this is not a hold
|
||||
// timeout. Locks protect state, not global render-order equivalence: a cold seek
|
||||
// cannot reconstruct frames the host has never supplied.
|
||||
@interface PGRealtimeFaceTracker : NSObject {
|
||||
NSLock *_mutex;
|
||||
NSMutableArray<NSDictionary *> *_entries;
|
||||
NSUInteger _estimatedBytes;
|
||||
}
|
||||
- (NSDictionary *)resultForGeometry:(NSArray<NSDictionary *> *)raw settings:(PGBeautySettings)settings time:(CMTime)time extent:(CGRect)extent;
|
||||
- (void)reset;
|
||||
- (NSDictionary *)cacheStatistics;
|
||||
@end
|
||||
@implementation PGRealtimeFaceTracker
|
||||
- (instancetype)init {if((self=[super init])){_mutex=[NSLock new];_entries=[NSMutableArray new];}return self;}
|
||||
- (void)reset {[_mutex lock];[_entries removeAllObjects];_estimatedBytes=0;[_mutex unlock];}
|
||||
- (NSDictionary *)cacheStatistics {[_mutex lock];NSDictionary *stats=@{@"frames":@(_entries.count),@"estimatedBytes":@(_estimatedBytes)};[_mutex unlock];return stats;}
|
||||
- (NSDictionary *)resultForGeometry:(NSArray<NSDictionary *> *)input settings:(PGBeautySettings)s time:(CMTime)time extent:(CGRect)extent {
|
||||
NSMutableArray *valid=[NSMutableArray new];
|
||||
for(id item in input) {NSDictionary *g=PGSanitizedGeometry(item);if(g && valid.count<16)[valid addObject:g];}
|
||||
NSArray *raw=[valid copy];
|
||||
NSUInteger mode=s.fastMultiFace?2:(s.singleFace?1:0);
|
||||
NSString *key=PGTimeKey(time);
|
||||
NSString *context=[NSString stringWithFormat:@"%lu/%u/%lld/%.17g/%.17g/%.17g/%.17g/%.9g",(unsigned long)mode,s.targetIndex,
|
||||
(long long)time.epoch,extent.origin.x,extent.origin.y,extent.size.width,extent.size.height,s.faceJumpGuardRange];
|
||||
[_mutex lock];
|
||||
NSDictionary *head=nil;
|
||||
for(NSDictionary *record in _entries) if([record[@"context"] isEqual:context] &&
|
||||
(!head || CMTimeCompare(PGRecordTime(record),PGRecordTime(head))>=0)) head=record;
|
||||
NSString *scope=@"automatic";
|
||||
double seedX=PGClamp(s.selectionX,0,1),seedY=PGClamp(s.selectionY,0,1);
|
||||
if(mode==1) {
|
||||
// A real point change (> .01) starts a new identity scope. Older render
|
||||
// snapshots may use their own past scope, but never replace a newer head.
|
||||
NSDictionary *seedRecord=head;
|
||||
if(seedRecord && CMTimeCompare(time,PGRecordTime(seedRecord))<0) {
|
||||
seedRecord=nil;
|
||||
for(NSDictionary *record in _entries) if([record[@"context"] isEqual:context] && PGSeedDistance(record,s)<=.01 &&
|
||||
CMTimeCompare(PGRecordTime(record),time)<=0 && (!seedRecord || CMTimeCompare(PGRecordTime(record),PGRecordTime(seedRecord))>=0))seedRecord=record;
|
||||
}
|
||||
if(seedRecord && PGSeedDistance(seedRecord,s)<=.01) {
|
||||
scope=seedRecord[@"scope"];seedX=[seedRecord[@"seedX"] doubleValue];seedY=[seedRecord[@"seedY"] doubleValue];
|
||||
} else scope=[NSString stringWithFormat:@"%@/%.9g/%.9g",key?:@"invalid",seedX,seedY];
|
||||
}
|
||||
NSDictionary *reference=nil;
|
||||
if(key) for(NSDictionary *record in _entries) {
|
||||
if(![record[@"context"] isEqual:context] || ![record[@"scope"] isEqual:scope])continue;
|
||||
if([record[@"key"] isEqual:key] && [record[@"raw"] isEqual:raw]) {[_mutex unlock];return record;}
|
||||
CMTime stored=PGRecordTime(record);
|
||||
// Same-time changed detections recompute from an earlier frame, never
|
||||
// feed an already smoothed result into another pass at that same time.
|
||||
if(CMTimeCompare(stored,time)>=0)continue;
|
||||
double dt=CMTimeGetSeconds(CMTimeSubtract(time,stored));
|
||||
if(dt<=.35 && (!reference || CMTimeCompare(stored,PGRecordTime(reference))>0))reference=record;
|
||||
}
|
||||
NSArray *prior=reference[@"output"];
|
||||
NSDictionary *previous=prior.count?prior.firstObject:nil;
|
||||
CGRect previousBox=PGArrayRect(previous[@"box"]);
|
||||
NSInteger index=-1;double best=-DBL_MAX;
|
||||
if(mode!=2) for(NSUInteger i=0;i<raw.count;i++) {
|
||||
CGRect box=PGArrayRect(raw[i][@"box"]);CGPoint center=PGCenter(box);double score;
|
||||
if(PGValidRect(previousBox)) {
|
||||
CGFloat factor=PGClamp(s.faceJumpGuardRange,1,4);
|
||||
CGRect guard=CGRectIntersection(CGRectMake(0,0,1,1),CGRectInset(previousBox,-previousBox.size.width*(factor-1)*.5,-previousBox.size.height*(factor-1)*.5));
|
||||
if(center.x<CGRectGetMinX(guard)||center.x>CGRectGetMaxX(guard)||center.y<CGRectGetMinY(guard)||center.y>CGRectGetMaxY(guard))continue;
|
||||
CGPoint p=PGCenter(previousBox);score=-((center.x-p.x)*(center.x-p.x)+(center.y-p.y)*(center.y-p.y));
|
||||
} else if(mode==1) score=-((center.x-seedX)*(center.x-seedX)+(center.y-seedY)*(center.y-seedY));
|
||||
else {CGRect clipped=CGRectIntersection(box,CGRectMake(0,0,1,1));CGPoint p=PGCenter(clipped);
|
||||
score=1.8*fmin(1,clipped.size.width*clipped.size.height)+1.1*(1-fmin(1,hypot(p.x-.5,p.y-.5)/.72));}
|
||||
if(index<0 || score>best) {index=(NSInteger)i;best=score;}
|
||||
}
|
||||
NSArray *output;BOOL held=NO;
|
||||
if(mode==2) output=raw;
|
||||
else if(index>=0) {
|
||||
NSDictionary *current=raw[(NSUInteger)index];
|
||||
double dt=reference?CMTimeGetSeconds(CMTimeSubtract(time,PGRecordTime(reference))):0;
|
||||
output=@[previous?PGStableGeometry(current,previous,dt,![reference[@"held"] boolValue]):current];
|
||||
} else {output=prior?:@[];held=output.count>0;}
|
||||
NSMutableDictionary *record=[@{@"context":context,@"scope":scope,@"key":key?:@"invalid",@"value":@(time.value),@"scale":@(time.timescale),@"epoch":@(time.epoch),
|
||||
@"seedX":@(seedX),@"seedY":@(seedY),@"raw":raw,@"output":output,@"index":@(index),@"held":@(held),@"referenceKey":reference[@"key"]?:@""} mutableCopy];
|
||||
if(key) {
|
||||
NSData *serialized=[NSPropertyListSerialization dataWithPropertyList:record format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil];
|
||||
NSUInteger bytes=serialized.length+512;
|
||||
record[@"bytes"]=@(bytes);
|
||||
// Replace changed raw geometry at the same timestamp rather than letting
|
||||
// callbacks grow history or make smoothing depend on redraw count.
|
||||
for(NSInteger i=(NSInteger)_entries.count-1;i>=0;i--) {
|
||||
NSDictionary *old=_entries[(NSUInteger)i];
|
||||
if([old[@"context"] isEqual:context] && [old[@"scope"] isEqual:scope] && [old[@"key"] isEqual:key]) {
|
||||
_estimatedBytes-=[old[@"bytes"] unsignedIntegerValue];[_entries removeObjectAtIndex:(NSUInteger)i];
|
||||
}
|
||||
}
|
||||
if(bytes<=16*1024*1024) {[_entries addObject:[record copy]];_estimatedBytes+=bytes;}
|
||||
while(_entries.count>1200 || _estimatedBytes>16*1024*1024) {
|
||||
_estimatedBytes-=[_entries.firstObject[@"bytes"] unsignedIntegerValue];[_entries removeObjectAtIndex:0];
|
||||
}
|
||||
}
|
||||
[_mutex unlock];return [record copy];
|
||||
}
|
||||
@end
|
||||
|
||||
@interface PGFaceProcessor () {
|
||||
NSLock *_lock;
|
||||
NSMutableArray<NSMutableDictionary<NSString *, NSDictionary *> *> *_targets;
|
||||
CIWarpKernel *_warp;
|
||||
CIColorKernel *_ellipse;
|
||||
CIColorKernel *_subtract;
|
||||
CIColorKernel *_box;
|
||||
PGRealtimeFaceTracker *_realtime;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation PGFaceProcessor
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_lock = [[NSLock alloc] init];
|
||||
_realtime = [PGRealtimeFaceTracker new];
|
||||
_targets = [NSMutableArray arrayWithObjects:[NSMutableDictionary dictionary], [NSMutableDictionary dictionary], [NSMutableDictionary dictionary], nil];
|
||||
// Local thinning and chin use the recovered 17/3-pin coordinate math.
|
||||
// The other seven controls retain this project's analytic approximation.
|
||||
// Chin follows those analytic body controls and precedes the eye warp;
|
||||
// this combined order is not a reconstruction of the full reference chain.
|
||||
// Each pin changes the sampling coordinate, with only one image resample.
|
||||
NSMutableString *source = [NSMutableString stringWithString:
|
||||
@"vec2 pgThinPin(vec2 q, vec4 control, float ratio) {"
|
||||
" vec2 direction=control.zw-control.xy; float radius=length(direction);"
|
||||
" if(radius<=0.0001) return q;"
|
||||
" float weight=clamp(1.0-length(q-control.xy)/radius,0.0,1.0);"
|
||||
" return clamp(q-direction*ratio*weight,vec2(0.0),vec2(1.0)); }"
|
||||
" kernel vec2 pgFaceWarp(vec2 c, vec2 size, vec2 le, vec2 re, vec2 er, float thinRatio, float bigEye, float shortFace, float narrow, float chinRatio, float vFace, float jaw, float cheek, float smallHead, float headRange, vec2 imageOrigin, vec2 imageSize"];
|
||||
for (NSUInteger i=0; i<17; i++) [source appendFormat:@", vec4 pin%lu",(unsigned long)i];
|
||||
for (NSUInteger i=0; i<3; i++) [source appendFormat:@", vec4 chinPin%lu",(unsigned long)i];
|
||||
[source appendString:@") { vec2 p=destCoord();"
|
||||
" if(thinRatio>0.000001) { vec2 q=(p-imageOrigin)/imageSize; q.y=1.0-q.y;"];
|
||||
for (NSUInteger i=0; i<17; i++) [source appendFormat:@" q=pgThinPin(q,pin%lu,thinRatio);",(unsigned long)i];
|
||||
[source appendString:@" q.y=1.0-q.y; p=imageOrigin+q*imageSize; }"
|
||||
" vec2 n = (p-c)/size;"
|
||||
" float body = 1.0-smoothstep(0.38,0.76,length(n*vec2(1.0,0.82)));"
|
||||
" float lower = (1.0-smoothstep(-0.12,0.27,n.y))*body;"
|
||||
" float sides = smoothstep(0.08,0.32,abs(n.x));"
|
||||
" float j=(n.y+0.23)/0.22; float jawBand = exp(-j*j)*body;"
|
||||
" float ch=(n.y-0.02)/0.19; float cheekBand = exp(-ch*ch)*body;"
|
||||
" float head = 1.0-smoothstep(0.38,0.75+0.32*headRange,length(n*vec2(0.9,0.65)));"
|
||||
" p += (p-c)*smallHead*0.18*head;"
|
||||
" p.x += size.x*n.x*(narrow*0.30*body + vFace*0.24*jawBand + jaw*0.20*jawBand + cheek*0.18*cheekBand)*sides;"
|
||||
" p.y += size.y*(-shortFace*0.16*lower - vFace*0.025*lower);"
|
||||
" if(abs(chinRatio)>0.000001) { vec2 q=(p-imageOrigin)/imageSize; q.y=1.0-q.y;"];
|
||||
for (NSUInteger i=0; i<3; i++) [source appendFormat:@" q=pgThinPin(q,chinPin%lu,chinRatio);",(unsigned long)i];
|
||||
[source appendString:@" q.y=1.0-q.y; p=imageOrigin+q*imageSize; }"
|
||||
" vec2 dl = (p-le)/er; float wl = 1.0-smoothstep(0.0,1.0,length(dl));"
|
||||
" p -= (p-le)*bigEye*0.33*wl*wl;"
|
||||
" vec2 dr = (p-re)/er; float wr = 1.0-smoothstep(0.0,1.0,length(dr));"
|
||||
" p -= (p-re)*bigEye*0.33*wr*wr; return p; }"];
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
_warp = [CIWarpKernel kernelWithString:source];
|
||||
_ellipse = [CIColorKernel kernelWithString:@"kernel vec4 pgEllipse(vec2 c, vec2 r, float inner) { float v=1.0-smoothstep(inner,1.0,length((destCoord()-c)/max(r,vec2(0.001)))); return vec4(v,v,v,1.0); }"];
|
||||
_subtract = [CIColorKernel kernelWithString:@"kernel vec4 pgSubtract(__sample a, __sample b) { float v=max(0.0,a.r-b.r); return vec4(v,v,v,1.0); }"];
|
||||
_box = [CIColorKernel kernelWithString:@"kernel vec4 pgBox(vec4 b, float width, vec4 color) { vec2 p=destCoord(); float inside=step(b.x,p.x)*step(b.y,p.y)*step(p.x,b.z)*step(p.y,b.w); float edge=min(min(abs(p.x-b.x),abs(p.x-b.z)),min(abs(p.y-b.y),abs(p.y-b.w))); float a=inside*(1.0-smoothstep(width,width+1.0,edge)); return color*a; }"];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSArray<VNFaceObservation *> *)observationsInImage:(CIImage *)image settings:(PGBeautySettings)settings error:(NSError **)error {
|
||||
CGRect extent = image.extent;
|
||||
CGFloat longest = MAX(extent.size.width, extent.size.height);
|
||||
CGFloat scale = MIN(1.0, (settings.fastMultiFace ? 720.0 : 1280.0) / longest);
|
||||
CIImage *prepared = [image imageByApplyingTransform:CGAffineTransformMakeTranslation(-extent.origin.x, -extent.origin.y)];
|
||||
prepared = [prepared imageByApplyingTransform:CGAffineTransformMakeScale(scale, scale)];
|
||||
VNDetectFaceLandmarksRequest *request = [[VNDetectFaceLandmarksRequest alloc] init];
|
||||
request.revision = VNDetectFaceLandmarksRequestRevision3;
|
||||
request.constellation = VNRequestFaceLandmarksConstellation76Points;
|
||||
// An image request handler is intentionally local to one invocation. A shared
|
||||
// sequence handler would make parallel / reverse render order observable.
|
||||
VNImageRequestHandler *handler = [[VNImageRequestHandler alloc] initWithCIImage:prepared options:@{}];
|
||||
if (![handler performRequests:@[request] error:error]) return nil;
|
||||
NSArray<VNFaceObservation *> *faces = request.results ?: @[];
|
||||
return [faces sortedArrayUsingComparator:^NSComparisonResult(VNFaceObservation *a, VNFaceObservation *b) {
|
||||
CGRect x = a.boundingBox, y = b.boundingBox;
|
||||
if (CGRectGetMidX(x) != CGRectGetMidX(y)) return CGRectGetMidX(x) < CGRectGetMidX(y) ? NSOrderedAscending : NSOrderedDescending;
|
||||
if (CGRectGetMidY(x) != CGRectGetMidY(y)) return CGRectGetMidY(x) < CGRectGetMidY(y) ? NSOrderedAscending : NSOrderedDescending;
|
||||
return x.size.width > y.size.width ? NSOrderedAscending : NSOrderedDescending;
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSDictionary *)recordForTime:(CMTime)time target:(NSUInteger)target nearest:(BOOL)nearest {
|
||||
NSString *key = PGTimeKey(time);
|
||||
if (!key || target >= 3) return nil;
|
||||
[_lock lock];
|
||||
NSDictionary *record = _targets[target][key];
|
||||
if (nearest && [record[@"faces"] count] == 0) record = nil;
|
||||
if (!record && nearest) {
|
||||
// Pick a deterministic analyzed identity, retaining the last valid face
|
||||
// across lost detections. Only an explicit reset/reselection reacquires
|
||||
// a different identity; render ordering never updates this anchor.
|
||||
double distance = DBL_MAX;
|
||||
CMTime selectedTime = kCMTimeInvalid;
|
||||
for (NSDictionary *candidate in _targets[target].allValues) {
|
||||
if ([candidate[@"faces"] count] == 0) continue;
|
||||
CMTime stored = CMTimeMakeWithEpoch([candidate[@"value"] longLongValue], [candidate[@"scale"] intValue], [candidate[@"epoch"] longLongValue]);
|
||||
if (stored.epoch != time.epoch) continue;
|
||||
double delta = fabs(CMTimeGetSeconds(CMTimeSubtract(stored, time)));
|
||||
if (delta < distance || (delta == distance && CMTIME_IS_VALID(selectedTime) && CMTimeCompare(stored, selectedTime) < 0)) {
|
||||
record = candidate; distance = delta; selectedTime = stored;
|
||||
}
|
||||
}
|
||||
}
|
||||
[_lock unlock];
|
||||
return record;
|
||||
}
|
||||
|
||||
- (NSArray<NSDictionary *> *)geometryForObservations:(NSArray<VNFaceObservation *> *)observations
|
||||
settings:(PGBeautySettings)settings previous:(NSDictionary *)previous {
|
||||
BOOL selectOne = settings.singleFace || settings.trackingEnabled;
|
||||
if (!selectOne) {
|
||||
NSMutableArray *all = [NSMutableArray array];
|
||||
for (VNFaceObservation *face in observations) if (PGValidRect(face.boundingBox) && all.count < 16) [all addObject:PGGeometry(face)];
|
||||
return all;
|
||||
}
|
||||
CGPoint selection = CGPointMake(PGClamp(settings.selectionX, 0, 1), PGClamp(settings.selectionY, 0, 1));
|
||||
NSArray *previousFaces = previous[@"faces"];
|
||||
CGRect prior = previousFaces.count ? PGArrayRect(previousFaces.firstObject[@"box"]) : CGRectZero;
|
||||
BOOL guard = settings.faceJumpGuardRange > 0 && PGValidRect(prior);
|
||||
CGPoint reference = guard ? PGCenter(prior) : selection;
|
||||
VNFaceObservation *best = nil;
|
||||
double bestScore = DBL_MAX;
|
||||
for (VNFaceObservation *face in observations) {
|
||||
CGRect box = face.boundingBox;
|
||||
if (!PGValidRect(box)) continue;
|
||||
CGPoint center = PGCenter(box);
|
||||
double dx = (center.x - reference.x), dy = (center.y - reference.y);
|
||||
if (guard) {
|
||||
double radius = MAX(prior.size.width, prior.size.height) * PGClamp(settings.faceJumpGuardRange, 0.1, 8);
|
||||
double ratio = box.size.width / prior.size.width;
|
||||
if (hypot(dx, dy) > radius || ratio < 0.4 || ratio > 2.5) continue;
|
||||
}
|
||||
double score = dx * dx + dy * dy;
|
||||
if (!guard && CGRectContainsPoint(box, selection)) score -= 2;
|
||||
if (score < bestScore) { best = face; bestScore = score; }
|
||||
}
|
||||
return best ? @[PGGeometry(best)] : @[];
|
||||
}
|
||||
|
||||
- (BOOL)analyzeImage:(CIImage *)image settings:(PGBeautySettings)settings time:(CMTime)time error:(NSError **)error {
|
||||
NSString *key = PGTimeKey(time);
|
||||
if (!image || !key || CGRectIsInfinite(image.extent) || CGRectIsEmpty(image.extent)) {
|
||||
if (error) *error = [NSError errorWithDomain:PGFaceErrorDomain code:1 userInfo:@{NSLocalizedDescriptionKey:@"Face analysis requires a finite image and valid frame time."}];
|
||||
return NO;
|
||||
}
|
||||
NSArray *observations = [self observationsInImage:image settings:settings error:error];
|
||||
if (!observations) return NO;
|
||||
NSUInteger target = MIN((NSUInteger)settings.targetIndex, 2);
|
||||
NSDictionary *prior = [self recordForTime:time target:target nearest:YES];
|
||||
NSArray *geometry = [self geometryForObservations:observations settings:settings previous:prior];
|
||||
NSDictionary *record = @{@"value":@(time.value), @"scale":@(time.timescale), @"epoch":@(time.epoch), @"faces":geometry};
|
||||
[_lock lock];
|
||||
_targets[target][key] = record;
|
||||
[_lock unlock];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (CIImage *)ellipseInRect:(CGRect)rect extent:(CGRect)extent inner:(CGFloat)inner {
|
||||
return [_ellipse applyWithExtent:extent arguments:@[PGVector(PGCenter(rect)), [CIVector vectorWithX:rect.size.width * 0.5 Y:rect.size.height * 0.5], @(inner)]];
|
||||
}
|
||||
|
||||
- (CIImage *)warpImage:(CIImage *)image geometry:(NSArray<NSDictionary *> *)geometry settings:(PGBeautySettings)s extent:(CGRect)extent {
|
||||
if (!(s.faceThin || s.faceBigEye || s.faceShort || s.faceNarrow || s.faceChin || s.faceV || s.faceJaw || s.faceCheekbone || s.faceSmallHead)) return [image imageByCroppingToRect:extent];
|
||||
CIImage *result = image;
|
||||
for (NSDictionary *face in geometry) {
|
||||
CGRect box = PGArrayRect(face[@"box"]), pixels = PGToPixels(box, extent);
|
||||
NSArray *points76 = PGSanitizedPoints76(face[@"points76"]);
|
||||
float thinRatio = 0, chinRatio = 0;
|
||||
if (points76) {
|
||||
double width = hypot(([points76[75][0] doubleValue]-[points76[59][0] doubleValue])*extent.size.width,
|
||||
([points76[75][1] doubleValue]-[points76[59][1] doubleValue])*extent.size.height);
|
||||
double midX = ([points76[74][0] doubleValue]+[points76[60][0] doubleValue])*.5;
|
||||
double midY = ([points76[74][1] doubleValue]+[points76[60][1] doubleValue])*.5;
|
||||
double height = 2*hypot(([points76[67][0] doubleValue]-midX)*extent.size.width,
|
||||
([points76[67][1] doubleValue]-midY)*extent.size.height);
|
||||
double gain = fmin(2.25,fmax(1,280/fmax(1,fmax(width,height))));
|
||||
thinRatio = PGClamp(s.faceThin,0,1)*.025*gain;
|
||||
float chin = PGClamp(s.faceChin,-1,1);
|
||||
chinRatio = chin*(chin>0 ? .04 : .02)*gain;
|
||||
}
|
||||
BOOL otherControls = s.faceBigEye || s.faceShort || s.faceNarrow || s.faceV || s.faceJaw || s.faceCheekbone || s.faceSmallHead;
|
||||
// No full topology means no guessed bbox thin/chin deformation. Legacy
|
||||
// archives using only those controls remain exact image/mask pass-through.
|
||||
if (thinRatio <= .000001 && fabsf(chinRatio) <= .000001 && !otherControls) continue;
|
||||
CGRect le = PGRegionBounds(face, @"leftEye", PGInFace(box, 0.16, 0.53, 0.25, 0.15));
|
||||
CGRect re = PGRegionBounds(face, @"rightEye", PGInFace(box, 0.59, 0.53, 0.25, 0.15));
|
||||
CGPoint left = PGCenter(PGToPixels(le, extent)), right = PGCenter(PGToPixels(re, extent));
|
||||
NSMutableArray *arguments = [@[PGVector(PGCenter(pixels)), [CIVector vectorWithX:pixels.size.width Y:pixels.size.height],
|
||||
PGVector(left), PGVector(right), [CIVector vectorWithX:MAX(2, pixels.size.width * 0.24) Y:MAX(2, pixels.size.height * 0.17)],
|
||||
@(thinRatio), @(PGClamp(s.faceBigEye, 0, 1)), @(PGClamp(s.faceShort, -1, 1)),
|
||||
@(PGClamp(s.faceNarrow, -1, 1)), @(chinRatio), @(PGClamp(s.faceV, 0, 1)),
|
||||
@(PGClamp(s.faceJaw, 0, 1)), @(PGClamp(s.faceCheekbone, 0, 1)), @(PGClamp(s.faceSmallHead, 0, 1)), @(PGClamp(s.faceSmallHeadRange, 0, 1)),
|
||||
[CIVector vectorWithX:extent.origin.x Y:extent.origin.y], [CIVector vectorWithX:extent.size.width Y:extent.size.height]] mutableCopy];
|
||||
for (NSUInteger i=0; i<17; i++) {
|
||||
double ox=0,oy=0,ax=0,ay=0;
|
||||
if (points76) {
|
||||
NSArray *origin=points76[PGThinPins[i][0]],*anchor=points76[PGThinPins[i][1]];
|
||||
ox=[origin[0] doubleValue];oy=1-[origin[1] doubleValue];
|
||||
ax=[anchor[0] doubleValue];ay=1-[anchor[1] doubleValue];
|
||||
}
|
||||
[arguments addObject:[CIVector vectorWithX:ox Y:oy Z:ax W:ay]];
|
||||
}
|
||||
for (NSUInteger i=0; i<3; i++) {
|
||||
double ox=0,oy=0,ax=0,ay=0;
|
||||
if (points76) {
|
||||
NSArray *origin=points76[PGChinPins[i][0]],*anchor=points76[PGChinPins[i][1]];
|
||||
ox=[origin[0] doubleValue];oy=1-[origin[1] doubleValue];
|
||||
ax=[anchor[0] doubleValue];ay=1-[anchor[1] doubleValue];
|
||||
}
|
||||
[arguments addObject:[CIVector vectorWithX:ox Y:oy Z:ax W:ay]];
|
||||
}
|
||||
CIImage *input = [result imageByClampingToExtent];
|
||||
result = [_warp applyWithExtent:extent roiCallback:^CGRect(int index, CGRect rect) { (void)index; (void)rect; return extent; } inputImage:input arguments:arguments];
|
||||
if (!result) return nil;
|
||||
}
|
||||
return [result imageByCroppingToRect:extent];
|
||||
}
|
||||
|
||||
- (PGFaceFrame *)processImage:(CIImage *)image settings:(PGBeautySettings)settings time:(CMTime)time error:(NSError **)error {
|
||||
CGRect extent = image.extent;
|
||||
if (!image || CGRectIsInfinite(extent) || CGRectIsEmpty(extent) || !_ellipse || !_warp || !_box || !_subtract) {
|
||||
if (error) *error = [NSError errorWithDomain:PGFaceErrorDomain code:2 userInfo:@{NSLocalizedDescriptionKey:@"Unable to initialize face processing for this image."}];
|
||||
return nil;
|
||||
}
|
||||
NSDictionary *record = settings.trackingEnabled ? [self recordForTime:time target:MIN((NSUInteger)settings.targetIndex, 2) nearest:NO] : nil;
|
||||
NSArray<NSDictionary *> *geometry,*appearance;
|
||||
NSArray<VNFaceObservation *> *observations;
|
||||
if (record) {
|
||||
geometry = record[@"faces"];
|
||||
appearance = geometry;
|
||||
NSMutableArray *restored = [NSMutableArray array];
|
||||
for (NSDictionary *face in geometry) [restored addObject:[VNFaceObservation faceObservationWithRequestRevision:VNDetectFaceLandmarksRequestRevision3 boundingBox:PGArrayRect(face[@"box"]) roll:nil yaw:nil pitch:nil]];
|
||||
observations = restored;
|
||||
} else {
|
||||
NSArray *detected = [self observationsInImage:image settings:settings error:error];
|
||||
if (!detected) return nil;
|
||||
if(settings.trackingEnabled) {
|
||||
// Preserve the explicit offline-analysis contract. Its stored
|
||||
// identity fallback never reads or writes realtime temporal state.
|
||||
NSDictionary *anchor=[self recordForTime:time target:MIN((NSUInteger)settings.targetIndex,2) nearest:YES];
|
||||
geometry=[self geometryForObservations:detected settings:settings previous:anchor];
|
||||
appearance=geometry;
|
||||
NSMutableArray *selected=[NSMutableArray new];
|
||||
for(NSDictionary *face in geometry) for(VNFaceObservation *observation in detected)
|
||||
if(CGRectEqualToRect(observation.boundingBox,PGArrayRect(face[@"box"]))) {[selected addObject:observation];break;}
|
||||
observations=selected;
|
||||
} else {
|
||||
NSMutableArray *raw=[NSMutableArray new],*validObservations=[NSMutableArray new];
|
||||
for(VNFaceObservation *observation in detected) if(PGValidRect(observation.boundingBox) && raw.count<16) {
|
||||
[raw addObject:PGGeometry(observation)];[validObservations addObject:observation];
|
||||
}
|
||||
NSDictionary *live=[_realtime resultForGeometry:raw settings:settings time:time extent:extent];
|
||||
geometry=live[@"output"];
|
||||
if(settings.singleFace && !settings.fastMultiFace) {
|
||||
NSInteger index=[live[@"index"] integerValue];
|
||||
if(index>=0 && (NSUInteger)index<validObservations.count) {
|
||||
// Retain the selected original Vision observation. A stable
|
||||
// geometry box need not equal its unsmoothed detection box.
|
||||
observations=@[validObservations[(NSUInteger)index]];
|
||||
appearance=@[raw[(NSUInteger)index]];
|
||||
} else {
|
||||
appearance=geometry;
|
||||
NSMutableArray *held=[NSMutableArray new];
|
||||
for(NSDictionary *face in geometry) [held addObject:[VNFaceObservation faceObservationWithRequestRevision:VNDetectFaceLandmarksRequestRevision3 boundingBox:PGArrayRect(face[@"box"]) roll:nil yaw:nil pitch:nil]];
|
||||
observations=held;
|
||||
}
|
||||
} else {
|
||||
// Default single-primary geometry does not narrow the separate
|
||||
// skin/teeth/eyes support, which still covers the detected group.
|
||||
appearance=raw;observations=validObservations;
|
||||
}
|
||||
}
|
||||
}
|
||||
CIImage *skin = PGBlack(extent), *teeth = PGBlack(extent), *eyes = PGBlack(extent), *protection = PGBlack(extent), *overlay = PGTransparent(extent);
|
||||
for (NSDictionary *face in appearance) {
|
||||
CGRect box = PGArrayRect(face[@"box"]);
|
||||
CGRect skinRect = PGInFace(box, -0.10, -0.04, 1.20, 1.16);
|
||||
skin = PGMaximum(skin, [self ellipseInRect:PGToPixels(skinRect, extent) extent:extent inner:0.78]);
|
||||
NSArray *names = @[@"leftEye", @"rightEye", @"leftBrow", @"rightBrow", @"lips", @"nose"];
|
||||
NSArray<NSValue *> *fallbacks = @[[NSValue valueWithRect:PGInFace(box, 0.16, 0.53, 0.25, 0.15)],
|
||||
[NSValue valueWithRect:PGInFace(box, 0.59, 0.53, 0.25, 0.15)],
|
||||
[NSValue valueWithRect:PGInFace(box, 0.12, 0.72, 0.29, 0.08)],
|
||||
[NSValue valueWithRect:PGInFace(box, 0.59, 0.72, 0.29, 0.08)],
|
||||
[NSValue valueWithRect:PGInFace(box, 0.27, 0.16, 0.46, 0.18)],
|
||||
[NSValue valueWithRect:PGInFace(box, 0.39, 0.33, 0.22, 0.16)]];
|
||||
for (NSUInteger i = 0; i < names.count; i++) {
|
||||
CGRect region = PGToPixels(PGRegionBounds(face, names[i], fallbacks[i].rectValue), extent);
|
||||
CGRect expanded = CGRectInset(region, -region.size.width * 0.15, -region.size.height * 0.35);
|
||||
CIImage *mask = [self ellipseInRect:expanded extent:extent inner:0.45];
|
||||
protection = PGMaximum(protection, mask);
|
||||
if (i < 2) eyes = PGMaximum(eyes, [self ellipseInRect:region extent:extent inner:0.45]);
|
||||
}
|
||||
CGRect mouth = PGRegionBounds(face, @"mouth", PGInFace(box, 0.30, 0.20, 0.40, 0.08));
|
||||
teeth = PGMaximum(teeth, [self ellipseInRect:PGToPixels(mouth, extent) extent:extent inner:0.35]);
|
||||
if (settings.showFaceBoxes) {
|
||||
CGRect pixels = PGToPixels(box, extent);
|
||||
CGFloat inset = MAX(1, MIN(pixels.size.width, pixels.size.height) * 0.005);
|
||||
CIImage *outline = [_box applyWithExtent:extent arguments:@[[CIVector vectorWithX:CGRectGetMinX(pixels) Y:CGRectGetMinY(pixels) Z:CGRectGetMaxX(pixels) W:CGRectGetMaxY(pixels)], @(inset), [CIVector vectorWithX:0.12 Y:1.0 Z:0.35 W:0.95]]];
|
||||
overlay = [outline imageByCompositingOverImage:overlay];
|
||||
}
|
||||
}
|
||||
if(settings.showTrackBox) for(NSDictionary *face in geometry) {
|
||||
CGRect pixels=PGToPixels(PGArrayRect(face[@"box"]),extent);
|
||||
CGFloat inset=MAX(1,MIN(pixels.size.width,pixels.size.height)*.005);
|
||||
CIImage *outline=[_box applyWithExtent:extent arguments:@[[CIVector vectorWithX:CGRectGetMinX(pixels) Y:CGRectGetMinY(pixels) Z:CGRectGetMaxX(pixels) W:CGRectGetMaxY(pixels)],@(inset),[CIVector vectorWithX:1 Y:.8 Z:.2 W:.95]]];
|
||||
overlay=[outline imageByCompositingOverImage:overlay];
|
||||
}
|
||||
CIImage *colorCoverage=skin;
|
||||
if (settings.featureProtection) skin = [_subtract applyWithExtent:extent arguments:@[skin, protection]];
|
||||
PGFaceFrame *frame = [[PGFaceFrame alloc] init];
|
||||
frame.faces = observations ?: @[];
|
||||
frame.deformationGeometry=geometry?:@[];
|
||||
frame.image = [self warpImage:image geometry:geometry settings:settings extent:extent];
|
||||
frame.skinMask = [self warpImage:skin geometry:geometry settings:settings extent:extent];
|
||||
frame.colorMask = [self warpImage:colorCoverage geometry:geometry settings:settings extent:extent];
|
||||
frame.teethMask = [self warpImage:teeth geometry:geometry settings:settings extent:extent];
|
||||
frame.eyeMask = [self warpImage:eyes geometry:geometry settings:settings extent:extent];
|
||||
frame.overlayImage = [overlay imageByCroppingToRect:extent];
|
||||
if (!frame.image || !frame.skinMask || !frame.colorMask || !frame.teethMask || !frame.eyeMask) {
|
||||
if (error) *error = [NSError errorWithDomain:PGFaceErrorDomain code:3 userInfo:@{NSLocalizedDescriptionKey:@"Unable to apply face geometry."}];
|
||||
return nil;
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
- (void)resetTracking {
|
||||
[_lock lock];
|
||||
for (NSMutableDictionary *target in _targets) [target removeAllObjects];
|
||||
[_lock unlock];
|
||||
[_realtime reset];
|
||||
}
|
||||
|
||||
- (NSData *)trackingDataForTarget:(NSUInteger)target {
|
||||
if (target >= 3) return nil;
|
||||
[_lock lock];
|
||||
NSDictionary *frames = [_targets[target] copy];
|
||||
[_lock unlock];
|
||||
if (!frames.count) return nil;
|
||||
return [NSPropertyListSerialization dataWithPropertyList:@{@"version":@1, @"frames":frames} format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil];
|
||||
}
|
||||
|
||||
- (void)setTrackingData:(NSData *)data forTarget:(NSUInteger)target {
|
||||
if (target >= 3) return;
|
||||
NSMutableDictionary *frames = [NSMutableDictionary dictionary];
|
||||
if (data.length && data.length <= 128 * 1024 * 1024) {
|
||||
NSDictionary *archive = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListImmutable format:nil error:nil];
|
||||
if ([archive isKindOfClass:NSDictionary.class] && [archive[@"version"] isEqual:@1] && [archive[@"frames"] isKindOfClass:NSDictionary.class] && [archive[@"frames"] count] <= 250000) {
|
||||
for (id key in archive[@"frames"]) {
|
||||
id raw = archive[@"frames"][key];
|
||||
if (![key isKindOfClass:NSString.class] || ![raw isKindOfClass:NSDictionary.class]) continue;
|
||||
NSNumber *value = raw[@"value"], *scale = raw[@"scale"], *epoch = raw[@"epoch"];
|
||||
if (![value isKindOfClass:NSNumber.class] || ![scale isKindOfClass:NSNumber.class] || ![epoch isKindOfClass:NSNumber.class] || scale.intValue <= 0) continue;
|
||||
CMTime time = CMTimeMakeWithEpoch(value.longLongValue, scale.intValue, epoch.longLongValue);
|
||||
if (![key isEqualToString:PGTimeKey(time)] || ![raw[@"faces"] isKindOfClass:NSArray.class] || [raw[@"faces"] count] > 16) continue;
|
||||
NSMutableArray *faces = [NSMutableArray array];
|
||||
BOOL valid = YES;
|
||||
for (id geometry in raw[@"faces"]) {
|
||||
NSDictionary *face = PGSanitizedGeometry(geometry);
|
||||
if (!face) { valid = NO; break; }
|
||||
[faces addObject:face];
|
||||
}
|
||||
if (valid) frames[key] = @{@"value":value, @"scale":scale, @"epoch":epoch, @"faces":[faces copy]};
|
||||
}
|
||||
}
|
||||
}
|
||||
[_lock lock];
|
||||
_targets[target] = frames;
|
||||
[_lock unlock];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,170 @@
|
||||
#import <FxPlug/FxPlugSDK.h>
|
||||
#import <CoreImage/CoreImage.h>
|
||||
#import <IOSurface/IOSurface.h>
|
||||
#import "PGParameterIDs.h"
|
||||
#include <math.h>
|
||||
|
||||
@interface PGFaceSelectOSC : NSObject <FxOnScreenControl_v4>
|
||||
@property (assign) id<PROAPIAccessing> apiManager;
|
||||
@property BOOL dragging;
|
||||
@property BOOL undoOpen;
|
||||
@property BOOL pointSetterUsesPixels;
|
||||
@property BOOL loggedCoordinates;
|
||||
@property BOOL loggedPointSetter;
|
||||
@end
|
||||
@implementation PGFaceSelectOSC
|
||||
- (instancetype)initWithAPIManager:(id<PROAPIAccessing>)api {
|
||||
if((self=[super init])) _apiManager=api; return self;
|
||||
}
|
||||
- (FxDrawingCoordinates)drawingCoordinates {return kFxDrawingCoordinates_CANVAS;}
|
||||
- (id<FxOnScreenControlAPI_v4>)oscAPI {return [_apiManager apiForProtocol:@protocol(FxOnScreenControlAPI_v4)];}
|
||||
- (BOOL)objectPointFromCanvasX:(double)x y:(double)y objectX:(double *)objectX objectY:(double *)objectY {
|
||||
id<FxOnScreenControlAPI_v4> api=[self oscAPI];
|
||||
if(!api || !objectX || !objectY || !isfinite(x) || !isfinite(y)) return NO;
|
||||
// Request CANVAS events explicitly, then let the host invert its current
|
||||
// zoom, pan and object transform. The conversion API has no success result;
|
||||
// untouched or nonfinite outputs must never become a parameter write.
|
||||
double nx=NAN,ny=NAN;
|
||||
[api convertPointFromSpace:kFxDrawingCoordinates_CANVAS fromX:x fromY:y
|
||||
toSpace:kFxDrawingCoordinates_OBJECT toX:&nx toY:&ny];
|
||||
if(!isfinite(nx) || !isfinite(ny)) return NO;
|
||||
*objectX=nx;*objectY=ny;return YES;
|
||||
}
|
||||
- (BOOL)selectionEnabledAtTime:(CMTime)time {
|
||||
BOOL selected=NO;id<FxParameterRetrievalAPI_v6> api=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
|
||||
return api && [api getBoolValue:&selected fromParameter:PGSingleFace atTime:time] && selected;
|
||||
}
|
||||
- (void)drawOSCWithWidth:(NSInteger)width height:(NSInteger)height activePart:(NSInteger)part destinationImage:(FxImageTile *)tile atTime:(CMTime)time {
|
||||
if(!tile.ioSurface||width<=0||height<=0) return;
|
||||
size_t surfaceW=IOSurfaceGetWidth((__bridge IOSurfaceRef)tile.ioSurface),surfaceH=IOSurfaceGetHeight((__bridge IOSurfaceRef)tile.ioSurface);
|
||||
if(!surfaceW||!surfaceH) return;
|
||||
CIImage *output=[[CIImage imageWithColor:CIColor.clearColor] imageByCroppingToRect:CGRectMake(0,0,surfaceW,surfaceH)];
|
||||
if([self selectionEnabledAtTime:time] && [self oscAPI]) {
|
||||
double x=.5,y=.5;
|
||||
id<FxParameterRetrievalAPI_v6> params=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
|
||||
BOOL valid=[params getXValue:&x YValue:&y fromParameter:PGSelection atTime:time] && isfinite(x) && isfinite(y);
|
||||
double canvasX=NAN,canvasY=NAN;
|
||||
// FxPlug OBJECT space is normalized 0..1, exactly like point parameters.
|
||||
// objectBounds is a pixel/document rectangle and must not be applied here.
|
||||
if(valid) [[self oscAPI] convertPointFromSpace:kFxDrawingCoordinates_OBJECT fromX:x fromY:y toSpace:kFxDrawingCoordinates_CANVAS toX:&canvasX toY:&canvasY];
|
||||
if(!_loggedCoordinates) {
|
||||
_loggedCoordinates=YES;
|
||||
CGRect objectBounds=[[self oscAPI] objectBounds];
|
||||
NSLog(@"PGFaceOSC coordinates selection=(%.9g,%.9g) objectBounds=(%.9g,%.9g,%.9g,%.9g) canvas=(%.9g,%.9g) surface=%zux%zu origin=%lu",
|
||||
x,y,objectBounds.origin.x,objectBounds.origin.y,objectBounds.size.width,objectBounds.size.height,
|
||||
canvasX,canvasY,surfaceW,surfaceH,(unsigned long)tile.imageOrigin);
|
||||
}
|
||||
// CANVAS coordinates address the OSC surface. Callback width/height can
|
||||
// describe the effect image and must not rescale the converted point.
|
||||
double backing=[[self oscAPI] backingScaleFactor];
|
||||
CGFloat scale=isfinite(backing)?MAX(1,backing):1;
|
||||
CGFloat radius=10*scale;
|
||||
if(valid && isfinite(canvasX) && isfinite(canvasY)) {
|
||||
CIColor *color=part?[CIColor colorWithRed:1 green:.8 blue:.2]:[CIColor colorWithRed:.1 green:.9 blue:.8];
|
||||
CIImage *ring=[CIFilter filterWithName:@"CIRadialGradient" withInputParameters:@{@"inputCenter":[CIVector vectorWithX:canvasX Y:canvasY],@"inputRadius0":@(radius-2),@"inputRadius1":@(radius),@"inputColor0":color,@"inputColor1":CIColor.clearColor}].outputImage;
|
||||
CIImage *hole=[CIFilter filterWithName:@"CIRadialGradient" withInputParameters:@{@"inputCenter":[CIVector vectorWithX:canvasX Y:canvasY],@"inputRadius0":@(radius-3),@"inputRadius1":@(radius-2),@"inputColor0":CIColor.blackColor,@"inputColor1":CIColor.whiteColor}].outputImage;
|
||||
ring=[ring imageByApplyingFilter:@"CIBlendWithMask" withInputParameters:@{kCIInputBackgroundImageKey:output,kCIInputMaskImageKey:hole}];
|
||||
CIImage *v=[[CIImage imageWithColor:color] imageByCroppingToRect:CGRectMake(canvasX-scale,canvasY-16*scale,2*scale,32*scale)];
|
||||
CIImage *h=[[CIImage imageWithColor:color] imageByCroppingToRect:CGRectMake(canvasX-16*scale,canvasY-scale,32*scale,2*scale)];
|
||||
output=[[h imageByCompositingOverImage:v] imageByCompositingOverImage:ring];
|
||||
}
|
||||
}
|
||||
CIContext *context=[CIContext contextWithOptions:@{kCIContextCacheIntermediates:@NO}];
|
||||
size_t w=IOSurfaceGetWidth((__bridge IOSurfaceRef)tile.ioSurface),h=IOSurfaceGetHeight((__bridge IOSurfaceRef)tile.ioSurface);
|
||||
if(tile.imageOrigin==kFxImageOrigin_TOP_LEFT) output=[output imageByApplyingTransform:CGAffineTransformMake(1,0,0,-1,0,h)];
|
||||
[context render:output toIOSurface:(__bridge IOSurfaceRef)tile.ioSurface bounds:CGRectMake(0,0,w,h) colorSpace:tile.colorSpace];
|
||||
}
|
||||
- (void)hitTestOSCAtMousePositionX:(double)x mousePositionY:(double)y activePart:(NSInteger *)part atTime:(CMTime)time {
|
||||
if(!part) return;*part=0;
|
||||
double nx=NAN,ny=NAN;
|
||||
if([self selectionEnabledAtTime:time] && [self objectPointFromCanvasX:x y:y objectX:&nx objectY:&ny] &&
|
||||
nx>=0 && nx<=1 && ny>=0 && ny<=1) *part=1;
|
||||
}
|
||||
- (BOOL)pointWrite:(id<FxParameterSettingAPI_v5>)setter x:(double)x y:(double)y time:(CMTime)time {
|
||||
return setter && isfinite(x) && isfinite(y) && [setter setXValue:x YValue:y toParameter:PGSelection atTime:time];
|
||||
}
|
||||
- (BOOL)readPoint:(id<FxParameterRetrievalAPI_v6>)reader x:(double *)x y:(double *)y time:(CMTime)time {
|
||||
return reader && [reader getXValue:x YValue:y fromParameter:PGSelection atTime:time] && isfinite(*x) && isfinite(*y);
|
||||
}
|
||||
- (BOOL)pointPixelSize:(CGSize *)size {
|
||||
id<FxOnScreenControlAPI_v4> api=[self oscAPI];if(!api || !size)return NO;
|
||||
CGRect bounds=[api objectBounds];
|
||||
if(!isfinite(bounds.size.width)||!isfinite(bounds.size.height)||bounds.size.width<=4||bounds.size.height<=4)return NO;
|
||||
*size=bounds.size;return YES;
|
||||
}
|
||||
- (void)setSelectionX:(double)x y:(double)y time:(CMTime)time update:(BOOL *)update {
|
||||
if(update) *update=NO;
|
||||
if(!isfinite(x)||!isfinite(y)) return;
|
||||
double nx=fmin(1,fmax(0,x)),ny=fmin(1,fmax(0,y));
|
||||
id<FxParameterSettingAPI_v5> setter=[_apiManager apiForProtocol:@protocol(FxParameterSettingAPI_v5)];
|
||||
id<FxParameterRetrievalAPI_v6> reader=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
|
||||
double previousX=NAN,previousY=NAN;
|
||||
if(!setter || ![self readPoint:reader x:&previousX y:&previousY time:time])return;
|
||||
CGSize pixels=CGSizeZero;
|
||||
if(_pointSetterUsesPixels && ![self pointPixelSize:&pixels])return;
|
||||
double rawX=_pointSetterUsesPixels ? (nx-.5)*pixels.width : nx;
|
||||
double rawY=_pointSetterUsesPixels ? (ny-.5)*pixels.height : ny;
|
||||
BOOL accepted=[self pointWrite:setter x:rawX y:rawY time:time];
|
||||
BOOL changed=accepted,pixelUnits=_pointSetterUsesPixels;
|
||||
double actualX=NAN,actualY=NAN;
|
||||
BOOL readBack=accepted && [self readPoint:reader x:&actualX y:&actualY time:time];
|
||||
const double tolerance=0.00001;
|
||||
BOOL verified=readBack && fabs(actualX-nx)<=tolerance && fabs(actualY-ny)<=tolerance;
|
||||
// Motion's FxPlug 4 bridge can normalize point reads while its setter still
|
||||
// writes centered pixel values directly to OZChannel2D. Detect that exact
|
||||
// response instead of choosing units from the app version or coordinate size.
|
||||
if(readBack && !verified && !_pointSetterUsesPixels && [self pointPixelSize:&pixels] &&
|
||||
fabs(actualX-(.5+nx/pixels.width))<=tolerance &&
|
||||
fabs(actualY-(.5+ny/pixels.height))<=tolerance) {
|
||||
pixelUnits=YES;
|
||||
rawX=(nx-.5)*pixels.width;rawY=(ny-.5)*pixels.height;
|
||||
accepted=[self pointWrite:setter x:rawX y:rawY time:time];changed=changed||accepted;
|
||||
actualX=actualY=NAN;
|
||||
readBack=accepted && [self readPoint:reader x:&actualX y:&actualY time:time];
|
||||
verified=readBack && fabs(actualX-nx)<=tolerance && fabs(actualY-ny)<=tolerance;
|
||||
if(verified)_pointSetterUsesPixels=YES;
|
||||
}
|
||||
if(!verified && changed) {
|
||||
// Restore the previous point when verification fails. A failure must not
|
||||
// look like a successful drag or silently retain the calibration point.
|
||||
double restoreX=pixelUnits ? (previousX-.5)*pixels.width : previousX;
|
||||
double restoreY=pixelUnits ? (previousY-.5)*pixels.height : previousY;
|
||||
[self pointWrite:setter x:restoreX y:restoreY time:time];
|
||||
}
|
||||
if(!_loggedPointSetter) {
|
||||
_loggedPointSetter=YES;
|
||||
NSLog(@"PGFaceOSC point-write desired=(%.9g,%.9g) setter=(%.9g,%.9g) readback=(%.9g,%.9g) mode=%@ verified=%d",
|
||||
nx,ny,rawX,rawY,actualX,actualY,_pointSetterUsesPixels?@"centered-pixels":@"normalized",verified);
|
||||
}
|
||||
if(update)*update=verified;
|
||||
}
|
||||
- (void)mouseDownAtPositionX:(double)x positionY:(double)y activePart:(NSInteger)part modifiers:(FxModifierKeys)keys forceUpdate:(BOOL *)update atTime:(CMTime)time {
|
||||
if(update) *update=NO;
|
||||
double nx=NAN,ny=NAN;
|
||||
if(part!=1 || ![self selectionEnabledAtTime:time] ||
|
||||
![self objectPointFromCanvasX:x y:y objectX:&nx objectY:&ny] || nx<0 || nx>1 || ny<0 || ny>1) return;
|
||||
id<FxUndoAPI> undo=[_apiManager apiForProtocol:@protocol(FxUndoAPI)];
|
||||
if(_undoOpen) [undo endUndoGroup];
|
||||
_undoOpen=[undo startUndoGroup:@"选择人脸"];
|
||||
BOOL changed=NO;[self setSelectionX:nx y:ny time:time update:&changed];
|
||||
_dragging=changed;
|
||||
if(!changed && _undoOpen) {[undo endUndoGroup];_undoOpen=NO;}
|
||||
if(update) *update=changed;
|
||||
}
|
||||
- (void)mouseDraggedAtPositionX:(double)x positionY:(double)y activePart:(NSInteger)part modifiers:(FxModifierKeys)keys forceUpdate:(BOOL *)update atTime:(CMTime)time {
|
||||
if(update) *update=NO;
|
||||
double nx=NAN,ny=NAN;
|
||||
if(_dragging && part==1 && [self selectionEnabledAtTime:time] &&
|
||||
[self objectPointFromCanvasX:x y:y objectX:&nx objectY:&ny]) [self setSelectionX:nx y:ny time:time update:update];
|
||||
}
|
||||
- (void)mouseUpAtPositionX:(double)x positionY:(double)y activePart:(NSInteger)part modifiers:(FxModifierKeys)keys forceUpdate:(BOOL *)update atTime:(CMTime)time {
|
||||
if(update) *update=NO;
|
||||
double nx=NAN,ny=NAN;
|
||||
if(_dragging && part==1 && [self selectionEnabledAtTime:time] &&
|
||||
[self objectPointFromCanvasX:x y:y objectX:&nx objectY:&ny]) [self setSelectionX:nx y:ny time:time update:update];
|
||||
_dragging=NO;
|
||||
if(_undoOpen) {id<FxUndoAPI> undo=[_apiManager apiForProtocol:@protocol(FxUndoAPI)];[undo endUndoGroup];_undoOpen=NO;}
|
||||
}
|
||||
- (void)keyDownAtPositionX:(double)x positionY:(double)y keyPressed:(unsigned short)key modifiers:(FxModifierKeys)modifiers forceUpdate:(BOOL *)update didHandle:(BOOL *)handled atTime:(CMTime)time {if(handled)*handled=NO;if(update)*update=NO;}
|
||||
- (void)keyUpAtPositionX:(double)x positionY:(double)y keyPressed:(unsigned short)key modifiers:(FxModifierKeys)modifiers forceUpdate:(BOOL *)update didHandle:(BOOL *)handled atTime:(CMTime)time {if(handled)*handled=NO;if(update)*update=NO;}
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
// Existing brightness sample ID 1 is migrated to the portrait amount control.
|
||||
enum {
|
||||
PGAmount=1, PGBrightness=2, PGRadius=3, PGSharpness=4, PGMix=5,
|
||||
PGThreshold=7, PGSoftness=8, PGEdgeProtect=9, PGShowMask=10,
|
||||
PGMidtone=12, PGHue=14, PGSaturation=15, PGHighPass=17, PGThin=18,
|
||||
PGFaceBoxes=19, PGSingleFace=20, PGSelection=21, PGShort=22,
|
||||
PGNarrow=23, PGChin=24, PGVFace=25, PGJaw=26, PGCheekbone=27,
|
||||
PGSmallHead=28, PGSmallHeadRange=29, PGFeatureProtect=30, PGTeeth=31,
|
||||
PGEyeBright=32, PGBigEye=33, PGTrackBoxes=35, PGFastMulti=39,
|
||||
PGJumpGuard=40, PGEvenTone=41, PGTonePreset=42, PGInvertMask=43,
|
||||
PGDebugChannel=44, PGTrackEnabled=45, PGTarget=46,
|
||||
PGAnalyzeForward=50, PGAnalyzeBackward=51, PGClearTrack=52, PGStatus=53,
|
||||
PGTrackData1=60, PGTrackData2=61, PGTrackData3=62,
|
||||
PGSmoothingEnabled=70, PGColorEnabled=71, PGShapeEnabled=72,
|
||||
PGDetailEnabled=73, PGHelp=74
|
||||
};
|
||||
@@ -1,83 +0,0 @@
|
||||
//
|
||||
// TileableRemoteBrightness.metal
|
||||
// beatygril
|
||||
//
|
||||
// Created by dayu on 2026/10/21.
|
||||
//
|
||||
|
||||
#include <metal_stdlib>
|
||||
#include <simd/simd.h>
|
||||
|
||||
using namespace metal;
|
||||
|
||||
#include "TileableRemoteBrightnessShaderTypes.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
// The [[position]] attribute of this member indicates that this value is the clip space
|
||||
// position of the vertex when this structure is returned from the vertex function
|
||||
float4 clipSpacePosition [[position]];
|
||||
|
||||
// Since this member does not have a special attribute, the rasterizer interpolates
|
||||
// its value with the values of the other triangle vertices and then passes
|
||||
// the interpolated value to the fragment shader for each fragment in the triangle
|
||||
float2 textureCoordinate;
|
||||
|
||||
} RasterizerData;
|
||||
|
||||
vertex RasterizerData
|
||||
vertexShader(uint vertexID [[vertex_id]],
|
||||
constant Vertex2D *vertexArray [[buffer(BVI_Vertices)]],
|
||||
constant vector_uint2 *viewportSizePointer [[buffer(BVI_ViewportSize)]])
|
||||
{
|
||||
RasterizerData out;
|
||||
|
||||
// Index into our array of positions to get the current vertex
|
||||
// Our positions are specified in pixel dimensions (i.e. a value of 100 is 100 pixels from
|
||||
// the origin)
|
||||
float2 pixelSpacePosition = vertexArray[vertexID].position.xy;
|
||||
|
||||
// Get the size of the drawable so that we can convert to normalized device coordinates,
|
||||
float2 viewportSize = float2(*viewportSizePointer);
|
||||
|
||||
// The output position of every vertex shader is in clip space (also known as normalized device
|
||||
// coordinate space, or NDC). A value of (-1.0, -1.0) in clip-space represents the
|
||||
// lower-left corner of the viewport whereas (1.0, 1.0) represents the upper-right corner of
|
||||
// the viewport.
|
||||
|
||||
// In order to convert from positions in pixel space to positions in clip space we divide the
|
||||
// pixel coordinates by half the size of the viewport.
|
||||
out.clipSpacePosition.xy = pixelSpacePosition / (viewportSize / 2.0);
|
||||
|
||||
// Set the z component of our clip space position 0 (since we're only rendering in
|
||||
// 2-Dimensions for this sample)
|
||||
out.clipSpacePosition.z = 0.0;
|
||||
|
||||
// Set the w component to 1.0 since we don't need a perspective divide, which is also not
|
||||
// necessary when rendering in 2-Dimensions
|
||||
out.clipSpacePosition.w = 1.0;
|
||||
|
||||
// Pass our input textureCoordinate straight to our output RasterizerData. This value will be
|
||||
// interpolated with the other textureCoordinate values in the vertices that make up the
|
||||
// triangle.
|
||||
out.textureCoordinate = vertexArray[vertexID].textureCoordinate;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// Fragment function
|
||||
fragment float4 fragmentShader(RasterizerData in [[stage_in]],
|
||||
texture2d<half> colorTexture [[ texture(BTI_InputImage) ]],
|
||||
constant float* brightness [[ buffer(BFI_Brightness) ]])
|
||||
{
|
||||
constexpr sampler textureSampler (mag_filter::linear,
|
||||
min_filter::linear);
|
||||
|
||||
// Sample the texture to obtain a color
|
||||
half4 colorSample = colorTexture.sample(textureSampler, in.textureCoordinate);
|
||||
const half hBrightness = static_cast<half>(*brightness);
|
||||
colorSample.rgb = colorSample.rgb * hBrightness;
|
||||
|
||||
// We return the color of the texture
|
||||
return float4(colorSample);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
//
|
||||
// TileableRemoteBrightnessShaderTypes.h
|
||||
// beatygril
|
||||
//
|
||||
// Created by dayu on 2026/10/21.
|
||||
//
|
||||
|
||||
#ifndef TileableRemoteBrightnessShaderTypes_h
|
||||
#define TileableRemoteBrightnessShaderTypes_h
|
||||
|
||||
#import <simd/simd.h>
|
||||
|
||||
typedef enum BrightnessVertexInputIndex {
|
||||
BVI_Vertices = 0,
|
||||
BVI_ViewportSize = 1
|
||||
} BrightnessVertexInputIndex;
|
||||
|
||||
typedef enum BrightnessTextureIndex {
|
||||
BTI_InputImage = 0
|
||||
} BrightnessTextureIndex;
|
||||
|
||||
typedef enum BrightnessFragmentIndex {
|
||||
BFI_Brightness = 0
|
||||
} BrightnessFragmentIndex;
|
||||
|
||||
typedef struct Vertex2D {
|
||||
vector_float2 position;
|
||||
vector_float2 textureCoordinate;
|
||||
} Vertex2D;
|
||||
|
||||
|
||||
#endif /* TileableRemoteBrightnessShaderTypes_h */
|
||||
@@ -1,13 +1,11 @@
|
||||
//
|
||||
// beatygrilPlugIn.h
|
||||
// beatygril
|
||||
//
|
||||
// Created by dayu on 2026/10/21.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <FxPlug/FxPlugSDK.h>
|
||||
#import "PGBeautyTypes.h"
|
||||
|
||||
@interface beatygrilPlugIn : NSObject <FxTileableEffect>
|
||||
@property (assign) id<PROAPIAccessing> apiManager;
|
||||
@interface beatygrilPlugIn : NSObject <FxTileableEffect, FxAnalyzer>
|
||||
@property (nonatomic, assign) id<PROAPIAccessing> apiManager;
|
||||
- (PGBeautySettings)settingsAtTime:(CMTime)time;
|
||||
- (void)analyzeForward;
|
||||
- (void)analyzeBackward;
|
||||
- (void)clearTracking;
|
||||
@end
|
||||
@@ -1,320 +1,372 @@
|
||||
//
|
||||
// beatygrilPlugIn.m
|
||||
// beatygril
|
||||
//
|
||||
// Created by dayu on 2026/10/21.
|
||||
//
|
||||
|
||||
#import "beatygrilPlugIn.h"
|
||||
#import <IOSurface/IOSurfaceObjC.h>
|
||||
#import "TileableRemoteBrightnessShaderTypes.h"
|
||||
#import "MetalDeviceCache.h"
|
||||
#import "PGParameterIDs.h"
|
||||
#import "PGBeautyRenderer.h"
|
||||
#import "PGFaceProcessor.h"
|
||||
#import <Metal/Metal.h>
|
||||
#import <IOSurface/IOSurface.h>
|
||||
#import <stddef.h>
|
||||
#import <math.h>
|
||||
#import <os/log.h>
|
||||
|
||||
static os_log_t PGRenderLog(void) {
|
||||
static os_log_t log; static dispatch_once_t once;
|
||||
dispatch_once(&once, ^{ log=os_log_create("com.dayu.beatygril", "render"); });
|
||||
return log;
|
||||
}
|
||||
|
||||
typedef struct { UInt32 identifier; const char *name; size_t offset; float min, max, step; BOOL percent; } PGSlider;
|
||||
#define PGS(ID, NAME, FIELD, MIN, MAX, STEP, PERCENT) {ID, NAME, offsetof(PGBeautySettings, FIELD), MIN, MAX, STEP, PERCENT}
|
||||
static const PGSlider PGSliders[] = {
|
||||
PGS(PGAmount,"磨皮强度",amount,0,1,.01,YES),
|
||||
PGS(PGMidtone,"中频磨皮",midtoneDetail,0,1,.01,YES),
|
||||
PGS(PGRadius,"磨皮半径",radius,1,30,.5,NO),
|
||||
PGS(PGBrightness,"肤色提亮",skinBrightness,0,1,.01,YES),
|
||||
PGS(PGHue,"肤色色相",skinHue,-30,30,1,NO),
|
||||
PGS(PGSaturation,"肤色饱和度",skinSaturation,-100,100,1,NO),
|
||||
PGS(PGEvenTone,"均匀肤色",skinEvenTone,0,1,.01,YES),
|
||||
PGS(PGTeeth,"牙齿美白",teethWhitening,0,1,.01,YES),
|
||||
PGS(PGEyeBright,"眼睛提亮",eyeBrightening,0,1,.01,YES),
|
||||
PGS(PGHighPass,"高频细节平滑",highPassDetail,0,1,.01,YES),
|
||||
PGS(PGSharpness,"锐化",sharpness,0,1,.01,YES),
|
||||
PGS(PGThin,"瘦脸",faceThin,0,1,.01,YES),
|
||||
PGS(PGBigEye,"大眼",faceBigEye,0,1,.01,YES),
|
||||
PGS(PGShort,"短脸",faceShort,-1,1,.01,NO),
|
||||
PGS(PGNarrow,"窄脸",faceNarrow,-1,1,.01,NO),
|
||||
PGS(PGChin,"下巴",faceChin,-1,1,.01,NO),
|
||||
PGS(PGVFace,"V 脸",faceV,0,1,.01,YES),
|
||||
PGS(PGJaw,"下颌",faceJaw,0,1,.01,YES),
|
||||
PGS(PGCheekbone,"颧骨",faceCheekbone,0,1,.01,YES),
|
||||
PGS(PGSmallHead,"小头",faceSmallHead,0,1,.01,YES),
|
||||
PGS(PGSmallHeadRange,"小头范围",faceSmallHeadRange,0,1,.01,YES),
|
||||
PGS(PGThreshold,"蒙版阈值",maskThreshold,0,255,1,NO),
|
||||
PGS(PGSoftness,"蒙版柔度",maskSoftness,0,1,.01,YES),
|
||||
PGS(PGEdgeProtect,"边缘保护",maskEdgeProtect,0,1,.01,YES),
|
||||
PGS(PGJumpGuard,"防跳脸范围",faceJumpGuardRange,1,4,.05,NO),
|
||||
PGS(PGMix,"整体混合",mix,0,1,.01,YES),
|
||||
};
|
||||
#undef PGS
|
||||
|
||||
static BOOL PGFail(NSError **error, NSString *message) {
|
||||
os_log_error(PGRenderLog(), "%{public}@", message);
|
||||
if (error) *error = [NSError errorWithDomain:FxPlugErrorDomain code:kFxError_InvalidParameter
|
||||
userInfo:@{NSLocalizedDescriptionKey:message}];
|
||||
return NO;
|
||||
}
|
||||
static NSString * const PGHostCoordinateSpace = @"iosurface-ci-y-up-v1";
|
||||
static BOOL PGCompatibleHostTrack(NSData *data) {
|
||||
if (!data.length || data.length > 128*1024*1024) return NO;
|
||||
NSDictionary *archive = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListImmutable format:nil error:nil];
|
||||
return [archive isKindOfClass:NSDictionary.class] && [archive[@"hostCoordinateSpace"] isEqual:PGHostCoordinateSpace];
|
||||
}
|
||||
static NSData *PGMarkHostTrack(NSData *data) {
|
||||
if (!data) return nil;
|
||||
NSMutableDictionary *archive = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListMutableContainers format:nil error:nil];
|
||||
if (![archive isKindOfClass:NSMutableDictionary.class]) return nil;
|
||||
archive[@"hostCoordinateSpace"] = PGHostCoordinateSpace;
|
||||
return [NSPropertyListSerialization dataWithPropertyList:archive format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil];
|
||||
}
|
||||
static CIImage *PGImageFromTile(FxImageTile *tile) {
|
||||
if (!tile.ioSurface) return nil;
|
||||
// The host already supplies video-gamma RGB. Keep those numeric values in
|
||||
// every stage; implicit Core Image linearization changes mask thresholds.
|
||||
NSDictionary *options = @{kCIImageColorSpace:NSNull.null};
|
||||
CIImage *image = [CIImage imageWithIOSurface:(__bridge IOSurfaceRef)tile.ioSurface options:options];
|
||||
// Match the reference's CI/IOSurface path. imageOrigin describes the host's
|
||||
// coordinate axes; it is not an instruction to reverse IOSurface scanlines.
|
||||
// An extra input/output flip can hide in passthrough while inverting faces
|
||||
// inside Vision and the analytic shape/mask stages.
|
||||
FxRect b = tile.tilePixelBounds;
|
||||
return [image imageByApplyingTransform:CGAffineTransformMakeTranslation(b.left,b.bottom)];
|
||||
}
|
||||
|
||||
@interface beatygrilPlugIn ()
|
||||
@property (strong) PGBeautyRenderer *beautyRenderer;
|
||||
@property (strong) PGFaceProcessor *faceProcessor;
|
||||
@property (strong) PGFaceProcessor *analysisFaceProcessor;
|
||||
@property (strong) NSMutableDictionary<NSNumber *,CIContext *> *contexts;
|
||||
@property PGBeautySettings analysisSettings;
|
||||
@property CMTime analysisTime;
|
||||
@property NSUInteger analyzedFrames;
|
||||
@property BOOL analyzing;
|
||||
@property BOOL analysisForward;
|
||||
@property (strong) NSArray<NSString *> *loadedTracks;
|
||||
@property uint32_t compatibleTrackMask;
|
||||
@property BOOL reportedRender;
|
||||
@end
|
||||
|
||||
@implementation beatygrilPlugIn
|
||||
|
||||
//---------------------------------------------------------
|
||||
// initWithAPIManager:
|
||||
//
|
||||
// This method is called when a plug-in is first loaded, and
|
||||
// is a good point to conduct any checks for anti-piracy or
|
||||
// system compatibility. Returning NULL means that a plug-in
|
||||
// chooses not to be accessible for some reason.
|
||||
//---------------------------------------------------------
|
||||
|
||||
- (nullable instancetype)initWithAPIManager:(id<PROAPIAccessing>)newApiManager;
|
||||
{
|
||||
self = [super init];
|
||||
if (self != nil)
|
||||
{
|
||||
_apiManager = newApiManager;
|
||||
- (instancetype)initWithAPIManager:(id<PROAPIAccessing>)manager {
|
||||
if ((self=[super init])) {
|
||||
_apiManager=manager;
|
||||
_beautyRenderer=[PGBeautyRenderer new]; _faceProcessor=[PGFaceProcessor new];
|
||||
_analysisFaceProcessor=[PGFaceProcessor new];
|
||||
_contexts=[NSMutableDictionary new]; _analysisSettings=PGBeautyDefaults();
|
||||
_analysisTime=kCMTimeZero;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------
|
||||
// properties
|
||||
//
|
||||
// This method should return an NSDictionary defining the
|
||||
// properties of the effect.
|
||||
//---------------------------------------------------------
|
||||
|
||||
- (BOOL)properties:(NSDictionary * _Nonnull *)properties
|
||||
error:(NSError * _Nullable *)error
|
||||
{
|
||||
*properties = @{
|
||||
kFxPropertyKey_MayRemapTime : [NSNumber numberWithBool:NO],
|
||||
kFxPropertyKey_PixelTransformSupport : [NSNumber numberWithInt:kFxPixelTransform_ScaleTranslate],
|
||||
kFxPropertyKey_VariesWhenParamsAreStatic : [NSNumber numberWithBool:NO]
|
||||
};
|
||||
|
||||
- (BOOL)properties:(NSDictionary **)properties error:(NSError **)error {
|
||||
if (!properties) return PGFail(error,@"宿主未提供属性输出位置。");
|
||||
*properties=@{kFxPropertyKey_MayRemapTime:@NO,
|
||||
kFxPropertyKey_PixelTransformSupport:@(kFxPixelTransform_ScaleTranslate),
|
||||
kFxPropertyKey_VariesWhenParamsAreStatic:@NO,
|
||||
@"NeedsFullBuffer":@YES, @"ChangesOutputSize":@NO,
|
||||
kFxPropertyKey_DesiredProcessingColorInfo:@(kFxImageColorInfo_RGB_GAMMA_VIDEO)};
|
||||
return YES;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------
|
||||
// addParametersWithError
|
||||
//
|
||||
// This method is where a plug-in defines its list of parameters.
|
||||
//---------------------------------------------------------
|
||||
|
||||
- (BOOL)addParametersWithError:(NSError**)error
|
||||
{
|
||||
id<FxParameterCreationAPI_v5> paramAPI = [_apiManager apiForProtocol:@protocol(FxParameterCreationAPI_v5)];
|
||||
if (paramAPI == nil)
|
||||
{
|
||||
NSDictionary* userInfo = @{
|
||||
NSLocalizedDescriptionKey : @"Unable to obtain an FxPlug API Object"
|
||||
};
|
||||
if (error != NULL)
|
||||
*error = [NSError errorWithDomain:FxPlugErrorDomain
|
||||
code:kFxError_APIUnavailable
|
||||
userInfo:userInfo];
|
||||
|
||||
return NO;
|
||||
- (BOOL)addSlider:(UInt32)identifier api:(id<FxParameterCreationAPI_v5>)api {
|
||||
PGBeautySettings defaults=PGBeautyDefaults();
|
||||
for (NSUInteger i=0;i<sizeof(PGSliders)/sizeof(PGSlider);i++) {
|
||||
PGSlider spec=PGSliders[i]; if (spec.identifier!=identifier) continue;
|
||||
NSString *name=[NSString stringWithUTF8String:spec.name];
|
||||
float value=*(float *)((uint8_t *)&defaults+spec.offset);
|
||||
if (spec.percent) return [api addPercentSliderWithName:name parameterID:identifier defaultValue:value parameterMin:spec.min parameterMax:spec.max sliderMin:spec.min sliderMax:spec.max delta:spec.step parameterFlags:kFxParameterFlag_DEFAULT];
|
||||
return [api addFloatSliderWithName:name parameterID:identifier defaultValue:value parameterMin:spec.min parameterMax:spec.max sliderMin:spec.min sliderMax:spec.max delta:spec.step parameterFlags:kFxParameterFlag_DEFAULT];
|
||||
}
|
||||
|
||||
if (![paramAPI addFloatSliderWithName:@"Brightness"
|
||||
parameterID:1
|
||||
defaultValue:1.0
|
||||
parameterMin:0.0
|
||||
parameterMax:100.0
|
||||
sliderMin:0.0
|
||||
sliderMax:10.0
|
||||
delta:0.1
|
||||
parameterFlags:kFxParameterFlag_DEFAULT])
|
||||
{
|
||||
NSDictionary* userInfo = @{
|
||||
NSLocalizedDescriptionKey : @"Unable to add brightness slider"
|
||||
};
|
||||
if (error != NULL)
|
||||
*error = [NSError errorWithDomain:FxPlugErrorDomain
|
||||
code:kFxError_InvalidParameter
|
||||
userInfo:userInfo];
|
||||
|
||||
return NO;
|
||||
}
|
||||
- (BOOL)addParametersWithError:(NSError **)error {
|
||||
id<FxParameterCreationAPI_v5> api=[_apiManager apiForProtocol:@protocol(FxParameterCreationAPI_v5)];
|
||||
if (!api) return PGFail(error,@"无法取得 FxPlug 参数创建接口。");
|
||||
__block BOOL ok=YES;
|
||||
void (^slider)(UInt32)=^(UInt32 identifier){ok=[self addSlider:identifier api:api]&&ok;};
|
||||
void (^toggle)(NSString *,UInt32,BOOL)=^(NSString *name,UInt32 identifier,BOOL value){ok=[api addToggleButtonWithName:name parameterID:identifier defaultValue:value parameterFlags:kFxParameterFlag_DEFAULT]&&ok;};
|
||||
void (^group)(NSString *,UInt32)=^(NSString *name,UInt32 identifier){ok=[api startParameterSubGroup:name parameterID:identifier parameterFlags:kFxParameterFlag_DEFAULT]&&ok;};
|
||||
void (^end)(void)=^{ok=[api endParameterSubGroup]&&ok;};
|
||||
group(@"磨皮",100); toggle(@"启用磨皮",PGSmoothingEnabled,YES);
|
||||
slider(PGAmount); slider(PGMidtone); slider(PGRadius); toggle(@"保护五官",PGFeatureProtect,YES); end();
|
||||
group(@"肤色",101); toggle(@"启用肤色",PGColorEnabled,YES);
|
||||
ok=[api addPopupMenuWithName:@"肤色风格" parameterID:PGTonePreset defaultValue:0 menuEntries:@[@"原色",@"自然白",@"冷白",@"暖白",@"红润白",@"小麦",@"古铜"] parameterFlags:0]&&ok;
|
||||
slider(PGBrightness); slider(PGHue); slider(PGSaturation); slider(PGEvenTone); slider(PGTeeth); slider(PGEyeBright); end();
|
||||
group(@"细节",102); toggle(@"启用细节",PGDetailEnabled,YES); slider(PGHighPass); slider(PGSharpness); end();
|
||||
group(@"脸型",103); toggle(@"启用脸型",PGShapeEnabled,YES);
|
||||
UInt32 shape[]={PGThin,PGBigEye,PGShort,PGNarrow,PGChin,PGVFace,PGJaw,PGCheekbone,PGSmallHead,PGSmallHeadRange};
|
||||
for(NSUInteger i=0;i<sizeof(shape)/sizeof(UInt32);i++) slider(shape[i]); end();
|
||||
group(@"人脸选择与跟踪",104); toggle(@"只处理选中人脸",PGSingleFace,NO);
|
||||
ok=[api addPointParameterWithName:@"选脸位置" parameterID:PGSelection defaultX:.5 defaultY:.5 parameterFlags:0]&&ok;
|
||||
toggle(@"多人高速模式",PGFastMulti,NO); slider(PGJumpGuard);
|
||||
toggle(@"使用跟踪数据",PGTrackEnabled,NO);
|
||||
ok=[api addPopupMenuWithName:@"跟踪目标" parameterID:PGTarget defaultValue:0 menuEntries:@[@"目标 1",@"目标 2",@"目标 3"] parameterFlags:kFxParameterFlag_NOT_ANIMATABLE]&&ok;
|
||||
ok=[api addPushButtonWithName:@"向前分析" parameterID:PGAnalyzeForward selector:@selector(analyzeForward) parameterFlags:0]&&ok;
|
||||
ok=[api addPushButtonWithName:@"向后分析" parameterID:PGAnalyzeBackward selector:@selector(analyzeBackward) parameterFlags:0]&&ok;
|
||||
ok=[api addPushButtonWithName:@"清除当前目标" parameterID:PGClearTrack selector:@selector(clearTracking) parameterFlags:0]&&ok;
|
||||
ok=[api addStringParameterWithName:@"跟踪状态" parameterID:PGStatus defaultValue:@"选择画面中的人脸后开始分析" parameterFlags:kFxParameterFlag_NOT_ANIMATABLE|kFxParameterFlag_DISABLED]&&ok;
|
||||
toggle(@"显示人脸框",PGFaceBoxes,NO); toggle(@"显示跟踪框",PGTrackBoxes,NO); end();
|
||||
group(@"蒙版与输出",105); slider(PGThreshold); slider(PGSoftness); slider(PGEdgeProtect);
|
||||
toggle(@"预览蒙版",PGShowMask,NO); toggle(@"反转蒙版",PGInvertMask,NO);
|
||||
ok=[api addPopupMenuWithName:@"蒙版通道" parameterID:PGDebugChannel defaultValue:0 menuEntries:@[@"皮肤",@"牙齿",@"眼睛"] parameterFlags:0]&&ok;
|
||||
slider(PGMix); end();
|
||||
for(UInt32 identifier=PGTrackData1;identifier<=PGTrackData3;identifier++)
|
||||
ok=[api addStringParameterWithName:@"分析数据" parameterID:identifier defaultValue:@"" parameterFlags:kFxParameterFlag_HIDDEN|kFxParameterFlag_NOT_ANIMATABLE]&&ok;
|
||||
return ok ? YES : PGFail(error,@"宿主未能创建完整的人像美化参数。");
|
||||
}
|
||||
- (PGBeautySettings)settingsAtTime:(CMTime)time {
|
||||
PGBeautySettings s=PGBeautyDefaults();
|
||||
id<FxParameterRetrievalAPI_v6> api=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
|
||||
if(!api) return s;
|
||||
for(NSUInteger i=0;i<sizeof(PGSliders)/sizeof(PGSlider);i++) {
|
||||
PGSlider spec=PGSliders[i]; double value=*(float *)((uint8_t *)&s+spec.offset);
|
||||
if([api getFloatValue:&value fromParameter:spec.identifier atTime:time] && isfinite(value))
|
||||
*(float *)((uint8_t *)&s+spec.offset)=(float)fmin(spec.max,fmax(spec.min,value));
|
||||
}
|
||||
|
||||
struct {UInt32 identifier;size_t offset;} toggles[]={
|
||||
{PGFeatureProtect,offsetof(PGBeautySettings,featureProtection)}, {PGShowMask,offsetof(PGBeautySettings,showMask)},
|
||||
{PGInvertMask,offsetof(PGBeautySettings,invertMask)}, {PGFaceBoxes,offsetof(PGBeautySettings,showFaceBoxes)},
|
||||
{PGTrackBoxes,offsetof(PGBeautySettings,showTrackBox)}, {PGSingleFace,offsetof(PGBeautySettings,singleFace)},
|
||||
{PGFastMulti,offsetof(PGBeautySettings,fastMultiFace)}, {PGTrackEnabled,offsetof(PGBeautySettings,trackingEnabled)}};
|
||||
for(NSUInteger i=0;i<sizeof(toggles)/sizeof(toggles[0]);i++) {
|
||||
BOOL value=*(uint32_t *)((uint8_t *)&s+toggles[i].offset)!=0;
|
||||
[api getBoolValue:&value fromParameter:toggles[i].identifier atTime:time];
|
||||
*(uint32_t *)((uint8_t *)&s+toggles[i].offset)=value;
|
||||
}
|
||||
int value=0; [api getIntValue:&value fromParameter:PGTonePreset atTime:time]; s.skinTonePreset=MIN(6,MAX(0,value));
|
||||
value=0; [api getIntValue:&value fromParameter:PGDebugChannel atTime:time]; s.debugChannel=MIN(2,MAX(0,value));
|
||||
value=0; [api getIntValue:&value fromParameter:PGTarget atTime:time]; s.targetIndex=MIN(2,MAX(0,value));
|
||||
double x=.5,y=.5; [api getXValue:&x YValue:&y fromParameter:PGSelection atTime:time];
|
||||
s.selectionX=isfinite(x)?fmin(1,fmax(0,x)):.5; s.selectionY=isfinite(y)?fmin(1,fmax(0,y)):.5;
|
||||
BOOL enabled=YES;
|
||||
[api getBoolValue:&enabled fromParameter:PGSmoothingEnabled atTime:time]; if(!enabled) s.amount=s.midtoneDetail=0;
|
||||
enabled=YES; [api getBoolValue:&enabled fromParameter:PGColorEnabled atTime:time];
|
||||
if(!enabled) {s.skinBrightness=s.skinHue=s.skinSaturation=s.skinEvenTone=s.teethWhitening=s.eyeBrightening=0;s.skinTonePreset=0;}
|
||||
enabled=YES; [api getBoolValue:&enabled fromParameter:PGDetailEnabled atTime:time]; if(!enabled) s.highPassDetail=s.sharpness=0;
|
||||
enabled=YES; [api getBoolValue:&enabled fromParameter:PGShapeEnabled atTime:time];
|
||||
if(!enabled) s.faceThin=s.faceBigEye=s.faceShort=s.faceNarrow=s.faceChin=s.faceV=s.faceJaw=s.faceCheekbone=s.faceSmallHead=0;
|
||||
if(s.fastMultiFace) s.singleFace=0;
|
||||
return s;
|
||||
}
|
||||
- (NSArray<NSString *> *)trackStrings {
|
||||
id<FxParameterRetrievalAPI_v6> api=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
|
||||
NSMutableArray *tracks=[NSMutableArray new];
|
||||
for(UInt32 identifier=PGTrackData1;identifier<=PGTrackData3;identifier++) {
|
||||
NSString *value=@"";[api getStringParameterValue:&value fromParameter:identifier];
|
||||
[tracks addObject:value?:@""];
|
||||
}
|
||||
return tracks;
|
||||
}
|
||||
- (BOOL)pluginState:(NSData **)state atTime:(CMTime)time quality:(FxQuality)quality error:(NSError **)error {
|
||||
if(!state) return PGFail(error,@"缺少渲染状态输出位置。");
|
||||
if(![_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)]) return PGFail(error,@"无法读取宿主参数。");
|
||||
PGBeautySettings s=[self settingsAtTime:time];
|
||||
*state=[NSPropertyListSerialization dataWithPropertyList:@{@"settings":[NSData dataWithBytes:&s length:sizeof(s)],@"tracks":[self trackStrings]} format:NSPropertyListBinaryFormat_v1_0 options:0 error:error];
|
||||
return *state!=nil;
|
||||
}
|
||||
- (BOOL)destinationImageRect:(FxRect *)rect sourceImages:(NSArray<FxImageTile *> *)sources destinationImage:(FxImageTile *)destination pluginState:(NSData *)state atTime:(CMTime)time error:(NSError **)error {
|
||||
if(!rect||sources.count==0) return PGFail(error,@"缺少输入图像。");
|
||||
*rect=sources.firstObject.imagePixelBounds;return YES;
|
||||
}
|
||||
- (BOOL)sourceTileRect:(FxRect *)rect sourceImageIndex:(NSUInteger)index sourceImages:(NSArray<FxImageTile *> *)sources destinationTileRect:(FxRect)destinationRect destinationImage:(FxImageTile *)destination pluginState:(NSData *)state atTime:(CMTime)time error:(NSError **)error {
|
||||
if(!rect||index>=sources.count) return PGFail(error,@"输入图像索引无效。");
|
||||
*rect=sources[index].imagePixelBounds;return YES;
|
||||
}
|
||||
- (CIContext *)contextForTile:(FxImageTile *)tile {
|
||||
NSNumber *key=@(tile.deviceRegistryID);
|
||||
@synchronized(_contexts) {
|
||||
if(!_contexts[key]) {
|
||||
id<MTLDevice> device=nil;
|
||||
for(id<MTLDevice> candidate in MTLCopyAllDevices()) if(candidate.registryID==tile.deviceRegistryID) {device=candidate;break;}
|
||||
if(!device) device=MTLCreateSystemDefaultDevice();
|
||||
if(!device) return nil;
|
||||
_contexts[key]=[CIContext contextWithMTLDevice:device options:@{kCIContextCacheIntermediates:@NO,kCIContextWorkingColorSpace:NSNull.null,kCIContextOutputColorSpace:NSNull.null}];
|
||||
}
|
||||
return _contexts[key];
|
||||
}
|
||||
}
|
||||
- (BOOL)renderDestinationImage:(FxImageTile *)destination sourceImages:(NSArray<FxImageTile *> *)sources pluginState:(NSData *)state atTime:(CMTime)time error:(NSError **)error {
|
||||
if(sources.count==0||!sources.firstObject.ioSurface||!destination.ioSurface||![state isKindOfClass:NSData.class]||state.length==0||state.length>320*1024*1024) return PGFail(error,@"宿主传入的画面或状态无效。");
|
||||
NSDictionary *plist=[NSPropertyListSerialization propertyListWithData:state options:NSPropertyListImmutable format:nil error:error];
|
||||
if(![plist isKindOfClass:NSDictionary.class]||![plist[@"settings"] isKindOfClass:NSData.class]||[plist[@"settings"] length]!=sizeof(PGBeautySettings)) return PGFail(error,@"渲染状态版本无效。");
|
||||
PGBeautySettings s;[plist[@"settings"] getBytes:&s length:sizeof(s)];
|
||||
if(s.version!=1) return PGFail(error,@"此版本不支持保存的渲染状态。");
|
||||
CIImage *input=PGImageFromTile(sources.firstObject); if(!input) return PGFail(error,@"无法读取 IOSurface。");
|
||||
@synchronized(self) {
|
||||
if(!_reportedRender) {
|
||||
_reportedRender=YES;
|
||||
IOSurfaceRef src=(__bridge IOSurfaceRef)sources.firstObject.ioSurface;
|
||||
IOSurfaceRef dst=(__bridge IOSurfaceRef)destination.ioSurface;
|
||||
FxRect a=sources.firstObject.tilePixelBounds,b=destination.tilePixelBounds;
|
||||
os_log(PGRenderLog(), "source format=%u %zux%zu bytes=%zu planes=%zu origin=%lu bounds=%d,%d,%d,%d extent=%{public}@; destination format=%u %zux%zu bytes=%zu planes=%zu origin=%lu bounds=%d,%d,%d,%d; midtone=%f mix=%f mask=%u",
|
||||
IOSurfaceGetPixelFormat(src),IOSurfaceGetWidth(src),IOSurfaceGetHeight(src),IOSurfaceGetBytesPerElement(src),IOSurfaceGetPlaneCount(src),(unsigned long)sources.firstObject.imageOrigin,a.left,a.bottom,a.right,a.top,NSStringFromRect(input.extent),
|
||||
IOSurfaceGetPixelFormat(dst),IOSurfaceGetWidth(dst),IOSurfaceGetHeight(dst),IOSurfaceGetBytesPerElement(dst),IOSurfaceGetPlaneCount(dst),(unsigned long)destination.imageOrigin,b.left,b.bottom,b.right,b.top,s.midtoneDetail,s.mix,s.showMask);
|
||||
}
|
||||
}
|
||||
CIContext *context=[self contextForTile:destination]; if(!context) return PGFail(error,@"没有可用的 Metal 图形设备。");
|
||||
CIImage *output=nil; CIImage *overlay=nil;
|
||||
@synchronized(_faceProcessor) {
|
||||
NSArray *tracks=plist[@"tracks"];
|
||||
if(![tracks isKindOfClass:NSArray.class]||tracks.count!=3) return PGFail(error,@"跟踪状态无效。");
|
||||
for(id track in tracks) if(![track isKindOfClass:NSString.class]) return PGFail(error,@"跟踪记录无效。");
|
||||
if(![_loadedTracks isEqualToArray:tracks]) {
|
||||
_compatibleTrackMask=0;
|
||||
for(NSUInteger i=0;i<3;i++) {
|
||||
NSData *data=[[NSData alloc] initWithBase64EncodedString:tracks[i] options:0];
|
||||
BOOL compatible=PGCompatibleHostTrack(data);
|
||||
if(compatible) _compatibleTrackMask |= 1u<<i;
|
||||
[_faceProcessor setTrackingData:compatible?data:nil forTarget:i];
|
||||
}
|
||||
_loadedTracks=[tracks copy];
|
||||
}
|
||||
// Previous releases did not record which image-axis contract their
|
||||
// analysis used. Never replay potentially inverted coordinates.
|
||||
// Preserve the user's saved data and use realtime follow until they
|
||||
// reanalyze that target under this coordinate contract.
|
||||
if(s.trackingEnabled && !(_compatibleTrackMask & (1u<<MIN(s.targetIndex,2)))) s.trackingEnabled=0;
|
||||
NSError *stageError=nil;
|
||||
PGFaceFrame *frame=[_faceProcessor processImage:input settings:s time:time error:&stageError];
|
||||
if(!frame) { os_log_error(PGRenderLog(), "face processing failed: %{public}@",stageError); if(error)*error=stageError; return NO; }
|
||||
PGBeautySettings beauty=s; beauty.mix=1;
|
||||
output=[_beautyRenderer processImage:frame.image settings:beauty skinMask:frame.skinMask colorMask:frame.colorMask teethMask:frame.teethMask eyeMask:frame.eyeMask error:&stageError];
|
||||
if(!output) { os_log_error(PGRenderLog(), "beauty processing failed: %{public}@",stageError); if(error)*error=stageError; return NO; }
|
||||
overlay=frame.overlayImage;
|
||||
}
|
||||
if(!output) return NO;
|
||||
// Blend from the original frame so zero mix also disables geometry.
|
||||
if(s.mix<1 && !s.showMask) {
|
||||
output=[output imageByApplyingFilter:@"CIDissolveTransition" withInputParameters:@{kCIInputTargetImageKey:input,kCIInputTimeKey:@(1-s.mix)}];
|
||||
}
|
||||
if(overlay && !s.showMask) output=[overlay imageByCompositingOverImage:output];
|
||||
FxRect b=destination.tilePixelBounds;
|
||||
output=[output imageByApplyingTransform:CGAffineTransformMakeTranslation(-b.left,-b.bottom)];
|
||||
size_t width=IOSurfaceGetWidth((__bridge IOSurfaceRef)destination.ioSurface),height=IOSurfaceGetHeight((__bridge IOSurfaceRef)destination.ioSurface);
|
||||
@try {[context render:output toIOSurface:(__bridge IOSurfaceRef)destination.ioSurface bounds:CGRectMake(0,0,width,height) colorSpace:nil];}
|
||||
@catch(NSException *exception) {return PGFail(error,exception.reason?:@"图像处理失败。");}
|
||||
return YES;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------
|
||||
// pluginState:atTime:quality:error
|
||||
//
|
||||
// Your plug-in should get its parameter values, do any calculations it needs to
|
||||
// from those values, and package up the result to be used later with rendering.
|
||||
// The host application will call this method before rendering. The
|
||||
// FxParameterRetrievalAPI* is valid during this call. Use it to get the values of
|
||||
// your plug-in's parameters, then put those values or the results of any calculations
|
||||
// you need to do with those parameters to render into an NSData that you return
|
||||
// to the host application. The host will pass it back to you during subsequent calls.
|
||||
// Do not re-use the NSData; always create a new one as this method may be called
|
||||
// on multiple threads at the same time.
|
||||
//---------------------------------------------------------
|
||||
|
||||
- (BOOL)pluginState:(NSData**)pluginState
|
||||
atTime:(CMTime)renderTime
|
||||
quality:(FxQuality)qualityLevel
|
||||
error:(NSError**)error
|
||||
{
|
||||
BOOL succeeded = NO;
|
||||
id<FxParameterRetrievalAPI_v6> paramGetAPI = [_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
|
||||
if (paramGetAPI != nil)
|
||||
{
|
||||
double brightness = 1.0;
|
||||
[paramGetAPI getFloatValue:&brightness
|
||||
fromParameter:1
|
||||
atTime:renderTime];
|
||||
|
||||
*pluginState = [NSData dataWithBytes:&brightness
|
||||
length:sizeof(brightness)];
|
||||
|
||||
if (*pluginState != nil)
|
||||
{
|
||||
succeeded = YES;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (error != NULL)
|
||||
*error = [NSError errorWithDomain:FxPlugErrorDomain
|
||||
code:kFxError_ThirdPartyDeveloperStart + 20
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey :
|
||||
@"Unable to retrieve FxParameterRetrievalAPI_v6 in \
|
||||
[-pluginStateAtTime:]" }];
|
||||
}
|
||||
|
||||
return succeeded;
|
||||
- (void)setStatus:(NSString *)status {
|
||||
id<FxParameterSettingAPI_v5> api=[_apiManager apiForProtocol:@protocol(FxParameterSettingAPI_v5)];
|
||||
[api setStringParameterValue:status toParameter:PGStatus];
|
||||
}
|
||||
|
||||
//---------------------------------------------------------
|
||||
// destinationImageRect:sourceImages:destinationImage:pluginState:atTime:error
|
||||
//
|
||||
// This method will calculate the rectangular bounds of the output
|
||||
// image given the various inputs and plug-in state
|
||||
// at the given render time.
|
||||
// It will pass in an array of images, the plug-in state
|
||||
// returned from your plug-in's -pluginStateAtTime:error: method,
|
||||
// and the render time.
|
||||
//---------------------------------------------------------
|
||||
|
||||
- (BOOL)destinationImageRect:(FxRect *)destinationImageRect
|
||||
sourceImages:(NSArray<FxImageTile *> *)sourceImages
|
||||
destinationImage:(nonnull FxImageTile *)destinationImage
|
||||
pluginState:(NSData *)pluginState
|
||||
atTime:(CMTime)renderTime
|
||||
error:(NSError * _Nullable *)outError
|
||||
{
|
||||
if (sourceImages.count < 1)
|
||||
{
|
||||
NSLog (@"No inputImages list");
|
||||
return NO;
|
||||
}
|
||||
|
||||
// In the case of a filter that only changed RGB values,
|
||||
// the output rect is the same as the input rect.
|
||||
*destinationImageRect = sourceImages [ 0 ].imagePixelBounds;
|
||||
|
||||
return YES;
|
||||
|
||||
- (CMTime)currentHostTime {
|
||||
id<FxCustomParameterActionAPI_v4> api=[_apiManager apiForProtocol:@protocol(FxCustomParameterActionAPI_v4)];
|
||||
return api ? [api currentTime] : kCMTimeZero;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------
|
||||
// sourceTileRect:sourceImageIndex:sourceImages:destinationTileRect:destinationImage:pluginState:atTime:error
|
||||
//
|
||||
// Calculate tile of the source image we need
|
||||
// to render the given output tile.
|
||||
//---------------------------------------------------------
|
||||
|
||||
- (BOOL)sourceTileRect:(FxRect *)sourceTileRect
|
||||
sourceImageIndex:(NSUInteger)sourceImageIndex
|
||||
sourceImages:(NSArray<FxImageTile *> *)sourceImages
|
||||
destinationTileRect:(FxRect)destinationTileRect
|
||||
destinationImage:(FxImageTile *)destinationImage
|
||||
pluginState:(NSData *)pluginState
|
||||
atTime:(CMTime)renderTime
|
||||
error:(NSError * _Nullable *)outError
|
||||
{
|
||||
// Since this is a color-only filter, the input tile will be the same size as the output tile
|
||||
*sourceTileRect = destinationTileRect;
|
||||
|
||||
- (void)startAnalysisForward:(BOOL)forward {
|
||||
if(_analyzing) {[self setStatus:@"请先等待当前分析结束或取消分析。"];return;}
|
||||
NSError *error=nil;
|
||||
id<FxAnalysisAPI> api=[_apiManager apiForProtocol:@protocol(FxAnalysisAPI)];
|
||||
if(!api) {[self setStatus:@"当前宿主不提供帧分析接口。"];return;}
|
||||
_analysisTime=[self currentHostTime];
|
||||
PGBeautySettings s=[self settingsAtTime:_analysisTime];
|
||||
NSArray<NSString *> *tracks=[self trackStrings];
|
||||
// A separate processor owns the mutable analysis session. Render snapshots
|
||||
// cannot overwrite records being accumulated by the analyzer.
|
||||
_analysisFaceProcessor=[PGFaceProcessor new];
|
||||
NSData *previous=[[NSData alloc] initWithBase64EncodedString:tracks[s.targetIndex] options:0];
|
||||
[_analysisFaceProcessor setTrackingData:PGCompatibleHostTrack(previous)?previous:nil forTarget:s.targetIndex];
|
||||
_analysisSettings=s;_analysisSettings.trackingEnabled=1;_analysisSettings.singleFace=1;
|
||||
_analysisForward=forward;_analyzing=YES;
|
||||
BOOL ok=forward?[api startForwardAnalysis:kFxAnalysisLocation_CPU error:&error]:[api startBackwardAnalysis:kFxAnalysisLocation_CPU error:&error];
|
||||
if(!ok) _analyzing=NO;
|
||||
[self setStatus:ok?@"正在分析所选人脸…":(error.localizedDescription?:@"宿主未能开始帧分析。")];
|
||||
}
|
||||
- (void)analyzeForward {[self startAnalysisForward:YES];}
|
||||
- (void)analyzeBackward {[self startAnalysisForward:NO];}
|
||||
- (void)clearTracking {
|
||||
if(_analyzing) {[self setStatus:@"请先结束或取消分析,再清除跟踪数据。"];return;}
|
||||
PGBeautySettings s=[self settingsAtTime:[self currentHostTime]];
|
||||
@synchronized(_faceProcessor) {[_faceProcessor setTrackingData:nil forTarget:s.targetIndex];_loadedTracks=nil;}
|
||||
id<FxParameterSettingAPI_v5> api=[_apiManager apiForProtocol:@protocol(FxParameterSettingAPI_v5)];
|
||||
[api setStringParameterValue:@"" toParameter:PGTrackData1+s.targetIndex];
|
||||
[self setStatus:@"已清除当前目标的分析数据"];
|
||||
}
|
||||
- (BOOL)desiredAnalysisTimeRange:(CMTimeRange *)desired forInputWithTimeRange:(CMTimeRange)range error:(NSError **)error {
|
||||
if(!desired) return PGFail(error,@"缺少分析时间范围。");
|
||||
*desired=range;
|
||||
if(CMTIMERANGE_IS_VALID(range) && CMTIME_IS_NUMERIC(_analysisTime) && CMTimeRangeContainsTime(range,_analysisTime)) {
|
||||
if(_analysisForward) *desired=CMTimeRangeFromTimeToTime(_analysisTime,CMTimeRangeGetEnd(range));
|
||||
else {
|
||||
CMTime last=CMTimeAdd(_analysisTime,CMTimeMake(1,MAX(1,_analysisTime.timescale)));
|
||||
if(CMTimeCompare(last,CMTimeRangeGetEnd(range))>0) last=CMTimeRangeGetEnd(range);
|
||||
*desired=CMTimeRangeFromTimeToTime(range.start,last);
|
||||
}
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------
|
||||
// renderDestinationImage:sourceImages:pluginState:atTime:error:
|
||||
//
|
||||
// The host will call this method when it wants your plug-in to render an image
|
||||
// tile of the output image. It will pass in each of the input tiles needed as well
|
||||
// as the plug-in state needed for the calculations. Your plug-in should do all its
|
||||
// rendering in this method. It should not attempt to use the FxParameterRetrievalAPI*
|
||||
// object as it is invalid at this time. Note that this method will be called on
|
||||
// multiple threads at the same time.
|
||||
//---------------------------------------------------------
|
||||
|
||||
- (BOOL)renderDestinationImage:(FxImageTile *)destinationImage
|
||||
sourceImages:(NSArray<FxImageTile *> *)sourceImages
|
||||
pluginState:(NSData *)pluginState
|
||||
atTime:(CMTime)renderTime
|
||||
error:(NSError * _Nullable *)outError
|
||||
{
|
||||
if ((pluginState == nil) || (sourceImages [ 0 ].ioSurface == nil) || (destinationImage.ioSurface == nil))
|
||||
{
|
||||
NSDictionary* userInfo = @{
|
||||
NSLocalizedDescriptionKey : @"Invalid plugin state received from host"
|
||||
};
|
||||
if (outError != NULL)
|
||||
*outError = [NSError errorWithDomain:FxPlugErrorDomain
|
||||
code:kFxError_InvalidParameter
|
||||
userInfo:userInfo];
|
||||
return NO;
|
||||
- (BOOL)setupAnalysisForTimeRange:(CMTimeRange)range frameDuration:(CMTime)duration error:(NSError **)error {
|
||||
_analyzedFrames=0;_analyzing=YES;return YES;
|
||||
}
|
||||
- (BOOL)analyzeFrame:(FxImageTile *)frame atTime:(CMTime)time error:(NSError **)error {
|
||||
CIImage *image=PGImageFromTile(frame);if(!image) return PGFail(error,@"无法读取分析帧。");
|
||||
BOOL ok=NO;
|
||||
@synchronized(_analysisFaceProcessor) {ok=[_analysisFaceProcessor analyzeImage:image settings:_analysisSettings time:time error:error];}
|
||||
if(ok) _analyzedFrames++;
|
||||
return ok;
|
||||
}
|
||||
- (BOOL)cleanupAnalysis:(NSError **)error {
|
||||
_analyzing=NO;
|
||||
id<FxParameterSettingAPI_v5> api=[_apiManager apiForProtocol:@protocol(FxParameterSettingAPI_v5)];
|
||||
NSData *data=nil;
|
||||
@synchronized(_analysisFaceProcessor) {data=[_analysisFaceProcessor trackingDataForTarget:_analysisSettings.targetIndex];}
|
||||
data=PGMarkHostTrack(data);
|
||||
@synchronized(_faceProcessor) {_loadedTracks=nil;}
|
||||
if(data.length>64*1024*1024) {
|
||||
_analyzing=NO;
|
||||
return PGFail(error,@"当前目标的跟踪数据超过 64 MB。请将长素材分段分析;原先保存的数据仍保留。");
|
||||
}
|
||||
|
||||
// This is where you would access parameter values and other info about the source tile
|
||||
// from the pluginState.
|
||||
double brightness = 0.0;
|
||||
[pluginState getBytes:&brightness
|
||||
length:sizeof(brightness)];
|
||||
|
||||
// Set up the renderer, in this case we are using Metal.
|
||||
|
||||
MetalDeviceCache* deviceCache = [MetalDeviceCache deviceCache];
|
||||
MTLPixelFormat pixelFormat = [MetalDeviceCache MTLPixelFormatForImageTile:destinationImage];
|
||||
id<MTLCommandQueue> commandQueue = [deviceCache commandQueueWithRegistryID:sourceImages[0].deviceRegistryID
|
||||
pixelFormat:pixelFormat];
|
||||
if (commandQueue == nil)
|
||||
{
|
||||
return NO;
|
||||
if(data) {
|
||||
if(![api setStringParameterValue:[data base64EncodedStringWithOptions:0] toParameter:PGTrackData1+_analysisSettings.targetIndex]) return PGFail(error,@"无法将分析数据保存到工程。");
|
||||
[api setBoolValue:YES toParameter:PGTrackEnabled atTime:_analysisTime];
|
||||
}
|
||||
|
||||
id<MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer];
|
||||
commandBuffer.label = @"DynamicRegXPC Command Buffer";
|
||||
[commandBuffer enqueue];
|
||||
|
||||
id<MTLTexture> inputTexture = [sourceImages[0] metalTextureForDevice:[deviceCache deviceWithRegistryID:sourceImages[0].deviceRegistryID]];
|
||||
id<MTLTexture> outputTexture = [destinationImage metalTextureForDevice:[deviceCache deviceWithRegistryID:destinationImage.deviceRegistryID]];
|
||||
|
||||
MTLRenderPassColorAttachmentDescriptor* colorAttachmentDescriptor = [[MTLRenderPassColorAttachmentDescriptor alloc] init];
|
||||
colorAttachmentDescriptor.texture = outputTexture;
|
||||
colorAttachmentDescriptor.clearColor = MTLClearColorMake(1.0, 0.5, 0.0, 1.0);
|
||||
colorAttachmentDescriptor.loadAction = MTLLoadActionClear;
|
||||
MTLRenderPassDescriptor* renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor];
|
||||
renderPassDescriptor.colorAttachments [ 0 ] = colorAttachmentDescriptor;
|
||||
id<MTLRenderCommandEncoder> commandEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
|
||||
|
||||
// Rendering
|
||||
float outputWidth = (float)(destinationImage.tilePixelBounds.right - destinationImage.tilePixelBounds.left);
|
||||
float outputHeight = (float)(destinationImage.tilePixelBounds.top - destinationImage.tilePixelBounds.bottom);
|
||||
Vertex2D vertices[] = {
|
||||
{ { outputWidth / 2.0, -outputHeight / 2.0 }, { 1.0, 1.0 } },
|
||||
{ { -outputWidth / 2.0, -outputHeight / 2.0 }, { 0.0, 1.0 } },
|
||||
{ { outputWidth / 2.0, outputHeight / 2.0 }, { 1.0, 0.0 } },
|
||||
{ { -outputWidth / 2.0, outputHeight / 2.0 }, { 0.0, 0.0 } }
|
||||
};
|
||||
|
||||
MTLViewport viewport = {
|
||||
0, 0, outputWidth, outputHeight, -1.0, 1.0
|
||||
};
|
||||
[commandEncoder setViewport:viewport];
|
||||
|
||||
id<MTLRenderPipelineState> pipelineState = [deviceCache pipelineStateWithRegistryID:sourceImages[0].deviceRegistryID
|
||||
pixelFormat:pixelFormat];
|
||||
[commandEncoder setRenderPipelineState:pipelineState];
|
||||
|
||||
[commandEncoder setVertexBytes:vertices
|
||||
length:sizeof(vertices)
|
||||
atIndex:BVI_Vertices];
|
||||
|
||||
simd_uint2 viewportSize = {
|
||||
(unsigned int)(outputWidth),
|
||||
(unsigned int)(outputHeight)
|
||||
};
|
||||
[commandEncoder setVertexBytes:&viewportSize
|
||||
length:sizeof(viewportSize)
|
||||
atIndex:BVI_ViewportSize];
|
||||
|
||||
[commandEncoder setFragmentTexture:inputTexture
|
||||
atIndex:BTI_InputImage];
|
||||
|
||||
float fragmentBrightness = (float)brightness;
|
||||
[commandEncoder setFragmentBytes:&fragmentBrightness
|
||||
length:sizeof(fragmentBrightness)
|
||||
atIndex:BFI_Brightness];
|
||||
|
||||
[commandEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip
|
||||
vertexStart:0
|
||||
vertexCount:4];
|
||||
|
||||
[commandEncoder endEncoding];
|
||||
[commandBuffer commit];
|
||||
|
||||
[commandBuffer waitUntilCompleted];
|
||||
|
||||
[colorAttachmentDescriptor release];
|
||||
|
||||
[deviceCache returnCommandQueueToCache:commandQueue];
|
||||
|
||||
return YES;
|
||||
|
||||
[self setStatus:[NSString stringWithFormat:@"目标 %u:已分析 %lu 帧",_analysisSettings.targetIndex+1,(unsigned long)_analyzedFrames]];
|
||||
_analyzing=NO;return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,27 +1,34 @@
|
||||
//
|
||||
// AppDelegate.m
|
||||
// beatygril
|
||||
//
|
||||
// Created by dayu on 2026/10/21.
|
||||
//
|
||||
|
||||
#import "AppDelegate.h"
|
||||
|
||||
@interface AppDelegate ()
|
||||
|
||||
@property (weak) IBOutlet NSWindow *window;
|
||||
@end
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
||||
// Insert code here to initialize your application
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
|
||||
self.window.title=@"大鱼人像美化";
|
||||
[self.window setContentSize:NSMakeSize(600,360)];
|
||||
NSView *content=[NSView new];self.window.contentView=content;
|
||||
NSStackView *stack=[NSStackView new];stack.orientation=NSUserInterfaceLayoutOrientationVertical;
|
||||
stack.alignment=NSLayoutAttributeLeading;stack.spacing=16;stack.translatesAutoresizingMaskIntoConstraints=NO;
|
||||
[content addSubview:stack];
|
||||
[NSLayoutConstraint activateConstraints:@[[stack.leadingAnchor constraintEqualToAnchor:content.leadingAnchor constant:32],[stack.trailingAnchor constraintEqualToAnchor:content.trailingAnchor constant:-32],[stack.topAnchor constraintEqualToAnchor:content.topAnchor constant:30]]];
|
||||
NSTextField *title=[NSTextField labelWithString:@"大鱼人像美化"];
|
||||
title.font=[NSFont systemFontOfSize:28 weight:NSFontWeightSemibold];[stack addArrangedSubview:title];
|
||||
NSTextField *subtitle=[NSTextField labelWithString:@"beatygril · FxPlug 4"];
|
||||
subtitle.textColor=NSColor.secondaryLabelColor;[stack addArrangedSubview:subtitle];
|
||||
NSTextField *description=[NSTextField wrappingLabelWithString:@"在 Motion 的滤镜资源库中选择「大鱼 → 大鱼人像美化」。Final Cut Pro 的效果浏览器中也可使用配套效果。\n\n美化会随画面人脸自动更新,无需先点分析。脸型默认跟随主要人脸,多人模式可处理多张脸。启用「只处理选中人脸」后,可点击画面更换目标。向前/向后分析为可选功能,结果可随工程保存。"];
|
||||
description.font=[NSFont systemFontOfSize:13];[stack addArrangedSubview:description];
|
||||
NSStackView *buttons=[NSStackView new];buttons.orientation=NSUserInterfaceLayoutOrientationHorizontal;buttons.spacing=12;
|
||||
[buttons addArrangedSubview:[NSButton buttonWithTitle:@"打开 Motion" target:self action:@selector(openMotion:)]];
|
||||
[buttons addArrangedSubview:[NSButton buttonWithTitle:@"打开 Final Cut Pro" target:self action:@selector(openFinalCut:)]];
|
||||
[stack addArrangedSubview:buttons];
|
||||
NSTextField *note=[NSTextField wrappingLabelWithString:@"建议先调高中频磨皮,再按需调节肤色与脸型。肤色风格为本插件独立调色预设。"];
|
||||
note.textColor=NSColor.secondaryLabelColor;note.font=[NSFont systemFontOfSize:11];[stack addArrangedSubview:note];
|
||||
[self.window center];
|
||||
}
|
||||
|
||||
|
||||
- (void)applicationWillTerminate:(NSNotification *)aNotification {
|
||||
// Insert code here to tear down your application
|
||||
- (void)openMotion:(id)sender {[self openApplication:@"com.apple.motionapp"];}
|
||||
- (void)openFinalCut:(id)sender {[self openApplication:@"com.apple.FinalCut"];}
|
||||
- (void)openApplication:(NSString *)identifier {
|
||||
NSURL *url=[NSWorkspace.sharedWorkspace URLForApplicationWithBundleIdentifier:identifier];
|
||||
if(url) [NSWorkspace.sharedWorkspace openApplicationAtURL:url configuration:NSWorkspaceOpenConfiguration.configuration completionHandler:nil];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
@@ -17,9 +17,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>1.0.6</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>8</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# MSE18V2 分析与 beatygril 复刻
|
||||
|
||||
分析日期:2026-10-21。目标为用户指定的 `/Applications/MSE18V2.app`,原件保持只读。实现基于 PaintFCP 内原始 FxPlug4 工程,产品路径和插件 UUID 独立于原版。
|
||||
|
||||
## 证据与方法
|
||||
|
||||
REA 直接打开内嵌插件。原包为 arm64/x86_64 通用 Mach-O,Ghidra 无法保证通用包的分析切片,故用 `lipo -thin arm64` 创建临时分析副本,再用 REA/Ghidra 12.1.4 分析。所有反编译文本均为机器生成伪代码,属于静态观察与实现推断,不代表原始源码或运行记录。
|
||||
|
||||
- 原始插件 SHA-256:`6c5d74bc844a5c5f745718b48ea10ec6635d08aa08a5b6dc6e62653980849f8a`。
|
||||
- arm64 插件切片:`f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb`。
|
||||
- 外层 APP arm64 切片:`958d2ae106a023febe244397402cdc3f1977cbc529811335fca15b93ed569a10`。
|
||||
- 证据包:[插件](rea-plugin.json)、[外层 APP](rea-host.json)。参考 APP、metallib 和 LUT 保留在原位置,没有打包到本项目。
|
||||
|
||||
|问题|静态结果与证据|结论状态|
|
||||
|---|---|---|
|
||||
|APP 与插件职责|外层 `AppDelegate::applicationDidFinishLaunching:` 调用启动图窗口;主要功能在内嵌 `MSSuperSkinSmoothingPlugIn`。`ev_e57649ee7791e0ee7f491c2659e8aece223aca06c5a95f45406c5e27e5b0974b`|已回答|
|
||||
|参数与范围|`addParametersWithError:` 的常规参数含磨皮、肤色、细节、牙齿、九项脸型、选脸、跟踪和蒙版;多数原生参数隐藏,交由自定义面板。`ev_4ab9fb8e1643d141ad84355f7fde14e9269f94adadf9348b1175bf308cbcb7c1`|已回答主要参数,动态面板全部交互未运行覆盖|
|
||||
|处理顺序|`YUCIHighPassSkinSmoothing::outputImage` 与插件 `applySkinSmoothingToImage:` 提供局部细节、调色、锐化、预设、高频细节与牙齿处理入口。`ev_65936f315a3978bebcafd04e028acc5609c1c092282c05e0ca08cf2b279bc9ad`|已回答主链|
|
||||
|磨皮算法|局部/全局细节 helper 使用双 Gaussian 与亮度频带压缩;备用蒙版存在 G/B overlay、高通和三次 hard-light。`ev_0ea0cc3a7596fe3d6f4023cc4ab539bb81f65c6fdb3f28074712847e9dd048b8`|已回答主链,部分 mask 公式采用独立实现|
|
||||
|人脸与跟踪|Vision landmarks、原版 Metal 位移网格、OpenCV 动态库、历史脸框/关键点、选择及分析数据均有证据。`renderDestinationImage:` 及函数检索见插件证据包|部分回答;运动遮挡、切镜与完整身份逻辑未等价验证|
|
||||
|像素一致性|未获得原版相同输入/参数的可信渲染对照|未验证,不能声称逐像素一致|
|
||||
|
||||
## 原版 FCP 模板的实际发布范围
|
||||
|
||||
补充检查用户指定的 `~/Movies/Motion Templates.localized/Effects.localized/💙MS超级美颜/MS超级美颜V2/MS超级美颜V2.moef`:共15个发布入口,包括空名自定义面板入口、脸型、脸框预览、白牙、亮眼和混合,没有跟踪目标、跟踪开关或前向/后向分析按钮。清单和模板摘要见 [发布参数记录](reference-template-parameters.json)。二进制内部的离线分析工具不应据此解释成模板公开控件;本项目的三目标离线分析是独立扩展;原版无需离线分析就会自动跟随,不能从没有发布按钮推断没有自动跟随。
|
||||
|
||||
## 已实现的功能
|
||||
|
||||
- 中频磨皮、强度、半径、高频细节平滑、锐化、五官保护及各大功能分组开关。
|
||||
- 肤色提亮、色相、饱和度、均匀肤色;原色及自然白、冷白、暖白、红润白、小麦、古铜六个程序化风格。
|
||||
- 牙齿美白和眼睛提亮,基于 Vision 面部区域构建软蒙版。
|
||||
- 瘦脸、大眼、短脸、窄脸、下巴、V 脸、下颌、颧骨、小头和小头范围。
|
||||
- 默认自动主要人脸几何跟随、矩形防跳、关键点稳定、失检保持和时间快照;默认外观蒙版保留整组检测范围,显式选脸与快速多人分别控制。
|
||||
- 作为扩展提供三个独立跟踪目标,向前/向后分析,清除当前目标;精确 CMTime 数据持久化到宿主工程,可逆序和任意顺序读取。
|
||||
- 蒙版阈值/柔度/边缘保护、皮肤/牙齿/眼睛通道预览与反转、包含几何变形的整体混合。
|
||||
- 保留透明度、完整输入范围、非零图像原点与宿主上下原点;原生 FxPlug 参数可由宿主保存和制作关键帧。
|
||||
|
||||
## 复刻边界
|
||||
|
||||
本交付为功能相近的独立实现,算法结果存在以下差异:
|
||||
|
||||
1. 原版六个肤色 LUT 由独立颜色变换替代,风格名称接近,颜色值不相同。
|
||||
2. 原版人脸网格/曲线位移采用本项目的平滑椭圆采样位移替代。各脸型权重和曲线、眼嘴软蒙版由本项目编写。
|
||||
3. 皮肤支持区域使用 Vision 面部区域及独立的肤色/高通备用蒙版;原版完整皮肤分割、时间稳定和细部 mask 尚未等价恢复。
|
||||
4. 本版最多保存三个独立目标、每次最多处理十六张脸。原版存在动态目标列表与旧版三目标字段,本次未完整恢复动态列表上限及全部分页操作。
|
||||
5. 离线分析记录与默认实时自动跟随须分开比较。原版实时几何链已证实由Vision重检测、temporal主体关联、防跳范围、关键点稳定/保持及回看历史组成;OpenCV动态库与VNTrackObject备用方法的存在不足以证明它们驱动当前render主链。复刻版实时流程已按这些机制实现独立对齐,完整运动、多人交叉、遮挡与切镜仍需专项对照。
|
||||
6. 界面使用宿主的分组滑块与开关;原版卡片面板的布局没有照搬。眼睛提亮在本版可用,所分析原版主链实际将该项输入设为零。
|
||||
|
||||
跟踪数据每目标保存上限为 64 MB;超过上限时保留此前已保存的数据并报告错误。长素材应分段分析。仅实际验证通过的内容计入完成;本说明中的未知项不因编译或离屏测试通过而自动消除。
|
||||
|
||||
## 自动跟随实现范围
|
||||
|
||||
默认渲染已加入独立实时几何状态,完全不依赖三个离线目标或分析按钮。默认主要人脸按面积与居中评分、前框矩形防跳和最近中心关联;选点变化重新种子,快速多人采用多人变形。外观与变形几何分开,默认皮肤/牙齿/眼睛蒙版仍覆盖检测组,图像与蒙版再共享同一变形几何;显式“只处理选中人脸”保留本项目原有外观单脸语义,此显式范围未证明与原版完全等价。
|
||||
|
||||
自有稳定公式将旧关键点映射到当前框,再以0.9及时间因子混合,异常眼轴翻转时保持重映射点。最多1200条、序列化估算16MB的纯几何缓存不保存图像;仅使用不晚于请求且0.35秒内的参考,连续保持可推进输出时间。精确CMTime、原始几何与上下文一致的重复渲染直接复用,不反复平滑。原版还可取近未来历史,本版选择past-only,因此不是完全相同时间策略。
|
||||
|
||||
当前框仍以检测为准,稳定主要影响自有warp所使用的眼部点;没有恢复原版77点网格全部稳定公式。没有真正素材来源ID,同尺寸换源只能靠时间/原始几何变化隔离,不能保证识别任意换源。首次冷启动、热缓存与首次乱序可能存在差异;锁和缓存不等于全局顺序无关。
|
||||
|
||||
## 验证
|
||||
|
||||
`./scripts/test.sh` 运行实际 Metal/Core Image 像素测试及 Vision 人脸测试。测试素材是 scikit-image 发布的 NASA 公共领域人像,出处见 `tests/fixtures/ATTRIBUTION.txt`。测试包含实际人脸 landmarks、真实几何变化、五官蒙版、单次混合、透明度和跟踪存储恢复。
|
||||
|
||||
正式签名、公证、安装、宿主发现/加载以及最终远程恢复结果记录在 `docs/验证记录.md`。
|
||||
|
||||
## 默认自动跟随的补充分析
|
||||
|
||||
用户澄清后,已追查默认render→temporalFacePoints活跃路径,证据见 [自动跟随报告](auto-follow-report.json) 与 [REA证据包](rea-auto-tracking.json)。原版以Vision重检测当前帧,在默认非多人几何路径中自动选主要人脸;显式选点是重选种子,快速多人是独立分支。主要人脸初始化按面积与居中打分,其后将前框宽高扩到180%形成防跳矩形,在范围内选择最近中心。失检/无匹配可保持原点阵,关键点另有自适应稳定和轴向突变保护。默认几何单主脸不等于美肤/牙齿蒙版只能处理一人。
|
||||
|
||||
原版保存time/points/box历史,至多1200条并支持回看;平滑包含时间因子与约0.84–0.96的混合范围,完整组内公式及轴保护布尔组合未逐像素验证。原版存在近未来历史选择分支,所以不能宣称任意乱序严格确定。复刻将以独立公式实现机制,并限制读取当前时间之前的近时参考;该时间策略会比原版更保守,属于明确的实现差异。
|
||||
|
||||
本机原生观察:Motion中使用同一蓝发视频,原版UI默认多人关闭(单人)、防跳180%;启用检测框并调瘦脸/大眼后,播放时自动更新人脸框,没有先执行离线分析。此观察只覆盖该单人片段,不能替代多人遮挡与所有参数对照。
|
||||
|
||||
1.0.4已在Motion实际加载与不同播放头位置观察默认自动跟随输出;用户随后接管Motion/FCP自测,FCP该版自动跟随及显式OSC原生点击尚未完成自动验收。已完成范围与剩余验证见 [验证记录](../验证记录.md)。
|
||||
|
||||
## 最大瘦脸与转脸差异(用户复测反馈)
|
||||
|
||||
用户反馈跟踪正常,但最大瘦脸与原版不一致,侧脸出现异常变形。补充REA与Metal AIR分析确认,原版瘦脸以76点加中心点输入,执行17组轮廓到鼻部/瞳孔锚点的局部逆向采样;当前复刻采用矩形框水平对称场,两者没有对齐。已检查的GPU瘦脸路径没有显式yaw角度切换或侧脸衰减,不能将局部二维点自适应描述为完整3D姿态识别。详见 [脸型变形与转脸分析](脸型变形与转脸分析.md) 及 [结构报告](slim-pose-report.json)。本轮未修改安装版的变形代码。
|
||||
|
||||
## 1.0.5 对齐实现
|
||||
|
||||
已根据上述证据将瘦脸本身改为76点/17组局部逆向采样,`.025 × sizeGain`映射、UV距离、每组clamp和顺序均由独立数值场对照。没有复制原版metallib或模型。全76点参与当前实时稳定并保存离线记录;缺失完整点阵时不再退回整框瘦脸。其他8类几何控制、完整77点稳定系数和快速多人重叠采样仍有既有差异。阶段验证见验证记录;原分析中“1.0.4尚未修复”是历史状态。
|
||||
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"date": "2026-10-21",
|
||||
"scope": "只读静态 REA:原版无需离线分析的自动人脸跟随;未执行原版、未控制宿主 UI、未修改源码或原版。原生运行信息由 root 另行记录,不能把本报告静态结论称为已观察执行。",
|
||||
"target": {
|
||||
"original_binary": "/Applications/MSE18V2.app/Contents/PlugIns/MSE18V2.pluginkit/Contents/MacOS/MSE18V2",
|
||||
"original_sha256": "6c5d74bc844a5c5f745718b48ea10ec6635d08aa08a5b6dc6e62653980849f8a",
|
||||
"analyzed_arm64": "/tmp/paintfcp-rea.b3j9Te/MSE18V2-arm64",
|
||||
"analyzed_sha256": "f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb"
|
||||
},
|
||||
"evidence": {
|
||||
"new_bundle": "rea-auto-tracking.json",
|
||||
"existing_bundle": "rea-plugin.json",
|
||||
"parameter_population": "ev_4ab9fb8e1643d141ad84355f7fde14e9269f94adadf9348b1175bf308cbcb7c1",
|
||||
"render": "ev_65936f315a3978bebcafd04e028acc5609c1c092282c05e0ca08cf2b279bc9ad",
|
||||
"procedure_search": "ev_0719399b08d56c80e1ac22c44d76b02e1f05d307dcb5a95eac02f8dd0445ccb9",
|
||||
"detection_temporal_and_optional_tracker": "ev_2d9d2d633ad190e846ba2411ce96b44b9b12c0ee4d7f06e540673e8578628182",
|
||||
"tracker_dossier": "ev_445b4f2ff33a93ce32a49568a2417245fccd4c2fcf18d3bb12fef66e60d6c37b",
|
||||
"association_selection_rewind": "ev_65d6a413bebd10f2af76e06d523cde43146db3a53e7d028e4054e31c4b003990",
|
||||
"jump_guard_axis_guard_landmark_layout": "ev_ede612c32d16a78e3ac8ec9228fa0e71fcf2ffd827d99e649bdd73f5133163a5",
|
||||
"new_bundle_sha256": "dc1007f3798fade5e454bb2ef0b58e4cfe2bb0256948c364070fd9af70a97377"
|
||||
},
|
||||
"conclusion": "原版默认几何链为当前帧 Vision 人脸关键点检测 + 跨帧主要人脸关联 + 防跳范围 + 关键点稳定/保持与回看历史;不需要先按离线分析。仅加一个 VNTrackObjectRequest 或现有逐帧重检测,均不能等价复刻这条链。",
|
||||
"findings": [
|
||||
{
|
||||
"id": "live_entry",
|
||||
"confidence": "high-static",
|
||||
"addresses": [
|
||||
"0x10001cba4",
|
||||
"0x10002e4f4",
|
||||
"0x10002f700"
|
||||
],
|
||||
"observed": "renderDestinationImage 在需要几何、局部处理或预览时调用 cachedFaceObservations,再在没有持久跟踪框/选择框可用时直接调用 temporalFacePointsForObservations;该调用不以先完成离线分析为条件。原版字符串 vision_temporal 与 vision_temporal_held 位于此分支。",
|
||||
"limitations": "不是所有参数为零时都运行人脸检测;标签只证明代码分支,不能代替宿主运行日志。"
|
||||
},
|
||||
{
|
||||
"id": "detection_cache",
|
||||
"confidence": "high-static",
|
||||
"addresses": [
|
||||
"0x10002e4f4"
|
||||
],
|
||||
"observed": "缓存比较 CMTime、图像 extent 和 sourceTileRect;相同时间的相同区域可复用。否则创建 VNDetectFaceLandmarksRequest,失败/关键点不足可再试,结果包括空结果会缓存。共享缓存受锁保护。",
|
||||
"limitations": "allowForwardReuse 的可选时长常量未继续还原;render 的主要几何需求路径传 false,不能声称每帧复用旧检测。"
|
||||
},
|
||||
{
|
||||
"id": "default_primary_and_parameters",
|
||||
"confidence": "high-static",
|
||||
"addresses": [
|
||||
"0x10001b84c",
|
||||
"0x10002973c",
|
||||
"0x10002f700",
|
||||
"0x100023b60",
|
||||
"0x10002398c"
|
||||
],
|
||||
"observed": "id20(0x14)=faceSingleSelection,注册默认0,写入 params+0xf0;它控制显式选点种子,关闭时 temporalFacePoints 仍会自动选一个主要人脸。id39(0x27)=fastMultiFaceMode,注册默认0,写入 params+0xe2;开启时强制清除 faceSingleSelection。无前态、非显式选点时 helper 023b60 按面积与靠近画心的组合得分选一个人脸;有显式选点时 02398c 按选点到脸框中心的平方距离选种子。",
|
||||
"derived_formula": "默认 primary 得分约为 1.8*clamp(框面积,0,1)+1.1*(1-clamp(距画心/0.72,0,1));以标准化、裁剪后框为准,公式为反编译推导。",
|
||||
"implication": "faceSingleSelection=false 不等于默认处理全部人脸。UI 所谓多人模式对应 fastMultiFaceMode;不要将 id20 命名直译成 all/single 总开关。"
|
||||
},
|
||||
{
|
||||
"id": "association_jump_guard",
|
||||
"confidence": "high-static",
|
||||
"addresses": [
|
||||
"0x10002f700",
|
||||
"0x100031640",
|
||||
"0x1000319b8",
|
||||
"0x100025428"
|
||||
],
|
||||
"observed": "已有有效参考脸框时,025428 将其围绕中心按 faceJumpGuardRange 倍数扩成矩形并裁剪;031640 只保留脸框中心落在该范围内的候选;0319b8 在候选中选距上一框中心最近者。注册范围1..4,默认1.8即180%。候选不匹配/检测丢失时存在保持之前几何的分支,而非立即跳到画面另一个人。",
|
||||
"implication": "当前 PG 的圆形半径 guard 与原版矩形 guard 不同。原版证据显示几何关联,没有发现此链使用人脸 embedding/实名身份识别。",
|
||||
"limitations": "未建立任意遮挡时长下必然找回同一身份的承诺;不能把几何跟随称为生物识别。",
|
||||
"hold_details": "空检测或范围内无匹配时,valid参考>=77点的路径直接沿用旧点/旧框,返回strength=1,并可推进outputTime。恢复出的该默认控制流未见明确前向hold超时/逐帧衰减阈值;不得将0.35或0.45秒当作此处失检保留时限。"
|
||||
},
|
||||
{
|
||||
"id": "landmark_stability",
|
||||
"confidence": "high-static-structure-medium-exact-math",
|
||||
"addresses": [
|
||||
"0x10002f700",
|
||||
"0x100023e00",
|
||||
"0x100022c70"
|
||||
],
|
||||
"observed": "022c70 从 allPoints 取76点、变到全图标准坐标后追加一个中心点,后续链要求至少77点。temporalFacePoints 对同长度新旧数组进行混合;小于0.03秒的有效前向间隔有 dt/0.03 因子,一般混合系数由脸部左右几何对称程度调到约0.84..0.96并作0.82..0.96限制;部分点组另做更强稳定,点组59..75有额外修正,部分归一化位移限制为0.075。023e00 比较前后脸部轴向/几何,异常时可把旧点重映射到当前框并保持,render还有 axis_hold 分支。",
|
||||
"limitations": "Ghidra 对 ObjC/CGRect/CMTime ABI 恢复不完整,组名与完全等价公式尚未逐条汇编验证;建议先重建稳定机制再通过原生视频对照调系数,不能声称像素一致。",
|
||||
"axis_guard_constants": "023e00含框重叠约0.08、中心距离阈值max(0.04,0.82*参考框最大边)、轴长有效阈值max(0.003,0.055*较小框高)、轴方向点积阈值约-0.18等组合判定。它检查方向突变/轴翻转,返回后由调用者选择旧点重映射;系数与方向已恢复,整个布尔表达式尚未逐汇编校对。"
|
||||
},
|
||||
{
|
||||
"id": "time_seek_state",
|
||||
"confidence": "high-static",
|
||||
"addresses": [
|
||||
"0x100018fe0",
|
||||
"0x100018c10",
|
||||
"0x100019490",
|
||||
"0x10002f700"
|
||||
],
|
||||
"observed": "实例含 temporal points/box、detection time、output time、valid 及 rewind points/box/time/frames;历史保存 time/points/box 并排序,上限1200条,时间差<=0.0005秒替换原条目。reference helper 根据当前时间与 allowCurrentTime 选择历史:一个分支取不晚于当前时间的最新记录,另一个分支可选距当前0.0005..0.35秒的最近历史。temporal/reference 对倒退超过0.05秒有额外判定,限制直接沿用当前 temporal 状态。选择点位置变化>0.01与 selection revision 也参与重新选种子。",
|
||||
"limitations": "这不是简单 seek 就全部清空。最近历史分支可能使用时间上晚于请求的缓存,因此不能宣称原版任意乱序渲染严格无未来状态/完全确定性。首次远跳与长时间丢失的全部优先级未继续做运行验证。",
|
||||
"reselection_reset": "显式选择重种子时清理rewind references和部分待选框状态,缓存新的selection坐标与递增revision,随后新几何成为temporal参考。seek主要靠history/time-local reference,不是统一清空。"
|
||||
},
|
||||
{
|
||||
"id": "vn_object_tracker_presence_vs_reachability",
|
||||
"confidence": "high-static-presence-unproven-live-reachability",
|
||||
"addresses": [
|
||||
"0x10002ee74"
|
||||
],
|
||||
"observed": "trackedFaceBoxForInputImage 内确有 VNSequenceRequestHandler + VNTrackObjectRequest,检测框作 seed、支持 revision 选择、trackingLevel=0(本机 Vision header 为 Accurate)、performRequests:onCIImage:orientation:1、confidence>=0.22、时间间隔0.45秒及倒退/失败重置处理。",
|
||||
"bounded_negative": "该函数 dossier callers=[],仅两个 DATA incoming references;render/temporal pseudocode中未出现对它的调用,procedure搜索未找到其 objc_stub。",
|
||||
"inference": "它可能是备用/旧版/动态调用路径;当前已建立的 live render 证据是检测+temporal关联链。不得仅因导入存在便把自动跟随归因于 VNTrackObjectRequest。间接 ObjC 调用并未穷尽排除。"
|
||||
},
|
||||
{
|
||||
"id": "multi_and_beauty_scope",
|
||||
"confidence": "high-static",
|
||||
"addresses": [
|
||||
"0x10001cba4",
|
||||
"0x10002ae04"
|
||||
],
|
||||
"observed": "fastMultiFaceMode开启且有几何需求时,render另走检测矩形/landmark枚举,构建多套point sets并执行多人变形。默认几何使用单套temporal primary点。applySkinSmoothing 在局部美化/牙齿等条件下仍可获得整组faceObservations;全局肤色与皮肤频率处理也不等同于 primary 几何选脸。",
|
||||
"implication": "不要把默认几何单主脸扩大推断为所有美肤/牙齿只能处理一人。",
|
||||
"multi_details": "render fastMulti分支仅当fastMulti开启且非零几何参数产生需求时组装多点集;先枚举landmark候选,再枚举rectangle候选,使用统一block生成points/box,记录rememberFaceThinPointSets。后续points集>=2才走多套变形,只有一个时沿用单套路径。每张脸的具体去重/旧点补全block未继续反编译,不能保证它与默认单主脸temporal滤波完全相同。"
|
||||
}
|
||||
],
|
||||
"current_implementation_comparison": {
|
||||
"repo_head_at_read": "22cd96a772f84895aacd5ccc1ef24e0711e933fa",
|
||||
"source": "/Users/dayu/Documents/PaintFCP/beatygril/beatygril/Plugin/PGFaceProcessor.m",
|
||||
"source_sha256": "b5b5b57a4baaf1ba6d0d53af6602025c9897207826865b9cf215032f910b47d4",
|
||||
"findings": [
|
||||
"168..185: 每次 observationsInImage 创建 VNImageRequestHandler + VNDetectFaceLandmarksRequest;没有 live 几何缓存。",
|
||||
"214..243: selectOne=singleFace||trackingEnabled,默认两者false时取所有有效脸(最多16)。原版默认几何是单主脸,显式快速多人另开关。",
|
||||
"225..238: 当前 guard 是以max(width,height)*range为半径的圆+宽度比例限制;原版对应 helper 是扩展矩形内的候选中心筛选。",
|
||||
"288..315: trackingEnabled为false时不读取分析记录,也不给geometryForObservations传previous;因此默认实时帧没有跨帧主体关联、防跳、landmark smoothing/hold/replay状态。",
|
||||
"188..211和246..263: _targets是离线分析写入的持久记录;processImage不写历史。它保证当前设计的render顺序独立,但不能提供原版live temporal机制。",
|
||||
"需要自动跟随不等于需要用户点分析;默认检测确实每帧跟随脸部,但多人交叉/遮挡/检测抖动时无法稳定主体与关键点。"
|
||||
]
|
||||
},
|
||||
"recommended_minimum_alignment": [
|
||||
"仅在PGFaceProcessor新增实例级live状态,与离线_targets分离;无需新增UI或要求点分析。fastMultiFace决定多人几何;否则自动primary,singleFace决定显式选点种子。",
|
||||
"首帧primary按面积+居中评分;随后先用上一框的矩形180% guard筛候选,再选最近中心。选择点/target/revision变更时重新种子;短暂丢失保留几何,超出策略范围时重新检测且明确失效。",
|
||||
"在匹配身份后对同一布局关键点作时间感知平滑,必要时将旧点映射到新框;warp与其局部mask共享同一稳定几何。可以先用清晰的自有公式,再用原生运动片对照调整,不复制反编译文本。",
|
||||
"为相同CMTime/区域缓存结果,维护有界time-keyed几何快照;seek回看从近时历史恢复,离线轨迹优先、live为fallback。extent/来源/epoch/选择变更隔离状态。并发渲染下锁住状态快照和更新,避免跨时间抢写。",
|
||||
"时间乱序应明确定义:可只使用不晚于请求的参考,作为确定性设计选择;这是有意收紧原版最近历史行为,不应宣称完全同算法。",
|
||||
"先实现已证实的检测+关联+稳定链。VNTrackObjectRequest只能作为后续可测的优化/桥接;当前证据不足以将其列为必需对齐项。"
|
||||
],
|
||||
"meaningful_validation": [
|
||||
"不调用analyzeImage,默认参数但开启可见瘦脸:连续帧人脸移动时主体框/点位移动,分析记录保持空。",
|
||||
"两人接近/交叉:默认只改变primary;guard外更大的旁人不可抢占;显式fastMultiFace开启后双方可变形。",
|
||||
"选点更换另一人:立即重种子,后续移动自动跟随新主体,不反复选择距固定点击点最近的人。",
|
||||
"检测缺失1..数帧后恢复:几何短时保持;未匹配旁人不跳;超过自定hold策略须可验证回退。",
|
||||
"在固定脸框的抖动关键点序列上,输出关键点方差下降;真实平移不应长时间滞后,面部轴向翻转异常不应造成瞬时形变。",
|
||||
"同一时间重复/正向/倒放/远seek/并发请求:不会把远处未来几何写到请求帧,extent或选择变化不复用旧缓存;测试明确原版证据与自有确定性设计的差异。",
|
||||
"原生Motion/FCP使用同视频逐帧对照默认单主脸、显式选点、防跳与快速多人,记录实际主体验证,不能用纯mock替代宿主验收。"
|
||||
],
|
||||
"unresolved": [
|
||||
"VNTrackObject路径是否通过运行时动态发送调用;当前无运行追踪,静态无caller不等于绝对不可达。",
|
||||
"temporalFace全部系数、按点组语义、首帧/远seek/长丢失保持时间的完全等价行为。",
|
||||
"皮肤mask playbackStabilizedMask另有时序链;本次只确认其存在,未扩展到完整光流/蒙版跟踪还原。",
|
||||
"原生原版UI映射、实际render调用次数和宿主乱序行为由root当前运行观察补充。"
|
||||
],
|
||||
"documentation_correction": {
|
||||
"file": "/Users/dayu/Documents/PaintFCP/docs/analysis/REA-分析与复刻说明.md",
|
||||
"line": 46,
|
||||
"problem": "“原版OpenCV光流”把二进制依赖/其他代码存在推成当前实时几何主链的已证实实现。",
|
||||
"replacement": "本版离线分析记录与原版默认自动跟随须分开比较。原版实时几何链已证实由Vision检测、temporal主体关联、防跳范围、关键点稳定/保持及回看历史组成;OpenCV动态库和VNTrackObject备用方法存在,但不足以证明它们驱动当前render主链。完整运动、多人交叉、遮挡与切镜行为仍需同视频原生对照。"
|
||||
},
|
||||
"native_observation_by_root": {
|
||||
"host": "Motion 6.4",
|
||||
"test": "Same blue-haired portrait movie; ordinary local 25fps video project with original plugin UUID, face preview enabled and thin/bigEye set to 0.5 through original custom UI.",
|
||||
"result": "Original preview followed moving face during playback without offline analysis; default fast-multi switch read off/single, jump range 180%.",
|
||||
"scope": "Single-person smoke test; no claims of exhaustive identity/occlusion behavior."
|
||||
}
|
||||
}
|
||||
二进制文件未显示。
|
之后 宽度: | 高度: | 大小: 603 KiB |
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"date": "2026-10-21",
|
||||
"target_sha256": "f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb",
|
||||
"claim_type": "static CPU assembly/constants and Metal AIR reconstruction, independently tested; no native-host observation",
|
||||
"chin_pins": [
|
||||
[
|
||||
68,
|
||||
38
|
||||
],
|
||||
[
|
||||
67,
|
||||
37
|
||||
],
|
||||
[
|
||||
66,
|
||||
36
|
||||
]
|
||||
],
|
||||
"chin_parameter_index": 5,
|
||||
"signed_slider_range": [
|
||||
-1,
|
||||
1
|
||||
],
|
||||
"positive_scale": 0.04,
|
||||
"negative_scale": 0.02,
|
||||
"cpu_evidence": [
|
||||
"ev_640ccb714c5ce00d3864844af319fc574d223810dde08443d2ef2e8f1e989c47",
|
||||
"ev_8bc31392c07788d2482546121297c3f177d527282275ef21fd3e5edef9921544"
|
||||
],
|
||||
"air_sha256": "f97705b88f5491057df2117aef457ffca84e11020574cd57742b0670db4484d8",
|
||||
"air_file_local_ignored": "build/pose-analysis/face-thin.air.txt",
|
||||
"coordinate_contract": "Vision normalized y-up; upload pins and sampling coordinate converted once to y-down UV, result returned to CI y-up",
|
||||
"gain": "clamp(280/max(length((P75-P59)*imageSize),2*length((P67-(P74+P60)/2)*imageSize),1),1,2.25)",
|
||||
"pin_formula": "q=clamp(q-(anchor-origin)*ratio*clamp(1-distance(q,origin)/distance(anchor,origin),0,1),0,1)",
|
||||
"positive_visual_direction": "chin toward lower lip (shorter)",
|
||||
"negative_visual_direction": "chin away from lower lip (longer)",
|
||||
"verification": {
|
||||
"beauty": 49,
|
||||
"face": 116,
|
||||
"thin_chin": 394,
|
||||
"failures": 0,
|
||||
"maximum_uv_oracle_error": 1.09828022e-05,
|
||||
"realVideoASanChecks": 494,
|
||||
"realVideoFrames": 11,
|
||||
"independentAnatomicalChecks": 12,
|
||||
"fullChecks": 904,
|
||||
"hostAdapterASanChecks": 247,
|
||||
"oscASanChecks": 98,
|
||||
"allOriginTagsPixelIdentical": true,
|
||||
"maxByteErrorVsIndependentCIReference": 2,
|
||||
"oldFlipNegativeControl": {
|
||||
"checks": 120,
|
||||
"expectedFailures": 30,
|
||||
"topTopDefaultMaxByteError": 0,
|
||||
"topTopBigEyeMaxByteError": 66,
|
||||
"topTopChinPositiveMaxByteError": 52,
|
||||
"topTopChinNegativeMaxByteError": 21,
|
||||
"scope": "temporary current source with only two old flips restored; three-pin chin already fixed; not same as old Gaussian eye-hotspot counterexample"
|
||||
}
|
||||
},
|
||||
"limitations": [
|
||||
"Seven other shape controls remain approximations.",
|
||||
"Combined analytic controls and multi-face resampling order is not fully reference equivalent.",
|
||||
"Motion/FCP final visual validation is reserved for user."
|
||||
],
|
||||
"adapterCorrection": {
|
||||
"change": "remove imageOrigin-based input/output flips to match reference CI/IOSurface path",
|
||||
"legacyTrackPolicy": "preserve unmarked strings but use realtime follow; reanalysis writes hostCoordinateSpace=iosurface-ci-y-up-v1",
|
||||
"referenceOutputEvidence": [
|
||||
"ev_88a71ec62fade296495f1e3f5428cdb15ad4cda43866e09fb040fa3a4bdb0440",
|
||||
"ev_110d76f7958decb0c56b7daafe292d60a8bc56cd035b40a997f61395cb91ebfe"
|
||||
],
|
||||
"nativeBufferCaptured": false
|
||||
}
|
||||
}
|
||||
二进制文件未显示。
|
之后 宽度: | 高度: | 大小: 697 KiB |
@@ -0,0 +1 @@
|
||||
{"artifacts":[{"architecture":"arm64","digest":{"sha256":"f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb"},"format":"mach-o"}],"captures":[],"environments":[],"providers":[{"id":"ghidra","name":"Ghidra","version":"12.1.4"},{"id":"rea-workflow","name":"REA composed investigation workflow","version":"1"}],"records":[{"analysis_profile":{"digest":"20565088524a2d0359b9b96f46ac48e679d774c7b0092c56abc06de8fdbec11c","parameters":{"analyzer_preset":"ghidra-default","annotation_policy":"atomic-function-entry-metadata-v1","architecture":"arm64","available_architectures":["arm64"],"compiler_spec_id":"auto-default","decompiler_jump_loads":true,"function_body_evidence":"complete-inclusive-ranges-v1","function_boundary_observations":"ghidra-terminal-call-limitations-v1","function_references":"complete-body-and-entry-reference-manager-v2","import_mode":"ephemeral-source-immutable","instruction_flow_evidence":"decoded-return-pcode-v1","jump_table_evidence":"typed-case-default-blocks-v1","language_id":"auto-from-header","load_image_observations":"source-mappings-entry-context-v2","loader":"auto-from-header","location_resolution":"explicit-address-exact-entry-symbol-first-v3","no_return_repair":"returning-imports-decoded-return-v2","process_launch":"inspected-jvm-launch-support-v1","string_inventory_evidence":"defined-data-coverage-v1","target_format":"mach-o","target_kind":"executable"},"provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"}},"authority":"shipped-artifact","confidence":"observed","environment":null,"evidence_id":"ev_0719399b08d56c80e1ac22c44d76b02e1f05d307dcb5a95eac02f8dd0445ccb9","evidence_links":[],"limitations":["The session serves operations only after Ghidra reports default auto-analysis complete; incomplete analysis does not expose partial results.","The imported Program and temporary project are ephemeral and deleted on close. Annotation operations edit session database metadata; original executable bytes are never written.","Cancelling an active Ghidra request terminates its ephemeral database and discards session annotations. The selected target remains open; the next provider query imports the original artifact again.","Default-space addresses use lowercase 0x-prefixed hexadecimal; other address spaces use <percent-encoded-space>:0x<hex>.","Regex mode uses Java Pattern semantics. Matcher or compiler stack exhaustion returns a resource constraint without closing the session; literal mode avoids regex recursion and returns complete matching values.","macOS sessions require a matching executable Ghidra native decompiler; REA checks for it but does not build native components or change Gatekeeper quarantine state."],"locations":[],"normalized_result":[{"address":"0x100008948","value":"MSSSectionCardParameterValue::showFaceTrackBox"},{"address":"0x100008950","value":"MSSSectionCardParameterValue::setShowFaceTrackBox:"},{"address":"0x1000089b8","value":"MSSSectionCardParameterValue::faceTrackEnabled"},{"address":"0x1000089c0","value":"MSSSectionCardParameterValue::setFaceTrackEnabled:"},{"address":"0x1000089c8","value":"MSSSectionCardParameterValue::faceTrackStatus"},{"address":"0x1000089d0","value":"MSSSectionCardParameterValue::setFaceTrackStatus:"},{"address":"0x1000089d8","value":"MSSSectionCardParameterValue::faceTrackRevision"},{"address":"0x1000089e0","value":"MSSSectionCardParameterValue::setFaceTrackRevision:"},{"address":"0x1000089e8","value":"MSSSectionCardParameterValue::faceTrackFrameCount"},{"address":"0x1000089f0","value":"MSSSectionCardParameterValue::setFaceTrackFrameCount:"},{"address":"0x1000089f8","value":"MSSSectionCardParameterValue::faceTrackSeedTimeSeconds"},{"address":"0x100008a00","value":"MSSSectionCardParameterValue::setFaceTrackSeedTimeSeconds:"},{"address":"0x100008a08","value":"MSSSectionCardParameterValue::faceTrackProgress"},{"address":"0x100008a10","value":"MSSSectionCardParameterValue::setFaceTrackProgress:"},{"address":"0x100008a18","value":"MSSSectionCardParameterValue::faceTrackAppendMode"},{"address":"0x100008a20","value":"MSSSectionCardParameterValue::setFaceTrackAppendMode:"},{"address":"0x100008a28","value":"MSSSectionCardParameterValue::faceTrackData"},{"address":"0x100008a30","value":"MSSSectionCardParameterValue::setFaceTrackData:"},{"address":"0x100008a38","value":"MSSSectionCardParameterValue::faceTrackTargetCount"},{"address":"0x100008a40","value":"MSSSectionCardParameterValue::setFaceTrackTargetCount:"},{"address":"0x100008a48","value":"MSSSectionCardParameterValue::faceTrackActiveTargetIndex"},{"address":"0x100008a50","value":"MSSSectionCardParameterValue::setFaceTrackActiveTargetIndex:"},{"address":"0x100008a58","value":"MSSSectionCardParameterValue::faceTrackAnalysisTargetIndex"},{"address":"0x100008a60","value":"MSSSectionCardParameterValue::setFaceTrackAnalysisTargetIndex:"},{"address":"0x100008a68","value":"MSSSectionCardParameterValue::faceTrackTargetPageStartIndex"},{"address":"0x100008a70","value":"MSSSectionCardParameterLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"artifacts":[{"architecture":"arm64","digest":{"sha256":"f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb"},"format":"mach-o"}],"captures":[],"environments":[],"providers":[{"id":"ghidra","name":"Ghidra","version":"12.1.4"}],"records":[{"analysis_profile":{"digest":"20565088524a2d0359b9b96f46ac48e679d774c7b0092c56abc06de8fdbec11c","parameters":{"analyzer_preset":"ghidra-default","annotation_policy":"atomic-function-entry-metadata-v1","architecture":"arm64","available_architectures":["arm64"],"compiler_spec_id":"auto-default","decompiler_jump_loads":true,"function_body_evidence":"complete-inclusive-ranges-v1","function_boundary_observations":"ghidra-terminal-call-limitations-v1","function_references":"complete-body-and-entry-reference-manager-v2","import_mode":"ephemeral-source-immutable","instruction_flow_evidence":"decoded-return-pcode-v1","jump_table_evidence":"typed-case-default-blocks-v1","language_id":"auto-from-header","load_image_observations":"source-mappings-entry-context-v2","loader":"auto-from-header","location_resolution":"explicit-address-exact-entry-symbol-first-v3","no_return_repair":"returning-imports-decoded-return-v2","process_launch":"inspected-jvm-launch-support-v1","string_inventory_evidence":"defined-data-coverage-v1","target_format":"mach-o","target_kind":"executable"},"provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"}},"authority":"shipped-artifact","confidence":"observed","environment":null,"evidence_id":"ev_640ccb714c5ce00d3864844af319fc574d223810dde08443d2ef2e8f1e989c47","evidence_links":[],"limitations":["The session serves operations only after Ghidra reports default auto-analysis complete; incomplete analysis does not expose partial results.","The imported Program and temporary project are ephemeral and deleted on close. Annotation operations edit session database metadata; original executable bytes are never written.","Cancelling an active Ghidra request terminates its ephemeral database and discards session annotations. The selected target remains open; the next provider query imports the original artifact again.","Default-space addresses use lowercase 0x-prefixed hexadecimal; other address spaces use <percent-encoded-space>:0x<hex>.","Bytes reflect Ghidra's initialized memory, including loader fixups. Reads stop at an unmapped or uninitialized byte and report completeness explicitly.","macOS sessions require a matching executable Ghidra native decompiler; REA checks for it but does not build native components or change Gatekeeper quarantine state."],"locations":[],"normalized_result":{"address":"0x10005f9c0","bytes_hex":"7b14ae47e17a943f7b14ae47e17aa43f","complete":true,"requested_bytes":16,"returned_bytes":16},"operation":"read_bytes","parameters":{"address":"0x10005f9c0","length":16},"predicate_type":"rea.analysis","provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"},"raw_result":{"address":"0x10005f9c0","bytes_hex":"7b14ae47e17a943f7b14ae47e17aa43f","complete":true,"requested_bytes":16,"returned_bytes":16},"subject":{"architecture":"arm64","digest":{"sha256":"f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb"},"format":"mach-o","local_path":"/private/tmp/paintfcp-rea.b3j9Te/MSE18V2-arm64","name":"MSE18V2-arm64"}},{"analysis_profile":{"digest":"20565088524a2d0359b9b96f46ac48e679d774c7b0092c56abc06de8fdbec11c","parameters":{"analyzer_preset":"ghidra-default","annotation_policy":"atomic-function-entry-metadata-v1","architecture":"arm64","available_architectures":["arm64"],"compiler_spec_id":"auto-default","decompiler_jump_loads":true,"function_body_evidence":"complete-inclusive-ranges-v1","function_boundary_observations":"ghidra-terminal-call-limitations-v1","function_references":"complete-body-and-entry-reference-manager-v2","import_mode":"ephemeral-source-immutable","instruction_flow_evidence":"decoded-return-pcode-v1","jump_table_evidence":"typed-case-default-blocks-v1","language_id":"auto-from-header","load_image_observations":"source-mappings-entry-context-v2","loader":"auto-from-header","location_resolution":"explicit-address-exact-entry-symbol-first-v3","no_return_repair":"returning-imports-decoded-return-v2","process_launch":"inspected-jvm-launch-support-v1","string_inventory_evidence":"defined-data-coverage-v1","target_format":"mach-o","target_kind":"executable"},"provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"}},"authority":"shipped-artifact","confidence":"observed","environment":null,"evidence_id":"ev_8bc31392c07788d2482546121297c3f177d527282275ef21fd3e5edef9921544","evidence_links":[],"limitations":["The session serves operations only after Ghidra reports default auto-analysis complete; incomplete analysis does not expose partial results.","The imported Program and temporary project are ephemeral and deleted on close. Annotation operations edit session database metadata; original executable bytes are never written.","Cancelling an active Ghidra request terminates its ephemeral database and discards Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"artifacts":[{"architecture":"arm64","digest":{"sha256":"958d2ae106a023febe244397402cdc3f1977cbc529811335fca15b93ed569a10"},"format":"mach-o"}],"captures":[],"environments":[],"providers":[{"id":"ghidra","name":"Ghidra","version":"12.1.4"},{"id":"rea-workflow","name":"REA composed investigation workflow","version":"1"}],"records":[{"analysis_profile":{"digest":"20565088524a2d0359b9b96f46ac48e679d774c7b0092c56abc06de8fdbec11c","parameters":{"analyzer_preset":"ghidra-default","annotation_policy":"atomic-function-entry-metadata-v1","architecture":"arm64","available_architectures":["arm64"],"compiler_spec_id":"auto-default","decompiler_jump_loads":true,"function_body_evidence":"complete-inclusive-ranges-v1","function_boundary_observations":"ghidra-terminal-call-limitations-v1","function_references":"complete-body-and-entry-reference-manager-v2","import_mode":"ephemeral-source-immutable","instruction_flow_evidence":"decoded-return-pcode-v1","jump_table_evidence":"typed-case-default-blocks-v1","language_id":"auto-from-header","load_image_observations":"source-mappings-entry-context-v2","loader":"auto-from-header","location_resolution":"explicit-address-exact-entry-symbol-first-v3","no_return_repair":"returning-imports-decoded-return-v2","process_launch":"inspected-jvm-launch-support-v1","string_inventory_evidence":"defined-data-coverage-v1","target_format":"mach-o","target_kind":"executable"},"provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"}},"authority":"shipped-artifact","confidence":"observed","environment":null,"evidence_id":"ev_bf25a1cd9c181e8f034fdee81259118798ae67e640ade71238f4afb707eab142","evidence_links":[],"limitations":["The session serves operations only after Ghidra reports default auto-analysis complete; incomplete analysis does not expose partial results.","The imported Program and temporary project are ephemeral and deleted on close. Annotation operations edit session database metadata; original executable bytes are never written.","Cancelling an active Ghidra request terminates its ephemeral database and discards session annotations. The selected target remains open; the next provider query imports the original artifact again.","Default-space addresses use lowercase 0x-prefixed hexadecimal; other address spaces use <percent-encoded-space>:0x<hex>.","Regex mode uses Java Pattern semantics. Matcher or compiler stack exhaustion returns a resource constraint without closing the session; literal mode avoids regex recursion and returns complete matching values.","macOS sessions require a matching executable Ghidra native decompiler; REA checks for it but does not build native components or change Gatekeeper quarantine state."],"locations":[],"normalized_result":[{"address":"0x100000c24","value":"AppDelegate::applicationDidFinishLaunching:"},{"address":"0x100000c28","value":"AppDelegate::mss_configureLaunchArtworkWindow"},{"address":"0x100000eb4","value":"AppDelegate::mss_pixelSizeForImage:"},{"address":"0x10000100c","value":"AppDelegate::mss_backingScaleFactorForScreen:"},{"address":"0x10000104c","value":"AppDelegate::mss_fittedSizeForSize:onScreen:"},{"address":"0x1000010e4","value":"AppDelegate::window"},{"address":"0x1000010ec","value":"AppDelegate::setWindow:"}],"operation":"search_procedures","parameters":{"case_sensitive":false,"mode":"regex","pattern":"(AppDelegate|ViewController|WindowController)"},"predicate_type":"rea.analysis","provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"},"raw_result":[{"address":"0x100000c24","value":"AppDelegate::applicationDidFinishLaunching:"},{"address":"0x100000c28","value":"AppDelegate::mss_configureLaunchArtworkWindow"},{"address":"0x100000eb4","value":"AppDelegate::mss_pixelSizeForImage:"},{"address":"0x10000100c","value":"AppDelegate::mss_backingScaleFactorForScreen:"},{"address":"0x10000104c","value":"AppDelegate::mss_fittedSizeForSize:onScreen:"},{"address":"0x1000010e4","value":"AppDelegate::window"},{"address":"0x1000010ec","value":"AppDelegate::setWindow:"}],"subject":{"architecture":"arm64","digest":{"sha256":"958d2ae106a023febe244397402cdc3f1977cbc529811335fca15b93ed569a10"},"format":"mach-o","local_path":"/private/tmp/paintfcp-rea.b3j9Te/MSE18V2-host-arm64","name":"MSE18V2-host-arm64"}},{"analysis_profile":{"digest":"575a577ce88c350b8ef1d4768986d124a16377363cbf22c68a4f73cc917881d4","parameters":{"upstream_analysis_profile":{"digest":"20565088524a2d0359b9b96f46ac48e679d774c7b0092c56abc06de8fdbec11c","parameters":{"analyzer_preset":"ghidra-default","annotation_policy":"atomic-function-entry-metadata-v1","architecture":"arm64","available_architectures":["arm64"],"compiler_spec_id":"auto-default","decompiler_jump_loads":true,"function_body_evidence":"complete-inclusive-ranges-v1","function_boundary_observations":"ghidra-terminal-call-limitations-v1","function_references":"complete-body-and-entry-reference-manager-v2","import_mode":"ephemeral-source-immutable","instruction_flow_evidence":"decoded-return-pcode-v1","jump_table_evidenLine truncated
|
||||
文件差异过大,未显示。
加载差异
@@ -0,0 +1 @@
|
||||
{"artifacts":[{"architecture":"arm64","digest":{"sha256":"6c5d74bc844a5c5f745718b48ea10ec6635d08aa08a5b6dc6e62653980849f8a"},"format":"mach-o"},{"architecture":"arm64","digest":{"sha256":"f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb"},"format":"mach-o"}],"captures":[],"environments":[],"providers":[{"id":"ghidra","name":"Ghidra","version":"12.1.4"},{"id":"native-macos","name":"macOS native inspection utilities","version":null},{"id":"rea-workflow","name":"REA composed investigation workflow","version":"1"}],"records":[{"analysis_profile":{"digest":"575a577ce88c350b8ef1d4768986d124a16377363cbf22c68a4f73cc917881d4","parameters":{"upstream_analysis_profile":{"digest":"20565088524a2d0359b9b96f46ac48e679d774c7b0092c56abc06de8fdbec11c","parameters":{"analyzer_preset":"ghidra-default","annotation_policy":"atomic-function-entry-metadata-v1","architecture":"arm64","available_architectures":["arm64"],"compiler_spec_id":"auto-default","decompiler_jump_loads":true,"function_body_evidence":"complete-inclusive-ranges-v1","function_boundary_observations":"ghidra-terminal-call-limitations-v1","function_references":"complete-body-and-entry-reference-manager-v2","import_mode":"ephemeral-source-immutable","instruction_flow_evidence":"decoded-return-pcode-v1","jump_table_evidence":"typed-case-default-blocks-v1","language_id":"auto-from-header","load_image_observations":"source-mappings-entry-context-v2","loader":"auto-from-header","location_resolution":"explicit-address-exact-entry-symbol-first-v3","no_return_repair":"returning-imports-decoded-return-v2","process_launch":"inspected-jvm-launch-support-v1","string_inventory_evidence":"defined-data-coverage-v1","target_format":"mach-o","target_kind":"executable"},"provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"}},"workflow_revision":2},"provider":{"id":"rea-workflow","name":"REA composed investigation workflow","version":"1"}},"authority":"shipped-artifact","confidence":"derived","environment":null,"evidence_id":"ev_0ea0cc3a7596fe3d6f4023cc4ab539bb81f65c6fdb3f28074712847e9dd048b8","evidence_links":[],"limitations":["Derived by an REA composed workflow."],"locations":[{"address":"0x1000353b0","kind":"address"},{"address":"0x1000370a0","kind":"address"},{"address":"0x1000373ec","kind":"address"},{"address":"0x100035c2c","kind":"address"}],"normalized_result":{"failed":0,"items":[{"address":"0x1000353b0","procedure":{"address":"0x1000353b0","name":"YUCIHighPassSkinSmoothingMaskGenerator::outputImage","status":"resolved"},"pseudocode":"\n/* WARNING: Function: _objc_autorelease replaced with injection: _objc_retain_fixup */\n/* Function Stack Size: 0x10 bytes */\n\nID YUCIHighPassSkinSmoothingMaskGenerator::outputImage(ID param_1,SEL param_2)\n\n{\n ID IVar1;\n ID IVar2;\n class_t *pcVar3;\n class_t *self;\n \n IVar1 = CIFilter::filterWithName_((ID)&_OBJC_CLASS___CIFilter,&cf_CIExposureAdjust);\n IVar2 = inputImage(param_1);\n CIFilter::setValue_forKey_(IVar1,IVar2,*(undefined8 *)PTR__kCIInputImageKey_1000781a8);\n CIFilter::setValue_forKey_\n (IVar1,&PTR__OBJC_CLASS___NSConstantDoubleNumber_10007d848,\n *(undefined8 *)PTR__kCIInputEVKey_1000781a0);\n pcVar3 = &objc::class_t::YUCIGreenBlueChannelOverlayBlend;\n _objc_alloc_init();\n IVar1 = CIFilter::outputImage(IVar1);\n objc_stub::setInputImage_((ID)pcVar3,IVar1);\n self = &objc::class_t::YUCIHighPass;\n _objc_alloc_init();\n IVar1 = objc_stub::outputImage((ID)pcVar3);\n objc_stub::setInputImage_((ID)self,IVar1);\n IVar1 = inputRadius(param_1);\n objc_stub::setInputRadius_((ID)self,IVar1);\n pcVar3 = &objc::class_t::YUCIHighPassSkinSmoothingMaskBoost;\n _objc_alloc_init();\n IVar1 = objc_stub::outputImage((ID)self);\n objc_stub::setInputImage_((ID)pcVar3,IVar1);\n IVar1 = objc_stub::outputImage((ID)pcVar3);\n return IVar1;\n}\n\n","status":"ok"},{"address":"0x1000370a0","procedure":{"address":"0x1000370a0","name":"FUN_1000370a0","status":"resolved"},"pseudocode":"\nvoid FUN_1000370a0(double param_1,undefined8 param_2,undefined8 param_3,undefined8 param_4,\n ID param_5,undefined8 param_6)\n\n{\n ID IVar1;\n ID IVar2;\n ID IVar3;\n undefined8 uVar4;\n undefined8 uVar5;\n ID IVar6;\n double dVar7;\n double dVar8;\n double dVar9;\n double dVar10;\n undefined8 extraout_d0;\n double dVar11;\n double dVar12;\n ID local_b8;\n ID IStack_b0;\n ID local_a8;\n ID local_a0;\n long local_98;\n \n local_98 = *(long *)PTR____stack_chk_guard_1000780b8;\n if (param_5 != 0) {\n dVar7 = 1.0;\n if (param_1 <= 1.0) {\n dVar7 = param_1;\n }\n dVar8 = 0.0;\n if (0.0 <= param_1) {\n dVar8 = dVar7 + dVar7;\n }\n dVar7 = 2.0;\n if (dVar8 <= 2.0) {\n dVar7 = dVar8;\n }\n dVar11 = 0.0;\n if (0.0 <= dVar8) {\n dVar11 = dVar7;\n }\n IVar6 = param_5;\n if (2.220446049250313e-16 < dVar11) {\n dVar11 = dVar11 * 0.5;\n dVar7 = dVar11;\n _pow(dVar11,0x4001333333333333);\n Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"artifacts":[{"architecture":"arm64","digest":{"sha256":"f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb"},"format":"mach-o"}],"captures":[],"environments":[],"providers":[{"id":"ghidra","name":"Ghidra","version":"12.1.4"}],"records":[{"analysis_profile":{"digest":"20565088524a2d0359b9b96f46ac48e679d774c7b0092c56abc06de8fdbec11c","parameters":{"analyzer_preset":"ghidra-default","annotation_policy":"atomic-function-entry-metadata-v1","architecture":"arm64","available_architectures":["arm64"],"compiler_spec_id":"auto-default","decompiler_jump_loads":true,"function_body_evidence":"complete-inclusive-ranges-v1","function_boundary_observations":"ghidra-terminal-call-limitations-v1","function_references":"complete-body-and-entry-reference-manager-v2","import_mode":"ephemeral-source-immutable","instruction_flow_evidence":"decoded-return-pcode-v1","jump_table_evidence":"typed-case-default-blocks-v1","language_id":"auto-from-header","load_image_observations":"source-mappings-entry-context-v2","loader":"auto-from-header","location_resolution":"explicit-address-exact-entry-symbol-first-v3","no_return_repair":"returning-imports-decoded-return-v2","process_launch":"inspected-jvm-launch-support-v1","string_inventory_evidence":"defined-data-coverage-v1","target_format":"mach-o","target_kind":"executable"},"provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"}},"authority":"shipped-artifact","confidence":"observed","environment":null,"evidence_id":"ev_02f734a0d72f1b7b77a3112d1de297b9e99ea43d5377349e6eebb71ab017d650","evidence_links":[],"limitations":["The session serves operations only after Ghidra reports default auto-analysis complete; incomplete analysis does not expose partial results.","The imported Program and temporary project are ephemeral and deleted on close. Annotation operations edit session database metadata; original executable bytes are never written.","Cancelling an active Ghidra request terminates its ephemeral database and discards session annotations. The selected target remains open; the next provider query imports the original artifact again.","Default-space addresses use lowercase 0x-prefixed hexadecimal; other address spaces use <percent-encoded-space>:0x<hex>.","Pseudocode is Ghidra decompiler output, not original source and not text-equivalent to Hopper output.","External functions and functions without an analyzable body return null; other decompiler failures remain explicit.","Ghidra Listing reports a terminal call at 0x100016b84 to 0x10005c880 objc_stub::setUsesCPUOnly: (external=false, thunk=false, hasNoReturn=false); fallthrough is excluded by Ghidra's flow model. The reason for the terminal flow is not established by this observation.","macOS sessions require a matching executable Ghidra native decompiler; REA checks for it but does not build native components or change Gatekeeper quarantine state."],"locations":[],"normalized_result":"\nvoid FUN_100016a3c(ID param_1,ID param_2,ID param_3)\n\n{\n undefined8 uVar1;\n int iVar2;\n ID IVar3;\n ID IVar4;\n ID IVar5;\n \n if (param_1 == 0) {\n return;\n }\n if ((param_2 == 0) || (IVar3 = objc_stub::revision(param_2), IVar3 == 0)) {\n IVar5 = VNDetectFaceLandmarksRequest::supportedRevisions\n ((ID)&_OBJC_CLASS___VNDetectFaceLandmarksRequest);\n IVar3 = 3;\n IVar4 = VNDetectFaceLandmarksRequest::containsIndex_(IVar5,3);\n if ((IVar4 & 1) == 0) {\n IVar5 = VNDetectFaceLandmarksRequest::containsIndex_(IVar5,2);\n IVar3 = 1;\n if ((int)IVar5 != 0) {\n IVar3 = 2;\n }\n }\n }\n else {\n IVar3 = objc_stub::revision(param_2);\n }\n IVar5 = VNDetectFaceLandmarksRequest::supportedRevisions\n ((ID)&_OBJC_CLASS___VNDetectFaceLandmarksRequest);\n IVar5 = VNDetectFaceLandmarksRequest::containsIndex_(IVar5,IVar3);\n if ((IVar5 & 1) == 0) {\n IVar3 = VNDetectFaceLandmarksRequest::supportedRevisions\n ((ID)&_OBJC_CLASS___VNDetectFaceLandmarksRequest);\n IVar5 = VNDetectFaceLandmarksRequest::containsIndex_(IVar3,3);\n if ((int)IVar5 == 0) {\n IVar3 = VNDetectFaceLandmarksRequest::containsIndex_(IVar3,2);\n uVar1 = 1;\n if ((int)IVar3 != 0) {\n uVar1 = 2;\n }\n objc_stub::setRevision_(param_1,uVar1);\n goto LAB_100016b4c;\n }\n objc_stub::setRevision_(param_1,3);\n }\n else {\n objc_stub::setRevision_(param_1,IVar3);\n if (IVar3 != 3) goto LAB_100016b4c;\n }\n IVar3 = VNDetectFaceLandmarksRequest::revision_supportsConstellation_\n ((ID)&_OBJC_CLASS___VNDetectFaceLandmarksRequest,3,2);\n if ((int)IVar3 != 0) {\n objc_stub::setConstellation_(param_1,2);\n }\nLAB_100016b4c:\n IVar3 = objc_stub::count(param_3);\n iVar2 = 0;\n if (IVar3 != 0) {\n IVar3 = objc_stub::setInputFaceObservations_(param_1,param_3);\n iVar2 = (int)IVar3;\n }\n FUN_10003b084();\n if (iVar2 == 0) {\n return;\n }\n objc_stub::setUsesCPUOnly_(param_1,1Line truncated
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"path": "/Users/dayu/Movies/Motion Templates.localized/Effects.localized/💙MS超级美颜/MS超级美颜V2/MS超级美颜V2.moef",
|
||||
"sha256": "5c20b387f6a6438614a9182d3f93f67473881d433a25f7c3b557d063f55a5d92",
|
||||
"published_count": 15,
|
||||
"published": [
|
||||
{
|
||||
"object": "10030",
|
||||
"channel": "./210",
|
||||
"name": ""
|
||||
},
|
||||
{
|
||||
"object": "10030",
|
||||
"channel": "./18",
|
||||
"name": "瘦脸"
|
||||
},
|
||||
{
|
||||
"object": "10030",
|
||||
"channel": "./33",
|
||||
"name": "大眼"
|
||||
},
|
||||
{
|
||||
"object": "10030",
|
||||
"channel": "./22",
|
||||
"name": "短脸"
|
||||
},
|
||||
{
|
||||
"object": "10030",
|
||||
"channel": "./23",
|
||||
"name": "窄脸"
|
||||
},
|
||||
{
|
||||
"object": "10030",
|
||||
"channel": "./24",
|
||||
"name": "下巴"
|
||||
},
|
||||
{
|
||||
"object": "10030",
|
||||
"channel": "./25",
|
||||
"name": "V脸"
|
||||
},
|
||||
{
|
||||
"object": "10030",
|
||||
"channel": "./26",
|
||||
"name": "下颚骨"
|
||||
},
|
||||
{
|
||||
"object": "10030",
|
||||
"channel": "./27",
|
||||
"name": "颧骨"
|
||||
},
|
||||
{
|
||||
"object": "10030",
|
||||
"channel": "./28",
|
||||
"name": "小头"
|
||||
},
|
||||
{
|
||||
"object": "10030",
|
||||
"channel": "./29",
|
||||
"name": "小头范围"
|
||||
},
|
||||
{
|
||||
"object": "10030",
|
||||
"channel": "./19",
|
||||
"name": "人脸框预览"
|
||||
},
|
||||
{
|
||||
"object": "10030",
|
||||
"channel": "./31",
|
||||
"name": "白牙"
|
||||
},
|
||||
{
|
||||
"object": "10030",
|
||||
"channel": "./32",
|
||||
"name": "亮眼"
|
||||
},
|
||||
{
|
||||
"object": "10030",
|
||||
"channel": "./10001",
|
||||
"name": "混合"
|
||||
}
|
||||
],
|
||||
"tracking_controls_published": false
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"date": "2026-10-21",
|
||||
"request": "最大瘦脸差异与人物转脸时的变形机制",
|
||||
"reference": {
|
||||
"plugin_sha256": "6c5d74bc844a5c5f745718b48ea10ec6635d08aa08a5b6dc6e62653980849f8a",
|
||||
"arm64_sha256": "f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb",
|
||||
"metallib_sha256": "4e8c77db8be94ae169337a2e646407f69520b63afb248faa653e0e068237aaae"
|
||||
},
|
||||
"air_observation": {
|
||||
"authority": "static Apple metal-objdump AIR disassembly; not observed shader execution",
|
||||
"command": "xcrun metal-objdump --metallib --disassemble-symbols=faceThinFragmentShader <reference default.metallib>",
|
||||
"full_output_sha256": "f97705b88f5491057df2117aef457ffca84e11020574cd57742b0670db4484d8",
|
||||
"full_output_bytes": 274496,
|
||||
"local_output": "build/pose-analysis/face-thin.air.txt",
|
||||
"module": "faceThinFragmentShader",
|
||||
"module_metallib_offset": "0x5d5e",
|
||||
"function": "mssFaceShapePinCoordinate",
|
||||
"active_branch": "faceShapeParamsActive selects Shape helper; inactive branch alone selects legacy Thin helper",
|
||||
"local_pin_triplets": [
|
||||
[
|
||||
75,
|
||||
74,
|
||||
6
|
||||
],
|
||||
[
|
||||
59,
|
||||
60,
|
||||
13
|
||||
],
|
||||
[
|
||||
74,
|
||||
73,
|
||||
47
|
||||
],
|
||||
[
|
||||
60,
|
||||
61,
|
||||
47
|
||||
],
|
||||
[
|
||||
73,
|
||||
72,
|
||||
47
|
||||
],
|
||||
[
|
||||
61,
|
||||
62,
|
||||
47
|
||||
],
|
||||
[
|
||||
72,
|
||||
71,
|
||||
48
|
||||
],
|
||||
[
|
||||
62,
|
||||
63,
|
||||
48
|
||||
],
|
||||
[
|
||||
71,
|
||||
70,
|
||||
48
|
||||
],
|
||||
[
|
||||
63,
|
||||
64,
|
||||
48
|
||||
],
|
||||
[
|
||||
70,
|
||||
69,
|
||||
49
|
||||
],
|
||||
[
|
||||
64,
|
||||
65,
|
||||
49
|
||||
],
|
||||
[
|
||||
69,
|
||||
68,
|
||||
49
|
||||
],
|
||||
[
|
||||
65,
|
||||
66,
|
||||
49
|
||||
],
|
||||
[
|
||||
68,
|
||||
67,
|
||||
52
|
||||
],
|
||||
[
|
||||
66,
|
||||
67,
|
||||
52
|
||||
],
|
||||
[
|
||||
67,
|
||||
65,
|
||||
47
|
||||
]
|
||||
],
|
||||
"triplet_usage": "first=origin, third=anchor; second unused in thin loop",
|
||||
"thin_cpu_scale": 0.025,
|
||||
"size_gain": "clamp(280/max(length((P75-P59)*imageSize),2*length((P67-(P74+P60)/2)*imageSize),1),1,2.25)",
|
||||
"effective_pin_gain": "clamp(UI thin,0,1) * strengthMultiplier * 0.025 * size_gain",
|
||||
"radius": "length(origin-anchor) in normalized image UV",
|
||||
"falloff": "clamp(1-length(currentSampleCoordinate-origin)/radius,0,1)",
|
||||
"inverse_sample_step": "q = clamp(q-(anchor-origin)*effective_pin_gain*falloff,0,1)",
|
||||
"iteration": "17 sequential updates; not 17 independent offsets evaluated at one original coordinate",
|
||||
"bounded_negative_findings": [
|
||||
"No yaw/roll/pitch input to the checked GPU helpers",
|
||||
"No explicit side-view angle gate in the checked thin branch",
|
||||
"No explicit eye/nose restore mask in the checked thin branch; each single pin has zero weight at its own anchor"
|
||||
],
|
||||
"limits": [
|
||||
"This is a 2D local landmark deformation, not proof of 3D head pose estimation",
|
||||
"Static paths are not a capture of the exact runtime branch used in the supplied screenshots",
|
||||
"Other contour controls include fixed horizontal offsets; not all controls share rotation-equivariant behavior"
|
||||
]
|
||||
},
|
||||
"vision_probe": {
|
||||
"authority": "Independent public Apple Vision / AVFoundation invocation; no reference app or plug-in executed",
|
||||
"script": "scripts/inspect-face-geometry.m",
|
||||
"revision": 3,
|
||||
"constellation": 76,
|
||||
"maximum_dimension": 1280,
|
||||
"coordinate_system": "full-image normalized, y up",
|
||||
"region_indices": {
|
||||
"contour": [
|
||||
59,
|
||||
60,
|
||||
61,
|
||||
62,
|
||||
63,
|
||||
64,
|
||||
65,
|
||||
66,
|
||||
67,
|
||||
68,
|
||||
69,
|
||||
70,
|
||||
71,
|
||||
72,
|
||||
73,
|
||||
74,
|
||||
75
|
||||
],
|
||||
"innerLips": [
|
||||
42,
|
||||
40,
|
||||
43,
|
||||
45,
|
||||
41,
|
||||
44
|
||||
],
|
||||
"leftEye": [
|
||||
0,
|
||||
4,
|
||||
5,
|
||||
1,
|
||||
3,
|
||||
2
|
||||
],
|
||||
"leftPupil": [
|
||||
6
|
||||
],
|
||||
"nose": [
|
||||
47,
|
||||
56,
|
||||
54,
|
||||
53,
|
||||
52,
|
||||
51,
|
||||
50,
|
||||
55
|
||||
],
|
||||
"noseCrest": [
|
||||
46,
|
||||
47,
|
||||
48,
|
||||
49,
|
||||
58,
|
||||
57
|
||||
],
|
||||
"outerLips": [
|
||||
27,
|
||||
28,
|
||||
29,
|
||||
30,
|
||||
31,
|
||||
32,
|
||||
33,
|
||||
34,
|
||||
35,
|
||||
36,
|
||||
37,
|
||||
38,
|
||||
39,
|
||||
26
|
||||
],
|
||||
"rightEye": [
|
||||
7,
|
||||
11,
|
||||
12,
|
||||
8,
|
||||
10,
|
||||
9
|
||||
],
|
||||
"rightPupil": [
|
||||
13
|
||||
]
|
||||
},
|
||||
"frames": [
|
||||
{
|
||||
"time": 0,
|
||||
"face_count": 1,
|
||||
"all_point_counts": [
|
||||
76
|
||||
],
|
||||
"yaw_radians": [
|
||||
0
|
||||
],
|
||||
"pitch_radians": [
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"time": 2,
|
||||
"face_count": 1,
|
||||
"all_point_counts": [
|
||||
76
|
||||
],
|
||||
"yaw_radians": [
|
||||
0.7853981852531433
|
||||
],
|
||||
"pitch_radians": [
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"time": 4,
|
||||
"face_count": 1,
|
||||
"all_point_counts": [
|
||||
76
|
||||
],
|
||||
"yaw_radians": [
|
||||
1.5707963705062866
|
||||
],
|
||||
"pitch_radians": [
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"time": 6,
|
||||
"face_count": 1,
|
||||
"all_point_counts": [
|
||||
76
|
||||
],
|
||||
"yaw_radians": [
|
||||
0
|
||||
],
|
||||
"pitch_radians": [
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"time": 8,
|
||||
"face_count": 1,
|
||||
"all_point_counts": [
|
||||
76
|
||||
],
|
||||
"yaw_radians": [
|
||||
0
|
||||
],
|
||||
"pitch_radians": [
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"time": 10,
|
||||
"face_count": 1,
|
||||
"all_point_counts": [
|
||||
76
|
||||
],
|
||||
"yaw_radians": [
|
||||
0
|
||||
],
|
||||
"pitch_radians": [
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"time": 12,
|
||||
"face_count": 1,
|
||||
"all_point_counts": [
|
||||
76
|
||||
],
|
||||
"yaw_radians": [
|
||||
0
|
||||
],
|
||||
"pitch_radians": [
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"time": 14,
|
||||
"face_count": 1,
|
||||
"all_point_counts": [
|
||||
76
|
||||
],
|
||||
"yaw_radians": [
|
||||
0
|
||||
],
|
||||
"pitch_radians": [
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"time": 16,
|
||||
"face_count": 1,
|
||||
"all_point_counts": [
|
||||
76
|
||||
],
|
||||
"yaw_radians": [
|
||||
0
|
||||
],
|
||||
"pitch_radians": [
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"time": 18,
|
||||
"face_count": 1,
|
||||
"all_point_counts": [
|
||||
76
|
||||
],
|
||||
"yaw_radians": [
|
||||
1.5707963705062866
|
||||
],
|
||||
"pitch_radians": [
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"time": 20,
|
||||
"face_count": 1,
|
||||
"all_point_counts": [
|
||||
76
|
||||
],
|
||||
"yaw_radians": [
|
||||
0.7853981852531433
|
||||
],
|
||||
"pitch_radians": [
|
||||
null
|
||||
]
|
||||
}
|
||||
],
|
||||
"result": "All 11 sampled frames returned one face and 76 points; 4s/18s reported yaw approx pi/2; pitch unavailable",
|
||||
"privacy": "Source frames, complete landmarks and source path remain ignored under build/pose-analysis/vision and are not included in Git"
|
||||
},
|
||||
"reconstruction_baseline": {
|
||||
"commit": "e03e460a9b3e5ad489aad24095485ba10ceb987d",
|
||||
"installed_version": "1.0.4 (6)",
|
||||
"root_causes": [
|
||||
"thin deformation uses bounding box x/y/size rather than saved contour/nose points",
|
||||
"horizontal symmetric inverse warp is applied at full strength for any valid box",
|
||||
"no contour/landmark completeness guard or pose metadata preservation",
|
||||
"fixed coefficients do not match reference local-pin scaling"
|
||||
],
|
||||
"direction_check": "Positive thin samples outward and moves image features inward; the sign is not reversed",
|
||||
"test_gap": "Existing geometry tests establish pixel changes, extent and persistence, not reference likeness or profile shape preservation"
|
||||
},
|
||||
"checklist": {
|
||||
"why_maximum_slim_differs": "answered: different geometric model, locality and effective strength",
|
||||
"how_profile_is_handled": "answered within checked active thin path: projected 2D landmarks, local directions/radii; no established explicit yaw gate",
|
||||
"full_native_visual_equivalence": "unverified; screenshots are not a synchronized isolated-parameter capture",
|
||||
"product_fix": "not changed by this analysis; installed 1.0.4 retains the reported issue"
|
||||
},
|
||||
"host_boundary": "User controls Motion/FCP; no UI interaction, host shutdown, app install or plug-in replacement in this analysis.",
|
||||
"followup_implementation": {
|
||||
"version": "1.0.5 (7)",
|
||||
"signed_source_commit": "abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964",
|
||||
"status": "signed_notarized_installed_user_visual_qa_pending",
|
||||
"landmarks": "explicit revision3 / 76 constellation, points76 persisted and stabilized",
|
||||
"warp": "17 sequential UV pins, .025 * pixel-size gain, original thin bbox term removed",
|
||||
"offline_checks": 604,
|
||||
"extra_real_video_asan_checks": 264,
|
||||
"real_video_frames": 11,
|
||||
"max_uv_oracle_error": 1.09828022e-05,
|
||||
"limitations": [
|
||||
"Other eight controls retain prior approximations",
|
||||
"Fast multi-face overlapping resampling order is not equivalent to the original single fragment",
|
||||
"Full native Motion/FCP visual comparison reserved for user"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
# 下巴控制与图像坐标核对
|
||||
|
||||
2026-10-21,接用户反馈“调整下巴却调整眼睛”。本轮不操作、启动或退出Motion/FCP。以已保存的原版AIR和新REA CPU汇编/常量证据继续分析;没有执行原版或复制其二进制到产品。
|
||||
|
||||
## 已确认的下巴差异
|
||||
|
||||
参数发布、PGChin=24、settings.faceChin与GPU参数位置没有串到大眼。1.0.5的下巴仍用脸框高斯场,存在两项错误:作用范围延伸到嘴部甚至眼睑,下巴正负方向与原版相反。独立真实照片上,旧公式的最大位移出现在嘴附近;侧脸真实下巴位移接近零。这些结论来自离屏照片/坐标场探针,不能代替实际宿主当帧诊断。
|
||||
|
||||
原版`0x1000279f4`汇编将下巴滑块约束到[-1,1],正值乘0.04、负值乘0.02,写入脸型结构第5项(byte offset20);常量位于`0x10005f9c0`。REA记录:`ev_8bc31392c07788d2482546121297c3f177d527282275ef21fd3e5edef9921544`与`ev_640ccb714c5ce00d3864844af319fc574d223810dde08443d2ef2e8f1e989c47`,完整小型证据包见[rea-chin.json](rea-chin.json)。原件身份与上一轮一致,arm64切片SHA-256为`f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb`。
|
||||
|
||||
`mssFaceShapePinCoordinate`的AIR分支读取第5项及共同尺寸增益,顺序执行68→38、67→37、66→36三组局部采样位移。独立Vision区域核对:66/67/68在下巴轮廓,36/37/38在下唇。其半径、线性衰减、逐步clamp与17组瘦脸相同;支持负值。视觉正值缩短下巴、负值拉长,方向随下巴→嘴唇向量变化,不固定沿画面Y轴。细节和边界见[机器报告](chin-report.json)。
|
||||
|
||||
## 修正与测试
|
||||
|
||||
已移除旧下巴高斯项,改用原版三组点、正负系数、尺寸增益。完整76点缺失或不合法时跳过下巴,避免旧档案使用猜测框位置;其他七类脸型控制仍是现有近似实现,组合顺序未宣称全部恢复。
|
||||
|
||||
核心回归559项通过:美化49、人脸116、瘦脸/下巴394。新增正负下巴、半强度、左右侧脸投影、歪头、三种宽高比、非零原点、瘦脸叠加、坏点与旧档案保护。独立double采样场最大UV误差1.09828022e-5。真实NASA人像额外检查下巴位移、正负向量符号和两侧瞳孔保持不动;以坐标位移判定,避免只检查“像素有改变”。
|
||||
|
||||
## 图像适配层待核对项
|
||||
|
||||
纯CI图像中,76点落在真实瞳孔和下巴,没有全局Y轴反置。但FxPlug适配层曾根据imageOrigin在输入、输出各翻转一次。以相同物理top-first照片字节离屏构造TOP_LEFT输入输出时,旧代码默认直通仍精确一致,却能复现下巴作用到眼部的现象;这说明直通测试无法排除中间处理倒置。
|
||||
|
||||
imageOrigin描述坐标轴方向,不能单凭枚举推断IOSurface的物理行序。原版render主入口包装CIImage后仅见正尺度和位移,仍需核对其输出helper,不能仅凭主函数的有界检索宣称原版整个链条无翻转。原生FCP本次实际缓冲行序未捕获,最后视觉交由用户自测。临时照片、完整人脸坐标和探针输出均留在被忽略的build目录。
|
||||
|
||||
## 原版输出链闭环与适配修复
|
||||
|
||||
继续REA分析两个输出helper后,确认主入口所见的无额外翻转不是仅靠字符串缺失作出的猜测:
|
||||
|
||||
- `0x10002911c`直接调用`CIContext render:toIOSurface:bounds:colorSpace:`,没有修改图像方向。证据`ev_88a71ec62fade296495f1e3f5428cdb15ad4cda43866e09fb040fa3a4bdb0440`。
|
||||
- `0x1000291b4`备用路径先render到BGRA8/RGBAh/RGBAf位图,再按源、目的行步长同时正向递增复制到IOSurface,没有倒行。证据`ev_110d76f7958decb0c56b7daafe292d60a8bc56cd035b40a997f61395cb91ebfe`,另有汇编交叉核对。完整记录见[rea-image-orientation.json](rea-image-orientation.json)。
|
||||
|
||||
已移除我们输入/输出两处基于TOP_LEFT的额外翻转,保持与原版同类CI→IOSurface路径一致。独立探针在相同物理top-first字节下验证默认/大眼/下巴正负×四种源/目的标签共16组,全部与upright CI参考逐字节一致;旧代码在相同构造下出现下巴到眼部的热点,且默认直通0误差不能揭示内部倒置。这里验证的是物理缓冲构造与修复链,未捕获本次实际FCP缓冲。
|
||||
|
||||
Apple[FxImageOrigin](https://developer.apple.com/documentation/professional_video_applications/fximageorigin)和本机SDK说明其描述坐标轴方向;[FxPlug渲染说明](https://developer.apple.com/documentation/professional-video-applications/rendering-in-fxplug)确认FxPlug4经IOSurface传入图像。文档本身未规定每个宿主的物理行序,本次改动依据为原版链和独立物理测试,不能把文档扩张成原生内存捕获。
|
||||
|
||||
旧档案没有记录图像适配坐标契约,无法安全判断是否由旧翻转链产生。adapter现为新分析结果加`hostCoordinateSpace=iosurface-ci-y-up-v1`;未标记记录保留原字符串,渲染和续分析均不载入旧坐标。当前目标暂用实时跟随,新分析后恢复离线记录。不改通用PGFaceProcessor的旧档案读取能力,也不把用户记录直接删除或静默改写成新坐标。
|
||||
|
||||
独立生理热点检查:NASA、实际素材0秒、4秒侧脸三张图均在真实下巴1.2–2.9像素内获得最大位移,实际下巴正值上收、负值延伸,双瞳孔采样位移不超过0.00003像素;12项专项检查通过。另同一素材11帧共494项瘦脸/下巴ASan检查通过,含重复基础用例,不与完整回归简单相加。用户照片、帧及完整点位均没有入库。
|
||||
|
||||
## 新回归的负控
|
||||
|
||||
在临时副本只恢复旧的输入/输出翻转、保留已修正三pin后,120项解剖子测试产生30个预期失败:混合原点标签出现方向错误,TOP/TOP默认仍0误差,但大眼最大差66/255、下巴正值52/255、负值21/255,均被独立参考和跨标签逐字节一致性检出。此时三pin下巴热点仍在下巴,因为点驱动形变可随图像反射;“眼部热点”是此前旧高斯下巴与额外翻转共同构成的反例,二者不能混为同一代码状态。正式当前实现247项宿主适配ASan检查全部通过。
|
||||
@@ -0,0 +1,86 @@
|
||||
# 最大瘦脸与转脸变形分析
|
||||
|
||||
2026-10-21。用户已反馈自动跟随正常;本次检查四张实测图指出的脸型差异。当前安装版仍为1.0.4(6),本次没有修改产品变形代码、替换APP或操作Motion/FCP。
|
||||
|
||||
## 结论
|
||||
|
||||
我们的瘦脸按矩形脸框建立左右对称的水平收缩场;原版主要瘦脸路径根据完整二维面部关键点,顺序执行17组局部位移。两者的作用范围、方向和强度定义均不同。侧脸继续使用正面矩形场,是当前鼻尖、下巴和邻近区域异常变形的直接原因;跟踪框跟对人不能解决这个问题。
|
||||
|
||||
原版已检查的瘦脸路径没有读取yaw/roll/pitch后切换侧脸算法,也没有专门的侧脸角度衰减。它每帧获得随姿态变化的二维轮廓与鼻部位置,让局部收缩方向和作用半径随这些点变化。这属于二维关键点自适应,不能表述成已恢复完整3D姿态估计。
|
||||
|
||||
## 原版证据链
|
||||
|
||||
只读目标与上一轮一致:插件原件SHA-256为`6c5d74bc844a5c5f745718b48ea10ec6635d08aa08a5b6dc6e62653980849f8a`;arm64分析切片为`f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb`。本次开始后重新计算原件摘要,未发生变化。Metal库摘要为`4e8c77db8be94ae169337a2e646407f69520b63afb248faa653e0e068237aaae`。
|
||||
|
||||
|步骤|直接静态观察|REA Evidence|
|
||||
|---|---|---|
|
||||
|检测设置|`0x100016a3c`优先配置Vision revision 3及76点constellation;不支持时存在旧revision回退|`ev_02f734a0d72f1b7b77a3112d1de297b9e99ea43d5377349e6eebb71ab017d650`|
|
||||
|当前帧检测|`cachedFaceObservations…`发起`VNDetectFaceLandmarksRequest`,另有重试与缓存|`ev_61ea8c6e7117f18e47fe6cf820c9ec0a59865dedaf3d9c09813be0a5bd7a79bc`|
|
||||
|有效点筛选|`0x1000230cc`要求至少76点;`0x100022c70`转换76点并添加第77个中心点;`0x100017cd8`检查有限数值、空间范围及轮廓分布|`ev_63bb7c8fb07dbe456027f4d4e6be52118338b91ab39dbf9ed6620de9090bc260`、`ev_5f27f174c793c94cd45219d6c405e6b54b96e2650c7462ccc5eeaea14f2604c3`、`ev_1c62b4dab3cf8b661eb5c31b034a0b3cadf9f1f6cb512aace8b3b0838bc74b5c`|
|
||||
|输入几何|`0x1000274d0`打包77个float2,616字节,转换到输出纹理坐标并检查越界点/轮廓尺度|`ev_8a33a7bb27a1a966f9e4ee26380501f1cb009d7d4665ed93b73bd3f630436556`|
|
||||
|参数缩放|`0x1000279f4`将瘦脸滑块值乘0.025及渲染强度,写入十float脸型结构的第一项|`ev_449e1fc8883b8afe27240f672f98f300d4132ac4994db5e1590a4f7ca4e21278`|
|
||||
|GPU连接|`FxMTLDeviceCacheItem::initWithDevice:pixelFormat:`将`faceThinFragmentShader`接入瘦脸pipeline;`0x100027d38/0x10002807c`上传点阵、数量及脸型参数到该pipeline|`ev_1520c2d83845004994f2be940c8c5ea3f4a62daf2d8e3e06278f381ffca1a010`、`ev_dc4487de4891c4a663dab370f2e207799bec7e2ad305a98a1ed579e5de6ef263`、`ev_7d2cc9ff3bf8ed3ceea2d91ffb7c2c47ad45337a792f101c1d989272f37023b1`|
|
||||
|
||||
完整REA记录见[证据包](rea-slim-pose.json)。伪代码是Ghidra生成的静态分析结果,寄存器参数与CGRect返回类型部分恢复不完整;此处没有将其当作原始源码或实际执行记录。
|
||||
|
||||
## Metal瘦脸计算
|
||||
|
||||
用本机Apple工具对只读`default.metallib`输出AIR中间代码。它不是字符串猜测:`faceThinFragmentShader`在`faceShapeParamsActive`为真时调用`mssFaceShapePinCoordinate`,只有inactive分支调用旧`mssFaceThinPinCoordinate`。开启瘦脸后第一项脸型参数非零,因此可建立Shape分支的静态路径。原版截图当时具体走哪条宿主渲染分支没有运行捕获。
|
||||
|
||||
Shape helper中的瘦脸部分有17组点操作,每组实际读取origin和anchor,三元表中的第二项在这一循环中未使用。origin均为59–75号脸轮廓点,anchor为6、13、47、48、49、52号点;独立Vision区域对应检查确认,6/13是左右瞳孔,47/48/49/52位于鼻部或鼻梁。不能把所有锚点都叫下颌点或鼻部点。
|
||||
|
||||
单组逆向采样的结构为:
|
||||
|
||||
```text
|
||||
r = distance(origin, anchor)
|
||||
w = clamp(1 - distance(q, origin) / r, 0, 1)
|
||||
q = clamp(q - (anchor - origin) * effectiveStrength * w, 0, 1)
|
||||
```
|
||||
|
||||
17组顺序更新同一个采样坐标q,不能将其视为一次整脸等比缩放。这里的距离在整幅纹理归一化UV中计算;它并非完整按像素各向同性的旋转不变模型。单组在其anchor处权重为零,但其他组仍可能影响该处,不能说整个鼻部或所有五官被锁死。
|
||||
|
||||
有效强度另含尺寸补偿:`k=clamp(280/max(轮廓两端像素距离, 下巴到上侧轮廓中点的像素距离×2, 1), 1, 2.25)`,单pin系数约为`滑块×0.025×k×渲染强度`。它对小脸加大效果,**不是侧脸质量衰减**。最大单组系数0.05625也不是最终整脸缩窄百分比。
|
||||
|
||||
AIR结构摘要、全部17组索引、尺寸公式、输出摘要和复现命令在[机器可读报告](slim-pose-report.json)。完整AIR文本留在本地`build/pose-analysis/face-thin.air.txt`,未把原版metallib或原版程序加入产品或仓库。
|
||||
|
||||
## 为什么转脸时更自然
|
||||
|
||||
已证实的机制是:轮廓投影变化→局部origin、anchor间向量变化→各处的收缩方向、半径随之变化;并非先证明“人物转头90度”,再换一套固定对称瘦脸公式。另有上一轮已分析的时间点阵稳定、有效性检查和失检参考保持,这些可减少跳动;它们与瘦脸位移模型是两个层面。
|
||||
|
||||
本次检查的GPU瘦脸分支没有yaw/roll/pitch输入、左右脸对称比门限、侧脸强度开关或末端五官恢复蒙版。原生函数名检索也未发现`objc_stub::yaw/roll/pitch` getter;这是有界负面结果,不能据此宣称整个APP绝无任何姿态相关逻辑。其他脸型项仍有固定画面X方向位移,不能把瘦脸的局部点特性扩张成所有控制都具有相同的转头适应性。
|
||||
|
||||
## 同一素材的独立验证
|
||||
|
||||
运行独立的[离线检测工具](../../scripts/inspect-face-geometry.m),从用户视频在0、2、4、6、8、10、12、14、16、18、20秒各取一帧。请求使用Vision revision 3 / 76点,图像最长边1280。全部11帧均返回1张脸、76点;4秒和18秒的Vision yaw约为π/2,pitch均不可用。
|
||||
|
||||
这证明明显侧脸仍可以获得完整二维点阵,也校验了59–75号点确为轮廓、6/13号为瞳孔、47/48/49/52号为鼻部相关点。yaw为检测器返回值,不能当作实测头部角度。工具没有调用或执行原版滤镜,不是原版宿主像素等价测试;本机Vision版本、检测尺寸与宿主缓存可能使点坐标与原版当帧不同。Vision本身报告yaw,也不能说明原版瘦脸实际使用该属性。
|
||||
|
||||
用户素材、截图、导出的帧及完整人脸坐标均只保留在被忽略的本地build目录,没有上传远程仓库。入库的报告仅含汇总计数、区域索引和角度信息。
|
||||
|
||||
工具复现:
|
||||
|
||||
```sh
|
||||
xcrun clang -fobjc-arc -Wall -Wextra \
|
||||
-framework Foundation -framework AVFoundation -framework Vision \
|
||||
-framework ImageIO -framework CoreGraphics -framework CoreMedia \
|
||||
scripts/inspect-face-geometry.m -o /tmp/inspect-face-geometry
|
||||
/tmp/inspect-face-geometry '<用户视频路径>' '<本地输出目录>/geometry.json' 0 2 4 6 8 10 12 14 16 18 20
|
||||
```
|
||||
|
||||
## 当前实现差异与修复落点
|
||||
|
||||
`PGFaceProcessor.m:290–305`的瘦脸只用框中心/宽高,按画面水平轴同时处理左右;`warpImage:geometry:settings:extent:`额外读取的双眼仅用于大眼。已有`contour/nose/lips`未参与瘦脸,缺失轮廓时仍会按有效框足额变形。pose信息也没有贯通提取、净化、实时稳定和离线存储。
|
||||
|
||||
正值采样向外、视觉向内收的符号本身正确;简单反号无法修复。当前公式在框高35%处的测试位置将脸框两边缘间距缩小约20.2%,作用域还可延伸到框外。这个局部计算是当前公式的解析检查,不代表与原版截图的实测宽度差。
|
||||
|
||||
后续修正应让瘦脸直接使用稳定后的轮廓和鼻部控制点,按局部方向/半径位移,并校准尺寸补偿和滑块映射。若另加显式姿态或质量衰减,应标记为本项目设计,不能当作已证实的原版算法。完整点阵必须贯通实时与持久化路径,不能只在检测入口新增字段后被后续步骤丢弃。
|
||||
|
||||
现有测试只能证明各几何参数改变像素、范围保持和跟踪恢复一致,尚未验证最大瘦脸外观或侧脸形状。修复验证至少应包括同帧正面/半侧/全侧、向左右转脸、歪头、轮廓缺失、正逆播放、100%瘦脸与多个脸型项叠加,和原版在隔离相同其他参数后的对照。
|
||||
|
||||
本轮完成机制分析及独立检测,未宣称已修复安装版,也未将用户的“跟踪没问题”扩张为其他未覆盖的原生验收通过。
|
||||
|
||||
## 后续修复状态
|
||||
|
||||
1.0.5已实现并安装76点/17组局部瘦脸,旧的矩形框瘦脸已移除。上文“安装版未修复”为本次分析开始时1.0.4的历史状态。新增独立数值场与真实素材回归,签名公证及安装记录见 `../验证记录.md`。Motion/FCP最终视觉由用户自测,其他8类脸型控制与快速多人重叠的既有近似边界仍保留。
|
||||
|
||||
1.0.6继续分析下巴与宿主图像轴,新增原版三pin下巴及CI/IOSurface无额外翻转修复,详见[下巴与坐标分析](下巴与坐标分析.md)。其余七类控制仍为近似实现。
|
||||
@@ -0,0 +1,24 @@
|
||||
Executable=/Library/Plug-Ins/FxPlug/beatygril.app/Contents/MacOS/beatygril
|
||||
Identifier=com.dayu.beatygril
|
||||
Format=app bundle with Mach-O universal (x86_64 arm64)
|
||||
CodeDirectory v=20500 size=478 flags=0x10000(runtime) hashes=4+7 location=embedded
|
||||
Hash type=sha256 size=32
|
||||
CandidateCDHash sha256=b77375a5d7c6d5d866ff21bfdc3ce85412dc1d0b
|
||||
CandidateCDHashFull sha256=b77375a5d7c6d5d866ff21bfdc3ce85412dc1d0b463ff47fe896a52640762b4f
|
||||
Hash choices=sha256
|
||||
CMSDigest=b77375a5d7c6d5d866ff21bfdc3ce85412dc1d0b463ff47fe896a52640762b4f
|
||||
CMSDigestType=2
|
||||
CDHash=b77375a5d7c6d5d866ff21bfdc3ce85412dc1d0b
|
||||
Signature size=8968
|
||||
Authority=Developer ID Application: zhong luo (B3W25FX5H5)
|
||||
Authority=Developer ID Certification Authority
|
||||
Authority=Apple Root CA
|
||||
Timestamp=Oct 21, 2026 at 18:42:32
|
||||
Notarization Ticket=stapled
|
||||
Info.plist entries=22
|
||||
TeamIdentifier=B3W25FX5H5
|
||||
Runtime Version=27.0.0
|
||||
Sealed Resources version=2 rules=13 files=4
|
||||
Internal requirements count=1 size=180
|
||||
Total signatures=1
|
||||
Chosen signature=1
|
||||
@@ -0,0 +1,3 @@
|
||||
Face AddressSanitizer build and standalone run
|
||||
Face processing: 116 checks passed
|
||||
2026-10-21 18:40:05.422 face-tests[99927:5270835] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
|
||||
@@ -0,0 +1 @@
|
||||
{"status":"Accepted","id":"8eb96294-5935-49ff-a487-3c4f60c27395","message":"Processing complete"}
|
||||
@@ -0,0 +1,8 @@
|
||||
Arguments: ["zsh", "scripts/test.sh"]
|
||||
Environment additions: {}
|
||||
Exit code: 0
|
||||
|
||||
Beauty renderer: 49 checks, 0 failures (Metal).
|
||||
Face processing: 116 checks passed
|
||||
|
||||
2026-10-21 18:42:58.952 face-tests[1253:5276682] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
|
||||
@@ -0,0 +1,9 @@
|
||||
Arguments: ["zsh", "scripts/test-host.sh"]
|
||||
Environment additions: {"PAINTFCP_HOST_ASAN": "1"}
|
||||
Exit code: 0
|
||||
|
||||
Default IOSurface maximum byte difference: 0
|
||||
Alternate destination tag maximum byte difference: 0
|
||||
Host adapter: 117 checks, 0 failures
|
||||
|
||||
2026-10-21 18:42:58.460 host-tests[1241:5276594] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
|
||||
@@ -0,0 +1,26 @@
|
||||
Arguments: ["zsh", "scripts/test-osc.sh"]
|
||||
Environment additions: {"PAINTFCP_OSC_ASAN": "1"}
|
||||
Exit code: 0
|
||||
|
||||
OSC adapter: 98 checks, 0 failures
|
||||
|
||||
2026-10-21 18:42:57.862 osc-tests[1236:5276552] PGFaceOSC point-write desired=(0.25,0.75) setter=(0.25,0.75) readback=(0.25,0.75) mode=normalized verified=1
|
||||
2026-10-21 18:42:57.887 osc-tests[1236:5276552] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(80,180) surface=320x240 origin=0
|
||||
2026-10-21 18:42:58.633 osc-tests[1236:5276552] PGFaceOSC point-write desired=(0.8,0.3) setter=(1152,-432) readback=(0.8,0.3) mode=centered-pixels verified=1
|
||||
2026-10-21 18:42:58.633 osc-tests[1236:5276552] PGFaceOSC coordinates selection=(0.7,0.4) objectBounds=(-1920,-1080,3840,2160) canvas=(152,78) surface=320x240 origin=0
|
||||
2026-10-21 18:42:58.734 osc-tests[1236:5276552] PGFaceOSC point-write desired=(0.8,0.3) setter=(0.8,0.3) readback=(0.12345,0.87654) mode=normalized verified=0
|
||||
2026-10-21 18:42:58.734 osc-tests[1236:5276552] PGFaceOSC point-write desired=(0.75,0.25) setter=(0.75,0.25) readback=(0.75,0.25) mode=normalized verified=1
|
||||
2026-10-21 18:42:58.734 osc-tests[1236:5276552] PGFaceOSC coordinates selection=(0.75,0.25) objectBounds=(-100,-50,200,100) canvas=(160,60) surface=320x240 origin=0
|
||||
2026-10-21 18:42:58.813 osc-tests[1236:5276552] PGFaceOSC point-write desired=(0.625,0.375) setter=(0.625,0.375) readback=(0.625,0.375) mode=normalized verified=1
|
||||
2026-10-21 18:42:58.813 osc-tests[1236:5276552] PGFaceOSC coordinates selection=(0.625,0.375) objectBounds=(-100,-50,200,100) canvas=(240,60) surface=320x240 origin=0
|
||||
2026-10-21 18:42:58.892 osc-tests[1236:5276552] PGFaceOSC point-write desired=(0.25,0.75) setter=(0.25,0.75) readback=(0.25,0.75) mode=normalized verified=1
|
||||
2026-10-21 18:42:58.892 osc-tests[1236:5276552] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(144,180) surface=320x240 origin=0
|
||||
2026-10-21 18:42:58.976 osc-tests[1236:5276552] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
|
||||
2026-10-21 18:42:58.977 osc-tests[1236:5276552] PGFaceOSC coordinates selection=(0,1) objectBounds=(-100,-50,200,100) canvas=(100,140) surface=320x240 origin=0
|
||||
2026-10-21 18:42:59.070 osc-tests[1236:5276552] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(nan,nan) surface=320x240 origin=0
|
||||
2026-10-21 18:42:59.137 osc-tests[1236:5276552] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
|
||||
2026-10-21 18:42:59.137 osc-tests[1236:5276552] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(inf,nan) surface=320x240 origin=0
|
||||
2026-10-21 18:42:59.206 osc-tests[1236:5276552] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
|
||||
2026-10-21 18:42:59.206 osc-tests[1236:5276552] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(80,nan) surface=320x240 origin=0
|
||||
2026-10-21 18:42:59.265 osc-tests[1236:5276552] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
|
||||
2026-10-21 18:42:59.265 osc-tests[1236:5276552] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
|
||||
@@ -0,0 +1,21 @@
|
||||
Arguments: ["xcodebuild", "-list", "-project", "beatygril/beatygril.xcodeproj"]
|
||||
Environment additions: {}
|
||||
Exit code: 0
|
||||
|
||||
Command line invocation:
|
||||
/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild -list -project beatygril/beatygril.xcodeproj
|
||||
|
||||
Information about project "beatygril":
|
||||
Targets:
|
||||
Wrapper Application
|
||||
XPC Service
|
||||
|
||||
Build Configurations:
|
||||
Debug
|
||||
Release
|
||||
|
||||
If no build configuration is specified and -scheme is not passed then "Release" is used.
|
||||
|
||||
Schemes:
|
||||
Wrapper Application
|
||||
XPC Service
|
||||
@@ -0,0 +1,609 @@
|
||||
{
|
||||
"target": "e03e460a9b3e5ad489aad24095485ba10ceb987d",
|
||||
"remote": "https://dayu.media/git/dayu/paintfcp-fxplug.git",
|
||||
"startedAt": "2026-10-21T10:42:53.349001+00:00",
|
||||
"temporaryClone": "/var/folders/t4/kbc10sqs43b0rjf3nfh51nmw0000gn/T/paintfcp-final-recovery-e03e460-wsazshda/clone",
|
||||
"scope": "Independent remote source recovery; no APP build, installation, execution, UI, source edits or commit.",
|
||||
"cloneHead": "e03e460a9b3e5ad489aad24095485ba10ceb987d",
|
||||
"files": {
|
||||
"count": 51,
|
||||
"allByteAndBlobAndModeEqual": true,
|
||||
"entries": [
|
||||
{
|
||||
"path": ".gitignore",
|
||||
"bytes": 490,
|
||||
"mode": "100644",
|
||||
"gitBlob": "d2bca6e38076bb5f7574cc7781eadef27b41d9ff",
|
||||
"sha256": "34e63260d343569ad4d5a1ffd6c15647da5a7da2b8940dc93f6d987c7fec474e",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "README.md",
|
||||
"bytes": 2374,
|
||||
"mode": "100644",
|
||||
"gitBlob": "b23fa1dbe565ec9c076d5a070b9d3b15771d09eb",
|
||||
"sha256": "b483ab1c164af35003277dcdeecf3e2d66c2c608461c2ea7ab17d7b2b87a781e",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "RECOVERY.md",
|
||||
"bytes": 3722,
|
||||
"mode": "100644",
|
||||
"gitBlob": "321cbca369a7da1cb0da858381701d32d9145a6d",
|
||||
"sha256": "5d1ba0793bb476ce10bfbe5a5b9195cb8be33d9987a1de29975de5ec92c25201",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "Templates/大鱼人像美化/Drop Zone.tiff",
|
||||
"bytes": 420128,
|
||||
"mode": "100644",
|
||||
"gitBlob": "6d15c98bc221f999514f197ff67a7145836e6a38",
|
||||
"sha256": "a7f8e3a2a431dbb3ce9e777040bb55e701cfe9ab9632e8cbb10359732bc8692e",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "Templates/大鱼人像美化/PARAMETERS.json",
|
||||
"bytes": 9815,
|
||||
"mode": "100644",
|
||||
"gitBlob": "df2c01f370e6ee639d2950774fe8020742802c17",
|
||||
"sha256": "bb561b0dfff90223f0925a60ec05b28518e22163a6bf7922c4efc6d155ddbabb",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "Templates/大鱼人像美化/README.md",
|
||||
"bytes": 4180,
|
||||
"mode": "100644",
|
||||
"gitBlob": "e4317f896ea661029a9c3e6f38e1141692b20f92",
|
||||
"sha256": "cab22a7d453a197f6fad5f38508e9bbfb1f55fb67d368b255dc8cc8b973f116c",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "Templates/大鱼人像美化/VALIDATION.json",
|
||||
"bytes": 1749,
|
||||
"mode": "100644",
|
||||
"gitBlob": "725063ec5edc1d5fe39e9cf79a91f5e5da005853",
|
||||
"sha256": "8aa839a756a23a64ab631ff558e16a5542f5311d5f9d0f6212a62150eb75ad1d",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "Templates/大鱼人像美化/大鱼人像美化.moef",
|
||||
"bytes": 22996,
|
||||
"mode": "100644",
|
||||
"gitBlob": "98142a1ab47258a7fdd40c82ddc36ade6976fc04",
|
||||
"sha256": "d234d504eba9e6d37b910b622a46c92458e658c5bbdad2caf5831c2a1463a3e2",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril.xcodeproj/project.pbxproj",
|
||||
"bytes": 22986,
|
||||
"mode": "100644",
|
||||
"gitBlob": "db5251bf3c337b865621e771a4b28690214f1c8e",
|
||||
"sha256": "e9b9be100e53003f6bba96658d3a36078bb607236958e43aec6ed59f8c73ce32",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
|
||||
"bytes": 135,
|
||||
"mode": "100644",
|
||||
"gitBlob": "919434a6254f0e9651f402737811be6634a03e9c",
|
||||
"sha256": "7f3b00b5c3fdb45242d7b87e1e5c4e25d1fa8129a16c94295ecc4e8ea2235c5f",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/Info.plist",
|
||||
"bytes": 3030,
|
||||
"mode": "100644",
|
||||
"gitBlob": "956728344f758fd35239c8f0cfe1a532c11f3f16",
|
||||
"sha256": "b7285915328457ff63e146230a9db417c4a99a20b067930265dbc5b270988457",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/PGBeautyRenderer.h",
|
||||
"bytes": 1410,
|
||||
"mode": "100644",
|
||||
"gitBlob": "a0a6965c837072e6d837a98b701d59b1cc139655",
|
||||
"sha256": "4e9b663c47261e4feceb7ab38c25171b65f36c14417d82a8b7c49ee3201492d3",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/PGBeautyRenderer.m",
|
||||
"bytes": 17132,
|
||||
"mode": "100644",
|
||||
"gitBlob": "4c9fc6fbb1dc1f2c4048a16435f554cae9f2ca74",
|
||||
"sha256": "00b52b1f22c1e875f137ee4ea90f50d6caa38db277bebd0da538d0ba7e66072b",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/PGBeautyTypes.h",
|
||||
"bytes": 1149,
|
||||
"mode": "100644",
|
||||
"gitBlob": "e1cd3ef21f37b831cdaa7d9f52211cafaf243994",
|
||||
"sha256": "6ea8005accc22f85fe8be1b73bf6e89a5b37ffd6e97460d4f6dd69d55eaf33cc",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/PGFaceProcessor.h",
|
||||
"bytes": 2178,
|
||||
"mode": "100644",
|
||||
"gitBlob": "0b861830a6e9652d81ccfe62f2ce758febee449f",
|
||||
"sha256": "a3448bd75fd278c9702f079f55637ba2c26bc820bdbb81f49fb3a3d0e2a4aba5",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/PGFaceProcessor.m",
|
||||
"bytes": 36793,
|
||||
"mode": "100644",
|
||||
"gitBlob": "69359f52ef6473d034553d8cd9528881b4a0125f",
|
||||
"sha256": "49ee6e5632d7ec043cbadef8596a153f1f6841eb4e658ea67b436f4aca9e60c4",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/PGFaceSelectOSC.m",
|
||||
"bytes": 11630,
|
||||
"mode": "100644",
|
||||
"gitBlob": "ae0aa7ce1c2de355b2c1cb890314a0de689df81a",
|
||||
"sha256": "d5e1dd1db6825730803ddc3f21b588cd548cbbacc60760aa45ad9e2feedae307",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/PGParameterIDs.h",
|
||||
"bytes": 932,
|
||||
"mode": "100644",
|
||||
"gitBlob": "4cc0e20af1e659f1e8e666fabe01003c06cfd885",
|
||||
"sha256": "8fdfa2021131317ce09532d27fd6f34fb0b505f7b17c561819336ad2a06c42cc",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/beatygrilPlugIn.h",
|
||||
"bytes": 347,
|
||||
"mode": "100644",
|
||||
"gitBlob": "12dca07a744c064d5d474e0294304ca29055a077",
|
||||
"sha256": "4f00716328c9237b79cc58153ba47d8ac8ddc1786939cb206bd37b01e3b6397e",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/beatygrilPlugIn.m",
|
||||
"bytes": 24193,
|
||||
"mode": "100644",
|
||||
"gitBlob": "3f148956b32770c7db5b0df97700fd96f7e20025",
|
||||
"sha256": "b8f4326ed3eb2de74770e4ea266d09ebe4aea70428a4511f0ec501a0cd89b247",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/en.lproj/InfoPlist.strings",
|
||||
"bytes": 85,
|
||||
"mode": "100644",
|
||||
"gitBlob": "9a5f27348a39c8e3ee50cffc2fb359c78a1319c8",
|
||||
"sha256": "1b1cd5b74690bd5a74cf2b0a0c068bbc2c3462c5e9fe5ca3e61541bcbd451a7a",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/en.lproj/Localizable.strings",
|
||||
"bytes": 132,
|
||||
"mode": "100644",
|
||||
"gitBlob": "7f52f3dd719cb4f94e24867e36114cb061ef0b92",
|
||||
"sha256": "b3036a2339b97ebf264516e1e534351d1ceb64f45942595508f4d67d07b07769",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/main.m",
|
||||
"bytes": 114,
|
||||
"mode": "100644",
|
||||
"gitBlob": "34d50d3f8cdf438d3508d0aa94e3f2d20e45f962",
|
||||
"sha256": "5b812ddb1ef171fe075e4faf13d761935ca73ecbba1be0bd9d4802a64a7759cd",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Wrapper Application/AppDelegate.h",
|
||||
"bytes": 167,
|
||||
"mode": "100644",
|
||||
"gitBlob": "f3f8e19cfe057114c2289ff37c01c2e6e1aa7ed0",
|
||||
"sha256": "3fc6516be93176f90c2deb17332c9cf98e670458a33fbad4fe7b8f367ec9494f",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Wrapper Application/AppDelegate.m",
|
||||
"bytes": 3003,
|
||||
"mode": "100644",
|
||||
"gitBlob": "1cb56f073356dd30add33415117bf4c24fdc4ef0",
|
||||
"sha256": "acef6ade6591c039cd8b3e603f83d6d06f64da9709a0385b73c8588e824fe6de",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Wrapper Application/Base.lproj/MainMenu.xib",
|
||||
"bytes": 50614,
|
||||
"mode": "100644",
|
||||
"gitBlob": "57d03a50e249d189c5fa0a4602c5ec846f06612d",
|
||||
"sha256": "7f0945bc5903a658550b712267870edecbdf1b73ac83c71f1358468f40c1ab1d",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Wrapper Application/Info.plist",
|
||||
"bytes": 1084,
|
||||
"mode": "100644",
|
||||
"gitBlob": "94b192675bf5651a7c66da35f04ff23e7b8f01c2",
|
||||
"sha256": "b1e0922de60e32a703f62c34b508610d5a0329d70637911cfa4645c0fd19083d",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Wrapper Application/SandboxEntitlements.entitlements",
|
||||
"bytes": 240,
|
||||
"mode": "100644",
|
||||
"gitBlob": "852fa1a4728ae4789e3bca55dd07caef3b41f2a5",
|
||||
"sha256": "b746c74b201e6af4e0864fbd1f4f629f96156a7ec4b7208bc995d0a5089bb592",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Wrapper Application/en.lproj/Credits.rtf",
|
||||
"bytes": 264,
|
||||
"mode": "100644",
|
||||
"gitBlob": "da779d0f6bec11568066e347a01ccef0308488b2",
|
||||
"sha256": "ec846e198df85720e8443abb810334411b5690386f3062f90208c23b8575f28a",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Wrapper Application/en.lproj/InfoPlist.strings",
|
||||
"bytes": 0,
|
||||
"mode": "100644",
|
||||
"gitBlob": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Wrapper Application/main.m",
|
||||
"bytes": 112,
|
||||
"mode": "100644",
|
||||
"gitBlob": "99ad4384792f9b2050e0755c07522c0dab9b4b86",
|
||||
"sha256": "5c3dc97a53d0379af0e0ce75c5541f33c47f2da115d2d0018cfba003d185a63d",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/analysis/REA-分析与复刻说明.md",
|
||||
"bytes": 7652,
|
||||
"mode": "100644",
|
||||
"gitBlob": "dd32569f3d93f3533f257ed4d1ecd70a4eab271e",
|
||||
"sha256": "6ca162a8eb43ef67b5bf55954d15c16aecefbfdfa5a1b687e7516cee4f141c9e",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/analysis/auto-follow-report.json",
|
||||
"bytes": 15665,
|
||||
"mode": "100644",
|
||||
"gitBlob": "e60d6d055a513ab8e9c21d2b39460c9d0698e1d3",
|
||||
"sha256": "1a0c91efcaee058f331f6a7446bea8794af1dcf9120f5c2585f249b188d038e3",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/analysis/beauty-preview.png",
|
||||
"bytes": 617622,
|
||||
"mode": "100644",
|
||||
"gitBlob": "35ace472cb42629b01b3e05660cb7be8b9711fd7",
|
||||
"sha256": "37aa97855915a49ff1f225a9e1fa9d8bcbed0be5f4109b6835ba4db17f9113f6",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/analysis/face-preview.png",
|
||||
"bytes": 713552,
|
||||
"mode": "100644",
|
||||
"gitBlob": "9a5bab3ad706b95ac32875352d16d6580dcd48ab",
|
||||
"sha256": "6cc662cbb2b554a0dd8c926d94a0d819506ac76710d9c09c2d3d7f808fbf686b",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/analysis/rea-auto-tracking.json",
|
||||
"bytes": 2883488,
|
||||
"mode": "100644",
|
||||
"gitBlob": "2f5738f934f0ce89780cf4535c7257b54a397bef",
|
||||
"sha256": "dc1007f3798fade5e454bb2ef0b58e4cfe2bb0256948c364070fd9af70a97377",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/analysis/rea-host.json",
|
||||
"bytes": 10111,
|
||||
"mode": "100644",
|
||||
"gitBlob": "9de443173a74668f0a2dced3426dae20549a16dd",
|
||||
"sha256": "141866459948b19d5378de2f6534de333c795e317416f2ac6c9fdc86c94dd61a",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/analysis/rea-plugin.json",
|
||||
"bytes": 343165,
|
||||
"mode": "100644",
|
||||
"gitBlob": "79bcafbcfeda694ed466d95f81f30eb622a95a0b",
|
||||
"sha256": "dda6e450915e1054653c28addfed5254c678dccf14ee66643d508e7087c1f0c1",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/analysis/reference-template-parameters.json",
|
||||
"bytes": 1608,
|
||||
"mode": "100644",
|
||||
"gitBlob": "9947e2ff182a146bc4c9120dfff7eacc8cca9c0c",
|
||||
"sha256": "026e47e8ecbd9a967d8b8aabddac2a370479a43b9e1a7a7728b5b01a5002f006",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/验证记录.md",
|
||||
"bytes": 4783,
|
||||
"mode": "100644",
|
||||
"gitBlob": "22db834b9b61b5d1ede9fa4c16e0a8ffb2157821",
|
||||
"sha256": "b030e25d23264e8bdfbf172a4b9f417bc30fdd35029a97b3579e3d5c5a0cc798",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "scripts/checkpoint.py",
|
||||
"bytes": 2218,
|
||||
"mode": "100644",
|
||||
"gitBlob": "8248d1732aceed377d28cdae02873057f442474b",
|
||||
"sha256": "3c27b0e603dd2d94512f611cec97bd9afe5809aea0f5a4bb213995d2e23366f8",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "scripts/deliver.py",
|
||||
"bytes": 5858,
|
||||
"mode": "100755",
|
||||
"gitBlob": "1ab7b19e5e7f384e5491a93933b1cb5148a4fc1d",
|
||||
"sha256": "1192f8458d8127677ae5353fd15ee8426d76871de60aac719e52da951e86cec0",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "scripts/test-host.sh",
|
||||
"bytes": 1129,
|
||||
"mode": "100755",
|
||||
"gitBlob": "ef9c7f6436e3d1babbf272e9d329d65ec6c395cb",
|
||||
"sha256": "7f55ed6de05e6cc59884fb7c96062676bd086f4ac745ae847511eacb12316467",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "scripts/test-osc.sh",
|
||||
"bytes": 941,
|
||||
"mode": "100755",
|
||||
"gitBlob": "ffaed0b8d449eef6acf2fb352d9fea10fc1649fd",
|
||||
"sha256": "ed405cd214ce252641074d0ef0a12c4cc0faf3e1869fe92716a98fff7f9ac517",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "scripts/test.sh",
|
||||
"bytes": 765,
|
||||
"mode": "100755",
|
||||
"gitBlob": "2ec53312311eab71361399e2fb16a229042d4820",
|
||||
"sha256": "b848347a4bcaa883300e75ac78682ac1f82ce07f2696e2457ee22226c06c7131",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "tests/beauty_render_tests.m",
|
||||
"bytes": 16870,
|
||||
"mode": "100644",
|
||||
"gitBlob": "24050fb3a2630693253202ae1525c280f4f144eb",
|
||||
"sha256": "b04eb2f0d78faec13bbdd2a3286cc74c46c2f536358c7c1c3d5d03c54daa868c",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "tests/face_tests.m",
|
||||
"bytes": 33040,
|
||||
"mode": "100644",
|
||||
"gitBlob": "76a05235db8c7ab4c9b59ab6f184be2524443b6e",
|
||||
"sha256": "288654ac41fcbec3db79605a2c215a1284c9b431fcbc0379974a8b6e546d6da4",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "tests/fixtures/ATTRIBUTION.txt",
|
||||
"bytes": 498,
|
||||
"mode": "100644",
|
||||
"gitBlob": "6e439ab97256952b0acd1eef0e1c1b4ab3ea015c",
|
||||
"sha256": "8823ad8aed1a5c1bdda93946e4b5ed24cd542e996a2b8087517cf62f7f4f2a84",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "tests/fixtures/astronaut.png",
|
||||
"bytes": 791555,
|
||||
"mode": "100644",
|
||||
"gitBlob": "834cda0012478c5edc8d43bade96d315dedeaab4",
|
||||
"sha256": "88431cd9653ccd539741b555fb0a46b61558b301d4110412b5bc28b5e3ea6cb5",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "tests/host_adapter_tests.m",
|
||||
"bytes": 30363,
|
||||
"mode": "100644",
|
||||
"gitBlob": "8091c1523eb0b05295c391e20637c931d681e049",
|
||||
"sha256": "77131c770222d3b404204fb04bf47cf943885eeff6acfc7498d59feb28eb1e73",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "tests/osc_tests.m",
|
||||
"bytes": 22495,
|
||||
"mode": "100644",
|
||||
"gitBlob": "66fff5f78f8e0ba08026e53375b184a460a29162",
|
||||
"sha256": "a3199147e32facd63373b53f77840408411b04436fb1dd42e2fb71023c8f2f8a",
|
||||
"byteAndBlobAndModeEqual": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"requiredResources": [
|
||||
{
|
||||
"path": "Templates/大鱼人像美化/大鱼人像美化.moef",
|
||||
"exists": true,
|
||||
"bytes": 22996
|
||||
},
|
||||
{
|
||||
"path": "Templates/大鱼人像美化/Drop Zone.tiff",
|
||||
"exists": true,
|
||||
"bytes": 420128
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Wrapper Application/Base.lproj/MainMenu.xib",
|
||||
"exists": true,
|
||||
"bytes": 50614
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Wrapper Application/en.lproj/Credits.rtf",
|
||||
"exists": true,
|
||||
"bytes": 264
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Wrapper Application/en.lproj/InfoPlist.strings",
|
||||
"exists": true,
|
||||
"bytes": 0
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/en.lproj/InfoPlist.strings",
|
||||
"exists": true,
|
||||
"bytes": 85
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/en.lproj/Localizable.strings",
|
||||
"exists": true,
|
||||
"bytes": 132
|
||||
},
|
||||
{
|
||||
"path": "tests/fixtures/astronaut.png",
|
||||
"exists": true,
|
||||
"bytes": 791555
|
||||
},
|
||||
{
|
||||
"path": "tests/fixtures/ATTRIBUTION.txt",
|
||||
"exists": true,
|
||||
"bytes": 498
|
||||
}
|
||||
],
|
||||
"gitFsck": {
|
||||
"args": [
|
||||
"git",
|
||||
"fsck",
|
||||
"--full",
|
||||
"--strict"
|
||||
],
|
||||
"returncode": 0,
|
||||
"stdout": "",
|
||||
"stderr": ""
|
||||
},
|
||||
"submodules": {
|
||||
"args": [
|
||||
"git",
|
||||
"submodule",
|
||||
"status",
|
||||
"--recursive"
|
||||
],
|
||||
"returncode": 0,
|
||||
"stdout": "",
|
||||
"stderr": ""
|
||||
},
|
||||
"lfsPointerFiles": [],
|
||||
"lfsAttributesPresent": false,
|
||||
"remoteRefs": {
|
||||
"args": [
|
||||
"git",
|
||||
"ls-remote",
|
||||
"origin",
|
||||
"refs/heads/main"
|
||||
],
|
||||
"returncode": 0,
|
||||
"stdout": "e03e460a9b3e5ad489aad24095485ba10ceb987d\trefs/heads/main\n",
|
||||
"stderr": ""
|
||||
},
|
||||
"checkpointInvocation": {
|
||||
"mode": "100644",
|
||||
"correctInvocation": "python3 scripts/checkpoint.py ...",
|
||||
"helpCheck": {
|
||||
"args": [
|
||||
"python3",
|
||||
"scripts/checkpoint.py",
|
||||
"--help"
|
||||
],
|
||||
"returncode": 0,
|
||||
"stdout": "usage: checkpoint.py [-h] [--tag TAG] message files [files ...]\n\npositional arguments:\n message\n files\n\noptions:\n -h, --help show this help message and exit\n --tag TAG\n",
|
||||
"stderr": ""
|
||||
}
|
||||
},
|
||||
"checks": {
|
||||
"beauty-face": {
|
||||
"args": [
|
||||
"zsh",
|
||||
"scripts/test.sh"
|
||||
],
|
||||
"returncode": 0,
|
||||
"stdout": "Beauty renderer: 49 checks, 0 failures (Metal).\nFace processing: 116 checks passed\n",
|
||||
"stderr": "2026-10-21 18:42:58.952 face-tests[1253:5276682] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2\n",
|
||||
"elapsedSeconds": 2.662,
|
||||
"environment": {},
|
||||
"transcript": "/Users/dayu/Documents/PaintFCP/build/delivery/recovery-e03e460-beauty-face.txt"
|
||||
},
|
||||
"host-asan": {
|
||||
"args": [
|
||||
"zsh",
|
||||
"scripts/test-host.sh"
|
||||
],
|
||||
"returncode": 0,
|
||||
"stdout": "Default IOSurface maximum byte difference: 0\nAlternate destination tag maximum byte difference: 0\nHost adapter: 117 checks, 0 failures\n",
|
||||
"stderr": "2026-10-21 18:42:58.460 host-tests[1241:5276594] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2\n",
|
||||
"elapsedSeconds": 2.32,
|
||||
"environment": {
|
||||
"PAINTFCP_HOST_ASAN": "1"
|
||||
},
|
||||
"transcript": "/Users/dayu/Documents/PaintFCP/build/delivery/recovery-e03e460-host-asan.txt"
|
||||
},
|
||||
"osc-asan": {
|
||||
"args": [
|
||||
"zsh",
|
||||
"scripts/test-osc.sh"
|
||||
],
|
||||
"returncode": 0,
|
||||
"stdout": "OSC adapter: 98 checks, 0 failures\n",
|
||||
"stderr": "2026-10-21 18:42:57.862 osc-tests[1236:5276552] PGFaceOSC point-write desired=(0.25,0.75) setter=(0.25,0.75) readback=(0.25,0.75) mode=normalized verified=1\n2026-10-21 18:42:57.887 osc-tests[1236:5276552] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(80,180) surface=320x240 origin=0\n2026-10-21 18:42:58.633 osc-tests[1236:5276552] PGFaceOSC point-write desired=(0.8,0.3) setter=(1152,-432) readback=(0.8,0.3) mode=centered-pixels verified=1\n2026-10-21 18:42:58.633 osc-tests[1236:5276552] PGFaceOSC coordinates selection=(0.7,0.4) objectBounds=(-1920,-1080,3840,2160) canvas=(152,78) surface=320x240 origin=0\n2026-10-21 18:42:58.734 osc-tests[1236:5276552] PGFaceOSC point-write desired=(0.8,0.3) setter=(0.8,0.3) readback=(0.12345,0.87654) mode=normalized verified=0\n2026-10-21 18:42:58.734 osc-tests[1236:5276552] PGFaceOSC point-write desired=(0.75,0.25) setter=(0.75,0.25) readback=(0.75,0.25) mode=normalized verified=1\n2026-10-21 18:42:58.734 osc-tests[1236:5276552] PGFaceOSC coordinates selection=(0.75,0.25) objectBounds=(-100,-50,200,100) canvas=(160,60) surface=320x240 origin=0\n2026-10-21 18:42:58.813 osc-tests[1236:5276552] PGFaceOSC point-write desired=(0.625,0.375) setter=(0.625,0.375) readback=(0.625,0.375) mode=normalized verified=1\n2026-10-21 18:42:58.813 osc-tests[1236:5276552] PGFaceOSC coordinates selection=(0.625,0.375) objectBounds=(-100,-50,200,100) canvas=(240,60) surface=320x240 origin=0\n2026-10-21 18:42:58.892 osc-tests[1236:5276552] PGFaceOSC point-write desired=(0.25,0.75) setter=(0.25,0.75) readback=(0.25,0.75) mode=normalized verified=1\n2026-10-21 18:42:58.892 osc-tests[1236:5276552] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(144,180) surface=320x240 origin=0\n2026-10-21 18:42:58.976 osc-tests[1236:5276552] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1\n2026-10-21 18:42:58.977 osc-tests[1236:5276552] PGFaceOSC coordinates selection=(0,1) objectBounds=(-100,-50,200,100) canvas=(100,140) surface=320x240 origin=0\n2026-10-21 18:42:59.070 osc-tests[1236:5276552] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(nan,nan) surface=320x240 origin=0\n2026-10-21 18:42:59.137 osc-tests[1236:5276552] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1\n2026-10-21 18:42:59.137 osc-tests[1236:5276552] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(inf,nan) surface=320x240 origin=0\n2026-10-21 18:42:59.206 osc-tests[1236:5276552] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1\n2026-10-21 18:42:59.206 osc-tests[1236:5276552] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(80,nan) surface=320x240 origin=0\n2026-10-21 18:42:59.265 osc-tests[1236:5276552] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1\n2026-10-21 18:42:59.265 osc-tests[1236:5276552] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1\n",
|
||||
"elapsedSeconds": 2.201,
|
||||
"environment": {
|
||||
"PAINTFCP_OSC_ASAN": "1"
|
||||
},
|
||||
"transcript": "/Users/dayu/Documents/PaintFCP/build/delivery/recovery-e03e460-osc-asan.txt"
|
||||
},
|
||||
"xcode-list": {
|
||||
"args": [
|
||||
"xcodebuild",
|
||||
"-list",
|
||||
"-project",
|
||||
"beatygril/beatygril.xcodeproj"
|
||||
],
|
||||
"returncode": 0,
|
||||
"stdout": "Command line invocation:\n /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild -list -project beatygril/beatygril.xcodeproj\n\nInformation about project \"beatygril\":\n Targets:\n Wrapper Application\n XPC Service\n\n Build Configurations:\n Debug\n Release\n\n If no build configuration is specified and -scheme is not passed then \"Release\" is used.\n\n Schemes:\n Wrapper Application\n XPC Service\n\n",
|
||||
"stderr": "",
|
||||
"elapsedSeconds": 1.096,
|
||||
"environment": {},
|
||||
"transcript": "/Users/dayu/Documents/PaintFCP/build/delivery/recovery-e03e460-xcode-list.txt"
|
||||
}
|
||||
},
|
||||
"testSummary": {
|
||||
"checks": {
|
||||
"beauty": 49,
|
||||
"face": 116,
|
||||
"host": 117,
|
||||
"osc": 98
|
||||
},
|
||||
"total": 380,
|
||||
"failures": 0,
|
||||
"asan": [
|
||||
"host",
|
||||
"osc"
|
||||
]
|
||||
},
|
||||
"workspaceDifferencesFromTarget": [],
|
||||
"workspaceExpectedDocumentationDifferencesOnly": true,
|
||||
"clonePostCheckTrackedDiff": {
|
||||
"args": [
|
||||
"git",
|
||||
"status",
|
||||
"--porcelain",
|
||||
"--untracked-files=no"
|
||||
],
|
||||
"returncode": 0,
|
||||
"stdout": "",
|
||||
"stderr": ""
|
||||
},
|
||||
"passed": true,
|
||||
"notes": [
|
||||
"Expected RECOVERY.md/docs/验证记录.md edits after source freeze do not invalidate byte recovery of this commit.",
|
||||
"scripts/checkpoint.py mode 100644 is usable through explicit python3 invocation; --help passed.",
|
||||
"Only standalone CLI test programs and Xcode project listing were executed. No APP build or host/UI execution."
|
||||
],
|
||||
"temporaryCloneRemoved": true,
|
||||
"completedAt": "2026-10-21T10:42:59.816535+00:00"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
独立远端恢复验收:通过
|
||||
源码节点:e03e460a9b3e5ad489aad24095485ba10ceb987d
|
||||
远程:https://dayu.media/git/dayu/paintfcp-fxplug.git
|
||||
远程 main 与节点精确一致。
|
||||
|
||||
新临时克隆 detached 检出:51 个跟踪文件逐 byte/blob/模式一致。
|
||||
必要 9 项资源完整(包含标准 Drop Zone.tiff、NASA astronaut fixture 与授权说明)。
|
||||
git fsck --full --strict 通过;无 LFS 指针、无子模块。
|
||||
|
||||
完整离屏检查 380:Beauty49 + Face116 + Host117 + OSC98,零失败。
|
||||
Host、OSC 均使用 AddressSanitizer,通过。
|
||||
xcodebuild -list 通过;未构建 APP。
|
||||
|
||||
工作区相对源码节点的预期文档差异:
|
||||
- 无
|
||||
checkpoint.py 模式 100644;python3 scripts/checkpoint.py --help 已通过。
|
||||
未修改源码、提交、安装、启动 APP 或操作宿主 UI。
|
||||
本次临时克隆已清理。
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"version": "1.0.4",
|
||||
"build": "6",
|
||||
"mainBundleID": "com.dayu.beatygril",
|
||||
"pluginBundleID": "com.dayu.beatygril.fxplug",
|
||||
"sourceCommit": "e03e460a9b3e5ad489aad24095485ba10ceb987d",
|
||||
"notarization": {
|
||||
"status": "Accepted",
|
||||
"id": "8eb96294-5935-49ff-a487-3c4f60c27395",
|
||||
"message": "Processing complete"
|
||||
},
|
||||
"architectures": "x86_64 arm64",
|
||||
"inventoryRoots": [
|
||||
"/Users/dayu/Documents/PaintFCP",
|
||||
"/Users/dayu/Library/Developer/Xcode/DerivedData",
|
||||
"/Users/dayu/Downloads",
|
||||
"/Users/dayu/Desktop",
|
||||
"/Applications",
|
||||
"/Library/Plug-Ins/FxPlug",
|
||||
"/tmp/paintfcp-build-20261021"
|
||||
],
|
||||
"appCopiesBeforeCleanup": [
|
||||
"/Users/dayu/Documents/PaintFCP/build/delivery/previous-beatygril.app",
|
||||
"/Library/Plug-Ins/FxPlug/beatygril.app",
|
||||
"/tmp/paintfcp-build-20261021/Build/Products/Release/beatygril.app"
|
||||
],
|
||||
"inventoryErrors": [],
|
||||
"appCopiesAfterCleanup": [
|
||||
"/Library/Plug-Ins/FxPlug/beatygril.app"
|
||||
],
|
||||
"temporaryBuildRemoved": true,
|
||||
"previousBackupRemoved": true,
|
||||
"notarizationZipRemoved": true,
|
||||
"registry": "com.dayu.beatygril.fxplug(1.1)\n\t Path = /Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit\n\t UUID = 6E26839B-7A7D-4BF9-B3EA-EA0F0F35F54D\n\t Timestamp = 2026-10-21 10:43:01 +0000\n\t Parent Bundle = /Library/Plug-Ins/FxPlug/beatygril.app\n\t Display Name = 大鱼人像美化\n\t Short Name = beatygril XPC Service\n\t Parent Name = beatygril\n\t Platform = macOS\n\n (1 plug-in)",
|
||||
"hostValidation": {
|
||||
"Motion": "1.0.4 formal plugin loaded real-video project and produced beauty + primary tracking box; playhead changed across source positions; no offline analysis enabled in project. Native explicit OSC click test incomplete due user interaction.",
|
||||
"FCP": "Template input/brightness passed native tests before realtime alignment; 1.0.4 automatic-follow native FCP test not yet performed.",
|
||||
"handoff": "User explicitly requested to operate Motion/FCP for self-testing; automation ceased."
|
||||
},
|
||||
"installedFinalChecks": {
|
||||
"signature": {
|
||||
"exit": 0,
|
||||
"output": ""
|
||||
},
|
||||
"ticket": {
|
||||
"exit": 0,
|
||||
"output": "Processing: /Library/Plug-Ins/FxPlug/beatygril.app\nThe validate action worked!\n"
|
||||
},
|
||||
"gatekeeper": {
|
||||
"exit": 0,
|
||||
"output": "/Library/Plug-Ins/FxPlug/beatygril.app: accepted\nsource=Notarized Developer ID\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
Beauty renderer: 49 checks, 0 failures (Metal).
|
||||
2026-10-21 20:18:32.623 face-tests[6394:5408005] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
|
||||
Face processing: 116 checks passed
|
||||
2026-10-21 20:18:36.942 slim-tests[6433:5408137] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
|
||||
PASS: 224 slim alignment checks; maximum UV oracle error 1.09828022e-05
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"status": "installed_user_qa_pending",
|
||||
"bundleIdentifier": "com.dayu.beatygril",
|
||||
"installationPath": "/Library/Plug-Ins/FxPlug/beatygril.app",
|
||||
"options": {
|
||||
"waitForHosts": true,
|
||||
"userTests": true,
|
||||
"resumePrepared": true
|
||||
},
|
||||
"nativeEffectQA": {
|
||||
"status": "pending_user",
|
||||
"performedByScript": false
|
||||
},
|
||||
"source": {
|
||||
"commit": "abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964",
|
||||
"trackedWorktreeChanges": []
|
||||
},
|
||||
"updatedAt": "2026-10-21T12:23:23.275614+00:00",
|
||||
"version": "1.0.5",
|
||||
"build": "7",
|
||||
"stagingSignature": "Executable=/private/tmp/paintfcp-build-20261021/Build/Products/Release/beatygril.app/Contents/MacOS/beatygril\nIdentifier=com.dayu.beatygril\nFormat=app bundle with Mach-O universal (x86_64 arm64)\nCodeDirectory v=20500 size=478 flags=0x10000(runtime) hashes=4+7 location=embedded\nHash type=sha256 size=32\nCandidateCDHash sha256=573debde15f2628c7bc7c6ed9a4cdeabd33f7518\nCandidateCDHashFull sha256=573debde15f2628c7bc7c6ed9a4cdeabd33f7518762fa036f842d998dd623720\nHash choices=sha256\nCMSDigest=573debde15f2628c7bc7c6ed9a4cdeabd33f7518762fa036f842d998dd623720\nCMSDigestType=2\nCDHash=573debde15f2628c7bc7c6ed9a4cdeabd33f7518\nSignature size=8968\nAuthority=Developer ID Application: zhong luo (B3W25FX5H5)\nAuthority=Developer ID Certification Authority\nAuthority=Apple Root CA\nTimestamp=Oct 21, 2026 at 20:20:14\nInfo.plist entries=22\nTeamIdentifier=B3W25FX5H5\nRuntime Version=27.0.0\nSealed Resources version=2 rules=13 files=4\nInternal requirements count=1 size=180\nTotal signatures=1\nChosen signature=1\n",
|
||||
"notarization": {
|
||||
"id": "7d7034f8-7389-410f-ad8e-9e722408c0fc",
|
||||
"status": "Accepted",
|
||||
"message": "Processing complete"
|
||||
},
|
||||
"resumedBy": {
|
||||
"commit": "a6551c334fd1a0883259700b802174955c78376a",
|
||||
"trackedWorktreeChanges": []
|
||||
},
|
||||
"signingVerified": true,
|
||||
"stapleValidated": true,
|
||||
"stagingPluginUnregistered": true,
|
||||
"runningHosts": [],
|
||||
"installedSignature": "Executable=/Library/Plug-Ins/FxPlug/beatygril.app/Contents/MacOS/beatygril\nIdentifier=com.dayu.beatygril\nFormat=app bundle with Mach-O universal (x86_64 arm64)\nCodeDirectory v=20500 size=478 flags=0x10000(runtime) hashes=4+7 location=embedded\nHash type=sha256 size=32\nCandidateCDHash sha256=573debde15f2628c7bc7c6ed9a4cdeabd33f7518\nCandidateCDHashFull sha256=573debde15f2628c7bc7c6ed9a4cdeabd33f7518762fa036f842d998dd623720\nHash choices=sha256\nCMSDigest=573debde15f2628c7bc7c6ed9a4cdeabd33f7518762fa036f842d998dd623720\nCMSDigestType=2\nCDHash=573debde15f2628c7bc7c6ed9a4cdeabd33f7518\nSignature size=8968\nAuthority=Developer ID Application: zhong luo (B3W25FX5H5)\nAuthority=Developer ID Certification Authority\nAuthority=Apple Root CA\nTimestamp=Oct 21, 2026 at 20:20:14\nNotarization Ticket=stapled\nInfo.plist entries=22\nTeamIdentifier=B3W25FX5H5\nRuntime Version=27.0.0\nSealed Resources version=2 rules=13 files=4\nInternal requirements count=1 size=180\nTotal signatures=1\nChosen signature=1\n",
|
||||
"installedSignatureVerified": true,
|
||||
"installedStapleValidated": true,
|
||||
"gatekeeperAccepted": true,
|
||||
"pluginDiscovery": {
|
||||
"bundleIdentifier": "com.dayu.beatygril.fxplug",
|
||||
"formalPath": "/Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit",
|
||||
"output": " com.dayu.beatygril.fxplug(1.1)\tF419A538-F395-459D-9984-AC46EAB5E83D\t2026-10-21 12:23:23 +0000\t/Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit\n (1 plug-in)\n"
|
||||
},
|
||||
"removedGeneratedPaths": [
|
||||
"/Users/dayu/Documents/PaintFCP/build/delivery/previous-beatygril.app",
|
||||
"/tmp/paintfcp-build-20261021",
|
||||
"/Users/dayu/Documents/PaintFCP/build/delivery/notarization.zip"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
2026-10-21 20:18:33.844 host-tests[6427:5408121] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
|
||||
Default IOSurface maximum byte difference: 0
|
||||
Alternate destination tag maximum byte difference: 0
|
||||
Host adapter: 117 checks, 0 failures
|
||||
@@ -0,0 +1,2 @@
|
||||
com.dayu.beatygril.fxplug(1.1) F419A538-F395-459D-9984-AC46EAB5E83D 2026-10-21 12:23:23 +0000 /Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit
|
||||
(1 plug-in)
|
||||
@@ -0,0 +1,24 @@
|
||||
Executable=/Library/Plug-Ins/FxPlug/beatygril.app/Contents/MacOS/beatygril
|
||||
Identifier=com.dayu.beatygril
|
||||
Format=app bundle with Mach-O universal (x86_64 arm64)
|
||||
CodeDirectory v=20500 size=478 flags=0x10000(runtime) hashes=4+7 location=embedded
|
||||
Hash type=sha256 size=32
|
||||
CandidateCDHash sha256=573debde15f2628c7bc7c6ed9a4cdeabd33f7518
|
||||
CandidateCDHashFull sha256=573debde15f2628c7bc7c6ed9a4cdeabd33f7518762fa036f842d998dd623720
|
||||
Hash choices=sha256
|
||||
CMSDigest=573debde15f2628c7bc7c6ed9a4cdeabd33f7518762fa036f842d998dd623720
|
||||
CMSDigestType=2
|
||||
CDHash=573debde15f2628c7bc7c6ed9a4cdeabd33f7518
|
||||
Signature size=8968
|
||||
Authority=Developer ID Application: zhong luo (B3W25FX5H5)
|
||||
Authority=Developer ID Certification Authority
|
||||
Authority=Apple Root CA
|
||||
Timestamp=Oct 21, 2026 at 20:20:14
|
||||
Notarization Ticket=stapled
|
||||
Info.plist entries=22
|
||||
TeamIdentifier=B3W25FX5H5
|
||||
Runtime Version=27.0.0
|
||||
Sealed Resources version=2 rules=13 files=4
|
||||
Internal requirements count=1 size=180
|
||||
Total signatures=1
|
||||
Chosen signature=1
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"version": "1.0.5",
|
||||
"build": "7",
|
||||
"uiOperations": 0,
|
||||
"template": {
|
||||
"path": "/Users/dayu/Movies/Motion Templates.localized/Effects.localized/大鱼/大鱼人像美化/大鱼人像美化.moef",
|
||||
"sha256": "ba08de6916cd837b3733aa0c6fcb78f2e8224431d25e1310089ae67d18445f2d",
|
||||
"resourceVerified": true
|
||||
},
|
||||
"codesign": "",
|
||||
"xcrun": "Processing: /Library/Plug-Ins/FxPlug/beatygril.app\nThe validate action worked!",
|
||||
"spctl": "/Library/Plug-Ins/FxPlug/beatygril.app: accepted\nsource=Notarized Developer ID",
|
||||
"Contents/MacOS/beatygril": "x86_64 arm64",
|
||||
"Contents/PlugIns/beatygril XPC Service.pluginkit/Contents/MacOS/beatygril XPC Service": "x86_64 arm64",
|
||||
"duplicateInventory": {
|
||||
"scopes": [
|
||||
"/Users/dayu/Documents/PaintFCP",
|
||||
"/Users/dayu/Library/Developer/Xcode/DerivedData",
|
||||
"/Users/dayu/Desktop",
|
||||
"/Users/dayu/Downloads",
|
||||
"/Applications",
|
||||
"/Library/Plug-Ins/FxPlug",
|
||||
"/Users/dayu/Library/Plug-Ins/FxPlug"
|
||||
],
|
||||
"matchingOuterApps": [
|
||||
"/Library/Plug-Ins/FxPlug/beatygril.app"
|
||||
],
|
||||
"errors": []
|
||||
},
|
||||
"discovery": " com.dayu.beatygril.fxplug(1.1)\tF419A538-F395-459D-9984-AC46EAB5E83D\t2026-10-21 12:23:23 +0000\t/Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit\n (1 plug-in)\n",
|
||||
"status": "installed_verified_user_visual_tests_pending"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"id":"7d7034f8-7389-410f-ad8e-9e722408c0fc","status":"Accepted","message":"Processing complete"}
|
||||
@@ -0,0 +1,21 @@
|
||||
2026-10-21 20:18:34.451 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.25,0.75) setter=(0.25,0.75) readback=(0.25,0.75) mode=normalized verified=1
|
||||
2026-10-21 20:18:34.466 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(80,180) surface=320x240 origin=0
|
||||
2026-10-21 20:18:34.897 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.8,0.3) setter=(1152,-432) readback=(0.8,0.3) mode=centered-pixels verified=1
|
||||
2026-10-21 20:18:34.897 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0.7,0.4) objectBounds=(-1920,-1080,3840,2160) canvas=(152,78) surface=320x240 origin=0
|
||||
2026-10-21 20:18:34.948 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.8,0.3) setter=(0.8,0.3) readback=(0.12345,0.87654) mode=normalized verified=0
|
||||
2026-10-21 20:18:34.948 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.75,0.25) setter=(0.75,0.25) readback=(0.75,0.25) mode=normalized verified=1
|
||||
2026-10-21 20:18:34.948 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0.75,0.25) objectBounds=(-100,-50,200,100) canvas=(160,60) surface=320x240 origin=0
|
||||
2026-10-21 20:18:35.005 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.625,0.375) setter=(0.625,0.375) readback=(0.625,0.375) mode=normalized verified=1
|
||||
2026-10-21 20:18:35.005 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0.625,0.375) objectBounds=(-100,-50,200,100) canvas=(240,60) surface=320x240 origin=0
|
||||
2026-10-21 20:18:35.057 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.25,0.75) setter=(0.25,0.75) readback=(0.25,0.75) mode=normalized verified=1
|
||||
2026-10-21 20:18:35.057 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(144,180) surface=320x240 origin=0
|
||||
2026-10-21 20:18:35.108 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
|
||||
2026-10-21 20:18:35.108 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0,1) objectBounds=(-100,-50,200,100) canvas=(100,140) surface=320x240 origin=0
|
||||
2026-10-21 20:18:35.167 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(nan,nan) surface=320x240 origin=0
|
||||
2026-10-21 20:18:35.217 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
|
||||
2026-10-21 20:18:35.217 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(inf,nan) surface=320x240 origin=0
|
||||
2026-10-21 20:18:35.267 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
|
||||
2026-10-21 20:18:35.267 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(80,nan) surface=320x240 origin=0
|
||||
2026-10-21 20:18:35.316 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
|
||||
2026-10-21 20:18:35.316 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
|
||||
OSC adapter: 98 checks, 0 failures
|
||||
@@ -0,0 +1,11 @@
|
||||
Independent remote recovery test
|
||||
Target: abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964
|
||||
Command: zsh scripts/test.sh
|
||||
Exit code: 0
|
||||
|
||||
Beauty renderer: 49 checks, 0 failures (Metal).
|
||||
Face processing: 116 checks passed
|
||||
PASS: 224 slim alignment checks; maximum UV oracle error 1.09828022e-05
|
||||
|
||||
2026-10-21 20:20:38.246 face-tests[7122:5413459] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
|
||||
2026-10-21 20:20:40.227 slim-tests[7129:5413512] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
|
||||
@@ -0,0 +1,740 @@
|
||||
{
|
||||
"target": "abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964",
|
||||
"remote": "https://dayu.media/git/dayu/paintfcp-fxplug.git",
|
||||
"startedAt": "2026-10-21T12:20:32.822716+00:00",
|
||||
"temporaryClone": "/var/folders/t4/kbc10sqs43b0rjf3nfh51nmw0000gn/T/paintfcp-recovery-abe97ed-um2yk1vm/clone",
|
||||
"scope": "Independent source recovery plus scripts/test.sh only; no APP build, host execution/UI, installation, root source changes, commit, host/OSC ASan or delivery-lock operation.",
|
||||
"cloneHead": "abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964",
|
||||
"files": {
|
||||
"count": 69,
|
||||
"allByteBlobAndModeEqual": true,
|
||||
"entries": [
|
||||
{
|
||||
"path": ".gitignore",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "d2bca6e38076bb5f7574cc7781eadef27b41d9ff",
|
||||
"bytes": 490,
|
||||
"sha256": "34e63260d343569ad4d5a1ffd6c15647da5a7da2b8940dc93f6d987c7fec474e",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "README.md",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "d0282233a2a630f2a00a7bd1e3342ba4f3a0791a",
|
||||
"bytes": 3167,
|
||||
"sha256": "3f1d5c727a335ef862837db92a57ba684424e802cbdee4d64f6bdae069e43076",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "RECOVERY.md",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "d97c7731d0a3d349c72982ad156a581ef6a03a87",
|
||||
"bytes": 4183,
|
||||
"sha256": "2f310d4b5f4df318b8b522963c3f1bbe35c1e2a3b3ed85a112d59fc10205c36a",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "Templates/大鱼人像美化/Drop Zone.tiff",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "6d15c98bc221f999514f197ff67a7145836e6a38",
|
||||
"bytes": 420128,
|
||||
"sha256": "a7f8e3a2a431dbb3ce9e777040bb55e701cfe9ab9632e8cbb10359732bc8692e",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "Templates/大鱼人像美化/PARAMETERS.json",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "df2c01f370e6ee639d2950774fe8020742802c17",
|
||||
"bytes": 9815,
|
||||
"sha256": "bb561b0dfff90223f0925a60ec05b28518e22163a6bf7922c4efc6d155ddbabb",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "Templates/大鱼人像美化/README.md",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "46cf59f6b4500b32c5a203a63a79a491a2202cdf",
|
||||
"bytes": 4420,
|
||||
"sha256": "c57ef81f43271d566a0488385d5f6b8e492142bae0860049bcbf943206b0c567",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "Templates/大鱼人像美化/VALIDATION.json",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "63aa4f2ce4e1e72ce8b255d5f98fe209e2354c09",
|
||||
"bytes": 1872,
|
||||
"sha256": "558ac5ff3bee28e6d3df41cc5ffd9a43cd51fca2fd001fff8641260c327e351d",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "Templates/大鱼人像美化/大鱼人像美化.moef",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "f68569a62b399cf7ad1ee2b33427372fc9b6b913",
|
||||
"bytes": 22996,
|
||||
"sha256": "ba08de6916cd837b3733aa0c6fcb78f2e8224431d25e1310089ae67d18445f2d",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril.xcodeproj/project.pbxproj",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "db5251bf3c337b865621e771a4b28690214f1c8e",
|
||||
"bytes": 22986,
|
||||
"sha256": "e9b9be100e53003f6bba96658d3a36078bb607236958e43aec6ed59f8c73ce32",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "919434a6254f0e9651f402737811be6634a03e9c",
|
||||
"bytes": 135,
|
||||
"sha256": "7f3b00b5c3fdb45242d7b87e1e5c4e25d1fa8129a16c94295ecc4e8ea2235c5f",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/Info.plist",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "95542fa8c27e6770b9266389f0aa34027053e31f",
|
||||
"bytes": 3030,
|
||||
"sha256": "865f1775fb476ff1f22556141b41b25d3569c05e4aa1d2450cccdcb06de6aa4c",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/PGBeautyRenderer.h",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "a0a6965c837072e6d837a98b701d59b1cc139655",
|
||||
"bytes": 1410,
|
||||
"sha256": "4e9b663c47261e4feceb7ab38c25171b65f36c14417d82a8b7c49ee3201492d3",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/PGBeautyRenderer.m",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "4c9fc6fbb1dc1f2c4048a16435f554cae9f2ca74",
|
||||
"bytes": 17132,
|
||||
"sha256": "00b52b1f22c1e875f137ee4ea90f50d6caa38db277bebd0da538d0ba7e66072b",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/PGBeautyTypes.h",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "e1cd3ef21f37b831cdaa7d9f52211cafaf243994",
|
||||
"bytes": 1149,
|
||||
"sha256": "6ea8005accc22f85fe8be1b73bf6e89a5b37ffd6e97460d4f6dd69d55eaf33cc",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/PGFaceProcessor.h",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "0b861830a6e9652d81ccfe62f2ce758febee449f",
|
||||
"bytes": 2178,
|
||||
"sha256": "a3448bd75fd278c9702f079f55637ba2c26bc820bdbb81f49fb3a3d0e2a4aba5",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/PGFaceProcessor.m",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "08c1e85455da07203e4ee30a5410fb10cb9e78c7",
|
||||
"bytes": 42670,
|
||||
"sha256": "445aac56a8ff7332c3b94c6ad5b37b9e412af757995a8332ae2e70066d348d7e",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/PGFaceSelectOSC.m",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "ae0aa7ce1c2de355b2c1cb890314a0de689df81a",
|
||||
"bytes": 11630,
|
||||
"sha256": "d5e1dd1db6825730803ddc3f21b588cd548cbbacc60760aa45ad9e2feedae307",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/PGParameterIDs.h",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "4cc0e20af1e659f1e8e666fabe01003c06cfd885",
|
||||
"bytes": 932,
|
||||
"sha256": "8fdfa2021131317ce09532d27fd6f34fb0b505f7b17c561819336ad2a06c42cc",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/beatygrilPlugIn.h",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "12dca07a744c064d5d474e0294304ca29055a077",
|
||||
"bytes": 347,
|
||||
"sha256": "4f00716328c9237b79cc58153ba47d8ac8ddc1786939cb206bd37b01e3b6397e",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/beatygrilPlugIn.m",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "3f148956b32770c7db5b0df97700fd96f7e20025",
|
||||
"bytes": 24193,
|
||||
"sha256": "b8f4326ed3eb2de74770e4ea266d09ebe4aea70428a4511f0ec501a0cd89b247",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/en.lproj/InfoPlist.strings",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "9a5f27348a39c8e3ee50cffc2fb359c78a1319c8",
|
||||
"bytes": 85,
|
||||
"sha256": "1b1cd5b74690bd5a74cf2b0a0c068bbc2c3462c5e9fe5ca3e61541bcbd451a7a",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/en.lproj/Localizable.strings",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "7f52f3dd719cb4f94e24867e36114cb061ef0b92",
|
||||
"bytes": 132,
|
||||
"sha256": "b3036a2339b97ebf264516e1e534351d1ceb64f45942595508f4d67d07b07769",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/main.m",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "34d50d3f8cdf438d3508d0aa94e3f2d20e45f962",
|
||||
"bytes": 114,
|
||||
"sha256": "5b812ddb1ef171fe075e4faf13d761935ca73ecbba1be0bd9d4802a64a7759cd",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Wrapper Application/AppDelegate.h",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "f3f8e19cfe057114c2289ff37c01c2e6e1aa7ed0",
|
||||
"bytes": 167,
|
||||
"sha256": "3fc6516be93176f90c2deb17332c9cf98e670458a33fbad4fe7b8f367ec9494f",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Wrapper Application/AppDelegate.m",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "1cb56f073356dd30add33415117bf4c24fdc4ef0",
|
||||
"bytes": 3003,
|
||||
"sha256": "acef6ade6591c039cd8b3e603f83d6d06f64da9709a0385b73c8588e824fe6de",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Wrapper Application/Base.lproj/MainMenu.xib",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "57d03a50e249d189c5fa0a4602c5ec846f06612d",
|
||||
"bytes": 50614,
|
||||
"sha256": "7f0945bc5903a658550b712267870edecbdf1b73ac83c71f1358468f40c1ab1d",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Wrapper Application/Info.plist",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "a1fe6cdcfe8e41f2de4ee07525e192e52723d760",
|
||||
"bytes": 1084,
|
||||
"sha256": "7b3ff213b68dda774306a96835cf4743e3998e23dd7ac4b20cbdb3cf709d7b8b",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Wrapper Application/SandboxEntitlements.entitlements",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "852fa1a4728ae4789e3bca55dd07caef3b41f2a5",
|
||||
"bytes": 240,
|
||||
"sha256": "b746c74b201e6af4e0864fbd1f4f629f96156a7ec4b7208bc995d0a5089bb592",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Wrapper Application/en.lproj/Credits.rtf",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "da779d0f6bec11568066e347a01ccef0308488b2",
|
||||
"bytes": 264,
|
||||
"sha256": "ec846e198df85720e8443abb810334411b5690386f3062f90208c23b8575f28a",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Wrapper Application/en.lproj/InfoPlist.strings",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
|
||||
"bytes": 0,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Wrapper Application/main.m",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "99ad4384792f9b2050e0755c07522c0dab9b4b86",
|
||||
"bytes": 112,
|
||||
"sha256": "5c3dc97a53d0379af0e0ce75c5541f33c47f2da115d2d0018cfba003d185a63d",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/analysis/REA-分析与复刻说明.md",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "6581570f9d140ef8d0e00f19d9285d44a6ae3813",
|
||||
"bytes": 10519,
|
||||
"sha256": "ee64e260fd9261856047096570b8fed308284b3a02cd0dd892f3ceda6a5482ab",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/analysis/auto-follow-report.json",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "e60d6d055a513ab8e9c21d2b39460c9d0698e1d3",
|
||||
"bytes": 15665,
|
||||
"sha256": "1a0c91efcaee058f331f6a7446bea8794af1dcf9120f5c2585f249b188d038e3",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/analysis/beauty-preview.png",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "35ace472cb42629b01b3e05660cb7be8b9711fd7",
|
||||
"bytes": 617622,
|
||||
"sha256": "37aa97855915a49ff1f225a9e1fa9d8bcbed0be5f4109b6835ba4db17f9113f6",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/analysis/face-preview.png",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "9a5bab3ad706b95ac32875352d16d6580dcd48ab",
|
||||
"bytes": 713552,
|
||||
"sha256": "6cc662cbb2b554a0dd8c926d94a0d819506ac76710d9c09c2d3d7f808fbf686b",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/analysis/rea-auto-tracking.json",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "2f5738f934f0ce89780cf4535c7257b54a397bef",
|
||||
"bytes": 2883488,
|
||||
"sha256": "dc1007f3798fade5e454bb2ef0b58e4cfe2bb0256948c364070fd9af70a97377",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/analysis/rea-host.json",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "9de443173a74668f0a2dced3426dae20549a16dd",
|
||||
"bytes": 10111,
|
||||
"sha256": "141866459948b19d5378de2f6534de333c795e317416f2ac6c9fdc86c94dd61a",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/analysis/rea-plugin.json",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "79bcafbcfeda694ed466d95f81f30eb622a95a0b",
|
||||
"bytes": 343165,
|
||||
"sha256": "dda6e450915e1054653c28addfed5254c678dccf14ee66643d508e7087c1f0c1",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/analysis/rea-slim-pose.json",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "249eb3678b20c378e0d73239e0949cc86c2af436",
|
||||
"bytes": 343127,
|
||||
"sha256": "5f4a2dfe5796ce72bb3d022a85a42cd5ed0eceb0cd0655041812126ebec7933a",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/analysis/reference-template-parameters.json",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "9947e2ff182a146bc4c9120dfff7eacc8cca9c0c",
|
||||
"bytes": 1608,
|
||||
"sha256": "026e47e8ecbd9a967d8b8aabddac2a370479a43b9e1a7a7728b5b01a5002f006",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/analysis/slim-pose-report.json",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "755e0dfcb6eef016e932415ad8c391b97f8de575",
|
||||
"bytes": 8724,
|
||||
"sha256": "512d22abc2b08860adb64c38efa0f8e3474b45e3dbd19cb81406b1d9e7060f06",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/analysis/脸型变形与转脸分析.md",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "b98271ebd17b745d2e16e6357578eac6a757c1c1",
|
||||
"bytes": 8789,
|
||||
"sha256": "0121d022e4ba430b9fd62b9a13da5b84d03866eb0d541e64e4da3204730c03cf",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/verification/installed-signature.txt",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "78fc46fd6aee5443af9eeb53a97502d0251cf2a9",
|
||||
"bytes": 988,
|
||||
"sha256": "add93ef0fa60cd32ed766c965e6f2869f56b4da65030e774e76d133b02ef7aec",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/verification/live-face-asan.txt",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "b92d565bec7a49444b638437b4ae8e7400ce90f6",
|
||||
"bytes": 219,
|
||||
"sha256": "b956e0fe7a3fccdd8eff8788807c0aaf60da56aa1cb153087f25234cc156c27d",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/verification/notarization.json",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "86d17c6738b9e0aa6f92583ffc798c2e6b6bece1",
|
||||
"bytes": 98,
|
||||
"sha256": "91b4f5e239a50467bde83b8c40c1c1809f85cd4eb4b1f1091e1b2197c11a79b6",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/verification/recovery-e03e460-beauty-face.txt",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "2c6f0b5e7f7dedf9571d6da6ce67303bf34faedc",
|
||||
"bytes": 298,
|
||||
"sha256": "45698c80fdfc68c2dc68be59ba03380aa6e8194731d4858dc009f827dd06e226",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/verification/recovery-e03e460-host-asan.txt",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "7eeed088f0dad0564aa166232aedb9ae47a56dd0",
|
||||
"bytes": 380,
|
||||
"sha256": "65d0226f95e882702484849997fde558fae7af418e9243702f789b1e2fcacf14",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/verification/recovery-e03e460-osc-asan.txt",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "7abde29290bfea1e475af387899fb3160a60cc95",
|
||||
"bytes": 3336,
|
||||
"sha256": "2e326d2fc0eb5d2cdd9f07e2318dfd17a41d346a82f383b05b51f416ffdacdb3",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/verification/recovery-e03e460-xcode-list.txt",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "e90278ec1aedf6a1176682ec1037e49ba2f588c1",
|
||||
"bytes": 570,
|
||||
"sha256": "ff9ffb300caac840f4e64f97f24712e319af9bbe4cee1dbbd79b0a5caa1432d1",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/verification/recovery-e03e460.json",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "ecd24b6f35c893c3637dc4e268d0acabb4d09967",
|
||||
"bytes": 25360,
|
||||
"sha256": "559839e989fb4fa9589847632975a6d4d8548c00e6ad5b87bdba736bb3e1daf3",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/verification/recovery-e03e460.txt",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "ed81f4abdeada50c1afb2ac866d3cc79fd6579d9",
|
||||
"bytes": 846,
|
||||
"sha256": "3931beb81cf475ce1c44e5b90c8e9101abb1a7ba20b657d72090b6e7662530cc",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/verification/release-receipt.json",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "e53f21de9e8ea68a40f435cf9e3d1a0d95499bde",
|
||||
"bytes": 2454,
|
||||
"sha256": "07ba4f5bbba101a49d4579c0b79fbce0dad70a1e4b5887e61e052741c49486ee",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/verification/v1.0.5/core-tests.txt",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "76d29d91672ec631950b2f3ada1db39db4c877ac",
|
||||
"bytes": 427,
|
||||
"sha256": "408910aeedcf0155bc31ef43ca6e8994d9348f8fbdc0c25eb8a43ce89965e081",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/verification/v1.0.5/host-tests.txt",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "3e1c1c6b7c104995a2656e142671759634352525",
|
||||
"bytes": 271,
|
||||
"sha256": "c727d9d56a0b46a4cdbc79d24eeedc589e5984b38144f28ccf017f4d65f504f7",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/verification/v1.0.5/osc-tests.txt",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "c9c618896ca3ee0c2a943979103ab3b42335c7e0",
|
||||
"bytes": 3229,
|
||||
"sha256": "5e443009bc39635d01a1f6276b48170fe9da73c088646425c6ed140d87ba4518",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "docs/验证记录.md",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "2d2d97b0e13e876bc8ddf65aedf671cbeead8b6b",
|
||||
"bytes": 9154,
|
||||
"sha256": "8ece9ba47c81980a781ab00b73c961bd32c1acec2e09811f348375e162e29661",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "scripts/checkpoint.py",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "8248d1732aceed377d28cdae02873057f442474b",
|
||||
"bytes": 2218,
|
||||
"sha256": "3c27b0e603dd2d94512f611cec97bd9afe5809aea0f5a4bb213995d2e23366f8",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "scripts/deliver.py",
|
||||
"mode": "100755",
|
||||
"kind": "blob",
|
||||
"blob": "b20e2149f84307afd6198df71f967f4facb444bb",
|
||||
"bytes": 10848,
|
||||
"sha256": "dc5df519aaabc695adddff8e9087dcb6c6e9861036ab0efccf242a5fe12e0b78",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "scripts/inspect-face-geometry.m",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "d075cf7c8c9a23b9bd01bbcbc31da89fea84d5d3",
|
||||
"bytes": 6613,
|
||||
"sha256": "a2c54e7297f9f94e9c56c0b29f9ce607756705f2005c1f13e10996bc26c24b66",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "scripts/test-host.sh",
|
||||
"mode": "100755",
|
||||
"kind": "blob",
|
||||
"blob": "ef9c7f6436e3d1babbf272e9d329d65ec6c395cb",
|
||||
"bytes": 1129,
|
||||
"sha256": "7f55ed6de05e6cc59884fb7c96062676bd086f4ac745ae847511eacb12316467",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "scripts/test-osc.sh",
|
||||
"mode": "100755",
|
||||
"kind": "blob",
|
||||
"blob": "ffaed0b8d449eef6acf2fb352d9fea10fc1649fd",
|
||||
"bytes": 941,
|
||||
"sha256": "ed405cd214ce252641074d0ef0a12c4cc0faf3e1869fe92716a98fff7f9ac517",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "scripts/test.sh",
|
||||
"mode": "100755",
|
||||
"kind": "blob",
|
||||
"blob": "f13ba656929fe34b43c143bdfeb4a208c44c7428",
|
||||
"bytes": 932,
|
||||
"sha256": "3ca9cb120b452a0c402debbe5d3399fa4996e0ea0882253470791494815d530d",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "tests/beauty_render_tests.m",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "24050fb3a2630693253202ae1525c280f4f144eb",
|
||||
"bytes": 16870,
|
||||
"sha256": "b04eb2f0d78faec13bbdd2a3286cc74c46c2f536358c7c1c3d5d03c54daa868c",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "tests/face_tests.m",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "9fe21ddd021d1a2517fa8b75d7a65532f6fefedb",
|
||||
"bytes": 33180,
|
||||
"sha256": "0350abe9f206fc371f697c993f5dea420d512259f16947c7a1167569843b215a",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "tests/fixtures/ATTRIBUTION.txt",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "6e439ab97256952b0acd1eef0e1c1b4ab3ea015c",
|
||||
"bytes": 498,
|
||||
"sha256": "8823ad8aed1a5c1bdda93946e4b5ed24cd542e996a2b8087517cf62f7f4f2a84",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "tests/fixtures/astronaut.png",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "834cda0012478c5edc8d43bade96d315dedeaab4",
|
||||
"bytes": 791555,
|
||||
"sha256": "88431cd9653ccd539741b555fb0a46b61558b301d4110412b5bc28b5e3ea6cb5",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "tests/host_adapter_tests.m",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "8091c1523eb0b05295c391e20637c931d681e049",
|
||||
"bytes": 30363,
|
||||
"sha256": "77131c770222d3b404204fb04bf47cf943885eeff6acfc7498d59feb28eb1e73",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "tests/osc_tests.m",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "66fff5f78f8e0ba08026e53375b184a460a29162",
|
||||
"bytes": 22495,
|
||||
"sha256": "a3199147e32facd63373b53f77840408411b04436fb1dd42e2fb71023c8f2f8a",
|
||||
"byteBlobAndModeEqual": true
|
||||
},
|
||||
{
|
||||
"path": "tests/slim_tests.m",
|
||||
"mode": "100644",
|
||||
"kind": "blob",
|
||||
"blob": "3f7654f14e642f72ee57c2a5c3e92df39fe7346a",
|
||||
"bytes": 12759,
|
||||
"sha256": "701b52d7f803b44680586c215e5a351e4dc67661e5ea1a56ca5038567cd34cd6",
|
||||
"byteBlobAndModeEqual": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"requiredResources": [
|
||||
{
|
||||
"path": "Templates/大鱼人像美化/大鱼人像美化.moef",
|
||||
"exists": true,
|
||||
"bytes": 22996
|
||||
},
|
||||
{
|
||||
"path": "Templates/大鱼人像美化/Drop Zone.tiff",
|
||||
"exists": true,
|
||||
"bytes": 420128
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Wrapper Application/Base.lproj/MainMenu.xib",
|
||||
"exists": true,
|
||||
"bytes": 50614
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Wrapper Application/en.lproj/Credits.rtf",
|
||||
"exists": true,
|
||||
"bytes": 264
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Wrapper Application/en.lproj/InfoPlist.strings",
|
||||
"exists": true,
|
||||
"bytes": 0
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/en.lproj/InfoPlist.strings",
|
||||
"exists": true,
|
||||
"bytes": 85
|
||||
},
|
||||
{
|
||||
"path": "beatygril/beatygril/Plugin/en.lproj/Localizable.strings",
|
||||
"exists": true,
|
||||
"bytes": 132
|
||||
},
|
||||
{
|
||||
"path": "tests/fixtures/astronaut.png",
|
||||
"exists": true,
|
||||
"bytes": 791555
|
||||
},
|
||||
{
|
||||
"path": "tests/fixtures/ATTRIBUTION.txt",
|
||||
"exists": true,
|
||||
"bytes": 498
|
||||
}
|
||||
],
|
||||
"gitFsck": {
|
||||
"args": [
|
||||
"git",
|
||||
"fsck",
|
||||
"--full",
|
||||
"--strict"
|
||||
],
|
||||
"returncode": 0,
|
||||
"stdout": "",
|
||||
"stderr": ""
|
||||
},
|
||||
"submodules": {
|
||||
"args": [
|
||||
"git",
|
||||
"submodule",
|
||||
"status",
|
||||
"--recursive"
|
||||
],
|
||||
"returncode": 0,
|
||||
"stdout": "",
|
||||
"stderr": ""
|
||||
},
|
||||
"lfsPointers": [],
|
||||
"remoteMain": {
|
||||
"args": [
|
||||
"git",
|
||||
"ls-remote",
|
||||
"origin",
|
||||
"refs/heads/main"
|
||||
],
|
||||
"returncode": 0,
|
||||
"stdout": "abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964\trefs/heads/main\n",
|
||||
"stderr": ""
|
||||
},
|
||||
"processorUnchangedFromEc22": {
|
||||
"commit": "ec22f1b2cf541d02e7ffbd5cc4edbc4d0cd97311",
|
||||
"equal": true
|
||||
},
|
||||
"test": {
|
||||
"args": [
|
||||
"zsh",
|
||||
"scripts/test.sh"
|
||||
],
|
||||
"returncode": 0,
|
||||
"stdout": "Beauty renderer: 49 checks, 0 failures (Metal).\nFace processing: 116 checks passed\nPASS: 224 slim alignment checks; maximum UV oracle error 1.09828022e-05\n",
|
||||
"stderr": "2026-10-21 20:20:38.246 face-tests[7122:5413459] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2\n2026-10-21 20:20:40.227 slim-tests[7129:5413512] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2\n",
|
||||
"elapsedSeconds": 3.384
|
||||
},
|
||||
"testCounts": {
|
||||
"beauty": 49,
|
||||
"face": 116,
|
||||
"slim": 224
|
||||
},
|
||||
"testTotal": 389,
|
||||
"cloneTrackedWorktreeClean": true,
|
||||
"passed": true,
|
||||
"temporaryCloneRemoved": true,
|
||||
"completedAt": "2026-10-21T12:20:40.364542+00:00"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
独立恢复验收:通过
|
||||
源码节点:abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964
|
||||
远程:https://dayu.media/git/dayu/paintfcp-fxplug.git
|
||||
远程 main 与节点一致。
|
||||
全部 69 个跟踪文件逐 byte/blob/模式一致;9 项必要资源完整。
|
||||
git fsck --full --strict 通过;无 LFS 指针、无子模块。
|
||||
独立 clone 内 scripts/test.sh:Beauty 49、Face 116、Slim 224;共 389 检查通过。
|
||||
未重复 Host/OSC ASan;未构建 APP、启动宿主、操作 UI、安装、修改源码、提交或操作交付锁。
|
||||
本次临时 clone 已清理。
|
||||
@@ -0,0 +1,2 @@
|
||||
2026-10-21 20:20:01.427 slim-asan[6677:5410585] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
|
||||
PASS: 264 slim alignment checks; maximum UV oracle error 1.09828022e-05
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"checks": 12,
|
||||
"cases": 3,
|
||||
"includes_profile": true,
|
||||
"processor_sha256": "fe4f2a6fc28799cc9b81832baaa2d6739320bde9def175ecc8a4494d004d6b28",
|
||||
"max_pupil_sampling_displacement_px": 2.8653327774463833e-05,
|
||||
"peak_distance_to_real_chin_px": [
|
||||
1.1827483260479077,
|
||||
2.8140858161593
|
||||
],
|
||||
"positive_visual_direction": "toward lower lip",
|
||||
"negative_visual_direction": "away from lower lip",
|
||||
"actual_photos_and_full_geometry": "ignored local build only",
|
||||
"scope": "standalone actual PNG anatomy; bypasses FxPlug adapter; no native host operation"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
Beauty renderer: 49 checks, 0 failures (Metal).
|
||||
2026-10-21 20:48:04.999 face-tests[9473:5455578] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
|
||||
Face processing: 116 checks passed
|
||||
2026-10-21 20:48:08.750 slim-tests[9480:5455684] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
|
||||
PASS: 394 slim alignment checks; maximum UV oracle error 1.09828022e-05
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"status": "installed_user_qa_pending",
|
||||
"bundleIdentifier": "com.dayu.beatygril",
|
||||
"installationPath": "/Library/Plug-Ins/FxPlug/beatygril.app",
|
||||
"options": {
|
||||
"waitForHosts": true,
|
||||
"userTests": true
|
||||
},
|
||||
"nativeEffectQA": {
|
||||
"status": "pending_user",
|
||||
"performedByScript": false
|
||||
},
|
||||
"source": {
|
||||
"commit": "ad54726b17fabd11f46bc6121e664eac38960a23",
|
||||
"trackedWorktreeChanges": []
|
||||
},
|
||||
"updatedAt": "2026-10-21T13:53:56.294416+00:00",
|
||||
"version": "1.0.6",
|
||||
"build": "8",
|
||||
"stagingSignature": "Executable=/private/tmp/paintfcp-build-20261021/Build/Products/Release/beatygril.app/Contents/MacOS/beatygril\nIdentifier=com.dayu.beatygril\nFormat=app bundle with Mach-O universal (x86_64 arm64)\nCodeDirectory v=20500 size=478 flags=0x10000(runtime) hashes=4+7 location=embedded\nHash type=sha256 size=32\nCandidateCDHash sha256=66df95f2ce931ca96f52ee5cfa5ad664be49a6c8\nCandidateCDHashFull sha256=66df95f2ce931ca96f52ee5cfa5ad664be49a6c89b8cc1c206d68f0c162b8348\nHash choices=sha256\nCMSDigest=66df95f2ce931ca96f52ee5cfa5ad664be49a6c89b8cc1c206d68f0c162b8348\nCMSDigestType=2\nCDHash=66df95f2ce931ca96f52ee5cfa5ad664be49a6c8\nSignature size=8969\nAuthority=Developer ID Application: zhong luo (B3W25FX5H5)\nAuthority=Developer ID Certification Authority\nAuthority=Apple Root CA\nTimestamp=Oct 21, 2026 at 20:57:26\nInfo.plist entries=22\nTeamIdentifier=B3W25FX5H5\nRuntime Version=27.0.0\nSealed Resources version=2 rules=13 files=4\nInternal requirements count=1 size=180\nTotal signatures=1\nChosen signature=1\n",
|
||||
"notarization": {
|
||||
"id": "ab0271c8-0c4b-4b10-9474-cebe9b55385b",
|
||||
"message": "Processing complete",
|
||||
"status": "Accepted"
|
||||
},
|
||||
"signingVerified": true,
|
||||
"stapleValidated": true,
|
||||
"stagingPluginUnregistered": true,
|
||||
"runningHosts": [],
|
||||
"installedSignature": "Executable=/Library/Plug-Ins/FxPlug/beatygril.app/Contents/MacOS/beatygril\nIdentifier=com.dayu.beatygril\nFormat=app bundle with Mach-O universal (x86_64 arm64)\nCodeDirectory v=20500 size=478 flags=0x10000(runtime) hashes=4+7 location=embedded\nHash type=sha256 size=32\nCandidateCDHash sha256=66df95f2ce931ca96f52ee5cfa5ad664be49a6c8\nCandidateCDHashFull sha256=66df95f2ce931ca96f52ee5cfa5ad664be49a6c89b8cc1c206d68f0c162b8348\nHash choices=sha256\nCMSDigest=66df95f2ce931ca96f52ee5cfa5ad664be49a6c89b8cc1c206d68f0c162b8348\nCMSDigestType=2\nCDHash=66df95f2ce931ca96f52ee5cfa5ad664be49a6c8\nSignature size=8969\nAuthority=Developer ID Application: zhong luo (B3W25FX5H5)\nAuthority=Developer ID Certification Authority\nAuthority=Apple Root CA\nTimestamp=Oct 21, 2026 at 20:57:26\nNotarization Ticket=stapled\nInfo.plist entries=22\nTeamIdentifier=B3W25FX5H5\nRuntime Version=27.0.0\nSealed Resources version=2 rules=13 files=4\nInternal requirements count=1 size=180\nTotal signatures=1\nChosen signature=1\n",
|
||||
"installedSignatureVerified": true,
|
||||
"installedStapleValidated": true,
|
||||
"gatekeeperAccepted": true,
|
||||
"pluginDiscovery": {
|
||||
"bundleIdentifier": "com.dayu.beatygril.fxplug",
|
||||
"formalPath": "/Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit",
|
||||
"output": " com.dayu.beatygril.fxplug(1.1)\tA9F36A44-1E5E-4AB3-A2FD-5C66A76DB8BB\t2026-10-21 13:53:56 +0000\t/Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit\n (1 plug-in)\n"
|
||||
},
|
||||
"removedGeneratedPaths": [
|
||||
"/Users/dayu/Documents/PaintFCP/build/delivery/previous-beatygril.app",
|
||||
"/tmp/paintfcp-build-20261021",
|
||||
"/Users/dayu/Documents/PaintFCP/build/delivery/notarization.zip"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
2026-10-21 20:56:23.542 negative-control[10726:5467718] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
|
||||
FAIL: default sourceBOTTOM/destinationTOP matches independent upright CI output
|
||||
FAIL: default sourceBOTTOM/destinationTOP is byte-identical across coordinate tags
|
||||
FAIL: default sourceBOTTOM/destinationTOP preserves the independently decoded photo
|
||||
FAIL: default sourceTOP/destinationBOTTOM matches independent upright CI output
|
||||
FAIL: default sourceTOP/destinationBOTTOM is byte-identical across coordinate tags
|
||||
FAIL: default sourceTOP/destinationBOTTOM preserves the independently decoded photo
|
||||
FAIL: big-eye sourceBOTTOM/destinationTOP matches independent upright CI output
|
||||
FAIL: big-eye sourceBOTTOM/destinationTOP is byte-identical across coordinate tags
|
||||
FAIL: big-eye sourceBOTTOM/destinationTOP changes the two real eye regions
|
||||
FAIL: big-eye sourceTOP/destinationBOTTOM matches independent upright CI output
|
||||
FAIL: big-eye sourceTOP/destinationBOTTOM is byte-identical across coordinate tags
|
||||
FAIL: big-eye sourceTOP/destinationBOTTOM changes the two real eye regions
|
||||
FAIL: big-eye sourceTOP/destinationTOP matches independent upright CI output
|
||||
FAIL: big-eye sourceTOP/destinationTOP is byte-identical across coordinate tags
|
||||
FAIL: chin-positive sourceBOTTOM/destinationTOP matches independent upright CI output
|
||||
FAIL: chin-positive sourceBOTTOM/destinationTOP is byte-identical across coordinate tags
|
||||
FAIL: chin-positive sourceBOTTOM/destinationTOP changes the real chin rather than eyes or mirrored anatomy
|
||||
FAIL: chin-positive sourceTOP/destinationBOTTOM matches independent upright CI output
|
||||
FAIL: chin-positive sourceTOP/destinationBOTTOM is byte-identical across coordinate tags
|
||||
FAIL: chin-positive sourceTOP/destinationBOTTOM changes the real chin rather than eyes or mirrored anatomy
|
||||
FAIL: chin-positive sourceTOP/destinationTOP matches independent upright CI output
|
||||
FAIL: chin-positive sourceTOP/destinationTOP is byte-identical across coordinate tags
|
||||
FAIL: chin-negative sourceBOTTOM/destinationTOP matches independent upright CI output
|
||||
FAIL: chin-negative sourceBOTTOM/destinationTOP is byte-identical across coordinate tags
|
||||
FAIL: chin-negative sourceBOTTOM/destinationTOP changes the real chin rather than eyes or mirrored anatomy
|
||||
FAIL: chin-negative sourceTOP/destinationBOTTOM matches independent upright CI output
|
||||
FAIL: chin-negative sourceTOP/destinationBOTTOM is byte-identical across coordinate tags
|
||||
FAIL: chin-negative sourceTOP/destinationBOTTOM changes the real chin rather than eyes or mirrored anatomy
|
||||
FAIL: chin-negative sourceTOP/destinationTOP matches independent upright CI output
|
||||
FAIL: chin-negative sourceTOP/destinationTOP is byte-identical across coordinate tags
|
||||
Anatomy default 0/0 max=0 mean=0.0000000 eye=0/0 chin=0 inverted=0
|
||||
Anatomy default 0/1 max=255 mean=75.5590439 eye=512210/458308 chin=640530 inverted=2678038
|
||||
Anatomy default 1/0 max=255 mean=75.5590439 eye=512210/458308 chin=640530 inverted=2678038
|
||||
Anatomy default 1/1 max=0 mean=0.0000000 eye=0/0 chin=0 inverted=0
|
||||
Anatomy big-eye 0/0 max=2 mean=0.0008917 eye=33510/33880 chin=0 inverted=0
|
||||
Anatomy big-eye 0/1 max=255 mean=75.5509195 eye=512210/458308 chin=640530 inverted=2673702
|
||||
Anatomy big-eye 1/0 max=255 mean=75.5510902 eye=512210/458308 chin=640530 inverted=2673881
|
||||
Anatomy big-eye 1/1 max=66 mean=0.0180340 eye=30841/32051 chin=0 inverted=0
|
||||
Anatomy chin-positive 0/0 max=1 mean=0.0001459 eye=0/0 chin=116812 inverted=0
|
||||
Anatomy chin-positive 0/1 max=255 mean=75.5574808 eye=512210/458308 chin=640530 inverted=2671244
|
||||
Anatomy chin-positive 1/0 max=255 mean=75.5593863 eye=512210/458308 chin=640530 inverted=2671753
|
||||
Anatomy chin-positive 1/1 max=52 mean=0.0127611 eye=0/0 chin=121860 inverted=0
|
||||
Anatomy chin-negative 0/0 max=1 mean=0.0000849 eye=0/0 chin=60426 inverted=0
|
||||
Anatomy chin-negative 0/1 max=255 mean=75.5518637 eye=512210/458308 chin=640530 inverted=2681787
|
||||
Anatomy chin-negative 1/0 max=255 mean=75.5510559 eye=512210/458308 chin=640530 inverted=2681767
|
||||
Anatomy chin-negative 1/1 max=21 mean=0.0061512 eye=0/0 chin=63433 inverted=0
|
||||
Restored origin flips negative control: 120 checks, 30 expected failures
|
||||
@@ -0,0 +1,21 @@
|
||||
2026-10-21 20:55:46.681 host-tests[10689:5467036] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
|
||||
Default IOSurface maximum byte difference: 0
|
||||
Alternate destination tag maximum byte difference: 0
|
||||
Anatomy default 0/0 max=0 mean=0.0000000 eye=0/0 chin=0 inverted=0
|
||||
Anatomy default 0/1 max=0 mean=0.0000000 eye=0/0 chin=0 inverted=0
|
||||
Anatomy default 1/0 max=0 mean=0.0000000 eye=0/0 chin=0 inverted=0
|
||||
Anatomy default 1/1 max=0 mean=0.0000000 eye=0/0 chin=0 inverted=0
|
||||
Anatomy big-eye 0/0 max=2 mean=0.0008917 eye=33510/33880 chin=0 inverted=0
|
||||
Anatomy big-eye 0/1 max=2 mean=0.0008917 eye=33510/33880 chin=0 inverted=0
|
||||
Anatomy big-eye 1/0 max=2 mean=0.0008917 eye=33510/33880 chin=0 inverted=0
|
||||
Anatomy big-eye 1/1 max=2 mean=0.0008917 eye=33510/33880 chin=0 inverted=0
|
||||
Anatomy chin-positive 0/0 max=1 mean=0.0001459 eye=0/0 chin=116812 inverted=0
|
||||
Anatomy chin-positive 0/1 max=1 mean=0.0001459 eye=0/0 chin=116812 inverted=0
|
||||
Anatomy chin-positive 1/0 max=1 mean=0.0001459 eye=0/0 chin=116812 inverted=0
|
||||
Anatomy chin-positive 1/1 max=1 mean=0.0001459 eye=0/0 chin=116812 inverted=0
|
||||
Anatomy chin-negative 0/0 max=1 mean=0.0000849 eye=0/0 chin=60426 inverted=0
|
||||
Anatomy chin-negative 0/1 max=1 mean=0.0000849 eye=0/0 chin=60426 inverted=0
|
||||
Anatomy chin-negative 1/0 max=1 mean=0.0000849 eye=0/0 chin=60426 inverted=0
|
||||
Anatomy chin-negative 1/1 max=1 mean=0.0000849 eye=0/0 chin=60426 inverted=0
|
||||
Legacy fallback upright comparison: max=2 mean=0.0010357
|
||||
Host adapter: 247 checks, 0 failures
|
||||
@@ -0,0 +1,2 @@
|
||||
com.dayu.beatygril.fxplug(1.1) A9F36A44-1E5E-4AB3-A2FD-5C66A76DB8BB 2026-10-21 13:53:56 +0000 /Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit
|
||||
(1 plug-in)
|
||||
@@ -0,0 +1,24 @@
|
||||
Executable=/Library/Plug-Ins/FxPlug/beatygril.app/Contents/MacOS/beatygril
|
||||
Identifier=com.dayu.beatygril
|
||||
Format=app bundle with Mach-O universal (x86_64 arm64)
|
||||
CodeDirectory v=20500 size=478 flags=0x10000(runtime) hashes=4+7 location=embedded
|
||||
Hash type=sha256 size=32
|
||||
CandidateCDHash sha256=66df95f2ce931ca96f52ee5cfa5ad664be49a6c8
|
||||
CandidateCDHashFull sha256=66df95f2ce931ca96f52ee5cfa5ad664be49a6c89b8cc1c206d68f0c162b8348
|
||||
Hash choices=sha256
|
||||
CMSDigest=66df95f2ce931ca96f52ee5cfa5ad664be49a6c89b8cc1c206d68f0c162b8348
|
||||
CMSDigestType=2
|
||||
CDHash=66df95f2ce931ca96f52ee5cfa5ad664be49a6c8
|
||||
Signature size=8969
|
||||
Authority=Developer ID Application: zhong luo (B3W25FX5H5)
|
||||
Authority=Developer ID Certification Authority
|
||||
Authority=Apple Root CA
|
||||
Timestamp=Oct 21, 2026 at 20:57:26
|
||||
Notarization Ticket=stapled
|
||||
Info.plist entries=22
|
||||
TeamIdentifier=B3W25FX5H5
|
||||
Runtime Version=27.0.0
|
||||
Sealed Resources version=2 rules=13 files=4
|
||||
Internal requirements count=1 size=180
|
||||
Total signatures=1
|
||||
Chosen signature=1
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"version": "1.0.6",
|
||||
"build": "8",
|
||||
"uiOperations": false,
|
||||
"nativeVisualQA": "pending user",
|
||||
"installationPath": "/Library/Plug-Ins/FxPlug/beatygril.app",
|
||||
"codesign": {
|
||||
"exitCode": 0,
|
||||
"stdout": "",
|
||||
"stderr": ""
|
||||
},
|
||||
"staple": {
|
||||
"exitCode": 0,
|
||||
"stdout": "Processing: /Library/Plug-Ins/FxPlug/beatygril.app\nThe validate action worked!",
|
||||
"stderr": ""
|
||||
},
|
||||
"gatekeeper": {
|
||||
"exitCode": 0,
|
||||
"stdout": "",
|
||||
"stderr": "/Library/Plug-Ins/FxPlug/beatygril.app: accepted\nsource=Notarized Developer ID"
|
||||
},
|
||||
"identity": [
|
||||
{
|
||||
"path": "/Library/Plug-Ins/FxPlug/beatygril.app",
|
||||
"bundleIdentifier": "com.dayu.beatygril",
|
||||
"version": "1.0.6",
|
||||
"build": "8",
|
||||
"archs": "x86_64 arm64",
|
||||
"signature": "Executable=/Library/Plug-Ins/FxPlug/beatygril.app/Contents/MacOS/beatygril\nIdentifier=com.dayu.beatygril\nFormat=app bundle with Mach-O universal (x86_64 arm64)\nCodeDirectory v=20500 size=478 flags=0x10000(runtime) hashes=4+7 location=embedded\nHash type=sha256 size=32\nCandidateCDHash sha256=66df95f2ce931ca96f52ee5cfa5ad664be49a6c8\nCandidateCDHashFull sha256=66df95f2ce931ca96f52ee5cfa5ad664be49a6c89b8cc1c206d68f0c162b8348\nHash choices=sha256\nCMSDigest=66df95f2ce931ca96f52ee5cfa5ad664be49a6c89b8cc1c206d68f0c162b8348\nCMSDigestType=2\nCDHash=66df95f2ce931ca96f52ee5cfa5ad664be49a6c8\nSignature size=8969\nAuthority=Developer ID Application: zhong luo (B3W25FX5H5)\nAuthority=Developer ID Certification Authority\nAuthority=Apple Root CA\nTimestamp=Oct 21, 2026 at 20:57:26\nNotarization Ticket=stapled\nInfo.plist entries=22\nTeamIdentifier=B3W25FX5H5\nRuntime Version=27.0.0\nSealed Resources version=2 rules=13 files=4\nInternal requirements count=1 size=180\nTotal signatures=1\nChosen signature=1\n"
|
||||
},
|
||||
{
|
||||
"path": "/Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit",
|
||||
"bundleIdentifier": "com.dayu.beatygril.fxplug",
|
||||
"version": "1.0.6",
|
||||
"build": "8",
|
||||
"archs": "x86_64 arm64",
|
||||
"signature": "Executable=/Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit/Contents/MacOS/beatygril XPC Service\nIdentifier=com.dayu.beatygril.fxplug\nFormat=bundle with Mach-O universal (x86_64 arm64)\nCodeDirectory v=20500 size=645 flags=0x10000(runtime) hashes=13+3 location=embedded\nHash type=sha256 size=32\nCandidateCDHash sha256=ded7ed887bb9c90fbf0c9e9b25a5b279c97e1da6\nCandidateCDHashFull sha256=ded7ed887bb9c90fbf0c9e9b25a5b279c97e1da6809e5a5bf42bd0dd56d42dfd\nHash choices=sha256\nCMSDigest=ded7ed887bb9c90fbf0c9e9b25a5b279c97e1da6809e5a5bf42bd0dd56d42dfd\nCMSDigestType=2\nCDHash=ded7ed887bb9c90fbf0c9e9b25a5b279c97e1da6\nSignature size=8969\nAuthority=Developer ID Application: zhong luo (B3W25FX5H5)\nAuthority=Developer ID Certification Authority\nAuthority=Apple Root CA\nTimestamp=Oct 21, 2026 at 20:57:25\nInfo.plist entries=26\nTeamIdentifier=B3W25FX5H5\nRuntime Version=27.0.0\nSealed Resources version=2 rules=13 files=4\nInternal requirements count=1 size=188\nTotal signatures=1\nChosen signature=1\n"
|
||||
}
|
||||
],
|
||||
"template": {
|
||||
"previousTemplateSHA256": "ba08de6916cd837b3733aa0c6fcb78f2e8224431d25e1310089ae67d18445f2d",
|
||||
"files": [
|
||||
{
|
||||
"path": "/Users/dayu/Movies/Motion Templates.localized/Effects.localized/大鱼/大鱼人像美化/大鱼人像美化.moef",
|
||||
"bytes": 22996,
|
||||
"sha256": "30ca8edf36801264c5de2c753c40b720aba8052633620713a12401098fcd2409"
|
||||
},
|
||||
{
|
||||
"path": "/Users/dayu/Movies/Motion Templates.localized/Effects.localized/大鱼/大鱼人像美化/Drop Zone.tiff",
|
||||
"bytes": 420128,
|
||||
"sha256": "a7f8e3a2a431dbb3ce9e777040bb55e701cfe9ab9632e8cbb10359732bc8692e"
|
||||
}
|
||||
],
|
||||
"installedWithHostsClosed": true
|
||||
},
|
||||
"discovery": " com.dayu.beatygril.fxplug(1.1)\tA9F36A44-1E5E-4AB3-A2FD-5C66A76DB8BB\t2026-10-21 13:53:56 +0000\t/Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit\n (1 plug-in)\n",
|
||||
"duplicateInventory": {
|
||||
"scopes": [
|
||||
"/Users/dayu/Documents/PaintFCP",
|
||||
"/Users/dayu/.codex/worktrees",
|
||||
"/Users/dayu/Library/Developer/Xcode/DerivedData",
|
||||
"/Users/dayu/Desktop",
|
||||
"/Users/dayu/Downloads",
|
||||
"/Applications",
|
||||
"/Library/Plug-Ins/FxPlug",
|
||||
"/Users/dayu/Library/Plug-Ins/FxPlug",
|
||||
"/private/tmp/paintfcp-format-probe.PsxvYQ",
|
||||
"/private/tmp/paintfcp-y-review.KpPi8T",
|
||||
"/private/tmp/paintfcp-live-review.ItH4EG",
|
||||
"/private/tmp/paintfcp-slim-field.6fDdJj",
|
||||
"/private/tmp/paintfcp-chin-smoke.rfco16hq",
|
||||
"/private/tmp/paintfcp-rea.b3j9Te",
|
||||
"/private/tmp/paintfcp-slim-smoke.3ncdDe"
|
||||
],
|
||||
"matchingOuterApps": [
|
||||
"/Library/Plug-Ins/FxPlug/beatygril.app"
|
||||
],
|
||||
"errors": []
|
||||
},
|
||||
"status": "installed, formal identity/ticket/discovery/template verified; user visual QA pending"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"id":"ab0271c8-0c4b-4b10-9474-cebe9b55385b","message":"Processing complete","status":"Accepted"}
|
||||
@@ -0,0 +1,21 @@
|
||||
2026-10-21 20:53:14.697 osc-tests[10458:5463835] PGFaceOSC point-write desired=(0.25,0.75) setter=(0.25,0.75) readback=(0.25,0.75) mode=normalized verified=1
|
||||
2026-10-21 20:53:14.705 osc-tests[10458:5463835] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(80,180) surface=320x240 origin=0
|
||||
2026-10-21 20:53:15.118 osc-tests[10458:5463835] PGFaceOSC point-write desired=(0.8,0.3) setter=(1152,-432) readback=(0.8,0.3) mode=centered-pixels verified=1
|
||||
2026-10-21 20:53:15.118 osc-tests[10458:5463835] PGFaceOSC coordinates selection=(0.7,0.4) objectBounds=(-1920,-1080,3840,2160) canvas=(152,78) surface=320x240 origin=0
|
||||
2026-10-21 20:53:15.168 osc-tests[10458:5463835] PGFaceOSC point-write desired=(0.8,0.3) setter=(0.8,0.3) readback=(0.12345,0.87654) mode=normalized verified=0
|
||||
2026-10-21 20:53:15.168 osc-tests[10458:5463835] PGFaceOSC point-write desired=(0.75,0.25) setter=(0.75,0.25) readback=(0.75,0.25) mode=normalized verified=1
|
||||
2026-10-21 20:53:15.168 osc-tests[10458:5463835] PGFaceOSC coordinates selection=(0.75,0.25) objectBounds=(-100,-50,200,100) canvas=(160,60) surface=320x240 origin=0
|
||||
2026-10-21 20:53:15.218 osc-tests[10458:5463835] PGFaceOSC point-write desired=(0.625,0.375) setter=(0.625,0.375) readback=(0.625,0.375) mode=normalized verified=1
|
||||
2026-10-21 20:53:15.218 osc-tests[10458:5463835] PGFaceOSC coordinates selection=(0.625,0.375) objectBounds=(-100,-50,200,100) canvas=(240,60) surface=320x240 origin=0
|
||||
2026-10-21 20:53:15.268 osc-tests[10458:5463835] PGFaceOSC point-write desired=(0.25,0.75) setter=(0.25,0.75) readback=(0.25,0.75) mode=normalized verified=1
|
||||
2026-10-21 20:53:15.268 osc-tests[10458:5463835] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(144,180) surface=320x240 origin=0
|
||||
2026-10-21 20:53:15.318 osc-tests[10458:5463835] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
|
||||
2026-10-21 20:53:15.318 osc-tests[10458:5463835] PGFaceOSC coordinates selection=(0,1) objectBounds=(-100,-50,200,100) canvas=(100,140) surface=320x240 origin=0
|
||||
2026-10-21 20:53:15.369 osc-tests[10458:5463835] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(nan,nan) surface=320x240 origin=0
|
||||
2026-10-21 20:53:15.418 osc-tests[10458:5463835] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
|
||||
2026-10-21 20:53:15.418 osc-tests[10458:5463835] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(inf,nan) surface=320x240 origin=0
|
||||
2026-10-21 20:53:15.467 osc-tests[10458:5463835] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
|
||||
2026-10-21 20:53:15.467 osc-tests[10458:5463835] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(80,nan) surface=320x240 origin=0
|
||||
2026-10-21 20:53:15.517 osc-tests[10458:5463835] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
|
||||
2026-10-21 20:53:15.517 osc-tests[10458:5463835] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
|
||||
OSC adapter: 98 checks, 0 failures
|
||||
@@ -0,0 +1,5 @@
|
||||
Beauty renderer: 49 checks, 0 failures (Metal).
|
||||
2026-10-21 20:58:16.457 face-tests[11385:5472531] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
|
||||
Face processing: 116 checks passed
|
||||
2026-10-21 20:58:19.882 slim-tests[11391:5472584] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
|
||||
PASS: 394 slim alignment checks; maximum UV oracle error 1.09828022e-05
|
||||
文件差异过大,未显示。
加载差异
@@ -0,0 +1,22 @@
|
||||
PASS: independent remote recovery
|
||||
Remote: https://dayu.media/git/dayu/paintfcp-fxplug.git
|
||||
SHA: ad54726b17fabd11f46bc6121e664eac38960a23
|
||||
Temporary clone: /tmp/paintfcp-recovery-ad54726.nARdL7/checkout
|
||||
Temporary clone removed: True
|
||||
Tracked files: 87
|
||||
Git blob / checked-out bytes / modes mismatches: 0
|
||||
Git fsck --strict --full exit: 0
|
||||
LFS pointers: 0; submodules: 0
|
||||
Resource files verified: 14; missing required: 0
|
||||
Drop Zone.tiff: Templates/大鱼人像美化/Drop Zone.tiff: TIFF image data, big-endian, direntries=21, height=1200, bps=0, compression=LZW, PhotometricIntepretation=RGB, orientation=upper-left, width=1200
|
||||
Core test exit: 0
|
||||
Core counts: Beauty49 + Face116 + Slim394 =559; failures0
|
||||
Maximum UV oracle error: 1.09828022e-05
|
||||
Core test duration: 5.394s
|
||||
ASan requested: false; sanitizer diagnostics: 0
|
||||
Restored worktree after tests: clean
|
||||
APP outputs:0; no xcodebuild build / host operations / root source edits / push
|
||||
All modes exactly match Git; scripts/test.sh, test-host.sh, test-osc.sh and deliver.py restore executable.
|
||||
External Apple SDK/frameworks are installed on this machine and must be installed separately when recovering.
|
||||
Finished UTC: 2026-10-21T12:58:52.192540+00:00
|
||||
Detailed JSON: /Users/dayu/Documents/PaintFCP/build/pose-analysis/recovery-v106/recovery-ad54726.json
|
||||
@@ -0,0 +1,2 @@
|
||||
2026-10-21 20:50:27.412 slim-chin-asan[10123:5460221] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
|
||||
PASS: 494 slim alignment checks; maximum UV oracle error 1.09828022e-05
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
# 大鱼人像美化 1.0.4 验证记录
|
||||
|
||||
日期:2026-10-21。环境:Apple Silicon M1 Max,macOS27.2、Motion6.4、Final Cut Pro12.4、Xcode27.1、FxPlug SDK4.3.4、PluginManager1.7.4。Release包含arm64和x86_64;原生宿主实测为arm64,未在Intel机器实测。
|
||||
|
||||
## 用户报告的两个问题
|
||||
|
||||
FCP黑屏来自初版效果模板的输入链:Source Media=0且无clip/footage。已恢复标准Effect Source→clip→footage→本地Drop Zone.tiff。新模板在FCP的独立验收项目中成功替换为实际片段,并能调整美化参数;不能把此前XML通过当作原生输入链通过。
|
||||
|
||||
肤色提亮出现眼、鼻、唇周围圆洞,原因是调色复用了为磨皮扣除五官的蒙版。现在磨皮保留五官保护,肤色改用连续的人脸覆盖与柔和肤色分类,并羽化过渡、抑制蓝发溢色。提供的实际素材以75%提亮/50%中频在Motion复测,首帧及转头后画面未再见原圆洞;离线同帧旧/新算法对照保存在本机build目录,未将用户素材或截图上传源码仓库。强提亮仍会放大原素材的日光与阴影差异,不等于自动重布光。
|
||||
|
||||
## 宿主测试范围
|
||||
|
||||
FCP:使用用户给定的3840×2160、25fps蓝发人像视频。新建独立验收事件与10秒项目“ 大鱼人像美化验收-20261021 ”,位于当前已打开的素材库中,未修改原生产时间线;前5秒原片、后5秒同一段原片加效果。实际看到正常输入、非黑画面和提亮变化。勾选选脸并执行向前分析后,使用跟踪数据由0变为1,按钮路由与基本分析完成可用;未将其描述为125帧逐帧核验或复杂遮挡恢复通过。
|
||||
|
||||
Motion:独立普通视频项目,源PTS0、1920×1080/25fps/10秒,媒体通过本机链接引用给定视频;开启50%中频、75%提亮与人脸框。切换滤镜启用、从第1帧播放到第179帧均正常出图,人脸框随画面位置更新;选中滤镜可正常建立OSC连接。此QA工程依赖外置卷原视频,不作为可移植产品资源交付。
|
||||
|
||||
选脸控件:原生验收额外发现OBJECT坐标被二次归一化,十字落在画面角落;现直接使用归一化OBJECT坐标转换至CANVAS像素。Motion点参数桥接会归一化读取,却按中心像素写入,新增事件内读回检测及兼容转换,并校验最终位置;不依赖宿主版本号猜测。98项OSC测试同时覆盖文档约定与Motion像素桥接。事件现显式使用CANVAS坐标并由宿主转换回OBJECT,避免OBJECT鼠标事件的单位歧义。1.0.4的显式选脸点击回写尚未完成原生复测;界面被用户接管自测,已按要求停止自动化操作。98项离屏OSC/ASan检查不能替代这项原生验收。公开坐标依据:[Apple Creating Onscreen Controls](https://developer.apple.com/library/archive/documentation/AppleApplications/Conceptual/FXPlug_overview/OnScreenControls/OnScreenControls.html)。
|
||||
|
||||
宿主在更新前退出,新正式版安装后重新打开。曾在旧XPC连接未释放、临时构建插件仍被注册的热替换环境出现宿主关闭挂起/OSC连接崩溃;交付流程已增加宿主关闭检查,并在原生验收前注销构建目录的插件。没有把该崩溃直接归因为某条提亮公式。
|
||||
|
||||
## 功能边界与原版跟踪选项
|
||||
|
||||
用户给出的MS超级美颜V2.moef共发布15个入口,没有跟踪、前向或后向分析控件。没有额外分析按钮不影响原版默认自动跟随;活跃render路径与Motion运行均已确认这一点。复刻版的三目标离线分析与持久化属于独立扩展,应与默认实时跟随分开理解。
|
||||
|
||||
六种肤色风格采用自写调色变换,脸型当前使用Vision脸框场与眼部点位置的近似反向采样;瘦脸本身没有使用下颌轮廓点。未复制原版私有LUT或算法资源。没有完成原版逐像素对齐、全部脸型强度主观比对、多人交叉遮挡、所有宿主版本及Intel硬件验收。源码层保存/恢复测试不代替两宿主所有工程关闭重开场景。
|
||||
|
||||
## 自动化与恢复
|
||||
|
||||
渲染/人脸/宿主适配器/OSC测试使用公共NASA人像与合成数据,覆盖默认直通、像素格式与画面原点、极值、连续调色蒙版、蓝发抑制、选脸/分析状态和错误处理。最终代码节点为e03e460a9b3e5ad489aad24095485ba10ceb987d,380项检查通过:渲染49、人脸116、宿主适配117、OSC98,零失败。宿主适配器和OSC测试启用AddressSanitizer。
|
||||
|
||||
远端独立克隆核验检查受跟踪文件字节、工程资源、Git对象完整性和可执行脚本模式,无LFS与子模块。scripts/checkpoint.py以100644保存,按文档使用python3调用;它不是可直接执行的入口,恢复测试报告中该权限提示不表示源码丢失。
|
||||
|
||||
## 正式交付
|
||||
|
||||
1.0.4 build6为通用Release,已按由内到外正式签名,codesign严格验证、Apple公证Accepted(8eb96294-5935-49ff-a487-3c4f60c27395)、票据粘附/验证、Gatekeeper及安装身份验证通过。临时构建APP、旧版备份APP及公证ZIP已清理,检查的项目、DerivedData、桌面、下载、Applications、正式FxPlug目录与本次临时构建路径只剩正式APP,该产品插件注册为1个正式路径;详细扫描范围见verification/release-receipt.json。唯一正式APP路径固定为 /Library/Plug-Ins/FxPlug/beatygril.app;FCP效果模板在用户Movies/Motion Templates.localized/Effects.localized/大鱼/大鱼人像美化内,必须同时有.moef与Drop Zone.tiff。
|
||||
|
||||
## 1.0.4 自动跟随专项
|
||||
|
||||
已按原版活跃路径加入自动主体关联、矩形防跳、关键点稳定、失检保持与独立时间缓存;默认不要求分析。新增51项人脸回归,覆盖移动、多脸作用范围、重选、重复帧、失检、矩形边界、轴向保护、历史缓存、并发及不改离线存档;独立审查曾发现A→B→A重选后的回看范围混淆,修复后独立复现保持同一输出。Face116另有ASan通过。
|
||||
|
||||
原版完整数学系数未恢复,本版使用自有平滑公式与past-only时间策略;当前检测框未做原版77点网格级稳定,没有真正sourceID,不能声称任意同尺寸换源或首次乱序完全一致。原生1.0.4:Motion成功加载正式路径插件,实际视频在不同播放头位置正常显示提亮、脸型和黄色主要人脸框;项目未执行离线分析,“使用跟踪数据”读取为0。用户随后明确接管Motion/FCP进行自测,自动化已停止,因此尚未完成1.0.4的FCP自动跟随、显式OSC点击回写及两宿主复杂场景验收。FCP黑屏输入链与提亮此前已原生复测,不能把它们混称为1.0.4所有自动跟随场景通过。详见分析说明的具体边界。
|
||||
|
||||
代码节点已从远端独立克隆恢复,51项受跟踪文件与9项必要资源逐字节/对象/文件模式一致;380项检查、严格fsck与Xcode工程读取全部通过,无LFS或子模块。验收报告见 [独立恢复摘要](verification/recovery-e03e460.txt) 与 [完整记录](verification/recovery-e03e460.json)。最终文档标签另作独立克隆核验,源码构建依赖没有因文档更新改变。
|
||||
|
||||
## 用户复测与瘦脸专项分析
|
||||
|
||||
用户反馈“跟踪没问题”,同时提供最大瘦脸与侧脸变形对照。记录为用户实测反馈,不扩张为显式OSC点击、两宿主复杂场景或全部脸型验收通过。
|
||||
|
||||
后续静态REA和Apple Metal AIR分析确认:原版瘦脸以17组轮廓到鼻部/瞳孔锚点的局部位移驱动,当前瘦脸按框水平对称变形,两者的方向、范围与强度模型不同。独立Vision76点离线探针在同一视频11个采样时刻均检出1脸76点,含明显侧脸;它只证明关键点仍可获得,不代替原版宿主输出对照。探针使用Wall/Wextra编译成功并完成采样,没有执行原版APP、替换安装版或操作Motion/FCP。
|
||||
|
||||
分析报告见 [脸型变形与转脸分析](analysis/脸型变形与转脸分析.md)。当前安装1.0.4的变形问题尚未修复,本轮的诊断代码/证据/文档不改变已安装产品。
|
||||
|
||||
## 1.0.5 瘦脸修复
|
||||
|
||||
已将最大瘦脸改为76点、17组局部位移;明确revision3/76点,保留图像及四类蒙版共用变形,旧档案缺完整点阵时只跳过瘦脸。新增224项数值与数据回归:48组尺寸/左右侧脸/歪头投影、非零原点、0/50/100%强度、独立double逆向坐标场、旧档案/坏点保护、失检重获及真实Vision76点与存储回放。最大UV坐标误差1.09828022e-5。
|
||||
|
||||
完整回归604项通过(渲染49、人脸116、瘦脸224、宿主适配117、OSC98);宿主适配与OSC启用ASan。独立审查另用程序生成的UV图与1×1读取验证36点,最大误差1.12695e-7。位图数值测试曾发现测试输入行方向与CI坐标不一致,修正测试桥接后通过,生产算法未因此反改。
|
||||
|
||||
1.0.5(7)已完成通用Release编译、Developer ID逐层签名、Apple公证Accepted(7d7034f8-7389-410f-ad8e-9e722408c0fc)、票据粘附/验证、Gatekeeper、安装身份与正式路径注册验证。用户保存退出宿主后安装;本轮没有操作或启动Motion/FCP界面,最终视觉由用户自测。默认单主要人脸对齐17-pin数学;快速多人仍沿用逐脸采样,重叠区不宣称与原版单fragment全脸序列等价。
|
||||
|
||||
额外用用户视频11帧(含4秒及18秒明显侧脸)运行264项瘦脸ASan检查,通过且未发现内存错误;此数包含重复的220项基础数值/数据检查,不与604简单相加。独立恢复从远程abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964克隆,69文件/9资源和模式一致、strict fsck通过,clone内389项Metal/人脸/瘦脸测试通过。签名构建源码就是该节点;后续交付脚本补充了已公证产物校验恢复与重复注销兼容,产品源码未再变化。
|
||||
|
||||
正式安装APP仅位于 `/Library/Plug-Ins/FxPlug/beatygril.app`;配套1.0.5模板及占位资源已同步并逐字节核验。临时构建、旧版APP备份、公证ZIP已清理;项目、DerivedData、桌面、下载、Applications及系统/用户FxPlug目录扫描只发现该正式APP,无扫描错误。原生效果验收状态为用户自测待完成。票据、发现、恢复与检测报告见 `docs/verification/v1.0.5/`。
|
||||
|
||||
## 1.0.6 下巴与图像轴修复(构建前)
|
||||
|
||||
用户反馈“下巴调整作用到眼睛”。纯CI76点物理位置并未全局反置,但FxPlug适配器曾将TOP_LEFT当作物理行序,在输入和输出各多翻转一次;默认直通会抵消,无法验证中间Vision和变形的解剖方向。独立top-first照片构造复现下巴跑向眼部;原版REA输入及两条CI→IOSurface输出路径均没有同样额外翻转,现删除这两处。未捕获本次实际FCP缓冲,原生视觉由用户自测。
|
||||
|
||||
下巴另有旧框高斯算法和正负符号错误,现按原版68→38、67→37、66→36三pin、正0.04/负0.02和共同尺寸增益对齐。数学场、真实图物理下巴/瞳孔、正负符号与侧脸热点均已检查;其余七类控制及全部组合顺序未宣称完整恢复。新分析记录写入显式坐标标记;旧未标记记录保留但不再重放,自动回退实时跟随,重新分析可恢复离线记录。
|
||||
|
||||
本轮不操作、启动或退出Motion/FCP。完整分析证据见[下巴与坐标分析](analysis/下巴与坐标分析.md)。完整904项通过(美化49、人脸116、瘦脸/下巴394、宿主适配247、OSC98),宿主适配和OSC启用ASan。宿主适配16种默认/大眼/下巴正负×源/目的原点组合彼此逐字节一致,与独立upright文件参考最大差2/255;独立文件解码和IOSurface检测路径存在微小数值差,阈值另限制均差<0.005/255,并检查实际解剖热点。额外用户视频11帧494项ASan通过,含重复基础用例,不与904简单相加。用户帧和完整点位未上传远程。当前正式APP仍为1.0.5,不能将构建前验证描述为1.0.6已安装。
|
||||
|
||||
1.0.6(8)通用Release的签名构建源码为`ad54726b17fabd11f46bc6121e664eac38960a23`,构建开始时工作区干净。Developer ID逐层签名/严格验证、Apple公证Accepted(ab0271c8-0c4b-4b10-9474-cebe9b55385b)、票据粘附/验证均已通过。检测到Motion仍运行,当前在产品交付锁内只读等待用户保存退出,尚未替换正式1.0.5。
|
||||
|
||||
签名源码已从大鱼远程独立克隆恢复:87个受跟踪文件的字节、blob和模式全部一致,14项资源完整,strict/full fsck通过,无LFS/子模块;clone内559项核心测试通过,未构建APP或操作宿主,临时克隆已清理。报告在`verification/v1.0.6/recovery-ad54726.json`。
|
||||
|
||||
## 1.0.6 正式安装完成
|
||||
|
||||
用户确认保存并退出Motion/FCP后,安装流程完成1.0.6(8)替换,正式APP为`/Library/Plug-Ins/FxPlug/beatygril.app`。安装版APP与嵌套插件均为x86_64/arm64,Developer ID团队B3W25FX5H5、可信时间戳和Hardened Runtime正确;codesign严格验证、票据验证、Gatekeeper与PluginKit正式路径发现通过。全过程没有操作、启动或退出宿主界面,原生视觉仍由用户自测。
|
||||
|
||||
配套1.0.6效果模板及Drop Zone.tiff已在宿主退出时同步并逐字节核验;moef SHA-256为`30ca8edf36801264c5de2c753c40b720aba8052633620713a12401098fcd2409`。模板仍保留固定UUID及原有参数发布。旧未标记的跟踪数据会回退实时跟随;需要离线重放时请重新分析目标。
|
||||
|
||||
临时Release APP、旧APP备份和公证ZIP已清理。项目、Codex工作树、DerivedData、桌面、下载、Applications、系统/用户FxPlug目录及本次paintfcp临时目录扫描仅发现正式路径外层APP,扫描错误0;没有将这项有界扫描表述为所有卷的完整搜索。完整回执、签名、发现、模板摘要和扫描范围在`verification/v1.0.6/`。最终发布标签为`v1.0.6`,产品源码与签名构建节点ad54726一致;其后的提交仅更新交付文档、模板说明和验证回执。
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Save only explicitly listed project files, under the shared dayu Git lock."""
|
||||
import argparse, hashlib, json, os, pathlib, subprocess, time
|
||||
p=argparse.ArgumentParser();p.add_argument('message');p.add_argument('files',nargs='+');p.add_argument('--tag');a=p.parse_args()
|
||||
root=pathlib.Path(__file__).resolve().parent.parent
|
||||
run=lambda *args,**kw:subprocess.run(args,cwd=root,check=True,text=True,**kw)
|
||||
remote=run('git','remote','get-url','origin',capture_output=True).stdout.strip()
|
||||
if not remote.startswith('https://dayu.media/git/') or '@' in remote:raise SystemExit('Unexpected project remote')
|
||||
lock=pathlib.Path('/Users/dayu/.codex/git-backup-locks')/(hashlib.sha256(remote.lower().rstrip('/').encode()).hexdigest()+'.lock')
|
||||
lock.parent.mkdir(parents=True,exist_ok=True)
|
||||
for n in range(120):
|
||||
try:lock.mkdir();break
|
||||
except FileExistsError:
|
||||
if n==119:raise SystemExit('Repository lock still held; inspect owner.json')
|
||||
time.sleep(.5)
|
||||
(lock/'owner.json').write_text(json.dumps({'pid':os.getpid(),'cwd':str(root),'remote':remote}))
|
||||
try:
|
||||
run('git','add','--',*a.files)
|
||||
run('git','diff','--cached','--check')
|
||||
run('git','diff','--cached','--stat')
|
||||
changed=subprocess.run(['git','diff','--cached','--quiet'],cwd=root).returncode
|
||||
if changed:run('git','commit','-m',a.message)
|
||||
branch=run('git','branch','--show-current',capture_output=True).stdout.strip()
|
||||
if not branch:raise SystemExit('Detached HEAD: create a durable branch before saving')
|
||||
run('git','push','-u','origin',branch)
|
||||
sha=run('git','rev-parse','HEAD',capture_output=True).stdout.strip()
|
||||
observed=run('git','ls-remote','origin','refs/heads/'+branch,capture_output=True).stdout.split()[0]
|
||||
if observed!=sha:raise SystemExit('Remote checkpoint SHA mismatch')
|
||||
if a.tag:
|
||||
run('git','tag',a.tag)
|
||||
run('git','push','origin','refs/tags/'+a.tag)
|
||||
observed=run('git','ls-remote','origin','refs/tags/'+a.tag,capture_output=True).stdout.split()[0]
|
||||
if observed!=sha:raise SystemExit('Remote tag SHA mismatch')
|
||||
print(json.dumps({'branch':branch,'sha':sha,'remote_verified':True,'tag':a.tag}))
|
||||
finally:
|
||||
(lock/'owner.json').unlink();lock.rmdir()
|
||||
可执行文件
+189
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Release, notarize and install the unique FxPlug app under its delivery lock.
|
||||
The final prompt keeps the lock held while native-host validation is performed.
|
||||
"""
|
||||
import argparse, datetime, json, os, pathlib, plistlib, re, shutil, subprocess, sys, time
|
||||
root=pathlib.Path(__file__).resolve().parent.parent
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--wait-for-hosts',action='store_true',help='After notarization, retain the delivery lock and wait for Motion/FCP to exit naturally before installing; never control or quit either host.')
|
||||
parser.add_argument('--user-tests',action='store_true',help='After non-UI installed-app verification, record native effect QA as pending user testing and clean known generated copies without the interactive validation prompt.')
|
||||
parser.add_argument('--resume-prepared',action='store_true',help='Resume an already Accepted and stapled staged app after checking its recorded signature, version and unchanged product source.')
|
||||
args=parser.parse_args()
|
||||
bundle_id='com.dayu.beatygril'
|
||||
lock=pathlib.Path('/Users/dayu/.codex/app-delivery-locks')/(bundle_id+'.lock')
|
||||
lock.parent.mkdir(parents=True,exist_ok=True)
|
||||
try:lock.mkdir()
|
||||
except FileExistsError:raise SystemExit('Delivery lock occupied; inspect owner.json and the owner process')
|
||||
(lock/'owner.json').write_text(json.dumps({'pid':os.getpid(),'cwd':str(root),'bundle_id':bundle_id}))
|
||||
staging=pathlib.Path('/tmp/paintfcp-build-20261021')
|
||||
logs=root/'build'/'delivery';logs.mkdir(parents=True,exist_ok=True)
|
||||
app=staging/'Build/Products/Release/beatygril.app'
|
||||
installed=pathlib.Path('/Library/Plug-Ins/FxPlug/beatygril.app')
|
||||
archive=logs/'notarization.zip'
|
||||
identity='Developer ID Application: zhong luo (B3W25FX5H5)'
|
||||
keychain='/Users/dayu/Library/Keychains/login.keychain-db'
|
||||
previous=logs/'previous-beatygril.app'
|
||||
receipt_path=logs/'delivery-receipt.json'
|
||||
receipt={'status':'preparing','bundleIdentifier':bundle_id,'installationPath':str(installed),
|
||||
'options':{'waitForHosts':args.wait_for_hosts,'userTests':args.user_tests},
|
||||
'nativeEffectQA':{'status':'pending_user' if args.user_tests else 'pending_operator','performedByScript':False}}
|
||||
def run(args, log=None):
|
||||
if log:
|
||||
with open(log,'w') as output:subprocess.run(args,cwd=root,stdout=output,stderr=subprocess.STDOUT,check=True)
|
||||
return ''
|
||||
return subprocess.run(args,cwd=root,text=True,capture_output=True,check=True).stdout
|
||||
def save_receipt(status,**fields):
|
||||
receipt.update(fields);receipt['status']=status
|
||||
receipt['updatedAt']=datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||
receipt_path.write_text(json.dumps(receipt,ensure_ascii=False,indent=2)+'\n')
|
||||
def running_hosts():
|
||||
# Read process state only. Match executable suffixes so renamed installation
|
||||
# roots do not accidentally allow replacing a live host's plug-in connection.
|
||||
suffixes=('/Motion.app/Contents/MacOS/Motion','/Final Cut Pro.app/Contents/MacOS/Final Cut Pro')
|
||||
found=[]
|
||||
for line in run(['ps','-axo','pid=,comm=']).splitlines():
|
||||
columns=line.strip().split(None,1)
|
||||
if len(columns)==2 and columns[1].endswith(suffixes):found.append({'pid':int(columns[0]),'executable':columns[1]})
|
||||
return found
|
||||
def await_host_exit():
|
||||
active=running_hosts()
|
||||
if active and not args.wait_for_hosts:
|
||||
raise RuntimeError('Close Motion and Final Cut Pro before installing; installed app retained')
|
||||
if active:
|
||||
save_receipt('waiting_for_host_exit',runningHosts=active)
|
||||
print('HOST_EXIT_REQUIRED: Motion/Final Cut Pro must exit naturally before installation. The delivery lock remains held; no host/UI action will be taken.',flush=True)
|
||||
while active:
|
||||
time.sleep(3)
|
||||
active=running_hosts()
|
||||
print('HOSTS_EXITED: continuing with installation under the same delivery lock.',flush=True)
|
||||
save_receipt('ready_to_install',runningHosts=[])
|
||||
def require_product(path):
|
||||
info=plistlib.loads((path/'Contents/Info.plist').read_bytes())
|
||||
if info.get('CFBundleIdentifier')!=bundle_id:raise RuntimeError('Generated/installed app identity mismatch: '+str(path))
|
||||
return info
|
||||
def unregister_generated_plugin(plugin):
|
||||
# Removing an already absent registration can return 1. Establish absence
|
||||
# by discovery rather than turning that idempotent state into a failure.
|
||||
removed=subprocess.run(['pluginkit','-r',str(plugin)],capture_output=True,text=True)
|
||||
info=plistlib.loads((plugin/'Contents/Info.plist').read_bytes())
|
||||
discovered=run(['pluginkit','-m','-A','-D','-v','-i',info['CFBundleIdentifier']])
|
||||
if str(plugin) in discovered:raise RuntimeError('Generated plug-in is still discoverable: '+removed.stderr)
|
||||
def cleanup_known_products():
|
||||
removed=[]
|
||||
if previous.exists():
|
||||
require_product(previous);shutil.rmtree(previous);removed.append(str(previous))
|
||||
if staging.exists():
|
||||
if app.exists():require_product(app)
|
||||
shutil.rmtree(staging);removed.append(str(staging))
|
||||
if archive.exists():archive.unlink();removed.append(str(archive))
|
||||
return removed
|
||||
try:
|
||||
delivery_source={'commit':run(['git','rev-parse','HEAD']).strip(),
|
||||
'trackedWorktreeChanges':run(['git','status','--porcelain','--untracked-files=no']).splitlines()}
|
||||
plugin=app/'Contents/PlugIns/beatygril XPC Service.pluginkit'
|
||||
if args.resume_prepared:
|
||||
old_receipt=json.loads(receipt_path.read_text())
|
||||
result=old_receipt.get('notarization',{})
|
||||
if result.get('status')!='Accepted':raise RuntimeError('No Accepted staged notarization to resume')
|
||||
info=require_product(app)
|
||||
run(['codesign','--verify','--deep','--strict',str(app)])
|
||||
run(['xcrun','stapler','validate',str(app)])
|
||||
signature=subprocess.run(['codesign','-dvvv',str(app)],capture_output=True,text=True,check=True).stderr
|
||||
expected=re.search(r'^CDHash=(.+)$',old_receipt.get('stagingSignature',''),re.M)
|
||||
actual=re.search(r'^CDHash=(.+)$',signature,re.M)
|
||||
if not expected or not actual or expected.group(1)!=actual.group(1):raise RuntimeError('Prepared signature hash changed')
|
||||
if info.get('CFBundleShortVersionString')!=old_receipt.get('version') or info.get('CFBundleVersion')!=old_receipt.get('build'):raise RuntimeError('Prepared version changed')
|
||||
source_commit=old_receipt.get('source',{}).get('commit')
|
||||
if not source_commit or run(['git','diff',source_commit,'--','beatygril']).strip():raise RuntimeError('Product source changed since prepared build')
|
||||
receipt.update(old_receipt);receipt.pop('failure',None)
|
||||
receipt['resumedBy']=delivery_source
|
||||
receipt['options']={'waitForHosts':args.wait_for_hosts,'userTests':args.user_tests,'resumePrepared':True}
|
||||
receipt['nativeEffectQA']={'status':'pending_user' if args.user_tests else 'pending_operator','performedByScript':False}
|
||||
save_receipt('resuming_prepared')
|
||||
print('Previously Accepted and stapled Release verified; resuming installation.',flush=True)
|
||||
else:
|
||||
receipt['source']=delivery_source
|
||||
save_receipt('building')
|
||||
identities=run(['security','find-identity','-v','-p','codesigning',keychain])
|
||||
if identity not in identities:raise RuntimeError('Valid Developer ID Application identity is unavailable')
|
||||
print('Building universal Release…',flush=True)
|
||||
run(['xcodebuild','-project','beatygril/beatygril.xcodeproj','-scheme','Wrapper Application','-configuration','Release','-derivedDataPath',str(staging),'-destination','generic/platform=macOS','build'],logs/'build.log')
|
||||
# Xcode can register a fresh build. Remove this known generated path before
|
||||
# lengthy signing/notarization while a user may still be testing the old app.
|
||||
unregister_generated_plugin(plugin)
|
||||
for framework in sorted((plugin/'Contents/Frameworks').glob('*.framework')):
|
||||
run(['codesign','--force','--timestamp','--options','runtime','--sign',identity,str(framework)])
|
||||
run(['codesign','--force','--timestamp','--options','runtime','--sign',identity,str(plugin)])
|
||||
run(['codesign','--force','--timestamp','--options','runtime','--entitlements',str(root/'beatygril/beatygril/Wrapper Application/SandboxEntitlements.entitlements'),'--sign',identity,str(app)])
|
||||
run(['codesign','--verify','--deep','--strict',str(app)])
|
||||
info=require_product(app)
|
||||
receipt['version']=info.get('CFBundleShortVersionString');receipt['build']=info.get('CFBundleVersion')
|
||||
receipt['stagingSignature']=subprocess.run(['codesign','-dvvv',str(app)],capture_output=True,text=True,check=True).stderr
|
||||
run(['ditto','-c','-k','--keepParent',str(app),str(archive)])
|
||||
print('Submitting signed app to Apple notarization…',flush=True)
|
||||
response=run(['xcrun','notarytool','submit',str(archive),'--keychain-profile','codex-dayu-notary-B3W25FX5H5','--keychain',keychain,'--wait','--output-format','json'])
|
||||
(logs/'notarization.json').write_text(response)
|
||||
result=json.loads(response)
|
||||
receipt['notarization']=result
|
||||
if result.get('status')!='Accepted':
|
||||
if result.get('id'):
|
||||
(logs/'notarization-details.json').write_text(run(['xcrun','notarytool','log',result['id'],'--keychain-profile','codex-dayu-notary-B3W25FX5H5','--keychain',keychain]))
|
||||
raise RuntimeError('Apple did not accept notarization; original installed app retained')
|
||||
run(['xcrun','stapler','staple',str(app)])
|
||||
run(['xcrun','stapler','validate',str(app)])
|
||||
# Keep a prepared build out of host discovery while the user still has the
|
||||
# old installed version open. This removes only this known staging path.
|
||||
unregister_generated_plugin(plugin)
|
||||
save_receipt('prepared',signingVerified=True,stapleValidated=True,stagingPluginUnregistered=True)
|
||||
# A live host retains its old XPC/OSC connection. Replacing the bundle while
|
||||
# that connection exists can deadlock host shutdown or select stale code.
|
||||
await_host_exit()
|
||||
if installed.exists():
|
||||
require_product(installed)
|
||||
if previous.exists():raise RuntimeError('Previous recovery app already exists')
|
||||
# Recheck immediately before touching the installed bundle. Waiting mode does
|
||||
# not grant permission for a hot replacement if a host was opened again.
|
||||
await_host_exit()
|
||||
save_receipt('installing')
|
||||
if installed.exists():
|
||||
shutil.move(str(installed),str(previous))
|
||||
try:
|
||||
shutil.copytree(app,installed,symlinks=True)
|
||||
run(['codesign','--verify','--deep','--strict',str(installed)])
|
||||
run(['xcrun','stapler','validate',str(installed)])
|
||||
run(['spctl','--assess','--type','execute','--verbose=2',str(installed)])
|
||||
info=plistlib.loads((installed/'Contents/Info.plist').read_bytes())
|
||||
signature=subprocess.run(['codesign','-dvvv',str(installed)],capture_output=True,text=True,check=True).stderr
|
||||
if info.get('CFBundleIdentifier')!=bundle_id or 'TeamIdentifier=B3W25FX5H5' not in signature:raise RuntimeError('Installed app identity mismatch')
|
||||
(logs/'installed-signature.txt').write_text(signature)
|
||||
run(['pluginkit','-a',str(installed/'Contents/PlugIns/beatygril XPC Service.pluginkit')])
|
||||
# Xcode may register its build product. Only the installed plugin should
|
||||
# be discoverable while native host validation is in progress.
|
||||
unregister_generated_plugin(plugin)
|
||||
installed_plugin=installed/'Contents/PlugIns/beatygril XPC Service.pluginkit'
|
||||
plugin_info=plistlib.loads((installed_plugin/'Contents/Info.plist').read_bytes())
|
||||
discovered=run(['pluginkit','-m','-A','-D','-v','-i',plugin_info['CFBundleIdentifier']])
|
||||
if str(installed_plugin) not in discovered:raise RuntimeError('PluginKit did not report the formal installed plug-in path')
|
||||
(logs/'installed-discovery.txt').write_text(discovered)
|
||||
save_receipt('installed',installedSignature=signature,installedSignatureVerified=True,
|
||||
installedStapleValidated=True,gatekeeperAccepted=True,
|
||||
pluginDiscovery={'bundleIdentifier':plugin_info['CFBundleIdentifier'],'formalPath':str(installed_plugin),'output':discovered})
|
||||
except Exception:
|
||||
if installed.exists():shutil.rmtree(installed)
|
||||
if previous.exists():shutil.move(str(previous),str(installed))
|
||||
raise
|
||||
print(json.dumps({'status':'installed','path':str(installed),'notarization':result}),flush=True)
|
||||
if args.user_tests:
|
||||
print('USER_TESTS_PENDING: installed identity, ticket, Gatekeeper and formal-path discovery verified. Native effect QA is left to the user; no host/UI operation was performed.',flush=True)
|
||||
else:
|
||||
input('HOST_VALIDATION_PENDING: press Return only after host verification and duplicate inventory.\n')
|
||||
receipt['nativeEffectQA']={'status':'operator_confirmed','performedByScript':False}
|
||||
# Only this task’s known generated product copies are removed.
|
||||
removed=cleanup_known_products()
|
||||
save_receipt('installed_user_qa_pending' if args.user_tests else 'complete',removedGeneratedPaths=removed)
|
||||
print('Delivery staging and notarization archive cleaned.',flush=True)
|
||||
except BaseException as error:
|
||||
save_receipt('failed',failure={'type':type(error).__name__,'message':str(error)})
|
||||
raise
|
||||
finally:
|
||||
(lock/'owner.json').unlink();lock.rmdir()
|
||||
@@ -0,0 +1,109 @@
|
||||
// Offline diagnostic only. It uses Apple's public Vision API, never a host UI
|
||||
// or the reference plug-in. User media and generated observations stay local.
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <Vision/Vision.h>
|
||||
#import <ImageIO/ImageIO.h>
|
||||
#include <math.h>
|
||||
|
||||
static NSArray *PGProbePoint(CGPoint p) { return @[@(p.x), @(p.y)]; }
|
||||
static NSArray *RegionIndices(VNFaceLandmarkRegion2D *region, VNFaceLandmarkRegion2D *all) {
|
||||
NSMutableArray *indices = [NSMutableArray array];
|
||||
for (NSUInteger i=0; i<region.pointCount; i++) {
|
||||
CGPoint p=region.normalizedPoints[i];
|
||||
double best=INFINITY; NSUInteger index=NSNotFound;
|
||||
for (NSUInteger j=0; j<all.pointCount; j++) {
|
||||
CGPoint q=all.normalizedPoints[j];
|
||||
double d=hypot(p.x-q.x,p.y-q.y);
|
||||
if (d<best) { best=d; index=j; }
|
||||
}
|
||||
// Preserve an unmatched result instead of inventing a point identity.
|
||||
[indices addObject:best<1e-6 ? @(index) : NSNull.null];
|
||||
}
|
||||
return indices;
|
||||
}
|
||||
|
||||
int main(int argc, const char *argv[]) { @autoreleasepool {
|
||||
if (argc<4) {
|
||||
fprintf(stderr,"usage: inspect-face-geometry <video> <output.json> <seconds>...\n");
|
||||
return 2;
|
||||
}
|
||||
NSURL *source=[NSURL fileURLWithPath:@(argv[1])];
|
||||
NSURL *output=[NSURL fileURLWithPath:@(argv[2])];
|
||||
if (![NSFileManager.defaultManager fileExistsAtPath:source.path]) return 2;
|
||||
NSError *error=nil;
|
||||
if (![NSFileManager.defaultManager createDirectoryAtURL:[output URLByDeletingLastPathComponent]
|
||||
withIntermediateDirectories:YES attributes:nil error:&error]) {
|
||||
fprintf(stderr,"%s\n",error.description.UTF8String); return 1;
|
||||
}
|
||||
AVURLAsset *asset=[AVURLAsset URLAssetWithURL:source options:nil];
|
||||
AVAssetImageGenerator *generator=[[AVAssetImageGenerator alloc] initWithAsset:asset];
|
||||
generator.appliesPreferredTrackTransform=YES;
|
||||
generator.maximumSize=CGSizeMake(1280,1280);
|
||||
generator.requestedTimeToleranceBefore=kCMTimeZero;
|
||||
generator.requestedTimeToleranceAfter=kCMTimeZero;
|
||||
NSMutableArray *frames=[NSMutableArray array];
|
||||
for (int a=3; a<argc; a++) { @autoreleasepool {
|
||||
char *end=NULL; double seconds=strtod(argv[a],&end);
|
||||
if (!end || end==argv[a] || *end || !isfinite(seconds) || seconds<0) return 2;
|
||||
CMTime actual=kCMTimeInvalid;
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
CGImageRef image=[generator copyCGImageAtTime:CMTimeMakeWithSeconds(seconds,60000)
|
||||
actualTime:&actual error:&error];
|
||||
#pragma clang diagnostic pop
|
||||
if (!image) { fprintf(stderr,"%s\n",error.description.UTF8String); return 1; }
|
||||
VNDetectFaceLandmarksRequest *request=[VNDetectFaceLandmarksRequest new];
|
||||
if (![VNDetectFaceLandmarksRequest.supportedRevisions containsIndex:VNDetectFaceLandmarksRequestRevision3] ||
|
||||
![VNDetectFaceLandmarksRequest revision:VNDetectFaceLandmarksRequestRevision3
|
||||
supportsConstellation:VNRequestFaceLandmarksConstellation76Points]) {
|
||||
CGImageRelease(image); fprintf(stderr,"Vision revision 3 / 76 points unavailable\n"); return 1;
|
||||
}
|
||||
request.revision=VNDetectFaceLandmarksRequestRevision3;
|
||||
request.constellation=VNRequestFaceLandmarksConstellation76Points;
|
||||
VNImageRequestHandler *handler=[[VNImageRequestHandler alloc] initWithCGImage:image options:@{}];
|
||||
if (![handler performRequests:@[request] error:&error]) {
|
||||
CGImageRelease(image); fprintf(stderr,"%s\n",error.description.UTF8String); return 1;
|
||||
}
|
||||
NSMutableArray *faces=[NSMutableArray array];
|
||||
for (VNFaceObservation *face in request.results) {
|
||||
VNFaceLandmarks2D *lm=face.landmarks;
|
||||
VNFaceLandmarkRegion2D *all=lm.allPoints;
|
||||
CGRect box=face.boundingBox;
|
||||
NSMutableArray *points=[NSMutableArray array];
|
||||
for (NSUInteger i=0; i<all.pointCount; i++) {
|
||||
CGPoint p=all.normalizedPoints[i];
|
||||
[points addObject:PGProbePoint(CGPointMake(box.origin.x+p.x*box.size.width,
|
||||
box.origin.y+p.y*box.size.height))];
|
||||
}
|
||||
NSDictionary *regions=@{@"contour":RegionIndices(lm.faceContour,all),
|
||||
@"leftEye":RegionIndices(lm.leftEye,all), @"rightEye":RegionIndices(lm.rightEye,all),
|
||||
@"leftPupil":RegionIndices(lm.leftPupil,all), @"rightPupil":RegionIndices(lm.rightPupil,all),
|
||||
@"nose":RegionIndices(lm.nose,all), @"noseCrest":RegionIndices(lm.noseCrest,all),
|
||||
@"outerLips":RegionIndices(lm.outerLips,all), @"innerLips":RegionIndices(lm.innerLips,all)};
|
||||
[faces addObject:@{@"box":@[@(box.origin.x),@(box.origin.y),@(box.size.width),@(box.size.height)],
|
||||
@"confidence":@(face.confidence), @"yaw":face.yaw?:NSNull.null,
|
||||
@"roll":face.roll?:NSNull.null, @"pitch":face.pitch?:NSNull.null,
|
||||
@"allPoints":points, @"regions":regions}];
|
||||
}
|
||||
NSString *name=[NSString stringWithFormat:@"frame-%0.3f.png",seconds];
|
||||
NSURL *png=[[output URLByDeletingLastPathComponent] URLByAppendingPathComponent:name];
|
||||
CGImageDestinationRef destination=CGImageDestinationCreateWithURL((__bridge CFURLRef)png,CFSTR("public.png"),1,NULL);
|
||||
if (!destination) { CGImageRelease(image); return 1; }
|
||||
CGImageDestinationAddImage(destination,image,NULL);
|
||||
BOOL saved=CGImageDestinationFinalize(destination); CFRelease(destination);
|
||||
[frames addObject:@{@"requestedSeconds":@(seconds), @"actualSeconds":@(CMTimeGetSeconds(actual)),
|
||||
@"width":@(CGImageGetWidth(image)), @"height":@(CGImageGetHeight(image)),
|
||||
@"image":name, @"faces":faces}];
|
||||
CGImageRelease(image); if (!saved) return 1;
|
||||
}}
|
||||
NSDictionary *result=@{@"schema":@1,@"source":source.path,@"coordinateSystem":@"full-image normalized, y up",
|
||||
@"authority":@"independent Apple Vision probe, not reference plug-in execution",
|
||||
@"requestRevision":@3,@"constellation":@76,@"frames":frames};
|
||||
NSData *json=[NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted|NSJSONWritingSortedKeys error:&error];
|
||||
if (!json || ![json writeToURL:output options:NSDataWritingAtomic error:&error]) {
|
||||
fprintf(stderr,"%s\n",error.description.UTF8String); return 1;
|
||||
}
|
||||
printf("Saved %lu independent frame observations.\n",(unsigned long)frames.count);
|
||||
return 0;
|
||||
}}
|
||||
可执行文件
+23
@@ -0,0 +1,23 @@
|
||||
#!/bin/zsh
|
||||
set -euo pipefail
|
||||
cd "${0:A:h:h}"
|
||||
output_dir=$(mktemp -d /tmp/paintfcp-host-tests.XXXXXX)
|
||||
trap 'rm -rf "$output_dir"' EXIT
|
||||
plugin_dir="$PWD/beatygril/beatygril/Plugin"
|
||||
sdk_frameworks=/Library/Developer/SDKs/FxPlug.sdk/Library/Frameworks
|
||||
runtime_frameworks=/Library/Developer/Frameworks
|
||||
compiler_extra=()
|
||||
if [[ "${PAINTFCP_HOST_ASAN:-0}" == 1 ]]; then
|
||||
compiler_extra=(-fsanitize=address -g)
|
||||
fi
|
||||
xcrun clang "${compiler_extra[@]}" -fobjc-arc -fmodules -Wall -Wextra -Wno-unused-parameter \
|
||||
-DCI_SILENCE_GL_DEPRECATION -mmacosx-version-min=13.0 \
|
||||
-I "$plugin_dir" -F "$sdk_frameworks" -F "$runtime_frameworks" \
|
||||
-Wl,-rpath,"$runtime_frameworks" \
|
||||
"$plugin_dir/PGBeautyRenderer.m" "$plugin_dir/PGFaceProcessor.m" \
|
||||
"$plugin_dir/beatygrilPlugIn.m" tests/host_adapter_tests.m \
|
||||
-framework Foundation -framework CoreImage -framework CoreGraphics \
|
||||
-framework CoreMedia -framework CoreVideo -framework IOSurface -framework Metal \
|
||||
-framework Vision -framework ImageIO -framework FxPlug -framework PluginManager \
|
||||
-o "$output_dir/host-tests"
|
||||
"$output_dir/host-tests" "$PWD/tests/fixtures/astronaut.png" "$@"
|
||||
可执行文件
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/zsh
|
||||
set -euo pipefail
|
||||
cd "${0:A:h:h}"
|
||||
output_dir=$(mktemp -d /tmp/paintfcp-osc-tests.XXXXXX)
|
||||
trap 'rm -rf "$output_dir"' EXIT
|
||||
plugin_dir="$PWD/beatygril/beatygril/Plugin"
|
||||
sdk_frameworks=/Library/Developer/SDKs/FxPlug.sdk/Library/Frameworks
|
||||
runtime_frameworks=/Library/Developer/Frameworks
|
||||
compiler_extra=()
|
||||
if [[ "${PAINTFCP_OSC_ASAN:-0}" == 1 ]]; then
|
||||
compiler_extra=(-fsanitize=address -g)
|
||||
fi
|
||||
xcrun clang "${compiler_extra[@]}" -fobjc-arc -fmodules -Wall -Wextra -Wno-unused-parameter \
|
||||
-mmacosx-version-min=13.0 -I "$plugin_dir" -F "$sdk_frameworks" -F "$runtime_frameworks" \
|
||||
-Wl,-rpath,"$runtime_frameworks" \
|
||||
"$plugin_dir/PGFaceSelectOSC.m" tests/osc_tests.m \
|
||||
-framework Foundation -framework CoreImage -framework CoreGraphics \
|
||||
-framework CoreMedia -framework CoreVideo -framework IOSurface \
|
||||
-framework FxPlug -framework PluginManager -o "$output_dir/osc-tests"
|
||||
"$output_dir/osc-tests" "$plugin_dir/Info.plist"
|
||||
可执行文件
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/zsh
|
||||
set -euo pipefail
|
||||
cd "${0:A:h:h}"
|
||||
output_dir=$(mktemp -d /tmp/paintfcp-tests.XXXXXX)
|
||||
trap 'rm -rf "$output_dir"' EXIT
|
||||
plugin_dir="$PWD/beatygril/beatygril/Plugin"
|
||||
common=(-fobjc-arc -fmodules -Wall -Wextra -Wno-unused-parameter -DCI_SILENCE_GL_DEPRECATION -mmacosx-version-min=13.0 -I "$plugin_dir" -framework Foundation -framework CoreImage -framework CoreGraphics -framework CoreMedia -framework Metal -framework Vision -framework ImageIO)
|
||||
xcrun clang "${common[@]}" "$plugin_dir/PGBeautyRenderer.m" tests/beauty_render_tests.m -o "$output_dir/beauty-tests"
|
||||
"$output_dir/beauty-tests"
|
||||
xcrun clang "${common[@]}" "$plugin_dir/PGFaceProcessor.m" tests/face_tests.m -o "$output_dir/face-tests"
|
||||
"$output_dir/face-tests" "$PWD/tests/fixtures/astronaut.png"
|
||||
xcrun clang "${common[@]}" "$plugin_dir/PGFaceProcessor.m" tests/slim_tests.m -o "$output_dir/slim-tests"
|
||||
"$output_dir/slim-tests" "$PWD/tests/fixtures/astronaut.png"
|
||||
@@ -0,0 +1,205 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreImage/CoreImage.h>
|
||||
#import <Metal/Metal.h>
|
||||
#import "PGBeautyRenderer.h"
|
||||
#include <math.h>
|
||||
|
||||
static const int W=96,H=64;
|
||||
static int failures=0,checks=0;
|
||||
static void Check(BOOL pass,NSString *message) {
|
||||
checks++;
|
||||
if (!pass) { failures++; fprintf(stderr,"FAIL: %s\n",message.UTF8String); }
|
||||
}
|
||||
static CIImage *Image(const float *pixels,CGRect extent) {
|
||||
NSData *data=[NSData dataWithBytes:pixels length:W*H*4*sizeof(float)];
|
||||
return [[CIImage imageWithBitmapData:data bytesPerRow:W*4*sizeof(float)
|
||||
size:CGSizeMake(W,H) format:kCIFormatRGBAf colorSpace:nil]
|
||||
imageByApplyingTransform:CGAffineTransformMakeTranslation(extent.origin.x,extent.origin.y)];
|
||||
}
|
||||
static float *Render(CIContext *context,CIImage *image,CGRect extent) {
|
||||
float *p=calloc(W*H*4,sizeof(float));
|
||||
if (image) [context render:image toBitmap:p rowBytes:W*4*sizeof(float)
|
||||
bounds:extent format:kCIFormatRGBAf colorSpace:nil];
|
||||
return p;
|
||||
}
|
||||
static float Difference(float *a,float *b,int x0,int x1) {
|
||||
float sum=0;int n=0;
|
||||
for(int y=8;y<H-8;y++) for(int x=x0;x<x1;x++) for(int c=0;c<3;c++) {
|
||||
sum+=fabsf(a[(y*W+x)*4+c]-b[(y*W+x)*4+c]); n++;
|
||||
}
|
||||
return sum/MAX(1,n);
|
||||
}
|
||||
static float Texture(float *a,int x0,int x1) {
|
||||
float sum=0;int n=0;
|
||||
for(int y=8;y<H-8;y++) for(int x=x0+1;x<x1;x++) {
|
||||
sum+=fabsf(a[(y*W+x)*4]-a[(y*W+x-1)*4]);n++;
|
||||
}
|
||||
return sum/MAX(1,n);
|
||||
}
|
||||
int main(void) { @autoreleasepool {
|
||||
NSDictionary *options=@{kCIContextWorkingColorSpace:[NSNull null],kCIContextOutputColorSpace:[NSNull null]};
|
||||
id<MTLDevice> device=MTLCreateSystemDefaultDevice();
|
||||
CIContext *context=device ? [CIContext contextWithMTLDevice:device options:options] : [CIContext contextWithOptions:options];
|
||||
PGBeautyRenderer *renderer=[PGBeautyRenderer new];
|
||||
CGRect extent=CGRectMake(13,-9,W,H);
|
||||
float *p=calloc(W*H*4,sizeof(float)),*mask=calloc(W*H*4,sizeof(float));
|
||||
for(int y=0;y<H;y++) for(int x=0;x<W;x++) {
|
||||
int i=(y*W+x)*4;float noise=((x+y)%2 ? 1 : -1)*0.045;
|
||||
p[i]=0.56+noise;p[i+1]=0.40+noise;p[i+2]=0.31+noise;p[i+3]=1;
|
||||
mask[i]=mask[i+1]=mask[i+2]=(x<W/2 ? 1:0);mask[i+3]=1;
|
||||
}
|
||||
CIImage *source=Image(p,extent),*half=Image(mask,extent);
|
||||
CIImage *white=[[CIImage imageWithColor:[CIColor colorWithRed:1 green:1 blue:1 alpha:1]] imageByCroppingToRect:extent];
|
||||
PGBeautySettings s=PGBeautyDefaults();NSError *error=nil;
|
||||
CIImage *output=[renderer processImage:source settings:s skinMask:white teethMask:nil eyeMask:nil error:&error];
|
||||
Check(output!=nil && error==nil,@"All custom kernels compile and construct a graph");
|
||||
Check(CGRectEqualToRect(output.extent,extent),@"Nonzero/negative source origin and finite extent survive");
|
||||
float *base=Render(context,source,extent),*neutral=Render(context,output,extent);
|
||||
Check(Difference(base,neutral,4,W-4)<0.00001,@"Neutral controls produce identity");free(neutral);
|
||||
s.midtoneDetail=1;s.maskEdgeProtect=0;s.featureProtection=0;
|
||||
output=[renderer processImage:source settings:s skinMask:white teethMask:nil eyeMask:nil error:&error];
|
||||
float *smooth=Render(context,output,extent);
|
||||
Check(Texture(smooth,8,W-8)<Texture(base,8,W-8)*0.82,@"Midtone compression reduces fine texture");
|
||||
s.mix=0;
|
||||
float *bypass=Render(context,[renderer processImage:source settings:s skinMask:white teethMask:nil eyeMask:nil error:&error],extent);
|
||||
Check(Difference(base,bypass,4,W-4)<0.00001,@"Mix zero is exact bypass");free(bypass);
|
||||
s=PGBeautyDefaults();s.skinBrightness=1;s.maskSoftness=0;s.maskEdgeProtect=0;
|
||||
float *bright=Render(context,[renderer processImage:source settings:s skinMask:half teethMask:nil eyeMask:nil error:&error],extent);
|
||||
Check(Difference(base,bright,6,40)>0.08,@"Skin brightness has a substantial visible effect");
|
||||
Check(Difference(base,bright,56,88)<0.00001,@"Skin-only color change respects the supplied mask");
|
||||
s.invertMask=1;
|
||||
float *inverse=Render(context,[renderer processImage:source settings:s skinMask:half teethMask:nil eyeMask:nil error:&error],extent);
|
||||
Check(Difference(base,inverse,6,40)<0.00001 && Difference(base,inverse,56,88)>0.08,@"Invert mask selects the complementary region");
|
||||
free(bright);free(inverse);
|
||||
s=PGBeautyDefaults();s.showMask=1;s.maskSoftness=0;s.maskEdgeProtect=0;
|
||||
float *preview=Render(context,[renderer processImage:source settings:s skinMask:half teethMask:nil eyeMask:nil error:&error],extent);
|
||||
Check(preview[(32*W+20)*4]>0.99 && preview[(32*W+72)*4]<0.01,@"Mask preview exposes actual coverage");free(preview);
|
||||
s=PGBeautyDefaults();s.highPassDetail=1;
|
||||
float *global=Render(context,[renderer processImage:source settings:s skinMask:nil teethMask:nil eyeMask:nil error:&error],extent);
|
||||
Check(Texture(global,8,W-8)<Texture(base,8,W-8)*0.72,@"Global high-frequency compression works without a skin mask");free(global);
|
||||
s=PGBeautyDefaults();s.teethWhitening=1;
|
||||
float *whiten=Render(context,[renderer processImage:source settings:s skinMask:nil teethMask:half eyeMask:nil error:&error],extent);
|
||||
Check(whiten[(32*W+20)*4+2]>base[(32*W+20)*4+2]+0.035,@"Teeth whitening neutralizes yellow and lifts blue");
|
||||
Check(Difference(base,whiten,56,88)<0.00001,@"Teeth whitening is spatially confined");free(whiten);
|
||||
s=PGBeautyDefaults();s.eyeBrightening=1;
|
||||
float *eyes=Render(context,[renderer processImage:source settings:s skinMask:nil teethMask:nil eyeMask:half error:&error],extent);
|
||||
Check(Difference(base,eyes,6,40)>0.04 && Difference(base,eyes,56,88)<0.00001,@"Eye brightening is visible and spatially confined");free(eyes);
|
||||
for(int preset=1;preset<=6;preset++) {
|
||||
s=PGBeautyDefaults();s.skinTonePreset=preset;s.maskSoftness=0;s.maskEdgeProtect=0;
|
||||
float *look=Render(context,[renderer processImage:source settings:s skinMask:half teethMask:nil eyeMask:nil error:&error],extent);
|
||||
Check(Difference(base,look,6,40)>0.002 && Difference(base,look,56,88)<0.00001,
|
||||
[NSString stringWithFormat:@"Independent skin preset %d changes only skin",preset]);free(look);
|
||||
}
|
||||
s=PGBeautyDefaults();s.skinHue=30;s.maskSoftness=0;s.maskEdgeProtect=0;
|
||||
float *hue=Render(context,[renderer processImage:source settings:s skinMask:half teethMask:nil eyeMask:nil error:&error],extent);
|
||||
Check(Difference(base,hue,6,40)>0.025 && Difference(base,hue,56,88)<0.00001,@"Hue rotates selected skin without changing the rest");free(hue);
|
||||
s.skinHue=0;s.skinSaturation=-100;
|
||||
float *desaturated=Render(context,[renderer processImage:source settings:s skinMask:half teethMask:nil eyeMask:nil error:&error],extent);
|
||||
int center=(32*W+20)*4;
|
||||
Check(fabsf(desaturated[center]-desaturated[center+1])<0.0002 && fabsf(desaturated[center+1]-desaturated[center+2])<0.0002,@"Minus 100 saturation produces neutral gray skin");free(desaturated);
|
||||
s=PGBeautyDefaults();s.sharpness=1;
|
||||
float *sharp=Render(context,[renderer processImage:source settings:s skinMask:nil teethMask:nil eyeMask:nil error:&error],extent);
|
||||
Check(Texture(sharp,8,W-8)>Texture(base,8,W-8)*1.05,@"Sharpening increases local edge contrast");free(sharp);
|
||||
s=PGBeautyDefaults();s.midtoneDetail=1;s.maskSoftness=0;s.maskEdgeProtect=0;s.featureProtection=1;
|
||||
float *protected=Render(context,[renderer processImage:source settings:s skinMask:white teethMask:nil eyeMask:half error:&error],extent);
|
||||
Check(Difference(base,protected,6,40)<0.00001 && Texture(protected,56,88)<Texture(base,56,88)*0.82,@"Feature protection retains eye detail while smoothing nearby skin");free(protected);
|
||||
// A skin exposure adjustment must stay continuous across detail-protection
|
||||
// holes. This models the eye/nose islands reported on the real beach clip.
|
||||
s=PGBeautyDefaults();s.skinBrightness=1;s.featureProtection=1;
|
||||
float *continuous=Render(context,[renderer processImage:source settings:s skinMask:half colorMask:white teethMask:half eyeMask:half error:&error],extent);
|
||||
Check(Difference(base,continuous,6,40)>0.08 && Difference(base,continuous,56,88)>0.08,@"Skin exposure does not leave dark detail-protection holes");
|
||||
s.featureProtection=0;
|
||||
float *unprotectedColor=Render(context,[renderer processImage:source settings:s skinMask:half colorMask:white teethMask:half eyeMask:half error:&error],extent);
|
||||
Check(Difference(continuous,unprotectedColor,4,W-4)<0.00001,@"Feature protection changes detail, not face exposure coverage");
|
||||
free(continuous);free(unprotectedColor);
|
||||
CIImage *flatSkin=[[CIImage imageWithColor:[CIColor colorWithRed:0.56 green:0.40 blue:0.31 alpha:1]] imageByCroppingToRect:extent];
|
||||
float *flatPixels=Render(context,flatSkin,extent);
|
||||
float *feathered=Render(context,[renderer processImage:flatSkin settings:s skinMask:half colorMask:half teethMask:nil eyeMask:nil error:&error],extent);
|
||||
float largestStep=0;
|
||||
for(int x=42;x<54;x++) {
|
||||
int i=(32*W+x)*4,j=i+4;
|
||||
largestStep=fmaxf(largestStep,fabsf((feathered[i]-flatPixels[i])-(feathered[j]-flatPixels[j])));
|
||||
}
|
||||
if(largestStep>=0.09) fprintf(stderr,"Face exposure boundary maximum step: %.6f\n",largestStep);
|
||||
Check(largestStep<0.09,@"Face color coverage feathers a hard support boundary without a visible exposure step");
|
||||
Check(Difference(flatPixels,feathered,64,88)<0.00001,@"Feathered face exposure stays local");free(feathered);free(flatPixels);
|
||||
CIImage *blueHair=[[CIImage imageWithColor:[CIColor colorWithRed:0.03 green:0.16 blue:0.70 alpha:1]] imageByCroppingToRect:extent];
|
||||
float *blueBefore=Render(context,blueHair,extent);
|
||||
float *blueAfter=Render(context,[renderer processImage:blueHair settings:s skinMask:white colorMask:white teethMask:nil eyeMask:nil error:&error],extent);
|
||||
Check(Difference(blueBefore,blueAfter,4,W-4)<0.00001,@"Skin exposure excludes saturated blue hair within face support");free(blueBefore);free(blueAfter);
|
||||
s=PGBeautyDefaults();s.showMask=1;s.maskSoftness=0;s.maskEdgeProtect=0;s.maskThreshold=255;
|
||||
float *strictMask=Render(context,[renderer processImage:source settings:s skinMask:half teethMask:nil eyeMask:nil error:&error],extent);
|
||||
s.maskThreshold=0;
|
||||
float *looseMask=Render(context,[renderer processImage:source settings:s skinMask:half teethMask:nil eyeMask:nil error:&error],extent);
|
||||
Check(looseMask[(32*W+20)*4]>=strictMask[(32*W+20)*4] && looseMask[(32*W+72)*4]==0,@"Mask threshold keeps zero coverage empty");free(strictMask);free(looseMask);
|
||||
CIImage *grayMask=[[CIImage imageWithColor:[CIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1]] imageByCroppingToRect:extent];
|
||||
s.maskThreshold=220;
|
||||
float *highThreshold=Render(context,[renderer processImage:source settings:s skinMask:grayMask teethMask:nil eyeMask:nil error:&error],extent);
|
||||
s.maskThreshold=60;
|
||||
float *lowThreshold=Render(context,[renderer processImage:source settings:s skinMask:grayMask teethMask:nil eyeMask:nil error:&error],extent);
|
||||
Check(highThreshold[(32*W+20)*4]<0.01 && lowThreshold[(32*W+20)*4]>0.99,@"Mask threshold materially changes intermediate confidence coverage");free(highThreshold);free(lowThreshold);
|
||||
// Chroma-only speckle should be reduced while its luminance stays stable.
|
||||
float *colorNoise=calloc(W*H*4,sizeof(float));
|
||||
for(int y=0;y<H;y++)for(int x=0;x<W;x++){
|
||||
int i=(y*W+x)*4;float n=((x+y)%2 ? 1:-1)*0.055;
|
||||
colorNoise[i]=0.56+n;colorNoise[i+1]=0.40-n*0.299/0.587;colorNoise[i+2]=0.31;colorNoise[i+3]=1;
|
||||
}
|
||||
CIImage *speckle=Image(colorNoise,extent);s=PGBeautyDefaults();s.skinEvenTone=1;s.maskEdgeProtect=0;
|
||||
float *evenColor=Render(context,[renderer processImage:speckle settings:s skinMask:white teethMask:nil eyeMask:nil error:&error],extent);
|
||||
float before=Texture(colorNoise,8,W-8),after=Texture(evenColor,8,W-8);
|
||||
Check(after<before*0.5,@"Even tone reduces chromatic skin mottling");
|
||||
float oldY=colorNoise[center]*0.299+colorNoise[center+1]*0.587+colorNoise[center+2]*0.114;
|
||||
float newY=evenColor[center]*0.299+evenColor[center+1]*0.587+evenColor[center+2]*0.114;
|
||||
Check(fabsf(oldY-newY)<0.001,@"Even tone preserves local luminance");free(colorNoise);free(evenColor);
|
||||
// Mask channel selection remains dormant until the preview checkbox is enabled.
|
||||
CIImage *quarterMask=[[CIImage imageWithColor:[CIColor colorWithRed:0.25 green:0.25 blue:0.25 alpha:1]] imageByCroppingToRect:extent];
|
||||
CIImage *eyeCoverage=[[CIImage imageWithColor:[CIColor colorWithRed:0.8 green:0.8 blue:0.8 alpha:1]] imageByCroppingToRect:extent];
|
||||
for(int channel=0;channel<3;channel++){
|
||||
s=PGBeautyDefaults();s.debugChannel=channel;
|
||||
float *normal=Render(context,[renderer processImage:source settings:s skinMask:half teethMask:quarterMask eyeMask:eyeCoverage error:&error],extent);
|
||||
Check(Difference(base,normal,4,W-4)<0.00001,[NSString stringWithFormat:@"Channel %d does not enable preview by itself",channel]);free(normal);
|
||||
s.showMask=1;s.maskSoftness=0;s.maskEdgeProtect=0;s.featureProtection=0;
|
||||
for(int invert=0;invert<2;invert++){
|
||||
s.invertMask=invert;
|
||||
float *shown=Render(context,[renderer processImage:source settings:s skinMask:half teethMask:quarterMask eyeMask:eyeCoverage error:&error],extent);
|
||||
float expected=channel==0 ? 1 : (channel==1 ? 0.25 : 0.8);
|
||||
if(invert)expected=1-expected;
|
||||
Check(fabsf(shown[(32*W+20)*4]-expected)<0.0002,[NSString stringWithFormat:@"Preview channel %d uses its supplied mask (invert %d)",channel,invert]);free(shown);
|
||||
}
|
||||
}
|
||||
float *looks[7]={0};
|
||||
for(int preset=0;preset<=6;preset++){
|
||||
s=PGBeautyDefaults();s.skinTonePreset=preset;s.maskSoftness=0;s.maskEdgeProtect=0;
|
||||
looks[preset]=Render(context,[renderer processImage:source settings:s skinMask:half teethMask:nil eyeMask:nil error:&error],extent);
|
||||
}
|
||||
Check(Difference(base,looks[0],4,W-4)<0.00001,@"Original color preset is exact identity");
|
||||
Check(looks[2][center+2]-looks[2][center]>looks[3][center+2]-looks[3][center]+0.04,@"Cool white and warm white follow popup order");
|
||||
Check(looks[4][center]-looks[4][center+1]>looks[1][center]-looks[1][center+1],@"Rosy white has stronger rosy chroma than natural white");
|
||||
float bronzeY=looks[6][center]*0.299+looks[6][center+1]*0.587+looks[6][center+2]*0.114;
|
||||
float wheatY=looks[5][center]*0.299+looks[5][center+1]*0.587+looks[5][center+2]*0.114;
|
||||
float sourceY=base[center]*0.299+base[center+1]*0.587+base[center+2]*0.114;
|
||||
Check(bronzeY<wheatY && bronzeY<sourceY-0.025 && looks[6][center]>looks[6][center+2],@"Bronze is a darker warm brown than wheat");
|
||||
for(int preset=0;preset<=6;preset++)free(looks[preset]);
|
||||
// The renderer's optional blend is linear once; a host that also blends must
|
||||
// pass mix=1 and blend the complete processed image against the original.
|
||||
s=PGBeautyDefaults();s.skinBrightness=1;s.skinHue=14;s.skinSaturation=20;s.maskSoftness=0;s.maskEdgeProtect=0;
|
||||
float *fullEffect=Render(context,[renderer processImage:source settings:s skinMask:half teethMask:nil eyeMask:nil error:&error],extent);
|
||||
s.mix=0.5;
|
||||
float *halfEffect=Render(context,[renderer processImage:source settings:s skinMask:half teethMask:nil eyeMask:nil error:&error],extent);
|
||||
BOOL linearMix=YES;
|
||||
for(int i=0;i<W*H*4;i++)if(fabsf(halfEffect[i]-(base[i]+fullEffect[i])*0.5f)>0.0002)linearMix=NO;
|
||||
Check(linearMix,@"Combined color controls receive exactly one linear global blend");free(fullEffect);free(halfEffect);
|
||||
// Every operation must preserve transparency rather than creating halos/opaque pixels.
|
||||
for(int i=0;i<W*H;i++){p[i*4]*=0.4;p[i*4+1]*=0.4;p[i*4+2]*=0.4;p[i*4+3]=0.4;}
|
||||
CIImage *transparent=Image(p,extent);s=PGBeautyDefaults();s.midtoneDetail=1;s.skinBrightness=1;s.teethWhitening=1;s.eyeBrightening=1;s.skinTonePreset=3;s.highPassDetail=1;
|
||||
float *alpha=Render(context,[renderer processImage:transparent settings:s skinMask:white teethMask:half eyeMask:half error:&error],extent);
|
||||
BOOL alphaOK=YES,finite=YES;
|
||||
for(int i=0;i<W*H;i++){if(fabsf(alpha[i*4+3]-0.4f)>0.0002)alphaOK=NO;for(int c=0;c<4;c++)if(!isfinite(alpha[i*4+c]))finite=NO;}
|
||||
Check(alphaOK,@"Combined beauty pipeline preserves fractional alpha");Check(finite,@"Combined beauty pipeline produces finite pixels");free(alpha);
|
||||
// Explicit failure path for unbounded images.
|
||||
CIImage *invalid=[renderer processImage:[CIImage imageWithColor:[CIColor whiteColor]] settings:s skinMask:nil teethMask:nil eyeMask:nil error:&error];
|
||||
Check(invalid==nil && error!=nil,@"Unbounded input produces a useful error");
|
||||
free(base);free(smooth);free(mask);free(p);
|
||||
printf("Beauty renderer: %d checks, %d failures (%s).\n",checks,failures,device ? "Metal" : "default Core Image");
|
||||
return failures ? 1:0;
|
||||
}}
|
||||
+403
@@ -0,0 +1,403 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreImage/CoreImage.h>
|
||||
#import <Vision/Vision.h>
|
||||
#import <ImageIO/ImageIO.h>
|
||||
#import "PGFaceProcessor.h"
|
||||
#import <dispatch/dispatch.h>
|
||||
#include <math.h>
|
||||
|
||||
// Exercise the independent selector with authored landmark geometry, without
|
||||
// requiring Vision to manufacture a particular jitter or eye-axis failure.
|
||||
@interface PGRealtimeFaceTracker : NSObject
|
||||
- (NSDictionary *)resultForGeometry:(NSArray<NSDictionary *> *)raw settings:(PGBeautySettings)settings time:(CMTime)time extent:(CGRect)extent;
|
||||
- (NSDictionary *)cacheStatistics;
|
||||
- (void)reset;
|
||||
@end
|
||||
|
||||
// Injectable detections let geometry, selection and saved-frame behavior be
|
||||
// tested independently of the separate public-domain NASA portrait fixture.
|
||||
@interface PGFaceProcessor (TestDetection)
|
||||
- (NSArray<VNFaceObservation *> *)observationsInImage:(CIImage *)image settings:(PGBeautySettings)settings error:(NSError **)error;
|
||||
@end
|
||||
@interface PGFaceFixture : PGFaceProcessor
|
||||
@property (nonatomic, copy) NSArray<VNFaceObservation *> *detections;
|
||||
@property (nonatomic) NSUInteger detectionCount;
|
||||
@end
|
||||
@implementation PGFaceFixture
|
||||
- (NSArray<VNFaceObservation *> *)observationsInImage:(CIImage *)image settings:(PGBeautySettings)settings error:(NSError **)error {
|
||||
self.detectionCount++;
|
||||
return self.detections ?: @[];
|
||||
}
|
||||
@end
|
||||
|
||||
static NSUInteger PGChecks = 0;
|
||||
static void Check(BOOL condition, NSString *description) {
|
||||
PGChecks++;
|
||||
if (!condition) { fprintf(stderr, "FAIL: %s\n", description.UTF8String); exit(1); }
|
||||
}
|
||||
static VNFaceObservation *Face(CGRect box) {
|
||||
return [VNFaceObservation faceObservationWithRequestRevision:VNDetectFaceLandmarksRequestRevision3 boundingBox:box roll:nil yaw:nil pitch:nil];
|
||||
}
|
||||
static NSData *Pixels(CIContext *context, CIImage *image) {
|
||||
CGRect bounds = image.extent;
|
||||
NSUInteger width = (NSUInteger)bounds.size.width, height = (NSUInteger)bounds.size.height;
|
||||
NSMutableData *data = [NSMutableData dataWithLength:width * height * 4];
|
||||
CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
||||
[context render:image toBitmap:data.mutableBytes rowBytes:width * 4 bounds:bounds format:kCIFormatRGBA8 colorSpace:colorSpace];
|
||||
CGColorSpaceRelease(colorSpace);
|
||||
return data;
|
||||
}
|
||||
static unsigned char Channel(CIContext *context, CIImage *image, double x, double y, NSUInteger channel) {
|
||||
unsigned char pixel[4] = {0};
|
||||
CGColorSpaceRef space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
||||
[context render:image toBitmap:pixel rowBytes:4 bounds:CGRectMake(x, y, 1, 1) format:kCIFormatRGBA8 colorSpace:space];
|
||||
CGColorSpaceRelease(space);
|
||||
return pixel[channel];
|
||||
}
|
||||
static CIImage *Gradient(void) {
|
||||
NSMutableData *data = [NSMutableData dataWithLength:128 * 128 * 4];
|
||||
unsigned char *p = data.mutableBytes;
|
||||
for (NSUInteger y = 0; y < 128; y++) for (NSUInteger x = 0; x < 128; x++) {
|
||||
NSUInteger index = 4 * (y * 128 + x);
|
||||
p[index] = x * 2; p[index + 1] = y * 2;
|
||||
p[index + 2] = ((x / 8 + y / 8) % 2) * 220 + 15; p[index + 3] = 255;
|
||||
}
|
||||
CGColorSpaceRef space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
||||
CIImage *image = [CIImage imageWithBitmapData:data bytesPerRow:128 * 4 size:CGSizeMake(128, 128) format:kCIFormatRGBA8 colorSpace:space];
|
||||
CGColorSpaceRelease(space);
|
||||
return image;
|
||||
}
|
||||
|
||||
static NSDictionary *Geometry(CGRect box,double jitter) {
|
||||
NSMutableDictionary *g=[NSMutableDictionary dictionaryWithObject:@[@(box.origin.x),@(box.origin.y),@(box.size.width),@(box.size.height)] forKey:@"box"];
|
||||
for(NSString *name in @[@"leftEye",@"rightEye",@"leftBrow",@"rightBrow",@"nose",@"lips",@"mouth",@"contour"]) {
|
||||
double x=[name isEqual:@"rightEye"]?.7:.3;
|
||||
g[name]=@[@[@(box.origin.x+(x+jitter)*box.size.width),@(box.origin.y+.65*box.size.height)]];
|
||||
}
|
||||
return [g copy];
|
||||
}
|
||||
static CGRect GeometryBox(NSDictionary *g) {
|
||||
NSArray *b=g[@"box"];return b.count==4?CGRectMake([b[0] doubleValue],[b[1] doubleValue],[b[2] doubleValue],[b[3] doubleValue]):CGRectZero;
|
||||
}
|
||||
static double EyeX(NSDictionary *g) {return [g[@"leftEye"][0][0] doubleValue];}
|
||||
static NSDictionary *Live(PGRealtimeFaceTracker *tracker,NSArray *g,PGBeautySettings s,int64_t tick) {
|
||||
return [tracker resultForGeometry:g settings:s time:CMTimeMake(tick,1000) extent:CGRectMake(0,0,128,128)];
|
||||
}
|
||||
static void RealtimeChecks(CIContext *context,CIImage *image) {
|
||||
NSError *error=nil;PGBeautySettings s=PGBeautyDefaults();
|
||||
VNFaceObservation *left=Face(CGRectMake(.10,.25,.30,.50));
|
||||
VNFaceObservation *right=Face(CGRectMake(.66,.30,.20,.40));
|
||||
PGFaceFixture *p=[PGFaceFixture new];p.detections=@[left,right];
|
||||
s.faceSmallHead=.8;
|
||||
PGFaceFrame *automatic=[p processImage:image settings:s time:kCMTimeZero error:&error];
|
||||
Check(automatic.deformationGeometry.count==1 && GeometryBox(automatic.deformationGeometry.firstObject).origin.x<.5,@"default realtime geometry chooses one area/center primary without analysis");
|
||||
Check(automatic.faces.count==2 && automatic.faces[0]==left && automatic.faces[1]==right,@"default appearance retains both original Vision observations");
|
||||
Check(Channel(context,automatic.teethMask,97,51,0)>20 && Channel(context,automatic.eyeMask,92,69,0)>20,@"default teeth and eye support covers the non-primary face");
|
||||
CGRect rightCrop=CGRectMake(84,38,30,52);
|
||||
Check([Pixels(context,[automatic.image imageByCroppingToRect:rightCrop]) isEqualToData:Pixels(context,[image imageByCroppingToRect:rightCrop])],@"default deformation leaves the distant secondary face unchanged");
|
||||
PGBeautySettings multi=s;multi.fastMultiFace=1;multi.singleFace=1;
|
||||
PGFaceFrame *many=[p processImage:image settings:multi time:kCMTimeZero error:&error];
|
||||
Check(many.deformationGeometry.count==2 && many.faces.count==2,@"fast-multi takes precedence over a stale single-face flag");
|
||||
Check(![Pixels(context,[many.image imageByCroppingToRect:rightCrop]) isEqualToData:Pixels(context,[image imageByCroppingToRect:rightCrop])],@"fast-multi visibly deforms the secondary face");
|
||||
Check([p trackingDataForTarget:0]==nil && [p trackingDataForTarget:1]==nil && [p trackingDataForTarget:2]==nil,@"realtime processing never creates persisted analysis records");
|
||||
p.detections=@[Face(CGRectMake(.15,.25,.30,.50)),Face(CGRectMake(.43,.005,.56,.99))];
|
||||
PGFaceFrame *moving=[p processImage:image settings:s time:CMTimeMake(1,25) error:&error];
|
||||
Check(GeometryBox(moving.deformationGeometry.firstObject).origin.x<.3,@"moving primary stays selected when a much larger stranger appears outside its rectangular guard");
|
||||
PGFaceFixture *cold=[PGFaceFixture new];cold.detections=p.detections;
|
||||
PGFaceFrame *coldFrame=[cold processImage:image settings:s time:CMTimeMake(1,25) error:&error];
|
||||
Check(GeometryBox(coldFrame.deformationGeometry.firstObject).origin.x>.4,@"cold initialization explicitly differs from warm identity association when a new primary dominates");
|
||||
PGBeautySettings manual=s;manual.singleFace=1;manual.selectionX=.78;manual.selectionY=.5;
|
||||
p.detections=@[left,right];
|
||||
PGFaceFrame *chosen=[p processImage:image settings:manual time:CMTimeMake(2,25) error:&error];
|
||||
Check(chosen.faces.count==1 && chosen.faces.firstObject==right,@"manual selection retains its original observation rather than matching a smoothed box");
|
||||
Check(GeometryBox(chosen.deformationGeometry.firstObject).origin.x>.5 && Channel(context,chosen.colorMask,26,54,0)==0,@"explicit single-face UX narrows appearance to the selected target");
|
||||
manual.selectionX=.25;
|
||||
chosen=[p processImage:image settings:manual time:CMTimeMake(2,25) error:&error];
|
||||
Check(chosen.faces.firstObject==left,@"manual reselection at the same exact time immediately selects the other face");
|
||||
|
||||
PGRealtimeFaceTracker *t=[PGRealtimeFaceTracker new];
|
||||
NSDictionary *a=Geometry(CGRectMake(.1,.25,.3,.5),0),*b=Geometry(CGRectMake(.66,.3,.2,.4),0);
|
||||
NSDictionary *r=Live(t,@[a,b],s,0);
|
||||
Check([r[@"output"] count]==1 && [r[@"index"] integerValue]==0,@"standalone default selector chooses the scored primary");
|
||||
PGBeautySettings ignoredSeed=s;ignoredSeed.selectionX=.9;ignoredSeed.selectionY=.1;
|
||||
Check([r isEqual:Live(t,@[a,b],ignoredSeed,0)],@"seed changes are irrelevant while explicit single-face selection is disabled");
|
||||
NSDictionary *aMoved=Geometry(CGRectMake(.15,.25,.3,.5),0),*large=Geometry(CGRectMake(.43,.005,.56,.99),0);
|
||||
r=Live(t,@[aMoved,large],s,40);
|
||||
Check([r[@"index"] integerValue]==0 && [r[@"referenceKey"] isEqual:@"0/1/0"],@"live association uses the previous frame rather than recomputing the primary score");
|
||||
NSDictionary *outside=Geometry(CGRectMake(.60,.30,.10,.10),0);
|
||||
r=Live(t,@[outside],s,80);
|
||||
Check([r[@"held"] boolValue] && [r[@"output"][0] isEqual:Live(t,@[aMoved,large],s,40)[@"output"][0]],@"no guard candidate retains the previous stable geometry at full strength");
|
||||
r=Live(t,@[],s,120);
|
||||
Check([r[@"held"] boolValue] && [r[@"referenceKey"] isEqual:@"2/25/0"],@"empty detections hold geometry while advancing output time");
|
||||
for(int64_t tick=160;tick<=2160;tick+=40)r=Live(t,@[],s,tick);
|
||||
Check([r[@"held"] boolValue] && [r[@"output"] count]==1,@"continuous close held frames can retain geometry beyond .35 seconds");
|
||||
r=Live(t,@[],s,3000);
|
||||
Check([r[@"output"] count]==0 && [r[@"referenceKey"] length]==0,@"a distant seek without a nearby past reference cold-starts without held geometry");
|
||||
r=Live(t,@[b],s,4000);
|
||||
Check(![r[@"held"] boolValue] && [r[@"index"] integerValue]==0,@"a far seek with a current face cold-acquires that face");
|
||||
|
||||
// Rectangular guard includes a diagonal candidate that a small circular
|
||||
// center threshold would reject, and excludes a point just over an edge.
|
||||
PGRealtimeFaceTracker *guard=[PGRealtimeFaceTracker new];
|
||||
PGBeautySettings guardSettings=s;guardSettings.faceJumpGuardRange=1.8;
|
||||
NSDictionary *square=Geometry(CGRectMake(.4,.4,.2,.2),0);
|
||||
Live(guard,@[square],guardSettings,0);
|
||||
NSDictionary *diagonal=Geometry(CGRectMake(.625,.625,.1,.1),0);
|
||||
r=Live(guard,@[diagonal],guardSettings,40);
|
||||
Check(![r[@"held"] boolValue] && [r[@"index"] integerValue]==0,@"expanded rectangular guard accepts a candidate near both positive corners");
|
||||
[guard reset];Live(guard,@[square],guardSettings,0);
|
||||
r=Live(guard,@[Geometry(CGRectMake(.631,.45,.1,.1),0)],guardSettings,40);
|
||||
Check([r[@"held"] boolValue],@"candidate just beyond a rectangular guard edge cannot replace the identity");
|
||||
|
||||
PGRealtimeFaceTracker *smooth=[PGRealtimeFaceTracker new];
|
||||
CGRect box=CGRectMake(.2,.2,.4,.6);
|
||||
NSDictionary *g0=Geometry(box,0),*jitter=Geometry(box,.06);
|
||||
Live(smooth,@[g0],s,0);
|
||||
NSDictionary *smoothed=Live(smooth,@[jitter],s,10);
|
||||
double expected=EyeX(g0)+(EyeX(jitter)-EyeX(g0))*.3;
|
||||
Check(fabs(EyeX(smoothed[@"output"][0])-expected)<1e-9,@"small-dt landmark smoothing uses a time-aware current weight");
|
||||
Check(fabs(EyeX(smoothed[@"output"][0])-EyeX(g0))<fabs(EyeX(jitter)-EyeX(g0)),@"landmark jitter amplitude is reduced");
|
||||
Check([smoothed isEqual:Live(smooth,@[jitter],s,10)] && [[smooth cacheStatistics][@"frames"] unsignedIntegerValue]==2,@"same exact time and raw geometry returns an identical cached result without another smoothing step");
|
||||
NSDictionary *equivalent=[smooth resultForGeometry:@[jitter] settings:s time:CMTimeMake(1,100) extent:CGRectMake(0,0,128,128)];
|
||||
Check([equivalent isEqual:smoothed],@"equivalent rational CMTime representations share one realtime result");
|
||||
NSDictionary *changed=Geometry(box,.08);
|
||||
NSDictionary *refreshed=Live(smooth,@[changed],s,10);
|
||||
Check(fabs(EyeX(refreshed[@"output"][0])-(EyeX(g0)+(EyeX(changed)-EyeX(g0))*.3))<1e-9,@"changed raw detections at the same time recompute from the earlier frame rather than compound smoothing");
|
||||
NSMutableDictionary *flipped=[changed mutableCopy];flipped[@"leftEye"]=changed[@"rightEye"];flipped[@"rightEye"]=changed[@"leftEye"];
|
||||
NSDictionary *axis=Live(smooth,@[flipped],s,20);
|
||||
Check(fabs(EyeX(axis[@"output"][0])-EyeX(refreshed[@"output"][0]))<1e-9,@"eye-axis reversal holds remapped old landmarks instead of flipping the warp");
|
||||
NSDictionary *translated=Geometry(CGRectOffset(box,.1,0),.08);
|
||||
NSDictionary *translatedResult=Live(smooth,@[translated],s,60);
|
||||
Check(fabs(GeometryBox(translatedResult[@"output"][0]).origin.x-.3)<1e-9,@"normal translation uses the current detection box without accumulated box lag");
|
||||
double mappedEye=EyeX(axis[@"output"][0])+.1;
|
||||
Check(fabs(EyeX(translatedResult[@"output"][0])-(mappedEye+.9*(EyeX(translated)-mappedEye)))<1e-9,@"old landmarks remap into the current moving box before blending");
|
||||
Check([smoothed[@"output"] isEqual:Live(smooth,@[jitter],s,10)[@"output"]],@"repeated rewind to a cached time reproduces its original stable geometry");
|
||||
|
||||
PGRealtimeFaceTracker *seeds=[PGRealtimeFaceTracker new];
|
||||
PGBeautySettings selected=s;selected.singleFace=1;selected.selectionX=.25;selected.selectionY=.5;
|
||||
Live(seeds,@[a,b],selected,0);
|
||||
selected.selectionX=.255;r=Live(seeds,@[],selected,40);
|
||||
Check([r[@"held"] boolValue],@"sub-.01 seed movement retains the existing identity");
|
||||
selected.selectionX=.27;r=Live(seeds,@[],selected,80);
|
||||
Check([r[@"output"] count]==0 && ![r[@"held"] boolValue],@"seed change over .01 starts a new identity and does not inherit an old held target");
|
||||
selected.selectionX=.78;r=Live(seeds,@[a,b],selected,80);
|
||||
Check([r[@"index"] integerValue]==1,@"large manual reselection immediately chooses the new seed target");
|
||||
NSDictionary *bMoved=Geometry(CGRectMake(.50,.30,.2,.4),0);
|
||||
r=Live(seeds,@[a,bMoved],selected,120);
|
||||
Check([r[@"index"] integerValue]==1,@"manually seeded target follows a near moving face");
|
||||
NSDictionary *bContinued=Geometry(CGRectMake(.45,.3,.2,.4),0),*seedStranger=Geometry(CGRectMake(.8,.3,.15,.4),0);
|
||||
r=Live(seeds,@[a,bContinued,seedStranger],selected,160);
|
||||
Check([r[@"index"] integerValue]==1,@"following does not repeatedly select the person nearest the fixed original click");
|
||||
|
||||
PGRealtimeFaceTracker *reseedRewind=[PGRealtimeFaceTracker new];
|
||||
PGBeautySettings seedA=s;seedA.singleFace=1;seedA.selectionX=.2;seedA.selectionY=.5;
|
||||
for(int64_t i=0;i<=20;i++) {
|
||||
NSDictionary *movingFace=Geometry(CGRectMake(.15+.02*i,.3,.1,.4),0);
|
||||
Live(reseedRewind,@[movingFace],seedA,i*40);
|
||||
}
|
||||
NSArray *rewindFaces=@[Geometry(CGRectMake(.15,.3,.1,.4),0),Geometry(CGRectMake(.55,.3,.1,.4),0),Geometry(CGRectMake(.75,.3,.1,.4),0)];
|
||||
NSDictionary *beforeReselect=Live(reseedRewind,rewindFaces,seedA,840);
|
||||
PGBeautySettings seedB=seedA;seedB.selectionX=.8;Live(reseedRewind,rewindFaces,seedB,900);
|
||||
Live(reseedRewind,rewindFaces,seedA,1000);
|
||||
Check([beforeReselect isEqual:Live(reseedRewind,rewindFaces,seedA,840)],@"A-to-B-to-A reselection does not let the future A scope replace an earlier A exact result");
|
||||
|
||||
PGRealtimeFaceTracker *order=[PGRealtimeFaceTracker new];
|
||||
NSDictionary *future=Live(order,@[b],s,1000);
|
||||
NSDictionary *past=Live(order,@[a],s,0);
|
||||
Check([past[@"referenceKey"] length]==0 && [past[@"output"][0] isEqual:a],@"uncached rewind never borrows geometry from a future frame");
|
||||
r=Live(order,@[Geometry(CGRectMake(.64,.3,.2,.4),0)],s,1040);
|
||||
Check([r[@"referenceKey"] isEqual:future[@"key"]],@"late older frames do not replace the newer live reference head");
|
||||
NSDictionary *nearRewind=Live(order,@[a],s,980);
|
||||
Check([nearRewind[@"referenceKey"] length]==0,@"uncached small rewind also refuses a closer future reference");
|
||||
NSDictionary *epoch=[order resultForGeometry:@[] settings:s time:CMTimeMakeWithEpoch(1080,1000,9) extent:CGRectMake(0,0,128,128)];
|
||||
Check([epoch[@"output"] count]==0,@"CMTime epochs isolate realtime identity caches");
|
||||
NSDictionary *extent=[order resultForGeometry:@[] settings:s time:CMTimeMake(1080,1000) extent:CGRectMake(20,30,256,128)];
|
||||
Check([extent[@"output"] count]==0,@"source extent size and origin changes isolate realtime geometry");
|
||||
PGBeautySettings target=s;target.targetIndex=1;r=Live(order,@[],target,1080);
|
||||
Check([r[@"output"] count]==0,@"target changes cannot reuse another target's realtime geometry");
|
||||
PGBeautySettings fast=s;fast.fastMultiFace=1;r=Live(order,@[a,b],fast,1080);
|
||||
Check([r[@"output"] count]==2 && [r[@"referenceKey"] length]==0,@"fast-multi mode has a separate geometry context");
|
||||
r=Live(order,@[],fast,1120);Check([r[@"output"] count]==0,@"fast-multi does not inherit primary hold behavior");
|
||||
Check([future isEqual:Live(order,@[b],s,1000)],@"same-time historical exact result remains stable after other contexts and seeks");
|
||||
|
||||
// Parallel calls may have cold/warm identity differences, but each must be
|
||||
// data safe, reject future references, and freeze exact repeated requests.
|
||||
PGRealtimeFaceTracker *parallel=[PGRealtimeFaceTracker new];
|
||||
dispatch_apply(40,dispatch_get_global_queue(QOS_CLASS_USER_INITIATED,0),^(size_t i) {
|
||||
@autoreleasepool {Live(parallel,@[a],s,(int64_t)i*40);}
|
||||
});
|
||||
BOOL pastOnly=YES,exact=YES;
|
||||
for(int64_t i=0;i<40;i++) {
|
||||
NSDictionary *first=Live(parallel,@[a],s,i*40),*again=Live(parallel,@[a],s,i*40);
|
||||
exact=exact&&[first isEqual:again];
|
||||
NSString *reference=first[@"referenceKey"];
|
||||
if(reference.length) {NSArray *parts=[reference componentsSeparatedByString:@"/"];pastOnly=pastOnly&&[parts[0] doubleValue]/[parts[1] doubleValue] <= i*.04;}
|
||||
}
|
||||
Check(pastOnly && exact,@"parallel result cache is safe and repeated requests retain past-only references");
|
||||
PGRealtimeFaceTracker *bounded=[PGRealtimeFaceTracker new];
|
||||
for(int64_t i=0;i<1210;i++) {@autoreleasepool {Live(bounded,@[],s,i*40);}}
|
||||
NSDictionary *stats=[bounded cacheStatistics];
|
||||
Check([stats[@"frames"] unsignedIntegerValue]==1200 && [stats[@"estimatedBytes"] unsignedIntegerValue]<=16*1024*1024,@"realtime geometry cache enforces frame and serialized-size bounds");
|
||||
[bounded reset];Check([[bounded cacheStatistics][@"frames"] unsignedIntegerValue]==0,@"realtime reset releases cached geometry");
|
||||
NSData *before=[p trackingDataForTarget:0];
|
||||
PGBeautySettings offline=s;offline.singleFace=1;offline.trackingEnabled=1;offline.selectionX=.25;offline.selectionY=.5;
|
||||
p.detections=@[left];Check([p analyzeImage:image settings:offline time:kCMTimeZero error:&error],@"offline analysis remains available beside live history");
|
||||
NSData *archive=[p trackingDataForTarget:0];
|
||||
p.detections=@[];[p processImage:image settings:s time:CMTimeMake(3,25) error:&error];
|
||||
Check(before==nil && [archive isEqual:[p trackingDataForTarget:0]],@"subsequent realtime hold does not mutate persisted offline geometry");
|
||||
}
|
||||
|
||||
int main(int argc, const char *argv[]) {
|
||||
@autoreleasepool {
|
||||
CIContext *context = [CIContext contextWithOptions:@{kCIContextCacheIntermediates:@NO}];
|
||||
CIImage *image = Gradient();
|
||||
PGBeautySettings settings = PGBeautyDefaults();
|
||||
PGFaceFixture *processor = [[PGFaceFixture alloc] init];
|
||||
VNFaceObservation *left = Face(CGRectMake(0.08, 0.18, 0.36, 0.62));
|
||||
VNFaceObservation *right = Face(CGRectMake(0.64, 0.23, 0.28, 0.53));
|
||||
processor.detections = @[left, right];
|
||||
NSError *error = nil;
|
||||
PGFaceFrame *frame = [processor processImage:image settings:settings time:kCMTimeZero error:&error];
|
||||
Check(frame != nil && error == nil, @"kernels initialize and process finite input");
|
||||
Check(frame.faces.count == 2, @"multi-face mode retains both detections");
|
||||
Check([Pixels(context, frame.image) isEqualToData:Pixels(context, image)], @"zero geometry controls preserve original pixels");
|
||||
Check(Channel(context, frame.skinMask, 1, 1, 0) == 0, @"skin mask excludes background");
|
||||
Check(Channel(context, frame.skinMask, 18, 54, 0) > 150, @"skin mask covers cheek");
|
||||
Check(Channel(context, frame.eyeMask, 23, 71, 0) > 40, @"eye mask includes estimated eye region");
|
||||
Check(Channel(context, frame.teethMask, 33, 42, 0) > 20, @"teeth mask includes inner mouth region");
|
||||
PGBeautySettings unprotected = settings; unprotected.featureProtection = 0;
|
||||
PGFaceFrame *withoutProtection = [processor processImage:image settings:unprotected time:kCMTimeZero error:&error];
|
||||
Check(Channel(context, frame.skinMask, 23, 71, 0) < Channel(context, withoutProtection.skinMask, 23, 71, 0), @"feature protection removes eye from skin smoothing");
|
||||
Check([Pixels(context,frame.colorMask) isEqualToData:Pixels(context,withoutProtection.colorMask)], @"color support remains continuous when detail protection is toggled");
|
||||
Check(Channel(context,frame.colorMask,23,71,0)>Channel(context,frame.skinMask,23,71,0), @"color support retains coverage inside eye protection cutouts");
|
||||
|
||||
settings.singleFace = 1; settings.selectionX = 0.78; settings.selectionY = 0.45;
|
||||
frame = [processor processImage:image settings:settings time:kCMTimeZero error:&error];
|
||||
Check(frame.faces.count == 1 && frame.faces.firstObject.boundingBox.origin.x > 0.5, @"single-face selection uses normalized bottom-left point");
|
||||
settings.selectionX = 0.25; settings.selectionY = 0.5;
|
||||
settings.trackingEnabled = 1; settings.faceJumpGuardRange = 0.5;
|
||||
CMTime first = CMTimeMake(1, 24), second = CMTimeMake(2, 24);
|
||||
Check([processor analyzeImage:image settings:settings time:first error:&error], @"analysis stores first precise frame");
|
||||
processor.detections = @[right];
|
||||
Check([processor analyzeImage:image settings:settings time:second error:&error], @"analysis stores guarded missing-target frame");
|
||||
NSUInteger detectionsBeforeRender = processor.detectionCount;
|
||||
PGFaceFrame *secondFrame = [processor processImage:image settings:settings time:second error:&error];
|
||||
PGFaceFrame *firstFrame = [processor processImage:image settings:settings time:CMTimeMake(2, 48) error:&error];
|
||||
Check(secondFrame.faces.count == 0, @"jump guard rejects distant replacement face");
|
||||
Check(firstFrame.faces.count == 1 && firstFrame.faces.firstObject.boundingBox.origin.x < 0.5, @"reverse render retrieves exact earlier frame");
|
||||
Check(processor.detectionCount == detectionsBeforeRender, @"saved frame lookup does not re-run detection");
|
||||
Check(Channel(context, secondFrame.skinMask, 33, 43, 0) == 0, @"rejected target has black mask");
|
||||
Check([processor analyzeImage:image settings:settings time:CMTimeMake(3, 24) error:&error], @"analysis continues after a lost target");
|
||||
Check([processor processImage:image settings:settings time:CMTimeMake(3, 24) error:&error].faces.count == 0, @"jump guard keeps last valid identity across a missed frame");
|
||||
Check([processor processImage:image settings:settings time:CMTimeMake(7, 48) error:&error].faces.count == 0, @"unsaved frame detects current image with deterministic analyzed identity guard");
|
||||
Check([processor analyzeImage:image settings:settings time:CMTimeMake(3, 1) error:&error], @"analysis tolerates a seek across lost detections");
|
||||
Check([processor processImage:image settings:settings time:CMTimeMake(3, 1) error:&error].faces.count == 0, @"jump guard does not silently reacquire a stranger after a long gap");
|
||||
NSData *archive = [processor trackingDataForTarget:0];
|
||||
Check(archive.length > 0, @"tracking is persistable");
|
||||
PGFaceFixture *restored = [[PGFaceFixture alloc] init];
|
||||
[restored setTrackingData:archive forTarget:0];
|
||||
PGFaceFrame *restoredFrame = [restored processImage:image settings:settings time:first error:&error];
|
||||
Check(restoredFrame.faces.count == 1 && restored.detectionCount == 0, @"restored target serves exact frame without detections");
|
||||
Check([Pixels(context, firstFrame.skinMask) isEqualToData:Pixels(context, restoredFrame.skinMask)], @"restored geometry reproduces feature masks");
|
||||
Check([restored trackingDataForTarget:1] == nil && [restored trackingDataForTarget:2] == nil, @"three target stores are independent");
|
||||
[restored setTrackingData:archive forTarget:2];
|
||||
settings.targetIndex = 2;
|
||||
Check([restored processImage:image settings:settings time:first error:&error].faces.count == 1, @"third target stores and restores separately");
|
||||
settings.targetIndex = 0;
|
||||
[restored processImage:image settings:settings time:CMTimeMake(4, 24) error:&error];
|
||||
Check(restored.detectionCount == 1, @"missing exact time uses stateless detection");
|
||||
[restored setTrackingData:[@"invalid archive" dataUsingEncoding:NSUTF8StringEncoding] forTarget:0];
|
||||
Check([restored trackingDataForTarget:0] == nil, @"invalid archive is rejected without exception");
|
||||
[restored resetTracking];
|
||||
Check([restored trackingDataForTarget:2] == nil, @"reset clears all targets");
|
||||
error = nil;
|
||||
Check(![restored analyzeImage:image settings:settings time:kCMTimeInvalid error:&error] && error != nil, @"analysis rejects invalid times");
|
||||
|
||||
processor.detections = @[Face(CGRectMake(0.2, 0.15, 0.6, 0.7))];
|
||||
settings = PGBeautyDefaults();
|
||||
NSData *base = Pixels(context, image);
|
||||
NSString *names[] = {@"thin", @"big eyes", @"short", @"narrow", @"chin", @"V", @"jaw", @"cheekbone", @"small head"};
|
||||
for (NSUInteger i = 0; i < 9; i++) {
|
||||
PGBeautySettings changed = settings;
|
||||
float *controls[] = {&changed.faceThin, &changed.faceBigEye, &changed.faceShort, &changed.faceNarrow, &changed.faceChin, &changed.faceV, &changed.faceJaw, &changed.faceCheekbone, &changed.faceSmallHead};
|
||||
*controls[i] = 1;
|
||||
PGFaceFrame *warped = [processor processImage:image settings:changed time:kCMTimeZero error:&error];
|
||||
BOOL changedPixels=![Pixels(context, warped.image) isEqualToData:base];
|
||||
BOOL requiresPoints=(i==0 || i==4);
|
||||
Check(warped != nil && (requiresPoints ? !changedPixels : changedPixels), requiresPoints ? @"thin/chin safely skip a face with no indexed landmarks" : [@"geometry control changes pixels: " stringByAppendingString:names[i]]);
|
||||
Check(CGRectEqualToRect(warped.image.extent, image.extent), @"warp preserves image extent");
|
||||
}
|
||||
settings.faceShort = 0.8;
|
||||
NSData *positive = Pixels(context, [processor processImage:image settings:settings time:kCMTimeZero error:&error].image);
|
||||
settings.faceShort = -0.8;
|
||||
Check(![positive isEqualToData:Pixels(context, [processor processImage:image settings:settings time:kCMTimeZero error:&error].image)], @"signed short-face control has distinct directions");
|
||||
settings = PGBeautyDefaults(); settings.showFaceBoxes = 1;
|
||||
frame = [processor processImage:image settings:settings time:kCMTimeZero error:&error];
|
||||
Check(Channel(context, frame.overlayImage, 26, 64, 3) > 0, @"face outlines provide separate final-composite overlay");
|
||||
CIImage *offset = [image imageByApplyingTransform:CGAffineTransformMakeTranslation(30, 40)];
|
||||
frame = [processor processImage:offset settings:settings time:kCMTimeZero error:&error];
|
||||
Check(CGRectEqualToRect(frame.image.extent, offset.extent), @"nonzero image origins survive masks and warp");
|
||||
|
||||
PGFaceProcessor *vision = [[PGFaceProcessor alloc] init];
|
||||
CIImage *blank = [[CIImage imageWithColor:CIColor.grayColor] imageByCroppingToRect:CGRectMake(0, 0, 128, 128)];
|
||||
frame = [vision processImage:blank settings:PGBeautyDefaults() time:kCMTimeZero error:&error];
|
||||
Check(frame != nil && frame.faces.count == 0, @"real Vision request accepts blank image without false faces");
|
||||
|
||||
NSString *fixturePath = argc > 1 ? @(argv[1]) : @"tests/fixtures/astronaut.png";
|
||||
CIImage *portrait = [CIImage imageWithContentsOfURL:[NSURL fileURLWithPath:fixturePath]];
|
||||
Check(portrait != nil, @"public-domain NASA portrait fixture opens");
|
||||
settings = PGBeautyDefaults();
|
||||
PGFaceFrame *portraitFrame = [vision processImage:portrait settings:settings time:kCMTimeZero error:&error];
|
||||
Check(portraitFrame.faces.count > 0, @"real Vision detects astronaut portrait");
|
||||
Check(CGRectEqualToRect(portraitFrame.skinMask.extent, portrait.extent) && CGRectEqualToRect(portraitFrame.teethMask.extent, portrait.extent) && CGRectEqualToRect(portraitFrame.eyeMask.extent, portrait.extent), @"all real portrait masks have finite input extent");
|
||||
VNFaceObservation *actual = portraitFrame.faces.firstObject;
|
||||
Check(actual.landmarks.allPoints.pointCount > 30 && actual.landmarks.leftEye.pointCount > 3, @"real Vision returns detailed facial landmarks");
|
||||
const CGPoint *eyePoints = actual.landmarks.leftEye.normalizedPoints;
|
||||
CGPoint eye = CGPointZero;
|
||||
for (NSUInteger i = 0; i < actual.landmarks.leftEye.pointCount; i++) { eye.x += eyePoints[i].x; eye.y += eyePoints[i].y; }
|
||||
eye.x /= actual.landmarks.leftEye.pointCount; eye.y /= actual.landmarks.leftEye.pointCount;
|
||||
CGRect actualBox = actual.boundingBox;
|
||||
eye.x = (actualBox.origin.x + eye.x * actualBox.size.width) * portrait.extent.size.width;
|
||||
eye.y = (actualBox.origin.y + eye.y * actualBox.size.height) * portrait.extent.size.height;
|
||||
Check(Channel(context, portraitFrame.eyeMask, floor(eye.x), floor(eye.y), 0) > 80, @"landmark-derived eye mask lands on real eye");
|
||||
Check(Channel(context, portraitFrame.skinMask, floor(eye.x), floor(eye.y), 0) < 100, @"real facial feature mask excludes eye from smoothing");
|
||||
settings.faceThin = 0.4; settings.faceBigEye = 0.25; settings.faceChin = 0.15; settings.faceSmallHead = 0.15;
|
||||
settings.faceSmallHeadRange = 0.6; settings.showFaceBoxes = 1;
|
||||
PGFaceFrame *beautified = [vision processImage:portrait settings:settings time:kCMTimeZero error:&error];
|
||||
Check(beautified != nil && ![Pixels(context, beautified.image) isEqualToData:Pixels(context, portrait)], @"real face geometry changes portrait pixels");
|
||||
Check(abs((int)Channel(context, beautified.image, 5, 300, 0) - (int)Channel(context, portrait, 5, 300, 0)) <= 1, @"face warp preserves distant portrait background pixels");
|
||||
settings.trackingEnabled = 1; settings.singleFace = 1;
|
||||
settings.selectionX = CGRectGetMidX(actualBox); settings.selectionY = CGRectGetMidY(actualBox);
|
||||
Check([vision analyzeImage:portrait settings:settings time:first error:&error], @"real landmarks analyze into persistent geometry");
|
||||
NSData *realArchive = [vision trackingDataForTarget:0];
|
||||
PGFaceFrame *analyzed = [vision processImage:portrait settings:settings time:first error:&error];
|
||||
PGFaceProcessor *realRestored = [[PGFaceProcessor alloc] init];
|
||||
[realRestored setTrackingData:realArchive forTarget:0];
|
||||
PGFaceFrame *restoredPortrait = [realRestored processImage:portrait settings:settings time:first error:&error];
|
||||
Check(restoredPortrait.faces.count == analyzed.faces.count && [Pixels(context, restoredPortrait.image) isEqualToData:Pixels(context, analyzed.image)], @"real face warp survives persistence byte-for-byte");
|
||||
Check([Pixels(context, restoredPortrait.eyeMask) isEqualToData:Pixels(context, analyzed.eyeMask)], @"real landmark eye mask survives persistence byte-for-byte");
|
||||
Check([Pixels(context, restoredPortrait.teethMask) isEqualToData:Pixels(context, analyzed.teethMask)], @"real landmark teeth mask survives persistence byte-for-byte");
|
||||
if (argc > 2) {
|
||||
CGFloat width = portrait.extent.size.width, height = portrait.extent.size.height;
|
||||
CIImage *after = [[beautified.overlayImage imageByCompositingOverImage:beautified.image] imageByCroppingToRect:portrait.extent];
|
||||
CGColorSpaceRef space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
||||
CGContextRef canvas = CGBitmapContextCreate(NULL, width * 3, height, 8, width * 3 * 4, space, (CGBitmapInfo)kCGImageAlphaPremultipliedLast);
|
||||
NSArray *panels = @[portrait, after, portraitFrame.skinMask];
|
||||
for (NSUInteger panel = 0; panel < panels.count; panel++) {
|
||||
CGImageRef content = [context createCGImage:panels[panel] fromRect:portrait.extent];
|
||||
CGContextDrawImage(canvas, CGRectMake(panel * width, 0, width, height), content);
|
||||
CGImageRelease(content);
|
||||
}
|
||||
CGImageRef preview = CGBitmapContextCreateImage(canvas);
|
||||
CGImageDestinationRef destination = CGImageDestinationCreateWithURL((__bridge CFURLRef)[NSURL fileURLWithPath:@(argv[2])], CFSTR("public.png"), 1, NULL);
|
||||
Check(destination != NULL && preview != NULL, @"portrait comparison preview allocates");
|
||||
CGImageDestinationAddImage(destination, preview, NULL);
|
||||
Check(CGImageDestinationFinalize(destination), @"portrait comparison preview writes PNG");
|
||||
CFRelease(destination); CGImageRelease(preview);
|
||||
CGContextRelease(canvas); CGColorSpaceRelease(space);
|
||||
}
|
||||
RealtimeChecks(context,image);
|
||||
printf("Face processing: %lu checks passed\n", (unsigned long)PGChecks);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
astronaut.png — NASA photograph of astronaut Eileen Collins.
|
||||
Obtained from the scikit-image v0.24.0 sample-data distribution:
|
||||
https://raw.githubusercontent.com/scikit-image/scikit-image/v0.24.0/skimage/data/astronaut.png
|
||||
The upstream project states that this image has no known copyright restrictions and is in the public domain:
|
||||
https://scikit-image.org/docs/stable/api/skimage.data.html#skimage.data.astronaut
|
||||
Used solely as a local image-processing regression fixture; no endorsement implied.
|
||||
二进制
二进制文件未显示。
|
之后 宽度: | 高度: | 大小: 773 KiB |
@@ -0,0 +1,582 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <FxPlug/FxPlugSDK.h>
|
||||
#import <CoreImage/CoreImage.h>
|
||||
#import <IOSurface/IOSurface.h>
|
||||
#import <Metal/Metal.h>
|
||||
#import <CoreVideo/CoreVideo.h>
|
||||
#import <ImageIO/ImageIO.h>
|
||||
#import "beatygrilPlugIn.h"
|
||||
#import "PGParameterIDs.h"
|
||||
#import "PGFaceProcessor.h"
|
||||
#import "PGBeautyRenderer.h"
|
||||
#include <math.h>
|
||||
|
||||
// This adapter simulates host callbacks, but uses real IOSurface buffers and the
|
||||
// production FxPlug entry points. It does not simulate FCP plugin discovery/UI.
|
||||
@interface PGHostAPI : NSObject <PROAPIAccessing>
|
||||
@property NSMutableDictionary<NSNumber *, id> *values;
|
||||
@property NSMutableDictionary<NSNumber *, NSDictionary *> *definitions;
|
||||
@property NSMutableSet<NSString *> *disabledAPIs;
|
||||
@property NSInteger depth;
|
||||
@property BOOL duplicate;
|
||||
@property UInt32 rejectedParameter;
|
||||
@property CMTime hostTime;
|
||||
@property CMTime lastReadTime;
|
||||
@property CMTime lastSetBoolTime;
|
||||
@property NSUInteger forwardRequests;
|
||||
@property NSUInteger backwardRequests;
|
||||
@property BOOL rejectAnalysis;
|
||||
@property BOOL targetAtCurrentTime;
|
||||
@property UInt32 rejectedWriteParameter;
|
||||
@end
|
||||
|
||||
@implementation PGHostAPI
|
||||
- (instancetype)init {
|
||||
if ((self = [super init])) {
|
||||
_values = [NSMutableDictionary dictionary]; _definitions = [NSMutableDictionary dictionary];
|
||||
_disabledAPIs = [NSMutableSet set]; _hostTime = CMTimeMake(120, 24);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
- (id)apiForProtocol:(Protocol *)protocol {
|
||||
NSString *name = NSStringFromProtocol(protocol);
|
||||
if ([_disabledAPIs containsObject:name]) return nil;
|
||||
if ([@[@"FxParameterCreationAPI_v5", @"FxParameterRetrievalAPI_v6", @"FxParameterSettingAPI_v5", @"FxCustomParameterActionAPI_v4", @"FxAnalysisAPI"] containsObject:name]) return self;
|
||||
return nil;
|
||||
}
|
||||
- (BOOL)define:(UInt32)identifier kind:(NSString *)kind value:(id)value flags:(FxParameterFlags)flags extra:(NSDictionary *)extra {
|
||||
if (_definitions[@(identifier)]) _duplicate = YES;
|
||||
NSMutableDictionary *definition = [@{@"kind":kind, @"flags":@(flags)} mutableCopy];
|
||||
if (extra) [definition addEntriesFromDictionary:extra];
|
||||
_definitions[@(identifier)] = definition;
|
||||
if (value) _values[@(identifier)] = value;
|
||||
return identifier != _rejectedParameter;
|
||||
}
|
||||
- (BOOL)addFloatSliderWithName:(NSString *)name parameterID:(UInt32)identifier defaultValue:(double)value parameterMin:(double)minimum parameterMax:(double)maximum sliderMin:(double)sliderMin sliderMax:(double)sliderMax delta:(double)delta parameterFlags:(FxParameterFlags)flags {
|
||||
return [self define:identifier kind:@"float" value:@(value) flags:flags extra:@{@"name":name, @"min":@(minimum), @"max":@(maximum)}];
|
||||
}
|
||||
- (BOOL)addPercentSliderWithName:(NSString *)name parameterID:(UInt32)identifier defaultValue:(double)value parameterMin:(double)minimum parameterMax:(double)maximum sliderMin:(double)sliderMin sliderMax:(double)sliderMax delta:(double)delta parameterFlags:(FxParameterFlags)flags {
|
||||
return [self define:identifier kind:@"percent" value:@(value) flags:flags extra:@{@"name":name, @"min":@(minimum), @"max":@(maximum)}];
|
||||
}
|
||||
- (BOOL)addToggleButtonWithName:(NSString *)name parameterID:(UInt32)identifier defaultValue:(BOOL)value parameterFlags:(FxParameterFlags)flags {
|
||||
return [self define:identifier kind:@"toggle" value:@(value) flags:flags extra:@{@"name":name}];
|
||||
}
|
||||
- (BOOL)addPopupMenuWithName:(NSString *)name parameterID:(UInt32)identifier defaultValue:(int)value menuEntries:(NSArray *)entries parameterFlags:(FxParameterFlags)flags {
|
||||
return [self define:identifier kind:@"popup" value:@(value) flags:flags extra:@{@"name":name, @"entries":entries}];
|
||||
}
|
||||
- (BOOL)addPointParameterWithName:(NSString *)name parameterID:(UInt32)identifier defaultX:(double)x defaultY:(double)y parameterFlags:(FxParameterFlags)flags {
|
||||
return [self define:identifier kind:@"point" value:@[@(x), @(y)] flags:flags extra:@{@"name":name}];
|
||||
}
|
||||
- (BOOL)addPushButtonWithName:(NSString *)name parameterID:(UInt32)identifier selector:(SEL)selector parameterFlags:(FxParameterFlags)flags {
|
||||
return [self define:identifier kind:@"button" value:nil flags:flags extra:@{@"name":name, @"selector":NSStringFromSelector(selector)}];
|
||||
}
|
||||
- (BOOL)addStringParameterWithName:(NSString *)name parameterID:(UInt32)identifier defaultValue:(NSString *)value parameterFlags:(FxParameterFlags)flags {
|
||||
return [self define:identifier kind:@"string" value:value flags:flags extra:@{@"name":name}];
|
||||
}
|
||||
- (BOOL)startParameterSubGroup:(NSString *)name parameterID:(UInt32)identifier parameterFlags:(FxParameterFlags)flags {
|
||||
_depth++;
|
||||
return [self define:identifier kind:@"group" value:nil flags:flags extra:@{@"name":name}];
|
||||
}
|
||||
- (BOOL)endParameterSubGroup { _depth--; return _depth >= 0; }
|
||||
- (BOOL)getFloatValue:(double *)value fromParameter:(UInt32)identifier atTime:(CMTime)time {
|
||||
_lastReadTime = time; id object = _values[@(identifier)];
|
||||
if (![object isKindOfClass:NSNumber.class]) return NO; *value = [object doubleValue]; return YES;
|
||||
}
|
||||
- (BOOL)getIntValue:(int *)value fromParameter:(UInt32)identifier atTime:(CMTime)time {
|
||||
_lastReadTime = time;
|
||||
if (identifier == PGTarget && _targetAtCurrentTime) { *value = CMTimeCompare(time, _hostTime) == 0 ? 2 : 0; return YES; }
|
||||
id object = _values[@(identifier)]; if (![object isKindOfClass:NSNumber.class]) return NO; *value = [object intValue]; return YES;
|
||||
}
|
||||
- (BOOL)getBoolValue:(BOOL *)value fromParameter:(UInt32)identifier atTime:(CMTime)time {
|
||||
_lastReadTime = time; id object = _values[@(identifier)];
|
||||
if (![object isKindOfClass:NSNumber.class]) return NO; *value = [object boolValue]; return YES;
|
||||
}
|
||||
- (BOOL)getXValue:(double *)x YValue:(double *)y fromParameter:(UInt32)identifier atTime:(CMTime)time {
|
||||
_lastReadTime = time; NSArray *point = _values[@(identifier)];
|
||||
if (![point isKindOfClass:NSArray.class] || point.count != 2) return NO;
|
||||
*x = [point[0] doubleValue]; *y = [point[1] doubleValue]; return YES;
|
||||
}
|
||||
- (BOOL)getStringParameterValue:(NSString **)value fromParameter:(UInt32)identifier {
|
||||
id object = _values[@(identifier)]; if (![object isKindOfClass:NSString.class]) return NO; *value = object; return YES;
|
||||
}
|
||||
- (BOOL)setStringParameterValue:(NSString *)value toParameter:(UInt32)identifier {
|
||||
if (identifier == _rejectedWriteParameter) return NO;
|
||||
_values[@(identifier)] = value; return YES;
|
||||
}
|
||||
- (BOOL)setBoolValue:(BOOL)value toParameter:(UInt32)identifier atTime:(CMTime)time { _lastSetBoolTime = time; _values[@(identifier)] = @(value); return YES; }
|
||||
- (CMTime)currentTime { return _hostTime; }
|
||||
- (BOOL)startForwardAnalysis:(FxAnalysisLocation)location error:(NSError **)error {
|
||||
_forwardRequests++; return !_rejectAnalysis;
|
||||
}
|
||||
- (BOOL)startBackwardAnalysis:(FxAnalysisLocation)location error:(NSError **)error {
|
||||
_backwardRequests++; return !_rejectAnalysis;
|
||||
}
|
||||
@end
|
||||
|
||||
@interface PGHostTile : FxImageTile
|
||||
@property (strong) IOSurface *storage;
|
||||
@property FxRect pixelBounds;
|
||||
@property FxRect fullBounds;
|
||||
@property FxImageOrigin origin;
|
||||
@property uint64_t registry;
|
||||
@property CGColorSpaceRef space;
|
||||
@end
|
||||
@implementation PGHostTile
|
||||
- (IOSurface *)ioSurface { return _storage; }
|
||||
- (FxRect)tilePixelBounds { return _pixelBounds; }
|
||||
- (FxRect)imagePixelBounds { return _fullBounds; }
|
||||
- (FxImageOrigin)imageOrigin { return _origin; }
|
||||
- (uint64_t)deviceRegistryID { return _registry; }
|
||||
- (CGColorSpaceRef)colorSpace { return _space; }
|
||||
- (void)dealloc { if (_space) CGColorSpaceRelease(_space); }
|
||||
@end
|
||||
|
||||
static NSUInteger Checks = 0, Failures = 0;
|
||||
static BOOL RectEqual(FxRect a, FxRect b) { return a.left == b.left && a.bottom == b.bottom && a.right == b.right && a.top == b.top; }
|
||||
static void Check(BOOL condition, NSString *description) {
|
||||
Checks++;
|
||||
if (!condition) { Failures++; fprintf(stderr, "FAIL: %s\n", description.UTF8String); }
|
||||
else if (getenv("PAINTFCP_HOST_TEST_VERBOSE")) fprintf(stdout, "PASS: %s\n", description.UTF8String);
|
||||
}
|
||||
static PGHostTile *Tile(NSUInteger width, NSUInteger height, NSInteger left, NSInteger bottom, FxImageOrigin origin) {
|
||||
IOSurfaceRef surface = IOSurfaceCreate((__bridge CFDictionaryRef)@{
|
||||
(__bridge id)kIOSurfaceWidth:@(width), (__bridge id)kIOSurfaceHeight:@(height),
|
||||
(__bridge id)kIOSurfaceBytesPerElement:@4, (__bridge id)kIOSurfaceBytesPerRow:@(width * 4),
|
||||
(__bridge id)kIOSurfacePixelFormat:@(kCVPixelFormatType_32BGRA),
|
||||
(__bridge id)kIOSurfaceAllocSize:@(width * height * 4)});
|
||||
PGHostTile *tile = [PGHostTile new]; tile.storage = CFBridgingRelease(surface);
|
||||
tile.pixelBounds = (FxRect){(SInt32)left, (SInt32)bottom, (SInt32)(left + width), (SInt32)(bottom + height)};
|
||||
tile.fullBounds = tile.pixelBounds; tile.origin = origin;
|
||||
tile.registry = MTLCreateSystemDefaultDevice().registryID;
|
||||
tile.space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
||||
return tile;
|
||||
}
|
||||
static void Fill(PGHostTile *tile) {
|
||||
IOSurfaceRef surface = (__bridge IOSurfaceRef)tile.storage;
|
||||
IOSurfaceLock(surface, 0, NULL);
|
||||
unsigned char *bytes = IOSurfaceGetBaseAddress(surface); size_t row = IOSurfaceGetBytesPerRow(surface);
|
||||
for (size_t y = 0; y < IOSurfaceGetHeight(surface); y++) for (size_t x = 0; x < IOSurfaceGetWidth(surface); x++) {
|
||||
unsigned char *p = bytes + y * row + x * 4;
|
||||
p[0] = 20 + x % 150; p[1] = 35 + y % 170; p[2] = 45 + (x * 3 + y * 5) % 180; p[3] = 255;
|
||||
}
|
||||
IOSurfaceUnlock(surface, 0, NULL);
|
||||
}
|
||||
static NSData *SurfaceBytes(PGHostTile *tile) {
|
||||
IOSurfaceRef surface = (__bridge IOSurfaceRef)tile.storage;
|
||||
IOSurfaceLock(surface, kIOSurfaceLockReadOnly, NULL);
|
||||
size_t width = IOSurfaceGetWidth(surface), height = IOSurfaceGetHeight(surface), row = IOSurfaceGetBytesPerRow(surface);
|
||||
NSMutableData *result = [NSMutableData dataWithLength:width * height * 4];
|
||||
for (size_t y = 0; y < height; y++) memcpy((unsigned char *)result.mutableBytes + y * width * 4, (unsigned char *)IOSurfaceGetBaseAddress(surface) + y * row, width * 4);
|
||||
IOSurfaceUnlock(surface, kIOSurfaceLockReadOnly, NULL);
|
||||
return result;
|
||||
}
|
||||
static NSUInteger MaximumDifference(NSData *a, NSData *b) {
|
||||
if (a.length != b.length) return NSUIntegerMax;
|
||||
const unsigned char *x = a.bytes, *y = b.bytes; NSUInteger maximum = 0;
|
||||
for (NSUInteger i = 0; i < a.length; i++) maximum = MAX(maximum, (NSUInteger)abs(x[i] - y[i]));
|
||||
return maximum;
|
||||
}
|
||||
static double MeanDifference(NSData *a, NSData *b) {
|
||||
if (!a.length || a.length != b.length) return INFINITY;
|
||||
const uint8_t *x = a.bytes, *y = b.bytes; double total = 0;
|
||||
for (NSUInteger i = 0; i < a.length; i++) total += abs(x[i]-y[i]);
|
||||
return total/a.length;
|
||||
}
|
||||
static NSData *ExpectedCrop(PGHostTile *source, NSUInteger x, NSUInteger y, NSUInteger width, NSUInteger height, BOOL flip) {
|
||||
NSData *sourceBytes = SurfaceBytes(source); size_t sourceWidth = IOSurfaceGetWidth((__bridge IOSurfaceRef)source.storage);
|
||||
NSMutableData *result = [NSMutableData dataWithLength:width * height * 4];
|
||||
for (NSUInteger row = 0; row < height; row++) {
|
||||
NSUInteger sourceRow = flip ? IOSurfaceGetHeight((__bridge IOSurfaceRef)source.storage) - 1 - (row + y) : row + y;
|
||||
memcpy((unsigned char *)result.mutableBytes + row * width * 4, (const unsigned char *)sourceBytes.bytes + (sourceRow * sourceWidth + x) * 4, width * 4);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
static NSData *Snapshot(beatygrilPlugIn *plugin, CMTime time) {
|
||||
NSData *state = nil; NSError *error = nil;
|
||||
Check([plugin pluginState:&state atTime:time quality:kFxQuality_HIGH error:&error] && state.length > 0 && !error, @"plugin snapshot serializes through host parameter API");
|
||||
return state;
|
||||
}
|
||||
static BOOL Render(beatygrilPlugIn *plugin, PGHostTile *destination, NSArray *sources, NSData *state, CMTime time, NSError **error, BOOL *escaped) {
|
||||
@try { return [plugin renderDestinationImage:destination sourceImages:sources pluginState:state atTime:time error:error]; }
|
||||
@catch (NSException *exception) {
|
||||
if (escaped) *escaped = YES;
|
||||
fprintf(stderr, "EXCEPTION: %s\n", exception.reason.UTF8String); return NO;
|
||||
}
|
||||
}
|
||||
static void RejectState(beatygrilPlugIn *plugin, PGHostTile *destination, PGHostTile *source, NSData *state, NSString *description) {
|
||||
NSError *error = nil; BOOL escaped = NO;
|
||||
BOOL rendered = Render(plugin, destination, @[source], state, kCMTimeZero, &error, &escaped);
|
||||
Check(!rendered && !escaped && error != nil, description);
|
||||
}
|
||||
static NSData *PlistState(id value) { return [NSPropertyListSerialization dataWithPropertyList:value format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil]; }
|
||||
// Decode independently of CI/IOSurface, so a pair of adapter flips cannot
|
||||
// silently cancel in our test input and expected output. ImageIO + Quartz gives
|
||||
// the NASA fixture top-first BGRA bytes, also cross-checked against Pillow.
|
||||
static NSData *DecodedPortrait(NSString *path, size_t *width, size_t *height) {
|
||||
CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)[NSURL fileURLWithPath:path], NULL);
|
||||
CGImageRef image = source ? CGImageSourceCreateImageAtIndex(source, 0, NULL) : NULL;
|
||||
if (source) CFRelease(source);
|
||||
if (!image) return nil;
|
||||
*width = CGImageGetWidth(image); *height = CGImageGetHeight(image);
|
||||
NSMutableData *bytes = [NSMutableData dataWithLength:*width * *height * 4];
|
||||
CGColorSpaceRef space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
||||
CGContextRef context = CGBitmapContextCreate(bytes.mutableBytes, *width, *height, 8, *width * 4, space,
|
||||
kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
|
||||
if (context) {
|
||||
CGContextSetBlendMode(context, kCGBlendModeCopy);
|
||||
CGContextSetInterpolationQuality(context, kCGInterpolationNone);
|
||||
CGContextDrawImage(context, CGRectMake(0, 0, *width, *height), image);
|
||||
CGContextRelease(context);
|
||||
}
|
||||
CGColorSpaceRelease(space); CGImageRelease(image);
|
||||
return context ? bytes : nil;
|
||||
}
|
||||
static BOOL StoreSurfaceBytes(PGHostTile *tile, NSData *bytes) {
|
||||
IOSurfaceRef surface = (__bridge IOSurfaceRef)tile.storage;
|
||||
size_t width = IOSurfaceGetWidth(surface), height = IOSurfaceGetHeight(surface);
|
||||
if (bytes.length != width * height * 4) return NO;
|
||||
IOSurfaceLock(surface, 0, NULL);
|
||||
for (size_t row = 0; row < height; row++)
|
||||
memcpy((uint8_t *)IOSurfaceGetBaseAddress(surface) + row * IOSurfaceGetBytesPerRow(surface),
|
||||
(const uint8_t *)bytes.bytes + row * width * 4, width * 4);
|
||||
IOSurfaceUnlock(surface, 0, NULL);
|
||||
return YES;
|
||||
}
|
||||
static CIContext *NumericContext(void) {
|
||||
return [CIContext contextWithMTLDevice:MTLCreateSystemDefaultDevice() options:@{
|
||||
kCIContextWorkingColorSpace:NSNull.null, kCIContextOutputColorSpace:NSNull.null}];
|
||||
}
|
||||
static NSData *ImageBytes(CIContext *context, CIImage *image) {
|
||||
if (!image) return nil;
|
||||
size_t width = (size_t)image.extent.size.width, height = (size_t)image.extent.size.height;
|
||||
NSMutableData *bytes = [NSMutableData dataWithLength:width * height * 4];
|
||||
[context render:image toBitmap:bytes.mutableBytes rowBytes:width * 4 bounds:image.extent format:kCIFormatBGRA8 colorSpace:nil];
|
||||
return bytes;
|
||||
}
|
||||
static CIImage *ProcessedReference(CIImage *image, PGBeautySettings settings, CMTime time, NSData *track) {
|
||||
PGFaceProcessor *faces = [PGFaceProcessor new];
|
||||
if (track) [faces setTrackingData:track forTarget:settings.targetIndex];
|
||||
NSError *error = nil;
|
||||
PGFaceFrame *frame = [faces processImage:image settings:settings time:time error:&error];
|
||||
if (!frame) return nil;
|
||||
return [[PGBeautyRenderer new] processImage:frame.image settings:settings skinMask:frame.skinMask
|
||||
colorMask:frame.colorMask teethMask:frame.teethMask eyeMask:frame.eyeMask error:&error];
|
||||
}
|
||||
static CGPoint LandmarkCenter(VNFaceLandmarkRegion2D *region, CGRect box, CGSize size) {
|
||||
CGPoint center = CGPointZero;
|
||||
if (!region.pointCount || !region.normalizedPoints) return center;
|
||||
for (NSUInteger i = 0; i < region.pointCount; i++) {
|
||||
center.x += region.normalizedPoints[i].x; center.y += region.normalizedPoints[i].y;
|
||||
}
|
||||
center.x /= region.pointCount; center.y /= region.pointCount;
|
||||
return CGPointMake((box.origin.x + center.x * box.size.width) * size.width,
|
||||
(1 - box.origin.y - center.y * box.size.height) * size.height);
|
||||
}
|
||||
static double ChangeMass(NSData *baseline, NSData *changed, size_t width, CGRect region) {
|
||||
if (baseline.length != changed.length || !width) return INFINITY;
|
||||
const uint8_t *a = baseline.bytes, *b = changed.bytes;
|
||||
size_t height = baseline.length / (width * 4); double total = 0;
|
||||
for (size_t row = 0; row < height; row++) for (size_t x = 0; x < width; x++)
|
||||
if (CGRectContainsPoint(region, CGPointMake(x + .5, row + .5))) {
|
||||
size_t i = (row * width + x) * 4;
|
||||
for (size_t c = 0; c < 3; c++) total += abs(a[i+c] - b[i+c]);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
static void PortraitAnatomyTests(NSString *fixture, CIImage *portrait) {
|
||||
size_t width = 0, height = 0;
|
||||
NSData *canonical = DecodedPortrait(fixture, &width, &height);
|
||||
Check(canonical != nil && width == 512 && height == 512, @"independent ImageIO/Quartz decodes the 512px NASA fixture");
|
||||
if (!canonical || width != 512 || height != 512 || !portrait) return;
|
||||
// Asymmetric samples pin the decoder's physical row/column order. These
|
||||
// values match Pillow's RGB decode exactly, independently of Core Image.
|
||||
const size_t positions[][2] = {{16,16},{100,250},{205,100},{222,178},{480,450}};
|
||||
const uint8_t samples[][4] = {{35,6,14,255},{93,132,234,255},{11,35,58,255},{42,73,102,255},{2,1,3,255}};
|
||||
BOOL samplesMatch = YES;
|
||||
for (size_t i = 0; i < 5; i++)
|
||||
samplesMatch &= memcmp((const uint8_t *)canonical.bytes + (positions[i][1]*width + positions[i][0])*4, samples[i], 4) == 0;
|
||||
Check(samplesMatch, @"independent asymmetric pixels establish top-first BGRA and the horizontal direction");
|
||||
CIContext *context = NumericContext();
|
||||
Check(MaximumDifference(canonical, ImageBytes(context, portrait)) <= 1, @"upright CI file decode agrees with independent physical source bytes");
|
||||
PGFaceFrame *detected = [[PGFaceProcessor new] processImage:portrait settings:PGBeautyDefaults() time:kCMTimeZero error:nil];
|
||||
VNFaceObservation *face = detected.faces.firstObject;
|
||||
Check(face.landmarks.allPoints.pointCount == 76, @"real portrait provides the 76-point anatomy used by chin pins");
|
||||
if (face.landmarks.allPoints.pointCount != 76) return;
|
||||
CGSize size = CGSizeMake(width, height); CGRect box = face.boundingBox;
|
||||
CGPoint left = LandmarkCenter(face.landmarks.leftEye, box, size), right = LandmarkCenter(face.landmarks.rightEye, box, size);
|
||||
CGPoint chin = face.landmarks.allPoints.normalizedPoints[67];
|
||||
chin = CGPointMake((box.origin.x + chin.x * box.size.width)*width, (1-box.origin.y-chin.y*box.size.height)*height);
|
||||
CGFloat fw = box.size.width*width, fh = box.size.height*height;
|
||||
CGRect leftEye = CGRectMake(left.x-fw*.24, left.y-fh*.17, fw*.48, fh*.34);
|
||||
CGRect rightEye = CGRectMake(right.x-fw*.24, right.y-fh*.17, fw*.48, fh*.34);
|
||||
CGRect chinArea = CGRectMake(chin.x-fw*.30, chin.y-fh*.22, fw*.60, fh*.44);
|
||||
CGRect upsideDownFace = CGRectMake(box.origin.x*width, box.origin.y*height, fw, fh);
|
||||
const UInt32 controls[] = {0, PGBigEye, PGChin, PGChin};
|
||||
const double values[] = {0, 1, 1, -1};
|
||||
NSArray<NSString *> *names = @[@"default", @"big-eye", @"chin-positive", @"chin-negative"];
|
||||
for (NSUInteger effect = 0; effect < 4; effect++) {
|
||||
PGBeautySettings settings = PGBeautyDefaults();
|
||||
if (effect == 1) settings.faceBigEye = 1;
|
||||
else if (effect > 1) settings.faceChin = values[effect];
|
||||
NSData *expected = ImageBytes(context, ProcessedReference(portrait, settings, kCMTimeZero, nil));
|
||||
Check(expected.length == canonical.length, [names[effect] stringByAppendingString:@" independent upright face/beauty chain renders"]);
|
||||
if (expected.length != canonical.length) continue;
|
||||
NSData *originBaseline = nil;
|
||||
for (NSUInteger sourceTop = 0; sourceTop < 2; sourceTop++) for (NSUInteger destinationTop = 0; destinationTop < 2; destinationTop++) {
|
||||
@autoreleasepool {
|
||||
PGHostAPI *api = [PGHostAPI new]; beatygrilPlugIn *plugin = [[beatygrilPlugIn alloc] initWithAPIManager:api];
|
||||
Check([plugin addParametersWithError:nil], @"anatomy case creates the real plugin parameters");
|
||||
if (effect) api.values[@(controls[effect])] = @(values[effect]);
|
||||
PGHostTile *input = Tile(width, height, 37, -21, sourceTop ? kFxImageOrigin_TOP_LEFT : kFxImageOrigin_BOTTOM_LEFT);
|
||||
PGHostTile *output = Tile(width, height, 37, -21, destinationTop ? kFxImageOrigin_TOP_LEFT : kFxImageOrigin_BOTTOM_LEFT);
|
||||
// imageOrigin changes coordinate semantics, never these bytes.
|
||||
Check(StoreSurfaceBytes(input, canonical), @"all origin combinations receive identical independently decoded physical rows");
|
||||
Check(Render(plugin, output, @[input], Snapshot(plugin, kCMTimeZero), kCMTimeZero, nil, NULL), @"active anatomy case renders with nonzero tile origin");
|
||||
NSData *actual = SurfaceBytes(output); NSUInteger difference = MaximumDifference(actual, expected);
|
||||
NSString *label = [NSString stringWithFormat:@"%@ source%@/destination%@", names[effect], sourceTop?@"TOP":@"BOTTOM", destinationTop?@"TOP":@"BOTTOM"];
|
||||
// Vision's file-image vs IOSurface path and translated float
|
||||
// coordinates can round by a few byte levels at high contrast.
|
||||
// Bound that error tightly; changing only origin tags must be
|
||||
// exactly invariant, independently of this reference tolerance.
|
||||
Check(difference <= 4 && MeanDifference(actual,expected) < .005, [label stringByAppendingString:@" matches independent upright CI output"]);
|
||||
if (!originBaseline) originBaseline = actual;
|
||||
Check(MaximumDifference(actual,originBaseline) == 0, [label stringByAppendingString:@" is byte-identical across coordinate tags"]);
|
||||
double lm = ChangeMass(canonical, actual, width, leftEye), rm = ChangeMass(canonical, actual, width, rightEye);
|
||||
double cm = ChangeMass(canonical, actual, width, chinArea), um = ChangeMass(canonical, actual, width, upsideDownFace);
|
||||
BOOL located = effect == 0 ? MaximumDifference(canonical, actual) <= 1 :
|
||||
(effect == 1 ? lm > 100 && rm > 100 && cm < (lm+rm)*.01 && um == 0 :
|
||||
cm > 100 && cm > 4*(lm+rm) && um == 0);
|
||||
Check(located, [label stringByAppendingString:effect == 1 ? @" changes the two real eye regions" : (effect ? @" changes the real chin rather than eyes or mirrored anatomy" : @" preserves the independently decoded photo")]);
|
||||
printf("Anatomy %-13s %lu/%lu max=%lu mean=%.7f eye=%.0f/%.0f chin=%.0f inverted=%.0f\n", names[effect].UTF8String,
|
||||
(unsigned long)sourceTop, (unsigned long)destinationTop, (unsigned long)difference, MeanDifference(actual,expected), lm, rm, cm, um);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
static void TrackingCoordinateTests(NSDictionary *archive, NSDictionary *hostValues, PGHostTile *source, CIImage *portrait, CMTime time) {
|
||||
Check([archive[@"hostCoordinateSpace"] isEqual:@"iosurface-ci-y-up-v1"], @"new host analysis stamps its explicit IOSurface/CI coordinate contract");
|
||||
NSMutableDictionary *forged = [NSPropertyListSerialization propertyListWithData:PlistState(archive)
|
||||
options:NSPropertyListMutableContainers format:nil error:nil];
|
||||
if (![forged[@"frames"] isKindOfClass:NSDictionary.class]) { Check(NO, @"analysis archive supplies frames for compatibility regression"); return; }
|
||||
// Retain fully valid topology but move the saved face away from the actual
|
||||
// portrait. A falsely replayed legacy archive must produce a visible error.
|
||||
for (NSMutableDictionary *frame in [forged[@"frames"] allValues]) for (NSMutableDictionary *face in frame[@"faces"]) {
|
||||
NSMutableArray *box = [face[@"box"] mutableCopy]; box[0] = @([box[0] doubleValue]+.2); box[1] = @([box[1] doubleValue]-.25); face[@"box"] = box;
|
||||
for (NSString *name in @[@"leftEye",@"rightEye",@"leftBrow",@"rightBrow",@"nose",@"lips",@"mouth",@"contour",@"points76"]) {
|
||||
if (!face[name]) continue;
|
||||
NSMutableArray *points = [NSMutableArray new];
|
||||
for (NSArray *point in face[name]) [points addObject:@[@([point[0] doubleValue]+.2),@([point[1] doubleValue]-.25)]];
|
||||
face[name] = points;
|
||||
}
|
||||
}
|
||||
[forged removeObjectForKey:@"hostCoordinateSpace"];
|
||||
NSString *legacyString = [PlistState(forged) base64EncodedStringWithOptions:0];
|
||||
NSMutableDictionary *values = [hostValues mutableCopy];
|
||||
values[@(PGTarget)] = @2; values[@(PGTrackEnabled)] = @YES; values[@(PGBigEye)] = @1; values[@(PGChin)] = @1;
|
||||
values[@(PGTrackData3)] = legacyString;
|
||||
PGHostAPI *legacyAPI = [PGHostAPI new]; legacyAPI.hostTime = time;
|
||||
beatygrilPlugIn *legacy = [[beatygrilPlugIn alloc] initWithAPIManager:legacyAPI]; [legacy addParametersWithError:nil]; legacyAPI.values = [values mutableCopy];
|
||||
PGHostTile *output = Tile(512,512,0,0,kFxImageOrigin_TOP_LEFT);
|
||||
Check(Render(legacy, output, @[source], Snapshot(legacy,time), time, nil, NULL), @"unmarked old coordinate archive renders through the real adapter");
|
||||
NSData *legacyBytes = SurfaceBytes(output);
|
||||
PGBeautySettings realtime = [legacy settingsAtTime:time]; realtime.trackingEnabled = 0;
|
||||
CIContext *context = NumericContext();
|
||||
NSData *expectedLive = ImageBytes(context, ProcessedReference(portrait,realtime,time,nil));
|
||||
printf("Legacy fallback upright comparison: max=%lu mean=%.7f\n",(unsigned long)MaximumDifference(legacyBytes,expectedLive),MeanDifference(legacyBytes,expectedLive));
|
||||
Check(expectedLive.length && MaximumDifference(legacyBytes,expectedLive) <= 4 && MeanDifference(legacyBytes,expectedLive) < .005, @"unmarked forged face geometry falls back to realtime upright detection");
|
||||
Check([legacyAPI.values[@(PGTrackData3)] isEqualToString:legacyString] && [legacyAPI.values[@(PGTrackEnabled)] boolValue], @"legacy fallback preserves the user's original archive string and enabled parameter");
|
||||
|
||||
forged[@"hostCoordinateSpace"] = @"iosurface-ci-y-up-v1";
|
||||
NSData *trustedTrack = PlistState(forged);
|
||||
PGHostAPI *trustedAPI = [PGHostAPI new]; trustedAPI.hostTime = time;
|
||||
beatygrilPlugIn *trusted = [[beatygrilPlugIn alloc] initWithAPIManager:trustedAPI]; [trusted addParametersWithError:nil];
|
||||
trustedAPI.values = [values mutableCopy]; trustedAPI.values[@(PGTrackData3)] = [trustedTrack base64EncodedStringWithOptions:0];
|
||||
Check(Render(trusted, output, @[source], Snapshot(trusted,time), time, nil, NULL), @"fresh plugin restores a marked coordinate archive");
|
||||
NSData *trustedBytes = SurfaceBytes(output);
|
||||
PGBeautySettings tracked = [trusted settingsAtTime:time];
|
||||
NSData *expectedSaved = ImageBytes(context, ProcessedReference(portrait,tracked,time,trustedTrack));
|
||||
Check(expectedSaved.length && MaximumDifference(trustedBytes,expectedSaved) <= 1, @"marked saved geometry matches independent exact-time archive replay");
|
||||
Check(MaximumDifference(trustedBytes,expectedLive) > 5, @"forged marked geometry is observably different, making the legacy non-replay assertion meaningful");
|
||||
|
||||
// Reanalysis of an unmarked target starts a fresh compatible archive.
|
||||
[legacy analyzeForward];
|
||||
CMTime next = CMTimeAdd(time,CMTimeMake(10,24));
|
||||
[legacy setupAnalysisForTimeRange:CMTimeRangeMake(time,CMTimeMake(240,24)) frameDuration:CMTimeMake(1,24) error:nil];
|
||||
BOOL analyzed = [legacy analyzeFrame:source atTime:next error:nil] && [legacy cleanupAnalysis:nil];
|
||||
NSData *resaved = [[NSData alloc] initWithBase64EncodedString:legacyAPI.values[@(PGTrackData3)] options:0];
|
||||
NSDictionary *fresh = resaved ? [NSPropertyListSerialization propertyListWithData:resaved options:0 format:nil error:nil] : nil;
|
||||
Check(analyzed && [fresh[@"frames"] count] == 1 && [fresh[@"hostCoordinateSpace"] isEqual:@"iosurface-ci-y-up-v1"], @"reanalysis replaces incompatible geometry with one newly marked frame");
|
||||
}
|
||||
static void WritePortrait(PGHostTile *tile, CIImage *image) {
|
||||
CIContext *context = [CIContext contextWithOptions:@{kCIContextWorkingColorSpace:NSNull.null, kCIContextOutputColorSpace:NSNull.null}];
|
||||
[context render:image toIOSurface:(__bridge IOSurfaceRef)tile.storage bounds:image.extent colorSpace:nil];
|
||||
}
|
||||
static BOOL Preview(PGHostTile *before, PGHostTile *after, NSString *path) {
|
||||
CIContext *context = [CIContext contextWithOptions:@{kCIContextWorkingColorSpace:NSNull.null, kCIContextOutputColorSpace:NSNull.null}];
|
||||
size_t width = IOSurfaceGetWidth((__bridge IOSurfaceRef)before.storage), height = IOSurfaceGetHeight((__bridge IOSurfaceRef)before.storage);
|
||||
CGColorSpaceRef space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
||||
CGContextRef canvas = CGBitmapContextCreate(NULL, width * 2, height, 8, width * 2 * 4, space, (CGBitmapInfo)kCGImageAlphaPremultipliedLast);
|
||||
NSArray *tiles = @[before, after];
|
||||
for (NSUInteger index = 0; index < 2; index++) {
|
||||
PGHostTile *tile = tiles[index];
|
||||
CIImage *image = [CIImage imageWithIOSurface:(__bridge IOSurfaceRef)tile.storage options:@{kCIImageColorSpace:NSNull.null}];
|
||||
CGImageRef panel = [context createCGImage:image fromRect:CGRectMake(0, 0, width, height)];
|
||||
CGContextDrawImage(canvas, CGRectMake(width * index, 0, width, height), panel); CGImageRelease(panel);
|
||||
}
|
||||
CGImageRef output = CGBitmapContextCreateImage(canvas);
|
||||
CGImageDestinationRef destination = CGImageDestinationCreateWithURL((__bridge CFURLRef)[NSURL fileURLWithPath:path], CFSTR("public.png"), 1, NULL);
|
||||
BOOL ok = NO;
|
||||
if (destination && output) { CGImageDestinationAddImage(destination, output, NULL); ok = CGImageDestinationFinalize(destination); }
|
||||
if (destination) CFRelease(destination); CGImageRelease(output); CGContextRelease(canvas); CGColorSpaceRelease(space);
|
||||
return ok;
|
||||
}
|
||||
|
||||
int main(int argc, const char *argv[]) {
|
||||
setbuf(stdout, NULL);
|
||||
@autoreleasepool {
|
||||
PGHostAPI *api = [PGHostAPI new]; beatygrilPlugIn *plugin = [[beatygrilPlugIn alloc] initWithAPIManager:api];
|
||||
NSError *error = nil;
|
||||
Check([plugin addParametersWithError:&error] && !error, @"host creates complete parameter set");
|
||||
UInt32 identifiers[] = {PGAmount,PGBrightness,PGRadius,PGSharpness,PGMix,PGThreshold,PGSoftness,PGEdgeProtect,PGShowMask,PGMidtone,PGHue,PGSaturation,PGHighPass,PGThin,PGFaceBoxes,PGSingleFace,PGSelection,PGShort,PGNarrow,PGChin,PGVFace,PGJaw,PGCheekbone,PGSmallHead,PGSmallHeadRange,PGFeatureProtect,PGTeeth,PGEyeBright,PGBigEye,PGTrackBoxes,PGFastMulti,PGJumpGuard,PGEvenTone,PGTonePreset,PGInvertMask,PGDebugChannel,PGTrackEnabled,PGTarget,PGAnalyzeForward,PGAnalyzeBackward,PGClearTrack,PGStatus,PGTrackData1,PGTrackData2,PGTrackData3,PGSmoothingEnabled,PGColorEnabled,PGShapeEnabled,PGDetailEnabled};
|
||||
for (NSUInteger i = 0; i < sizeof(identifiers) / sizeof(UInt32); i++) Check(api.definitions[@(identifiers[i])] != nil, [NSString stringWithFormat:@"required parameter %u exists", identifiers[i]]);
|
||||
Check(api.depth == 0 && !api.duplicate && api.definitions.count == sizeof(identifiers) / sizeof(UInt32) + 6, @"parameter groups close and IDs are unique");
|
||||
Check([api.values[@(PGMidtone)] doubleValue] == 0 && [api.values[@(PGMix)] doubleValue] == 1, @"default detail and mix form identity filter");
|
||||
Check([api.definitions[@(PGShort)][@"min"] doubleValue] == -1 && [api.definitions[@(PGShort)][@"max"] doubleValue] == 1, @"signed face control exposes both directions");
|
||||
Check([api.definitions[@(PGTrackData1)][@"flags"] unsignedLongLongValue] & kFxParameterFlag_HIDDEN, @"persistent tracking data stays hidden in host UI");
|
||||
NSDictionary *defaults = [api.values copy];
|
||||
NSDictionary *properties = nil;
|
||||
Check([plugin properties:&properties error:&error] && [properties[kFxPropertyKey_DesiredProcessingColorInfo] intValue] == kFxImageColorInfo_RGB_GAMMA_VIDEO, @"adapter requests host gamma-video samples");
|
||||
NSData *state = Snapshot(plugin, kCMTimeZero);
|
||||
NSDictionary *decoded = [NSPropertyListSerialization propertyListWithData:state options:0 format:nil error:nil];
|
||||
Check([decoded[@"tracks"] count] == 3 && [decoded[@"settings"] length] == sizeof(PGBeautySettings), @"snapshot contains fixed settings schema and three target slots");
|
||||
PGBeautySettings settings; [decoded[@"settings"] getBytes:&settings length:sizeof(settings)];
|
||||
Check(settings.version == 1 && settings.mix == 1 && settings.midtoneDetail == 0, @"snapshot round-trips default numeric state");
|
||||
api.values[@(PGThin)] = @5; api.values[@(PGRadius)] = @(-9); api.values[@(PGSelection)] = @[@(-1), @2];
|
||||
settings = [plugin settingsAtTime:api.hostTime];
|
||||
Check(settings.faceThin == 1 && settings.radius == 1 && settings.selectionX == 0 && settings.selectionY == 1, @"host parameter limits and normalized selection are enforced");
|
||||
api.values[@(PGShapeEnabled)] = @NO;
|
||||
Check([plugin settingsAtTime:kCMTimeZero].faceThin == 0, @"disabling face group disables shape amount");
|
||||
api.values = [defaults mutableCopy];
|
||||
PGHostTile *source = Tile(96, 80, 0, 0, kFxImageOrigin_BOTTOM_LEFT), *destination = Tile(96, 80, 0, 0, kFxImageOrigin_BOTTOM_LEFT);
|
||||
Fill(source);
|
||||
Check(Render(plugin, destination, @[source], state, kCMTimeZero, &error, NULL), @"production adapter renders real BGRA IOSurface");
|
||||
NSUInteger difference = MaximumDifference(SurfaceBytes(source), SurfaceBytes(destination));
|
||||
printf("Default IOSurface maximum byte difference: %lu\n", (unsigned long)difference);
|
||||
Check(difference <= 1, @"default adapter output preserves gamma-video numeric samples");
|
||||
CGColorSpaceRelease(destination.space); destination.space = CGColorSpaceCreateWithName(kCGColorSpaceLinearSRGB);
|
||||
Check(Render(plugin, destination, @[source], state, kCMTimeZero, &error, NULL), @"numeric host render accepts destination color-space metadata");
|
||||
difference = MaximumDifference(SurfaceBytes(source), SurfaceBytes(destination));
|
||||
printf("Alternate destination tag maximum byte difference: %lu\n", (unsigned long)difference);
|
||||
Check(difference <= 1, @"host color-space tags do not double-convert already-requested gamma-video samples");
|
||||
CGColorSpaceRelease(destination.space); destination.space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
||||
FxRect rectangle;
|
||||
Check([plugin destinationImageRect:&rectangle sourceImages:@[source] destinationImage:destination pluginState:state atTime:kCMTimeZero error:&error] && RectEqual(rectangle, source.fullBounds), @"destination bounds preserve full input extent");
|
||||
Check([plugin sourceTileRect:&rectangle sourceImageIndex:0 sourceImages:@[source] destinationTileRect:destination.pixelBounds destinationImage:destination pluginState:state atTime:kCMTimeZero error:&error] && RectEqual(rectangle, source.fullBounds), @"adapter requests full source buffer for face processing");
|
||||
RejectState(plugin, destination, source, nil, @"nil snapshot is rejected with NSError, without escaping exception");
|
||||
RejectState(plugin, destination, source, [NSData data], @"empty snapshot is rejected safely");
|
||||
RejectState(plugin, destination, source, (NSData *)(id)@"wrong type", @"non-NSData snapshot is rejected safely");
|
||||
RejectState(plugin, destination, source, [@"bad data" dataUsingEncoding:NSUTF8StringEncoding], @"malformed snapshot is rejected safely");
|
||||
RejectState(plugin, destination, source, PlistState(@[]), @"non-dictionary snapshot is rejected safely");
|
||||
RejectState(plugin, destination, source, PlistState(@{@"settings":[NSData data], @"tracks":@[@"",@"",@""]}), @"wrong settings size is rejected safely");
|
||||
settings = PGBeautyDefaults(); settings.version = 99;
|
||||
RejectState(plugin, destination, source, PlistState(@{@"settings":[NSData dataWithBytes:&settings length:sizeof(settings)], @"tracks":@[@"",@"",@""]}), @"unknown saved settings version is rejected safely");
|
||||
settings.version = 1;
|
||||
RejectState(plugin, destination, source, PlistState(@{@"settings":[NSData dataWithBytes:&settings length:sizeof(settings)], @"tracks":@[@"",@""]}), @"wrong target slot count is rejected safely");
|
||||
RejectState(plugin, destination, source, PlistState(@{@"settings":[NSData dataWithBytes:&settings length:sizeof(settings)], @"tracks":@[@"",@7,@""]}), @"non-string target slot is rejected safely");
|
||||
for (NSUInteger top = 0; top < 2; top++) {
|
||||
FxImageOrigin origin = top ? kFxImageOrigin_TOP_LEFT : kFxImageOrigin_BOTTOM_LEFT;
|
||||
PGHostTile *input = Tile(48, 32, 37, -21, origin), *output = Tile(48, 32, 37, -21, origin); Fill(input);
|
||||
Check(Render(plugin, output, @[input], state, kCMTimeZero, &error, NULL), @"nonzero-origin tile renders");
|
||||
Check(MaximumDifference(SurfaceBytes(input), SurfaceBytes(output)) <= 1, top ? @"top-left tile preserves sample rows at nonzero origin" : @"bottom-left tile preserves sample rows at nonzero origin");
|
||||
output.origin = top ? kFxImageOrigin_BOTTOM_LEFT : kFxImageOrigin_TOP_LEFT;
|
||||
Check(Render(plugin, output, @[input], state, kCMTimeZero, &error, NULL), @"opposite-origin tile renders");
|
||||
Check(MaximumDifference(SurfaceBytes(input), SurfaceBytes(output)) <= 1, @"opposite coordinate tags preserve the same physical IOSurface rows");
|
||||
}
|
||||
PGHostTile *full = Tile(48, 32, 37, -21, kFxImageOrigin_BOTTOM_LEFT), *crop = Tile(16, 12, 43, -17, kFxImageOrigin_BOTTOM_LEFT); Fill(full);
|
||||
Check(Render(plugin, crop, @[full], state, kCMTimeZero, &error, NULL), @"destination sub-tile renders from full input buffer");
|
||||
// IOSurface memory rows are top-down. In a BOTTOM_LEFT image, a logical
|
||||
// lower-edge offset of 4 selects physical row 32 - 4 - 12 = 16.
|
||||
NSData *expectedCrop = ExpectedCrop(full, 6, 16, 16, 12, NO), *actualCrop = SurfaceBytes(crop);
|
||||
NSUInteger cropDifference = MaximumDifference(expectedCrop, actualCrop);
|
||||
if (cropDifference > 1) {
|
||||
const unsigned char *a = expectedCrop.bytes, *b = actualCrop.bytes;
|
||||
fprintf(stderr, "Crop max difference %lu, first expected BGRA %u/%u/%u/%u, actual %u/%u/%u/%u\n", (unsigned long)cropDifference, a[0], a[1], a[2], a[3], b[0], b[1], b[2], b[3]);
|
||||
}
|
||||
Check(cropDifference <= 1, @"destination sub-tile translates global pixel bounds correctly");
|
||||
full.origin = kFxImageOrigin_TOP_LEFT; crop.origin = kFxImageOrigin_TOP_LEFT;
|
||||
Check(Render(plugin, crop, @[full], state, kCMTimeZero, &error, NULL), @"top-left destination sub-tile renders");
|
||||
Check(MaximumDifference(ExpectedCrop(full, 6, 16, 16, 12, NO), SurfaceBytes(crop)) <= 1, @"top-left coordinate tag does not reverse the sub-tile's physical source rows");
|
||||
|
||||
NSString *fixture = argc > 1 ? @(argv[1]) : @"tests/fixtures/astronaut.png";
|
||||
CIImage *portrait = [CIImage imageWithContentsOfURL:[NSURL fileURLWithPath:fixture] options:@{kCIImageColorSpace:NSNull.null}];
|
||||
Check(portrait != nil, @"NASA portrait opens for full adapter test");
|
||||
PortraitAnatomyTests(fixture, portrait);
|
||||
PGHostTile *portraitSource = Tile(512, 512, 0, 0, kFxImageOrigin_BOTTOM_LEFT), *portraitOutput = Tile(512, 512, 0, 0, kFxImageOrigin_BOTTOM_LEFT);
|
||||
WritePortrait(portraitSource, portrait);
|
||||
api.values[@(PGThin)] = @0.8; api.values[@(PGBigEye)] = @0.6; api.values[@(PGMidtone)] = @0.8; api.values[@(PGBrightness)] = @0.25; api.values[@(PGMix)] = @0;
|
||||
NSData *zeroMix = Snapshot(plugin, kCMTimeZero);
|
||||
Check(Render(plugin, portraitOutput, @[portraitSource], zeroMix, kCMTimeZero, &error, NULL), @"zero overall mix renders with active skin and face controls");
|
||||
Check(MaximumDifference(SurfaceBytes(portraitSource), SurfaceBytes(portraitOutput)) <= 1, @"zero mix also disables geometry and color/detail output");
|
||||
api.values[@(PGThin)] = @0.2; api.values[@(PGBigEye)] = @0; api.values[@(PGMix)] = @1;
|
||||
NSData *beautyState = Snapshot(plugin, kCMTimeZero);
|
||||
Check(Render(plugin, portraitOutput, @[portraitSource], beautyState, kCMTimeZero, &error, NULL), @"real portrait renders through face, skin and IOSurface adapter");
|
||||
Check(MaximumDifference(SurfaceBytes(portraitSource), SurfaceBytes(portraitOutput)) > 5, @"active portrait settings produce visible full-chain change");
|
||||
if (argc > 2) Check(Preview(portraitSource, portraitOutput, @(argv[2])), @"full adapter before/after preview writes PNG");
|
||||
|
||||
api.values = [defaults mutableCopy]; api.targetAtCurrentTime = YES;
|
||||
api.values[@(PGSelection)] = @[@0.43, @0.76];
|
||||
NSData *oldRenderState = Snapshot(plugin, api.hostTime);
|
||||
[plugin analyzeForward];
|
||||
Check(api.forwardRequests == 1, @"forward analyze button calls host analysis API");
|
||||
CMTimeRange desired, clip = CMTimeRangeMake(kCMTimeZero, CMTimeMake(240, 24));
|
||||
[plugin desiredAnalysisTimeRange:&desired forInputWithTimeRange:clip error:&error];
|
||||
Check(CMTimeCompare(desired.start, api.hostTime) == 0 && CMTimeCompare(CMTimeRangeGetEnd(desired), CMTimeRangeGetEnd(clip)) == 0, @"analysis seed comes from current host playhead time");
|
||||
[plugin analyzeForward];
|
||||
Check(api.forwardRequests == 1, @"second analyze click is blocked before host setup callback");
|
||||
Check([plugin setupAnalysisForTimeRange:desired frameDuration:CMTimeMake(1, 24) error:&error], @"host setup starts analysis session");
|
||||
api.values[@(PGTrackData3)] = @"preserve-until-analysis-finishes";
|
||||
[plugin clearTracking];
|
||||
Check([api.values[@(PGTrackData3)] isEqualToString:@"preserve-until-analysis-finishes"], @"clear is blocked while selected target analysis is active");
|
||||
api.values[@(PGTrackData3)] = @"";
|
||||
Check([plugin analyzeFrame:portraitSource atTime:api.hostTime error:&error], @"analyzer accepts real portrait IOSurface");
|
||||
Check(Render(plugin, portraitOutput, @[portraitSource], oldRenderState, api.hostTime, &error, NULL), @"rendering an older snapshot is allowed during analysis");
|
||||
Check([plugin analyzeFrame:portraitSource atTime:CMTimeAdd(api.hostTime, CMTimeMake(1, 24)) error:&error], @"analysis continues after old snapshot render");
|
||||
Check([plugin cleanupAnalysis:&error], @"analysis persists target data into host parameters");
|
||||
NSString *target = api.values[@(PGTrackData3)];
|
||||
Check(target.length > 0 && [api.values[@(PGTrackData1)] length] == 0 && [api.values[@(PGTrackData2)] length] == 0, @"analysis stores only target selected at current host time");
|
||||
NSData *tracking = [[NSData alloc] initWithBase64EncodedString:target options:0];
|
||||
NSDictionary *trackingArchive = tracking ? [NSPropertyListSerialization propertyListWithData:tracking options:0 format:nil error:nil] : nil;
|
||||
Check([trackingArchive[@"frames"] count] == 2, @"old render snapshot does not erase either newly analyzed frame");
|
||||
TrackingCoordinateTests(trackingArchive,api.values,portraitSource,portrait,api.hostTime);
|
||||
Check([api.values[@(PGTrackEnabled)] boolValue] && CMTimeCompare(api.lastSetBoolTime, api.hostTime) == 0, @"analysis enables saved target at host seed time");
|
||||
NSData *trackedState = Snapshot(plugin, api.hostTime);
|
||||
beatygrilPlugIn *restoredPlugin = [[beatygrilPlugIn alloc] initWithAPIManager:api];
|
||||
Check(Render(restoredPlugin, portraitOutput, @[portraitSource], trackedState, api.hostTime, &error, NULL), @"fresh plugin instance restores target data from serialized host snapshot");
|
||||
[plugin analyzeBackward];
|
||||
Check(api.backwardRequests == 1, @"backward analyze button calls host API");
|
||||
[plugin desiredAnalysisTimeRange:&desired forInputWithTimeRange:clip error:&error];
|
||||
Check(CMTimeCompare(desired.start, kCMTimeZero) == 0 && CMTimeCompare(CMTimeRangeGetEnd(desired), CMTimeAdd(api.hostTime, CMTimeMake(1, 24))) == 0, @"backward analysis range includes the current host frame");
|
||||
[plugin setupAnalysisForTimeRange:desired frameDuration:CMTimeMake(1, 24) error:&error]; [plugin cleanupAnalysis:&error];
|
||||
[plugin clearTracking]; Check([api.values[@(PGTrackData3)] length] == 0, @"clear button uses current host target");
|
||||
PGHostAPI *writeFailureAPI = [PGHostAPI new];
|
||||
beatygrilPlugIn *writeFailurePlugin = [[beatygrilPlugIn alloc] initWithAPIManager:writeFailureAPI];
|
||||
[writeFailurePlugin addParametersWithError:&error]; [writeFailurePlugin analyzeForward];
|
||||
[writeFailurePlugin setupAnalysisForTimeRange:clip frameDuration:CMTimeMake(1, 24) error:&error];
|
||||
[writeFailurePlugin analyzeFrame:portraitSource atTime:writeFailureAPI.hostTime error:&error];
|
||||
writeFailureAPI.rejectedWriteParameter = PGTrackData1; error = nil;
|
||||
Check(![writeFailurePlugin cleanupAnalysis:&error] && error != nil, @"host refusing saved tracking data reports persistence failure");
|
||||
writeFailureAPI.rejectedWriteParameter = 0;
|
||||
[writeFailurePlugin analyzeForward];
|
||||
Check(writeFailureAPI.forwardRequests == 2, @"persistence failure releases analysis busy state for retry");
|
||||
PGHostAPI *unavailable = [PGHostAPI new]; [unavailable.disabledAPIs addObject:@"FxParameterCreationAPI_v5"];
|
||||
beatygrilPlugIn *missingPlugin = [[beatygrilPlugIn alloc] initWithAPIManager:unavailable];
|
||||
error = nil; Check(![missingPlugin addParametersWithError:&error] && error != nil, @"missing host creation API reports error");
|
||||
[unavailable.disabledAPIs removeAllObjects]; unavailable.rejectedParameter = PGThin;
|
||||
error = nil; Check(![missingPlugin addParametersWithError:&error] && error != nil, @"host rejecting one control fails complete parameter creation");
|
||||
[unavailable.disabledAPIs addObject:@"FxParameterRetrievalAPI_v6"];
|
||||
NSData *invalid = nil; error = nil;
|
||||
Check(![missingPlugin pluginState:&invalid atTime:kCMTimeZero quality:kFxQuality_HIGH error:&error] && error != nil, @"missing host retrieval API rejects snapshot");
|
||||
printf("Host adapter: %lu checks, %lu failures\n", (unsigned long)Checks, (unsigned long)Failures);
|
||||
}
|
||||
return Failures ? 1 : 0;
|
||||
}
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <FxPlug/FxPlugSDK.h>
|
||||
#import <CoreImage/CoreImage.h>
|
||||
#import <CoreVideo/CoreVideo.h>
|
||||
#import <IOSurface/IOSurface.h>
|
||||
#import "PGParameterIDs.h"
|
||||
#include <math.h>
|
||||
|
||||
@interface PGFaceSelectOSC : NSObject <FxOnScreenControl_v4>
|
||||
- (instancetype)initWithAPIManager:(id<PROAPIAccessing>)api;
|
||||
@end
|
||||
|
||||
@interface PGOSCAPI : NSObject <PROAPIAccessing>
|
||||
@property BOOL enabled, rejectRead, rejectPoint, rejectWrite, rejectUndo;
|
||||
@property BOOL disableOSC, disableParams, disableSetting, disableUndo,pixelSetter;
|
||||
@property BOOL corruptReadback;
|
||||
@property BOOL rejectConversion,nonfiniteConversion,partialConversion;
|
||||
@property CGRect bounds;
|
||||
@property CGAffineTransform objectToCanvas;
|
||||
@property double x,y,backing;
|
||||
@property NSUInteger writes,undoStarts,undoEnds;
|
||||
@property double lastX,lastY,lastConvertX,lastConvertY;
|
||||
@property NSUInteger objectBoundsReads,canvasToObjectCalls,objectToCanvasCalls;
|
||||
@property FxDrawingCoordinates lastConvertFrom,lastConvertTo;
|
||||
@property CMTime lastTime;
|
||||
@end
|
||||
@implementation PGOSCAPI
|
||||
- (instancetype)init {if((self=[super init])){_enabled=YES;_bounds=CGRectMake(-100,-50,200,100);_x=.25;_y=.75;_backing=1;_objectToCanvas=CGAffineTransformMakeScale(320,240);}return self;}
|
||||
- (id)apiForProtocol:(Protocol *)protocol {
|
||||
NSString *name=NSStringFromProtocol(protocol);
|
||||
if([name isEqual:@"FxOnScreenControlAPI_v4"])return _disableOSC?nil:self;
|
||||
if([name isEqual:@"FxParameterRetrievalAPI_v6"])return _disableParams?nil:self;
|
||||
if([name isEqual:@"FxParameterSettingAPI_v5"])return _disableSetting?nil:self;
|
||||
if([name isEqual:@"FxUndoAPI"])return _disableUndo?nil:self;
|
||||
return nil;
|
||||
}
|
||||
- (BOOL)getBoolValue:(BOOL *)value fromParameter:(UInt32)identifier atTime:(CMTime)time {
|
||||
if(identifier!=PGSingleFace||_rejectRead)return NO;*value=_enabled;return YES;
|
||||
}
|
||||
- (BOOL)getXValue:(double *)x YValue:(double *)y fromParameter:(UInt32)identifier atTime:(CMTime)time {
|
||||
if(identifier!=PGSelection||_rejectPoint)return NO;*x=_x;*y=_y;return YES;
|
||||
}
|
||||
- (BOOL)setXValue:(double)x YValue:(double)y toParameter:(UInt32)identifier atTime:(CMTime)time {
|
||||
if(identifier!=PGSelection||_rejectWrite)return NO;
|
||||
_writes++;_lastX=x;_lastY=y;
|
||||
_x=_pixelSetter ? .5+x/_bounds.size.width : x;
|
||||
_y=_pixelSetter ? .5+y/_bounds.size.height : y;
|
||||
if(_corruptReadback){_x=.12345;_y=.87654;}
|
||||
_lastTime=time;return YES;
|
||||
}
|
||||
- (CGRect)objectBounds {_objectBoundsReads++;return _bounds;}
|
||||
- (double)backingScaleFactor {return _backing;}
|
||||
- (void)convertPointFromSpace:(FxDrawingCoordinates)from fromX:(double)x fromY:(double)y toSpace:(FxDrawingCoordinates)to toX:(double *)tx toY:(double *)ty {
|
||||
_lastConvertFrom=from;_lastConvertTo=to;_lastConvertX=x;_lastConvertY=y;
|
||||
CGPoint point=CGPointMake(NAN,NAN);
|
||||
if(from==kFxDrawingCoordinates_OBJECT && to==kFxDrawingCoordinates_CANVAS) {
|
||||
_objectToCanvasCalls++;point=CGPointApplyAffineTransform(CGPointMake(x,y),_objectToCanvas);
|
||||
} else if(from==kFxDrawingCoordinates_CANVAS && to==kFxDrawingCoordinates_OBJECT) {
|
||||
_canvasToObjectCalls++;point=CGPointApplyAffineTransform(CGPointMake(x,y),CGAffineTransformInvert(_objectToCanvas));
|
||||
}
|
||||
if(_rejectConversion)return; // A void host API can leave outputs untouched.
|
||||
if(_nonfiniteConversion)point=CGPointMake(INFINITY,NAN);
|
||||
*tx=point.x;if(!_partialConversion)*ty=point.y;
|
||||
}
|
||||
- (BOOL)startUndoGroup:(NSString *)name {_undoStarts++;return !_rejectUndo;}
|
||||
- (BOOL)endUndoGroup {_undoEnds++;return YES;}
|
||||
@end
|
||||
|
||||
@interface PGOSCTile : FxImageTile
|
||||
@property (strong) IOSurface *surface;
|
||||
@property FxImageOrigin origin;
|
||||
@end
|
||||
@implementation PGOSCTile
|
||||
- (IOSurface *)ioSurface {return _surface;}
|
||||
- (FxImageOrigin)imageOrigin {return _origin;}
|
||||
- (CGColorSpaceRef)colorSpace {return nil;}
|
||||
@end
|
||||
static NSUInteger Checks=0,Failures=0;
|
||||
static void Check(BOOL value,NSString *message) {Checks++;if(!value){Failures++;fprintf(stderr,"FAIL: %s\n",message.UTF8String);}}
|
||||
static PGOSCTile *Tile(void) {
|
||||
IOSurfaceRef surface=IOSurfaceCreate((__bridge CFDictionaryRef)@{(id)kIOSurfaceWidth:@320,(id)kIOSurfaceHeight:@240,(id)kIOSurfaceBytesPerElement:@4,(id)kIOSurfaceBytesPerRow:@1280,(id)kIOSurfacePixelFormat:@(kCVPixelFormatType_32BGRA),(id)kIOSurfaceAllocSize:@(320*240*4)});
|
||||
PGOSCTile *tile=[PGOSCTile new];tile.surface=CFBridgingRelease(surface);return tile;
|
||||
}
|
||||
static float AlphaAt(PGOSCTile *tile,int x,int y) {
|
||||
CIImage *image=[CIImage imageWithIOSurface:(__bridge IOSurfaceRef)tile.surface options:@{kCIImageColorSpace:NSNull.null}];
|
||||
CIContext *context=[CIContext contextWithOptions:@{kCIContextWorkingColorSpace:NSNull.null,kCIContextOutputColorSpace:NSNull.null}];
|
||||
float p[4]={0};[context render:image toBitmap:p rowBytes:sizeof(p) bounds:CGRectMake(x,y,1,1) format:kCIFormatRGBAf colorSpace:nil];return p[3];
|
||||
}
|
||||
int main(int argc,const char **argv) {@autoreleasepool {
|
||||
PGOSCAPI *api=[PGOSCAPI new];PGFaceSelectOSC *osc=[[PGFaceSelectOSC alloc] initWithAPIManager:api];
|
||||
CMTime time=CMTimeMake(121,24);NSInteger part=-1;BOOL update=YES,handled=YES;
|
||||
Check([osc conformsToProtocol:@protocol(FxOnScreenControl_v4)],@"OSC class conforms to the real FxPlug v4 protocol");
|
||||
NSArray *required=@[@"drawingCoordinates",@"drawOSCWithWidth:height:activePart:destinationImage:atTime:",@"hitTestOSCAtMousePositionX:mousePositionY:activePart:atTime:",@"mouseDownAtPositionX:positionY:activePart:modifiers:forceUpdate:atTime:",@"mouseDraggedAtPositionX:positionY:activePart:modifiers:forceUpdate:atTime:",@"mouseUpAtPositionX:positionY:activePart:modifiers:forceUpdate:atTime:",@"keyDownAtPositionX:positionY:keyPressed:modifiers:forceUpdate:didHandle:atTime:",@"keyUpAtPositionX:positionY:keyPressed:modifiers:forceUpdate:didHandle:atTime:"];
|
||||
for(NSString *selector in required)Check([osc respondsToSelector:NSSelectorFromString(selector)],[NSString stringWithFormat:@"Required callback exists: %@",selector]);
|
||||
Check(osc.drawingCoordinates==kFxDrawingCoordinates_CANVAS,@"Mouse callbacks explicitly request CANVAS coordinates");
|
||||
NSDictionary *info=argc>1 ? [NSDictionary dictionaryWithContentsOfFile:[NSString stringWithUTF8String:argv[1]]] : nil;
|
||||
NSArray *entries=info[@"ProPlugPlugInList"];NSDictionary *control=nil;NSMutableArray *filters=[NSMutableArray new];NSMutableSet *ids=[NSMutableSet new];BOOL unique=YES;
|
||||
for(NSDictionary *entry in entries){if([ids containsObject:entry[@"uuid"]])unique=NO;[ids addObject:entry[@"uuid"]];if([entry[@"protocolNames"] containsObject:@"FxOnScreenControl"])control=entry;if([entry[@"protocolNames"] containsObject:@"FxFilter"])[filters addObject:entry];}
|
||||
Check(unique && control && filters.count==1,@"Registration has unique filter and dedicated OSC identities");
|
||||
Check([control[@"className"] isEqual:@"PGFaceSelectOSC"] && NSClassFromString(control[@"className"])==PGFaceSelectOSC.class,@"Registered OSC resolves to the compiled class");
|
||||
Check([control[@"supportedPlugins"] isEqual:@[filters.firstObject[@"uuid"]]],@"Dedicated OSC links to the released filter UUID");
|
||||
api.enabled=NO;[osc hitTestOSCAtMousePositionX:0 mousePositionY:0 activePart:&part atTime:time];Check(part==0,@"Disabled selection does not claim mouse events");
|
||||
[osc mouseDownAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(!update && api.writes==0 && api.undoStarts==0,@"Disabled mouse-down writes nothing and opens no undo group");
|
||||
api.enabled=YES;[osc hitTestOSCAtMousePositionX:80 mousePositionY:180 activePart:&part atTime:time];Check(part==1,@"Enabled selection accepts a canvas point inside the converted source");
|
||||
[osc hitTestOSCAtMousePositionX:1000 mousePositionY:0 activePart:&part atTime:time];Check(part==0,@"Outside-object hit test is empty");
|
||||
[osc hitTestOSCAtMousePositionX:NAN mousePositionY:0 activePart:&part atTime:time];Check(part==0,@"Nonfinite hit coordinates are rejected");
|
||||
[osc hitTestOSCAtMousePositionX:0 mousePositionY:0 activePart:NULL atTime:time];Check(YES,@"Null hit-test output is safe");
|
||||
[osc mouseDownAtPositionX:0 positionY:0 activePart:0 modifiers:0 forceUpdate:&update atTime:time];Check(!update && api.writes==0,@"Inactive part does not start selection");
|
||||
update=YES;[osc mouseUpAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(!update && api.undoEnds==0,@"Mouse-up without mouse-down leaves undo balanced");
|
||||
[osc mouseDownAtPositionX:80 positionY:180 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(update && fabs(api.lastX-.25)<1e-8 && fabs(api.lastY-.75)<1e-8,@"CANVAS mouse-down converts to normalized point parameter coordinates");
|
||||
Check(CMTimeCompare(api.lastTime,time)==0 && api.undoStarts==1,@"Selection writes use callback time and open one undo group");
|
||||
[osc mouseDraggedAtPositionX:500 positionY:-500 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(update && api.lastX==1 && api.lastY==0,@"Dragging clamps the converted normalized point to the source");
|
||||
[osc mouseUpAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(update && api.undoEnds==1,@"Gesture completes its matching undo group");
|
||||
NSUInteger writes=api.writes;[osc mouseDraggedAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(!update && api.writes==writes,@"Stray drag after release writes nothing");
|
||||
api.bounds=CGRectMake(0,0,NAN,100);[osc hitTestOSCAtMousePositionX:160 mousePositionY:120 activePart:&part atTime:time];Check(part==1,@"Converted hit testing does not depend on document pixel bounds");
|
||||
update=YES;[osc mouseDownAtPositionX:NAN positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(!update && api.writes==writes,@"Nonfinite event initializes forceUpdate and avoids parameter writes");api.bounds=CGRectMake(-100,-50,200,100);
|
||||
api.rejectWrite=YES;[osc mouseDownAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(!update && api.undoStarts==api.undoEnds,@"Rejected parameter write closes the opened undo group");api.rejectWrite=NO;
|
||||
api.rejectUndo=YES;NSUInteger ends=api.undoEnds;[osc mouseDownAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];[osc mouseUpAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(api.undoEnds==ends,@"Failed undo start is never paired with an invalid end");api.rejectUndo=NO;
|
||||
for(int disabled=0;disabled<3;disabled++){
|
||||
api.disableOSC=disabled==0;api.disableParams=disabled==1;api.disableSetting=disabled==2;
|
||||
writes=api.writes;update=YES;[osc mouseDownAtPositionX:0 positionY:0 activePart:1 modifiers:0 forceUpdate:&update atTime:time];Check(!update && api.writes==writes,[NSString stringWithFormat:@"Missing host API %d is handled safely",disabled]);
|
||||
}
|
||||
api.disableOSC=api.disableParams=api.disableSetting=NO;
|
||||
[osc keyDownAtPositionX:0 positionY:0 keyPressed:0 modifiers:0 forceUpdate:&update didHandle:&handled atTime:time];Check(!update && !handled,@"Unrecognized key-down clears outputs");update=handled=YES;
|
||||
[osc keyUpAtPositionX:0 positionY:0 keyPressed:0 modifiers:0 forceUpdate:&update didHandle:&handled atTime:time];Check(!update && !handled,@"Unrecognized key-up clears outputs");
|
||||
PGOSCTile *tile=Tile();api.x=.25;api.y=.75;
|
||||
[osc drawOSCWithWidth:1920 height:1080 activePart:0 destinationImage:tile atTime:time];
|
||||
Check(AlphaAt(tile,80,180)>.9,@"Normalized selection converts to correct CANVAS pixels despite effect/surface size mismatch");
|
||||
Check(fabs(api.lastConvertX-.25)<1e-8 && fabs(api.lastConvertY-.75)<1e-8,@"Host receives normalized OBJECT coordinates rather than document pixel coordinates");
|
||||
Check(AlphaAt(tile,25,28)<.01,@"OSC surface is not incorrectly rescaled by effect image dimensions");
|
||||
api.enabled=NO;[osc drawOSCWithWidth:1920 height:1080 activePart:0 destinationImage:tile atTime:time];Check(AlphaAt(tile,80,180)<.01,@"Disabled drawing clears the prior handle");
|
||||
api.enabled=YES;api.backing=NAN;[osc drawOSCWithWidth:1920 height:1080 activePart:0 destinationImage:tile atTime:time];Check(AlphaAt(tile,80,180)>.9,@"Nonfinite backing scale uses a finite fallback");
|
||||
api.x=.5;api.y=.5;api.backing=1;
|
||||
[osc drawOSCWithWidth:1921 height:1080 activePart:0 destinationImage:tile atTime:time];
|
||||
Check(AlphaAt(tile,160,120)>.9 && AlphaAt(tile,0,0)<.01,@"Default .5/.5 selection draws in the center instead of a corner");
|
||||
Check(api.objectBoundsReads==1,@"Drawing reads object bounds once for diagnostics and never uses them to scale the normalized point");
|
||||
api.x=NAN;[osc drawOSCWithWidth:1920 height:1080 activePart:0 destinationImage:tile atTime:time];Check(AlphaAt(tile,80,180)<.01,@"Nonfinite stored selection does not create invalid drawing filters");
|
||||
api.x=.5;api.y=.5;
|
||||
[osc mouseDownAtPositionX:256 positionY:72 activePart:1 modifiers:0 forceUpdate:&update atTime:time];
|
||||
[osc mouseUpAtPositionX:256 positionY:72 activePart:1 modifiers:0 forceUpdate:&update atTime:time];
|
||||
[osc drawOSCWithWidth:1921 height:1080 activePart:0 destinationImage:tile atTime:time];
|
||||
Check(fabs(api.x-.8)<1e-8 && fabs(api.y-.3)<1e-8 && AlphaAt(tile,256,72)>.9,@"Click writes normalized selection and redraws the handle at that point");
|
||||
PGOSCAPI *motion=[PGOSCAPI new];motion.pixelSetter=YES;motion.bounds=CGRectMake(-1920,-1080,3840,2160);
|
||||
motion.objectToCanvas=CGAffineTransformMake(160,0,0,120,40,30);
|
||||
PGFaceSelectOSC *motionOSC=[[PGFaceSelectOSC alloc] initWithAPIManager:motion];
|
||||
[motionOSC mouseDownAtPositionX:168 positionY:66 activePart:1 modifiers:0 forceUpdate:&update atTime:time];
|
||||
Check(update && fabs(motion.x-.8)<1e-5 && fabs(motion.y-.3)<1e-5,@"Motion pixel-setter bridge is detected by verified parameter readback");
|
||||
Check(motion.writes==2 && fabs(motion.lastX-1152)<1e-5 && fabs(motion.lastY+432)<1e-5,@"Detected bridge receives the centered pixel inverse transform");
|
||||
NSUInteger beforeCached=motion.writes;
|
||||
[motionOSC mouseDraggedAtPositionX:152 positionY:78 activePart:1 modifiers:0 forceUpdate:&update atTime:time];
|
||||
Check(update && motion.writes==beforeCached+1 && fabs(motion.x-.7)<1e-5 && fabs(motion.y-.4)<1e-5,@"Pixel-setter mode is cached only on the same OSC instance");
|
||||
[motionOSC mouseUpAtPositionX:152 positionY:78 activePart:1 modifiers:0 forceUpdate:&update atTime:time];
|
||||
[motionOSC drawOSCWithWidth:3840 height:2160 activePart:0 destinationImage:tile atTime:time];
|
||||
Check(AlphaAt(tile,152,78)>.9,@"Motion-compatible setter and drawing round-trip through zoom and nonzero canvas translation");
|
||||
Check(motion.undoStarts==1 && motion.undoEnds==1,@"Calibration writes remain in one balanced undo gesture");
|
||||
PGOSCAPI *bad=[PGOSCAPI new];bad.corruptReadback=YES;
|
||||
PGFaceSelectOSC *badOSC=[[PGFaceSelectOSC alloc] initWithAPIManager:bad];
|
||||
[badOSC mouseDownAtPositionX:256 positionY:72 activePart:1 modifiers:0 forceUpdate:&update atTime:time];
|
||||
Check(!update && bad.undoStarts==bad.undoEnds,@"Unrecognized readback response is reported as failure with balanced undo");
|
||||
|
||||
// Each fixture describes a host projection independent of effect dimensions:
|
||||
// reduced view with padding, magnified/panned view, and a shifted canvas.
|
||||
const CGAffineTransform transforms[]={
|
||||
CGAffineTransformMake(160,0,0,120,40,30),
|
||||
CGAffineTransformMake(640,0,0,480,-160,-120),
|
||||
CGAffineTransformMake(192,0,0,144,96,72)};
|
||||
const CGPoint normalizedPoints[]={CGPointMake(.75,.25),CGPointMake(.625,.375),CGPointMake(.25,.75)};
|
||||
const CGPoint canvasPoints[]={CGPointMake(160,60),CGPointMake(240,60),CGPointMake(144,180)};
|
||||
for(NSUInteger i=0;i<3;i++) {
|
||||
PGOSCAPI *projected=[PGOSCAPI new];projected.objectToCanvas=transforms[i];
|
||||
PGFaceSelectOSC *projectedOSC=[[PGFaceSelectOSC alloc] initWithAPIManager:projected];
|
||||
CGPoint n=normalizedPoints[i],c=canvasPoints[i];
|
||||
[projectedOSC hitTestOSCAtMousePositionX:c.x mousePositionY:c.y activePart:&part atTime:time];
|
||||
Check(part==1,[NSString stringWithFormat:@"Projection %lu claims a visible source point",(unsigned long)i]);
|
||||
Check(projected.lastConvertFrom==kFxDrawingCoordinates_CANVAS && projected.lastConvertTo==kFxDrawingCoordinates_OBJECT,@"Hit test explicitly requests the inverse host conversion");
|
||||
[projectedOSC mouseDownAtPositionX:c.x positionY:c.y activePart:part modifiers:0 forceUpdate:&update atTime:time];
|
||||
Check(update && fabs(projected.x-n.x)<1e-8 && fabs(projected.y-n.y)<1e-8,@"Projected click writes the expected normalized parameter");
|
||||
[projectedOSC mouseUpAtPositionX:c.x positionY:c.y activePart:part modifiers:0 forceUpdate:&update atTime:time];
|
||||
Check(update && projected.undoStarts==1 && projected.undoEnds==1,@"Projected mouse-up converts and closes one undo gesture");
|
||||
[projectedOSC drawOSCWithWidth:3840 height:2160 activePart:0 destinationImage:tile atTime:time];
|
||||
Check(AlphaAt(tile,(int)c.x,(int)c.y)>.9,@"Projected click, parameter and rendered handle share the same canvas position");
|
||||
Check(projected.lastConvertFrom==kFxDrawingCoordinates_OBJECT && projected.lastConvertTo==kFxDrawingCoordinates_CANVAS,@"Draw explicitly requests the forward host conversion");
|
||||
Check(projected.canvasToObjectCalls==3 && projected.objectToCanvasCalls==1,@"Hit, down, up and draw each use the live host conversion");
|
||||
}
|
||||
PGOSCAPI *panned=[PGOSCAPI new];panned.objectToCanvas=transforms[0];
|
||||
PGFaceSelectOSC *pannedOSC=[[PGFaceSelectOSC alloc] initWithAPIManager:panned];
|
||||
[pannedOSC hitTestOSCAtMousePositionX:20 mousePositionY:90 activePart:&part atTime:time];
|
||||
Check(part==0,@"Canvas pixels inside the surface but outside a shifted source do not hit");
|
||||
[pannedOSC mouseDownAtPositionX:20 positionY:90 activePart:1 modifiers:0 forceUpdate:&update atTime:time];
|
||||
Check(!update && panned.writes==0 && panned.undoStarts==0,@"Outside-source mouse-down is rejected even with a stale active part");
|
||||
[pannedOSC mouseDownAtPositionX:120 positionY:90 activePart:1 modifiers:0 forceUpdate:&update atTime:time];
|
||||
Check(update && fabs(panned.x-.5)<1e-8 && fabs(panned.y-.5)<1e-8,@"Gesture starts at the shifted source center");
|
||||
panned.objectToCanvas=CGAffineTransformMake(80,0,0,60,100,80);
|
||||
[pannedOSC mouseDraggedAtPositionX:160 positionY:95 activePart:1 modifiers:0 forceUpdate:&update atTime:time];
|
||||
Check(update && fabs(panned.x-.75)<1e-8 && fabs(panned.y-.25)<1e-8,@"Drag follows host zoom and pan changes during the gesture");
|
||||
[pannedOSC mouseDraggedAtPositionX:240 positionY:20 activePart:1 modifiers:0 forceUpdate:&update atTime:time];
|
||||
Check(update && panned.x==1 && panned.y==0,@"Drag outside shifted source clamps after inverse conversion");
|
||||
[pannedOSC mouseUpAtPositionX:80 positionY:200 activePart:1 modifiers:0 forceUpdate:&update atTime:time];
|
||||
Check(update && panned.x==0 && panned.y==1 && panned.undoStarts==panned.undoEnds,@"Release outside shifted source clamps and balances undo");
|
||||
[pannedOSC drawOSCWithWidth:1920 height:1080 activePart:0 destinationImage:tile atTime:time];
|
||||
Check(AlphaAt(tile,100,140)>.9,@"Clamped release draws at the corresponding transformed source corner");
|
||||
|
||||
for(NSUInteger failure=0;failure<3;failure++) {
|
||||
PGOSCAPI *failed=[PGOSCAPI new];
|
||||
failed.rejectConversion=failure==0;failed.nonfiniteConversion=failure==1;failed.partialConversion=failure==2;
|
||||
PGFaceSelectOSC *failedOSC=[[PGFaceSelectOSC alloc] initWithAPIManager:failed];
|
||||
[failedOSC hitTestOSCAtMousePositionX:160 mousePositionY:120 activePart:&part atTime:time];
|
||||
Check(part==0,@"Untouched, nonfinite or partial conversion cannot claim a hit");
|
||||
[failedOSC mouseDownAtPositionX:160 positionY:120 activePart:1 modifiers:0 forceUpdate:&update atTime:time];
|
||||
Check(!update && failed.writes==0 && failed.undoStarts==0,@"Failed conversion cannot write a point or open undo");
|
||||
[failedOSC drawOSCWithWidth:1920 height:1080 activePart:0 destinationImage:tile atTime:time];
|
||||
Check(AlphaAt(tile,80,180)<.01,@"Failed forward conversion clears the prior OSC handle");
|
||||
failed.rejectConversion=failed.nonfiniteConversion=failed.partialConversion=NO;
|
||||
[failedOSC mouseDownAtPositionX:160 positionY:120 activePart:1 modifiers:0 forceUpdate:&update atTime:time];
|
||||
NSUInteger written=failed.writes;
|
||||
failed.rejectConversion=failure==0;failed.nonfiniteConversion=failure==1;failed.partialConversion=failure==2;
|
||||
[failedOSC mouseDraggedAtPositionX:200 positionY:100 activePart:1 modifiers:0 forceUpdate:&update atTime:time];
|
||||
Check(!update && failed.writes==written,@"Conversion failure during drag preserves the previous selection");
|
||||
[failedOSC mouseUpAtPositionX:200 positionY:100 activePart:1 modifiers:0 forceUpdate:&update atTime:time];
|
||||
Check(!update && failed.writes==written && failed.undoStarts==failed.undoEnds,@"Conversion failure on release still closes undo without a write");
|
||||
failed.rejectConversion=failed.nonfiniteConversion=failed.partialConversion=NO;
|
||||
[failedOSC mouseDraggedAtPositionX:200 positionY:100 activePart:1 modifiers:0 forceUpdate:&update atTime:time];
|
||||
Check(!update && failed.writes==written,@"Failed release ends dragging so later stray drag cannot write");
|
||||
}
|
||||
PGOSCAPI *invalidEvent=[PGOSCAPI new];
|
||||
PGFaceSelectOSC *invalidEventOSC=[[PGFaceSelectOSC alloc] initWithAPIManager:invalidEvent];
|
||||
[invalidEventOSC mouseDownAtPositionX:160 positionY:120 activePart:1 modifiers:0 forceUpdate:&update atTime:time];
|
||||
writes=invalidEvent.writes;NSUInteger conversions=invalidEvent.canvasToObjectCalls;
|
||||
[invalidEventOSC mouseDraggedAtPositionX:INFINITY positionY:120 activePart:1 modifiers:0 forceUpdate:&update atTime:time];
|
||||
Check(!update && invalidEvent.writes==writes && invalidEvent.canvasToObjectCalls==conversions,@"Nonfinite canvas drag is rejected before calling the host conversion");
|
||||
[invalidEventOSC mouseUpAtPositionX:160 positionY:NAN activePart:1 modifiers:0 forceUpdate:&update atTime:time];
|
||||
Check(!update && invalidEvent.writes==writes && invalidEvent.undoStarts==invalidEvent.undoEnds,@"Nonfinite canvas release balances undo without changing selection");
|
||||
printf("OSC adapter: %lu checks, %lu failures\n",(unsigned long)Checks,(unsigned long)Failures);return Failures?1:0;
|
||||
}}
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreImage/CoreImage.h>
|
||||
#import <Metal/Metal.h>
|
||||
#import "PGFaceProcessor.h"
|
||||
#include <math.h>
|
||||
|
||||
@interface PGFaceProcessor (SlimTest)
|
||||
- (CIImage *)warpImage:(CIImage *)image geometry:(NSArray<NSDictionary *> *)geometry settings:(PGBeautySettings)s extent:(CGRect)extent;
|
||||
@end
|
||||
@interface PGRealtimeFaceTracker : NSObject
|
||||
- (NSDictionary *)resultForGeometry:(NSArray *)raw settings:(PGBeautySettings)s time:(CMTime)time extent:(CGRect)extent;
|
||||
@end
|
||||
static NSUInteger checks=0;
|
||||
static double maxOracleError=0;
|
||||
static void Check(BOOL ok,NSString *message) {checks++;if(!ok){fprintf(stderr,"FAIL: %s\n",message.UTF8String);exit(1);}}
|
||||
static double Clamp(double x,double a,double b) {return fmin(b,fmax(a,x));}
|
||||
static CGPoint ReadPoint(NSArray *a) {return CGPointMake([a[0] doubleValue],[a[1] doubleValue]);}
|
||||
static NSArray *WritePoint(CGPoint p) {return @[@(p.x),@(p.y)];}
|
||||
|
||||
// Authored 2D projections, with the documented 76-point layout. They are not
|
||||
// extracted vendor assets or private face coordinates. Positive yaw projects
|
||||
// the nose toward the right while the silhouette becomes narrower.
|
||||
static NSDictionary *Geometry(double yaw,double roll) {
|
||||
NSMutableArray *p=[NSMutableArray new];
|
||||
for(NSUInteger i=0;i<76;i++)[p addObject:@[@.5,@.5]];
|
||||
p[6]=@[@.34,@.65];p[13]=@[@.66,@.65];
|
||||
p[47]=@[@.5,@.60];p[48]=@[@.5,@.52];p[49]=@[@.5,@.45];p[52]=@[@.5,@.42];
|
||||
p[36]=@[@.54,@.36];p[37]=@[@.5,@.35];p[38]=@[@.46,@.36];
|
||||
for(NSUInteger i=0;i<17;i++) {
|
||||
double t=M_PI+M_PI*i/16;
|
||||
p[59+i]=@[@(.5+.29*cos(t)),@(.65+.43*sin(t))];
|
||||
}
|
||||
for(NSUInteger i=0;i<76;i++) {
|
||||
CGPoint a=ReadPoint(p[i]);
|
||||
double depth=(i==47 || i==48 || i==49 || i==52)?.18:.03;
|
||||
double x=(a.x-.5)*cos(yaw)+depth*sin(yaw),y=a.y-.5;
|
||||
p[i]=WritePoint(CGPointMake(.5+x*cos(roll)-y*sin(roll),.5+x*sin(roll)+y*cos(roll)));
|
||||
}
|
||||
NSMutableDictionary *g=[NSMutableDictionary dictionaryWithDictionary:@{@"box":@[@.15,@.15,@.7,@.6],@"points76":p}];
|
||||
for(NSString *key in @[@"leftEye",@"rightEye",@"leftBrow",@"rightBrow",@"nose",@"lips",@"mouth",@"contour"])g[key]=@[];
|
||||
return g;
|
||||
}
|
||||
|
||||
// Independent scalar oracle from the recorded AIR specification. Its explicit
|
||||
// y-down conversion checks the Core Image implementation's coordinate bridge.
|
||||
static CGPoint Oracle(CGPoint pixel,NSDictionary *g,CGRect extent,double thin) {
|
||||
static const unsigned int origins[17]={75,59,74,60,73,61,72,62,71,63,70,64,69,65,68,66,67};
|
||||
static const unsigned int anchors[17]={6,13,47,47,47,47,48,48,48,48,49,49,49,49,52,52,47};
|
||||
NSArray *raw=g[@"points76"];CGPoint p[76];
|
||||
for(NSUInteger i=0;i<76;i++){p[i]=ReadPoint(raw[i]);p[i].y=1-p[i].y;}
|
||||
double width=hypot((p[75].x-p[59].x)*extent.size.width,(p[75].y-p[59].y)*extent.size.height);
|
||||
double height=hypot((p[67].x-(p[74].x+p[60].x)/2)*extent.size.width,(p[67].y-(p[74].y+p[60].y)/2)*extent.size.height);
|
||||
double gain=Clamp(280/fmax(1,fmax(width,2*height)),1,2.25)*.025*Clamp(thin,0,1);
|
||||
CGPoint q=CGPointMake((pixel.x-extent.origin.x)/extent.size.width,1-(pixel.y-extent.origin.y)/extent.size.height);
|
||||
for(NSUInteger i=0;i<17;i++) {
|
||||
CGPoint a=p[origins[i]],b=p[anchors[i]];
|
||||
double r=hypot(a.x-b.x,a.y-b.y);
|
||||
if(r<=.0001)continue;
|
||||
double w=Clamp(1-hypot(q.x-a.x,q.y-a.y)/r,0,1);
|
||||
q.x=Clamp(q.x-(b.x-a.x)*gain*w,0,1);q.y=Clamp(q.y-(b.y-a.y)*gain*w,0,1);
|
||||
}
|
||||
return CGPointMake(q.x,1-q.y);
|
||||
}
|
||||
static CGPoint ChinOracle(CGPoint pixel,NSDictionary *g,CGRect e,double thin,double chin) {
|
||||
CGPoint q=Oracle(pixel,g,e,thin);q.y=1-q.y;
|
||||
NSArray *raw=g[@"points76"];CGPoint points[76];
|
||||
for(NSUInteger i=0;i<76;i++){points[i]=ReadPoint(raw[i]);points[i].y=1-points[i].y;}
|
||||
double width=hypot((points[75].x-points[59].x)*e.size.width,(points[75].y-points[59].y)*e.size.height);
|
||||
double height=2*hypot((points[67].x-(points[74].x+points[60].x)/2)*e.size.width,(points[67].y-(points[74].y+points[60].y)/2)*e.size.height);
|
||||
double ratio=Clamp(chin,-1,1)*(chin>0?.04:.02)*Clamp(280/fmax(1,fmax(width,height)),1,2.25);
|
||||
for(NSUInteger i=0;i<3;i++) {
|
||||
CGPoint a=points[68-i],b=points[38-i];double r=hypot(a.x-b.x,a.y-b.y);
|
||||
if(r<=.0001)continue;
|
||||
double w=Clamp(1-hypot(q.x-a.x,q.y-a.y)/r,0,1);
|
||||
q.x=Clamp(q.x-(b.x-a.x)*ratio*w,0,1);q.y=Clamp(q.y-(b.y-a.y)*ratio*w,0,1);
|
||||
}
|
||||
return CGPointMake(q.x,1-q.y);
|
||||
}
|
||||
static CIImage *CoordinateImage(CGRect extent) {
|
||||
NSUInteger w=extent.size.width,h=extent.size.height;
|
||||
NSMutableData *d=[NSMutableData dataWithLength:w*h*4*sizeof(float)];float *p=d.mutableBytes;
|
||||
for(NSUInteger y=0;y<h;y++)for(NSUInteger x=0;x<w;x++) {
|
||||
NSUInteger k=(y*w+x)*4;p[k]=(x+.5)/w;p[k+1]=(y+.5)/h;p[k+2]=.25;p[k+3]=1;
|
||||
}
|
||||
return [[CIImage imageWithBitmapData:d bytesPerRow:w*4*sizeof(float) size:extent.size format:kCIFormatRGBAf colorSpace:nil]
|
||||
imageByApplyingTransform:CGAffineTransformMakeTranslation(extent.origin.x,extent.origin.y)];
|
||||
}
|
||||
static NSData *Pixels(CIContext *c,CIImage *image) {
|
||||
CGRect e=image.extent;NSUInteger w=e.size.width,h=e.size.height;
|
||||
NSMutableData *d=[NSMutableData dataWithLength:w*h*4*sizeof(float)];
|
||||
[c render:image toBitmap:d.mutableBytes rowBytes:w*4*sizeof(float) bounds:e format:kCIFormatRGBAf colorSpace:nil];return d;
|
||||
}
|
||||
static NSData *Archive(NSDictionary *geometry) {
|
||||
return [NSPropertyListSerialization dataWithPropertyList:@{@"version":@1,@"frames":@{@"0/1/0":@{@"value":@0,@"scale":@1,@"epoch":@0,@"faces":@[geometry]}}} format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil];
|
||||
}
|
||||
static void NumericSettingsCase(CIContext *c,PGFaceProcessor *p,CGRect e,NSDictionary *g,double amount,double chin,NSString *name) {
|
||||
PGBeautySettings s=PGBeautyDefaults();s.faceThin=amount;s.faceChin=chin;
|
||||
CIImage *input=CoordinateImage(e),*output=[p warpImage:input geometry:@[g] settings:s extent:e];
|
||||
Check(output!=nil && CGRectEqualToRect(output.extent,e),[name stringByAppendingString:@" preserves extent"]);
|
||||
NSData *data=Pixels(c,output);const float *pixels=data.bytes;double worst=0,maxMove=0;
|
||||
NSUInteger worstX=0,worstY=0;CGPoint worstExpected=CGPointZero,worstActual=CGPointZero;
|
||||
NSUInteger w=e.size.width,h=e.size.height;
|
||||
for(NSUInteger y=3;y<h-3;y+=7)for(NSUInteger x=3;x<w-3;x+=7) {
|
||||
// Bitmap rows are top first, while destCoord / geometry are y up.
|
||||
CGPoint q=ChinOracle(CGPointMake(e.origin.x+x+.5,e.origin.y+h-y-.5),g,e,amount,chin);
|
||||
double ex=Clamp(q.x,.5/w,1-.5/w),ey=Clamp(1-q.y,.5/h,1-.5/h);
|
||||
NSUInteger k=(y*w+x)*4;
|
||||
double difference=fmax(fabs(pixels[k]-ex),fabs(pixels[k+1]-ey));
|
||||
if(difference>worst){worst=difference;worstX=x;worstY=y;worstExpected=CGPointMake(ex,ey);worstActual=CGPointMake(pixels[k],pixels[k+1]);}
|
||||
maxMove=fmax(maxMove,hypot(ex-(x+.5)/w,ey-(y+.5)/h));
|
||||
}
|
||||
if(worst>0.00006)fprintf(stderr,"%s oracle max error=%g at %lu,%lu expected=(%g,%g) actual=(%g,%g)\n",name.UTF8String,worst,(unsigned long)worstX,(unsigned long)worstY,worstExpected.x,worstExpected.y,worstActual.x,worstActual.y);
|
||||
maxOracleError=fmax(maxOracleError,worst);
|
||||
Check(worst<0.00006,[name stringByAppendingString:@" GPU field matches the independent thin/chin oracle"]);
|
||||
Check((amount==0 && chin==0) || maxMove>.0001,[name stringByAppendingString:@" exercises a nonzero displacement"]);
|
||||
NSUInteger corner=4*((h-3)*w+(w-3));
|
||||
Check(fabs(pixels[corner]-(w-2.5)/w)<1e-6 && fabs(pixels[corner+1]-(h-2.5)/h)<1e-6,[name stringByAppendingString:@" preserves distant background"]);
|
||||
}
|
||||
static void NumericCase(CIContext *c,PGFaceProcessor *p,CGRect e,NSDictionary *g,double amount,NSString *name) {
|
||||
NumericSettingsCase(c,p,e,g,amount,0,name);
|
||||
}
|
||||
static CGPoint Displacement(CIContext *c,CIImage *ramp,CIImage *warped,CGPoint uv) {
|
||||
CGRect e=ramp.extent;CGRect point=CGRectMake(floor(e.origin.x+uv.x*e.size.width),floor(e.origin.y+uv.y*e.size.height),1,1);
|
||||
float a[4],b[4];
|
||||
[c render:ramp toBitmap:a rowBytes:sizeof(a) bounds:point format:kCIFormatRGBAf colorSpace:nil];
|
||||
[c render:warped toBitmap:b rowBytes:sizeof(b) bounds:point format:kCIFormatRGBAf colorSpace:nil];
|
||||
return CGPointMake((b[0]-a[0])*e.size.width,-(b[1]-a[1])*e.size.height);
|
||||
}
|
||||
static void AnatomicalChecks(CIContext *c,PGFaceProcessor *p,NSDictionary *g,CGRect e) {
|
||||
CIImage *ramp=CoordinateImage(e);NSArray *points=g[@"points76"];
|
||||
CGPoint chin=ReadPoint(points[67]),lip=ReadPoint(points[37]);
|
||||
double ax=(lip.x-chin.x)*e.size.width,ay=(lip.y-chin.y)*e.size.height;
|
||||
for(int sign=-1;sign<=1;sign+=2) {
|
||||
PGBeautySettings s=PGBeautyDefaults();s.faceChin=sign;
|
||||
CIImage *out=[p warpImage:ramp geometry:@[g] settings:s extent:e];
|
||||
CGPoint d=Displacement(c,ramp,out,chin);
|
||||
Check(hypot(d.x,d.y)>.05,@"chin control moves the detected physical chin landmark");
|
||||
Check(sign*(d.x*ax+d.y*ay)<0,@"signed chin direction follows chin-to-lip anchor instead of a fixed image Y axis");
|
||||
BOOL outside=YES,eyesUnchanged=YES;
|
||||
for(NSNumber *index in @[@6,@13]) {
|
||||
CGPoint eye=ReadPoint(points[index.unsignedIntegerValue]);
|
||||
for(NSUInteger i=0;i<3;i++) {
|
||||
CGPoint a=ReadPoint(points[68-i]),b=ReadPoint(points[38-i]);
|
||||
if(hypot(eye.x-a.x,eye.y-a.y)<=hypot(a.x-b.x,a.y-b.y)+.003)outside=NO;
|
||||
}
|
||||
CGPoint shift=Displacement(c,ramp,out,eye);eyesUnchanged &= hypot(shift.x,shift.y)<.002;
|
||||
}
|
||||
Check(outside && eyesUnchanged,@"both physical pupils outside chin supports remain unchanged");
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc,const char *argv[]) {@autoreleasepool {
|
||||
CIContext *c=[CIContext contextWithMTLDevice:MTLCreateSystemDefaultDevice() options:@{kCIContextWorkingColorSpace:NSNull.null,kCIContextOutputColorSpace:NSNull.null}];
|
||||
PGFaceProcessor *p=[PGFaceProcessor new];
|
||||
CGRect extents[]={CGRectMake(0,0,240,240),CGRectMake(0,0,320,180),CGRectMake(-17,29,180,320),CGRectMake(0,0,640,360)};
|
||||
double yaw[]={0,.65,1.40,-1.40},roll[]={0,.45,-.50};
|
||||
NumericCase(c,p,extents[1],Geometry(0,0),0,@"zero strength");
|
||||
for(NSUInteger e=0;e<4;e++)for(NSUInteger y=0;y<4;y++)for(NSUInteger r=0;r<3;r++) {
|
||||
NSString *name=[NSString stringWithFormat:@"size%lu yaw%lu roll%lu",(unsigned long)e,(unsigned long)y,(unsigned long)r];
|
||||
NumericCase(c,p,extents[e],Geometry(yaw[y],roll[r]),1,name);
|
||||
}
|
||||
NumericCase(c,p,extents[1],Geometry(.6,.2),.5,@"half strength");
|
||||
for(NSUInteger e=0;e<3;e++)for(NSUInteger y=0;y<3;y++)for(NSUInteger r=0;r<2;r++)for(int sign=-1;sign<=1;sign+=2) {
|
||||
double angle=y==0?0:(y==1?1.4:-1.4);
|
||||
NumericSettingsCase(c,p,extents[e],Geometry(angle,r?.45:0),0,sign,
|
||||
[NSString stringWithFormat:@"chin size%lu yaw%lu roll%lu sign%d",(unsigned long)e,(unsigned long)y,(unsigned long)r,sign]);
|
||||
}
|
||||
NumericSettingsCase(c,p,extents[1],Geometry(.4,.2),.8,1,@"thin plus positive chin");
|
||||
NumericSettingsCase(c,p,extents[2],Geometry(-.6,-.2),1,-1,@"thin plus negative chin");
|
||||
NumericSettingsCase(c,p,extents[0],Geometry(0,0),0,.5,@"half positive chin");
|
||||
NumericSettingsCase(c,p,extents[0],Geometry(0,0),0,-.5,@"half negative chin");
|
||||
CIImage *image=CoordinateImage(extents[1]);NSData *source=Pixels(c,image);
|
||||
PGBeautySettings s=PGBeautyDefaults();s.faceThin=1;s.trackingEnabled=1;NSError *error=nil;
|
||||
NSDictionary *g=Geometry(.9,.15);[p setTrackingData:Archive(g) forTarget:0];
|
||||
PGFaceFrame *frame=[p processImage:image settings:s time:kCMTimeZero error:&error];
|
||||
Check(frame!=nil && [frame.deformationGeometry[0][@"points76"] count]==76,@"offline restoration retains all 76 indexed points");
|
||||
PGFaceProcessor *restored=[PGFaceProcessor new];[restored setTrackingData:[p trackingDataForTarget:0] forTarget:0];
|
||||
Check([Pixels(c,[restored processImage:image settings:s time:kCMTimeZero error:&error].image) isEqualToData:Pixels(c,frame.image)],@"76-point geometry survives archive round trip with identical pixels");
|
||||
NSData *imageWarp=Pixels(c,[p warpImage:frame.colorMask geometry:@[] settings:s extent:image.extent]);
|
||||
Check(imageWarp.length==source.length,@"warped support masks retain dimensions");
|
||||
// Apply the same coordinate field to an independent scalar mask ramp.
|
||||
CIImage *mask=[image imageByApplyingFilter:@"CIColorMatrix" withInputParameters:@{@"inputRVector":[CIVector vectorWithX:1 Y:0 Z:0 W:0],@"inputGVector":[CIVector vectorWithX:1 Y:0 Z:0 W:0],@"inputBVector":[CIVector vectorWithX:1 Y:0 Z:0 W:0]}];
|
||||
NSData *maskData=Pixels(c,[p warpImage:mask geometry:@[g] settings:s extent:image.extent]),*frameData=Pixels(c,frame.image);
|
||||
const float *m=maskData.bytes,*v=frameData.bytes;
|
||||
BOOL masksAgree=YES;for(NSUInteger i=0;i<source.length/sizeof(float);i+=4)if(fabs(m[i]-v[i])>1e-6){masksAgree=NO;break;}
|
||||
Check(masksAgree,@"scalar support masks and RGB use the same source coordinates");
|
||||
for(NSUInteger mode=0;mode<4;mode++) {
|
||||
NSMutableDictionary *bad=[g mutableCopy];
|
||||
if(mode==0)[bad removeObjectForKey:@"points76"];
|
||||
if(mode==1)bad[@"points76"]=[g[@"points76"] subarrayWithRange:NSMakeRange(0,75)];
|
||||
if(mode==2){NSMutableArray *a=[g[@"points76"] mutableCopy];a[65]=@[@(NAN),@.5];bad[@"points76"]=a;}
|
||||
if(mode==3){NSMutableArray *a=[g[@"points76"] mutableCopy];a[65]=@[@9,@.5];bad[@"points76"]=a;}
|
||||
[p setTrackingData:Archive(bad) forTarget:0];frame=[p processImage:image settings:s time:kCMTimeZero error:&error];
|
||||
Check(frame!=nil && frame.deformationGeometry.count==1,@"legacy/malformed indexed points retain the valid face record");
|
||||
Check([Pixels(c,frame.image) isEqualToData:source],@"legacy/malformed indexed points never trigger a guessed symmetric thin warp");
|
||||
PGBeautySettings chinOnly=s;chinOnly.faceThin=0;chinOnly.faceChin=1;
|
||||
Check([Pixels(c,[p processImage:image settings:chinOnly time:kCMTimeZero error:&error].image) isEqualToData:source],@"legacy/malformed indexed points never trigger guessed bbox chin deformation");
|
||||
PGBeautySettings other=s;other.faceNarrow=.5;
|
||||
Check(![Pixels(c,[p processImage:image settings:other time:kCMTimeZero error:&error].image) isEqualToData:source],@"other controls remain available in legacy archives");
|
||||
}
|
||||
PGRealtimeFaceTracker *tracker=[PGRealtimeFaceTracker new];PGBeautySettings live=PGBeautyDefaults();
|
||||
NSDictionary *a=[tracker resultForGeometry:@[Geometry(0,0)] settings:live time:kCMTimeZero extent:image.extent];
|
||||
NSDictionary *b=[tracker resultForGeometry:@[Geometry(.2,0)] settings:live time:CMTimeMake(1,25) extent:image.extent];
|
||||
Check([a[@"output"][0][@"points76"] count]==76 && [b[@"output"][0][@"points76"] count]==76,@"realtime stabilization retains the complete indexed point layout");
|
||||
Check([b isEqual:[tracker resultForGeometry:@[Geometry(.2,0)] settings:live time:CMTimeMake(1,25) extent:image.extent]],@"repeated rendering does not smooth the 76-point layout again");
|
||||
NSDictionary *held=[tracker resultForGeometry:@[] settings:live time:CMTimeMake(2,25) extent:image.extent];
|
||||
Check([held[@"output"][0][@"points76"] isEqual:b[@"output"][0][@"points76"]],@"missing detection holds the complete point layout");
|
||||
NSDictionary *turned=Geometry(1.4,0);
|
||||
NSDictionary *back=[tracker resultForGeometry:@[turned] settings:live time:CMTimeMake(3,25) extent:image.extent];
|
||||
Check([back[@"output"][0][@"points76"] isEqual:turned[@"points76"]],@"reacquired side-face points do not blend with a held frontal pose");
|
||||
for(int index=1;index<argc;index++) {@autoreleasepool {
|
||||
CIImage *portrait=[CIImage imageWithContentsOfURL:[NSURL fileURLWithPath:@(argv[index])] options:@{kCIImageColorSpace:NSNull.null}];
|
||||
PGFaceProcessor *real=[PGFaceProcessor new];PGBeautySettings rs=PGBeautyDefaults();rs.faceThin=1;
|
||||
PGFaceFrame *f=[real processImage:portrait settings:rs time:kCMTimeZero error:&error];
|
||||
Check(f!=nil && f.faces.count>0 && [f.deformationGeometry[0][@"points76"] count]==76,@"real Vision detection supplies exactly 76 points to thin deformation");
|
||||
Check(![Pixels(c,f.image) isEqualToData:Pixels(c,portrait)],@"maximum thin changes a real detected portrait");
|
||||
Check([real analyzeImage:portrait settings:rs time:kCMTimeZero error:&error],@"real pose analysis saves the complete geometry");
|
||||
PGFaceProcessor *copy=[PGFaceProcessor new];[copy setTrackingData:[real trackingDataForTarget:0] forTarget:0];rs.trackingEnabled=1;
|
||||
Check([Pixels(c,[copy processImage:portrait settings:rs time:kCMTimeZero error:&error].image) isEqualToData:Pixels(c,f.image)],@"real pose replays from persisted 76 points with identical pixels");
|
||||
AnatomicalChecks(c,real,f.deformationGeometry.firstObject,portrait.extent);
|
||||
}}
|
||||
printf("PASS: %lu slim alignment checks; maximum UV oracle error %.9g\n",(unsigned long)checks,maxOracleError);return 0;
|
||||
}}
|
||||
在新工单中引用
屏蔽一个用户