From 7fa3298e022946e77dfd8cfba1bb5ff2c2a6361f Mon Sep 17 00:00:00 2001 From: dayu <506228475@qq.com> Date: Thu, 22 Oct 2026 16:26:42 +0800 Subject: [PATCH] Fix V-face deformation with lower-jaw landmark pins; pass regression --- README.md | 2 + beatygril/beatygril/Plugin/Info.plist | 8 +- beatygril/beatygril/Plugin/PGFaceProcessor.m | 45 ++- .../beatygril/Wrapper Application/Info.plist | 4 +- docs/analysis/V脸下颌区域修复.md | 42 +++ docs/analysis/rea-vface-evidence.json | 1 + docs/analysis/vface-air.txt | 71 +++++ docs/analysis/vface-findings.json | 258 ++++++++++++++++++ docs/verification/v1.3.1/fixed-core-tests.txt | 5 + docs/verification/v1.3.1/fixed-host-tests.txt | 37 +++ docs/verification/v1.3.1/inspector-tests.txt | 1 + docs/verification/v1.3.1/osc-tests.txt | 21 ++ docs/verification/v1.3.1/real-pose-asan.txt | 2 + docs/verification/v1.3.1/regression.json | 31 +++ docs/项目交接.md | 9 +- docs/验证记录.md | 5 + tests/face_tests.m | 4 +- tests/host_adapter_tests.m | 7 +- tests/slim_tests.m | 49 +++- 19 files changed, 566 insertions(+), 36 deletions(-) create mode 100644 docs/analysis/V脸下颌区域修复.md create mode 100644 docs/analysis/rea-vface-evidence.json create mode 100644 docs/analysis/vface-air.txt create mode 100644 docs/analysis/vface-findings.json create mode 100644 docs/verification/v1.3.1/fixed-core-tests.txt create mode 100644 docs/verification/v1.3.1/fixed-host-tests.txt create mode 100644 docs/verification/v1.3.1/inspector-tests.txt create mode 100644 docs/verification/v1.3.1/osc-tests.txt create mode 100644 docs/verification/v1.3.1/real-pose-asan.txt create mode 100644 docs/verification/v1.3.1/regression.json diff --git a/README.md b/README.md index 20c5a43..39299fe 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ 1.3.0 的磨皮卡片依次提供「磨皮程度、微细锐化、五官保护、非肤色区保护」。两种保护默认 100%;降低五官保护让五官区域参与更多磨皮,降低非肤色区保护则扩大磨皮范围,均不扩大肤色提亮范围。微细锐化在局部支持区域恢复纹理;旧全局磨皮和全局锐化仍保留在更多设置,旧工程的参数与归档继续兼容。迁移证据、实现边界与验证见[新版磨皮参数迁移](docs/analysis/新版磨皮参数迁移.md)。1.3.0(15)APP 已完成签名、公证及正式安装,1636 项自动检查通过;原生宿主效果由用户实测。1.2.0 下载链接仍对应原发行包,本次未更新文章或安装包。 +1.3.1(16)修复 V 脸影响眼区的问题,改为四组下颌轮廓到外唇的局部采样,并对齐新目标的 0.065 强度映射。1901 项自动检查通过;正式交付状态见验证记录。原理与边界见 [V 脸下颌区域修复](docs/analysis/V脸下颌区域修复.md)。 + ## 开发和恢复 需要 Xcode 27.1、FxPlug SDK 4.3.4 与 PluginManager 1.7.4;最低运行系统 macOS 13。正式产物为 arm64/x86_64 通用版本。 diff --git a/beatygril/beatygril/Plugin/Info.plist b/beatygril/beatygril/Plugin/Info.plist index 4f49c40..8f064d5 100644 --- a/beatygril/beatygril/Plugin/Info.plist +++ b/beatygril/beatygril/Plugin/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.3.0 + 1.3.1 CFBundleVersion - 15 + 16 NSHumanReadableCopyright PlugInKit @@ -72,7 +72,7 @@ uuid FB14B0E6-59B2-4836-916F-CD0C559E9241 version - 1.3.0 + 1.3.1 className @@ -84,7 +84,7 @@ uuid E0E432C8-3D71-4ABA-B006-E7F519E06C87 version - 1.3.0 + 1.3.1 protocolNames FxOnScreenControl diff --git a/beatygril/beatygril/Plugin/PGFaceProcessor.m b/beatygril/beatygril/Plugin/PGFaceProcessor.m index c758790..8e5dd9f 100644 --- a/beatygril/beatygril/Plugin/PGFaceProcessor.m +++ b/beatygril/beatygril/Plugin/PGFaceProcessor.m @@ -14,6 +14,8 @@ static const uint8_t PGThinPins[17][2] = { {69,49},{65,49},{68,52},{66,52},{67,47} }; static const uint8_t PGChinPins[3][2] = {{68,38},{67,37},{66,36}}; +// Lower contour to lip anchors, in the 2.4.0 V-face shader's update order. +static const uint8_t PGVFacePins[4][2] = {{69,39},{65,35},{70,26},{64,34}}; // Origin and radius neighbour; all six directions point toward landmark 29. static const uint8_t PGJawPins[6][2] = {{72,71},{62,63},{71,70},{63,64},{70,71},{64,63}}; @@ -347,11 +349,11 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ _lock = [[NSLock alloc] init]; _realtime = [PGRealtimeFaceTracker new]; _targets = [NSMutableArray arrayWithObjects:[NSMutableDictionary dictionary], [NSMutableDictionary dictionary], [NSMutableDictionary dictionary], nil]; - // Thin/chin/jaw/short reconstruct the recovered local coordinate math. - // The other five controls retain this project's analytic approximation. - // Recovered jaw -> chin -> short order follows the reference. The other - // analytic body controls still precede jaw, so the full combined chain - // (narrow/V/cheek/small-head/eyes) is not claimed reference-equivalent. + // Thin/V/chin/jaw/short reconstruct recovered local coordinate math. + // Four other controls retain this project's analytic approximation. + // Recovered V -> jaw -> chin -> short order follows the reference. Other + // analytic body controls still precede V, so the full combined chain + // with narrow/cheek/small-head/eyes is not claimed reference-equivalent. // Each pin changes the sampling coordinate, with only one image resample. NSMutableString *source = [NSMutableString stringWithString: @"vec2 pgThinPin(vec2 q, vec4 control, float ratio) {" @@ -371,8 +373,9 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ " float dy=b.y-a.y; if(abs(dy)<=0.000001) dy=dy<0.0?-0.000001:0.000001;" " if(q.x0.000001) { vec2 q=(p-imageOrigin)/imageSize; q.y=1.0-q.y;"]; + for (NSUInteger i=0; i<4; i++) [source appendFormat:@" q=pgThinPin(q,vPin%lu,vRatio);",(unsigned long)i]; + [source appendString:@" q.y=1.0-q.y; p=imageOrigin+q*imageSize; }" " if(abs(jawRatio)>0.000001) { vec2 q=(p-imageOrigin)/imageSize; q.y=1.0-q.y;"]; for (NSUInteger i=0; i<6; i++) [source appendFormat:@" q=pgJawPin(q,jawPin%lu,jawAnchor,jawRatio);",(unsigned long)i]; [source appendString:@" q.y=1.0-q.y; p=imageOrigin+q*imageSize; }" @@ -527,7 +530,7 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ for (NSDictionary *face in geometry) { CGRect box = PGArrayRect(face[@"box"]), pixels = PGToPixels(box, extent); NSArray *points76 = PGSanitizedPoints76(face[@"points76"]); - float thinRatio = 0, chinRatio = 0, jawRatio = 0, shortRatio = 0; + float thinRatio = 0, chinRatio = 0, jawRatio = 0, shortRatio = 0, vRatio = 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); @@ -541,18 +544,21 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ chinRatio = chin*(chin>0 ? .04 : .02)*gain; jawRatio = PGClamp(s.faceJaw,0,1)*.05*gain; shortRatio = PGClamp(s.faceShort,-1,1)*.25*gain; + // The specified 2.4.0 shader takes the CPU-scaled .065 V ratio + // directly; it no longer applies the older shader's size gain. + vRatio = PGClamp(s.faceV,0,1)*.065; } - BOOL otherControls = s.faceBigEye || s.faceNarrow || s.faceV || s.faceCheekbone || s.faceSmallHead; - // No full topology means no guessed bbox thin/chin/jaw/short deformation. Legacy + BOOL otherControls = s.faceBigEye || s.faceNarrow || s.faceCheekbone || s.faceSmallHead; + // No full topology means no guessed bbox thin/V/chin/jaw/short deformation. Legacy // archives using only those controls remain exact image/mask pass-through. - if (thinRatio <= .000001 && fabsf(chinRatio) <= .000001 && fabsf(jawRatio) <= .000001 && fabsf(shortRatio) <= .000001 && !otherControls) continue; + if (thinRatio <= .000001 && vRatio <= .000001 && fabsf(chinRatio) <= .000001 && fabsf(jawRatio) <= .000001 && fabsf(shortRatio) <= .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)), @(shortRatio), - @(PGClamp(s.faceNarrow, -1, 1)), @(chinRatio), @(PGClamp(s.faceV, 0, 1)), + @(PGClamp(s.faceNarrow, -1, 1)), @(chinRatio), @(vRatio), @(jawRatio), @(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++) { @@ -564,6 +570,15 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ } [arguments addObject:[CIVector vectorWithX:ox Y:oy Z:ax W:ay]]; } + for (NSUInteger i=0; i<4; i++) { + double ox=0,oy=0,ax=0,ay=0; + if (points76) { + NSArray *origin=points76[PGVFacePins[i][0]],*anchor=points76[PGVFacePins[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) { diff --git a/beatygril/beatygril/Wrapper Application/Info.plist b/beatygril/beatygril/Wrapper Application/Info.plist index 85c11dd..b1b98bc 100644 --- a/beatygril/beatygril/Wrapper Application/Info.plist +++ b/beatygril/beatygril/Wrapper Application/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.0 + 1.3.1 CFBundleVersion - 15 + 16 LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright diff --git a/docs/analysis/V脸下颌区域修复.md b/docs/analysis/V脸下颌区域修复.md new file mode 100644 index 0000000..022e0fe --- /dev/null +++ b/docs/analysis/V脸下颌区域修复.md @@ -0,0 +1,42 @@ +# V 脸下颌区域修复 + +2026-10-22,针对用户报告「V 脸作用到眼睛,应作用于下颌」。目标为本机 MSE18V2 2.4.0(20260724150000),产品修复版本为 1.3.1(16)。 + +## 原因与修改 + +1.3.0 仍使用脸框上的宽范围高斯带及纵向位移近似 V 脸。它不依赖真实下颌轮廓,在真实人像上能测到眼区变化。不是面板标签或参数 ID 接错;本次只替换 V 脸几何场。 + +新版目标的 AIR 按 `69→39、65→35、70→26、64→34` 顺序执行四组局部逆向采样。起点都是下颌轮廓,锚点都是外唇。每步使用前一步更新后的 q: + +```text +r = length(anchor − origin) +if r > 0.0001: + weight = clamp(1 − length(q − origin) / r, 0, 1) + q = clamp(q − (anchor − origin) × ratio × weight, 0, 1) +``` + +CPU 函数 `0x10002fb74` 从参数 `+0x90` 读取 V 值,乘位于 `0x100085ea0` 的 double `0.065` 和公共强度,再转 float 写入 shape 结构的 `+8`。新版三个 shader helper 都没有旧版的 `280/尺寸` 增益;本项目 V 使用 `clamp(faceV,0,1)×0.065`,整体混合沿用现有产品链。公共强度各调用方的完整来源尚未追踪,不将整体混合宣称为原版该参数的等价重建。 + +生产实现复用局部 pin 核心,四步严格顺序更新,与图像/四张支持蒙版共享同一坐标场;完整 76 点缺失或不合法时安全直通。Core Image 坐标和关键点一起转换到 Y 向下的全图 UV,结束后一起转回,支持非零图像原点、横竖画幅和歪头。 + +## 静态证据与复现 + +原始 APP、metallib、完整 AIR 与用户帧仅留本机 build,不是构建依赖。源码没有嵌入目标二进制、私有模型或授权逻辑。 + +- 原 APP 嵌套 Mach-O(fat)SHA-256:`577303e0de72e07feb576c882e82382b3f056b023a7e987d990ffb37f6b16109`。 +- arm64 切片 SHA-256:`afc71d0d21fd3c1ca95ca745ddafcd729e36ad95f17d8638009f2e45d9b7e8df`。 +- metallib SHA-256:`856849c45cea2c8f083eeba06a9e318c450f4a56694b438d1a5ba8ef752a0ee0`。 +- `xcrun metal-objdump --metallib --disassemble` 输出 SHA-256:`1d5bd8d5a3917afbb3b4fe1c40b44480760614102685ac826e84ad2e30a450f9`;V 分支 3364–3431 行,四组点索引在 3374、3376、3378、3380 行。 +- 精简 AIR 摘录:[vface-air.txt](vface-air.txt);CPU 字段/系数与分析条件:[vface-findings.json](vface-findings.json);REA CPU 汇编和常量证据:[rea-vface-evidence.json](rea-vface-evidence.json)。REA 操作为只读,未运行原 APP 或原 shader。 + +## 验证 + +修改前隔离 V 测试复现:NASA 人像瞳孔 6 的位移约 0.031493 像素;宿主物理行序测试四种原点组合的 V 输出与独立 upright 参考均不符。原宿主输出两眼区像素变化量分别为 12824、19304,下颌为 195206。 + +修改后瞳孔检查通过;宿主四种原点组合的 V 眼区像素变化量均为 0,下颌为 39246,独立 upright 对照最大差 1/255、均差 0.000124/255。数值场覆盖 4 种画幅、正侧脸和歪头、半强度、小脸不带旧增益、顺序组合、缺点/坏点以及四张蒙版同步。独立的真实解剖位置检查要求下颌有位移且两瞳孔位移小于 0.002 像素,避免只用复制实现的数值公式验收。 + +完整自动回归:1901 项通过(57 Beauty、116 Face、1033 几何、370 Host ASan、98 OSC ASan、227 Inspector ASan)。真实用户帧专项另记交付验证;与基础用例重叠,不相加。正式安装和原生宿主 V 脸视觉验收状态见 [验证记录](../验证记录.md)。 + +## 范围 + +V 脸现在加入已恢复的五项局部控制。瘦脸、下巴、短脸、下颌骨沿用此前版本的尺寸增益;未将新版目标的其他差异混入本次修复。大眼、窄脸、颧骨和小头仍为近似,整条混合控制链的数学、顺序和最终像素尚未全部对齐。静态重建、离屏/接口检查不等于原版 shader 的运行时逐像素比较,也不等于 Motion/FCP 原生视觉验收。 diff --git a/docs/analysis/rea-vface-evidence.json b/docs/analysis/rea-vface-evidence.json new file mode 100644 index 0000000..7677149 --- /dev/null +++ b/docs/analysis/rea-vface-evidence.json @@ -0,0 +1 @@ +{"artifacts":[{"architecture":"arm64","digest":{"sha256":"afc71d0d21fd3c1ca95ca745ddafcd729e36ad95f17d8638009f2e45d9b7e8df"},"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_32b6ee46d4fb5eab563883ba73e35a458d5da21229b8977fddcb4df656517152","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 :0x.","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":"0x100085ea0","bytes_hex":"a4703d0ad7a3b03f9a9999999999a93f","complete":true,"requested_bytes":16,"returned_bytes":16},"operation":"read_bytes","parameters":{"address":"0x100085ea0","length":16},"predicate_type":"rea.analysis","provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"},"raw_result":{"address":"0x100085ea0","bytes_hex":"a4703d0ad7a3b03f9a9999999999a93f","complete":true,"requested_bytes":16,"returned_bytes":16},"subject":{"architecture":"arm64","digest":{"sha256":"afc71d0d21fd3c1ca95ca745ddafcd729e36ad95f17d8638009f2e45d9b7e8df"},"format":"mach-o","local_path":"/Users/dayu/Documents/PaintFCP/build/body-analysis/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_6a88310573f4dfd2f23feef7d12c3f58eba366db1717861770656d55375e07a1","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 :0x.","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":[],"operation":"search_procedures","parameters":{"case_sensitive":false,"mode":"regex","pattern":"faceShape|shapeParams|ShapeParams|PinCoordinate"},"predicate_type":"rea.analysis","provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"},"raw_result":[],"subject":{"architecture":"arm64","digest":{"sha256":"afc71d0d21fd3c1ca95ca745ddafcd729e36ad95f17d8638009f2e45d9b7e8df"},"format":"mach-o","local_path":"/Users/dayu/Documents/PaintFCP/build/body-analysis/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_ee55925daca270c67d312953f9ce9f3ca580e0851e3d12e19e0e26d1a2c8a97c","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 :0x.","Assembly is Ghidra Listing text and does not claim textual equivalence with Hopper output.","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":"0x10002fb74: stp d9, d8, [sp, #-0x30]!\n0x10002fb78: stp x20, x19, [sp, #0x10]\n0x10002fb7c: stp x29, x30, [sp, #0x20]\n0x10002fb80: add x29, sp, #0x20\n0x10002fb84: fmov d8, d0\n0x10002fb88: str xzr, [x8, #0x20]\n0x10002fb8c: movi v0.2D, #0x0\n0x10002fb90: stp q0, q0, [x8]\n0x10002fb94: cbz x0, 0x10002fd38\n0x10002fb98: mov x20, x0\n0x10002fb9c: mov x19, x8\n0x10002fba0: ldrb w8, [x0, #0x172]\n0x10002fba4: cmp w8, #0x1\n0x10002fba8: b.ne 0x10002fd38\n0x10002fbac: bl 0x1000500b0\n0x10002fbb0: cbz w0, 0x10002fd38\n0x10002fbb4: fmov d1, 0x3ff0000000000000\n0x10002fbb8: fcmp d8, d1\n0x10002fbbc: fcsel d2, d1, d8, gt\n0x10002fbc0: fcmp d8, #0.0\n0x10002fbc4: movi d0, #0x0\n0x10002fbc8: fcsel d2, d0, d2, mi\n0x10002fbcc: fcvt s2, d2\n0x10002fbd0: ldr d3, [x20, #0x88]\n0x10002fbd4: fcmp d3, d1\n0x10002fbd8: fcsel d4, d1, d3, gt\n0x10002fbdc: fmov d5, -0x4010000000000000\n0x10002fbe0: fcmp d3, d5\n0x10002fbe4: fcsel d4, d5, d4, mi\n0x10002fbe8: fcmp d4, #0.0\n0x10002fbec: cset w8, gt\n0x10002fbf0: fcvt d3, s2\n0x10002fbf4: ldp d5, d2, [x20, #0x68]\n0x10002fbf8: fcmp d2, d1\n0x10002fbfc: fcsel d6, d1, d2, gt\n0x10002fc00: adrp x9, 0x100086000\n0x10002fc04: ldr d7, [x9, #0xe0]\n0x10002fc08: fmul d6, d6, d7\n0x10002fc0c: fcmp d2, #0.0\n0x10002fc10: fcsel d2, d0, d6, mi\n0x10002fc14: fmul d2, d2, d3\n0x10002fc18: fcvt s2, d2\n0x10002fc1c: add x9, x20, #0x80\n0x10002fc20: ldr q6, [x20, #0x90]\n0x10002fc24: ld1 v5, x9, \n0x10002fc28: adrp x9, 0x100085000\n0x10002fc2c: ldr q7, [x9, #0xe80]\n0x10002fc30: fcmgt v7.2D, v7.2D, v5.2D\n0x10002fc34: fcmlt v16.2D, v6.2D\n0x10002fc38: fmov v17.2D, #0x3ff0000000000000\n0x10002fc3c: fcmgt v18.2D, v5.2D, v17.2D\n0x10002fc40: bit v5.16B, v17.16B, v18.16B\n0x10002fc44: fcmgt v18.2D, v6.2D, v17.2D\n0x10002fc48: bit v6.16B, v17.16B, v18.16B\n0x10002fc4c: adrp x9, 0x100085000\n0x10002fc50: ldr q18, [x9, #0xe90]\n0x10002fc54: fmul v5.2D, v5.2D, v18.2D\n0x10002fc58: adrp x9, 0x100085000\n0x10002fc5c: ldr q18, [x9, #0xea0]\n0x10002fc60: fmul v6.2D, v6.2D, v18.2D\n0x10002fc64: adrp x9, 0x100085000\n0x10002fc68: ldr q18, [x9, #0xeb0]\n0x10002fc6c: bit v5.16B, v18.16B, v7.16B\n0x10002fc70: bic v6.16B, v6.16B, v16.16B\n0x10002fc74: fmul v6.2D, v6.2D, v3.D[0x0]\n0x10002fc78: fmul v5.2D, v5.2D, v3.D[0x0]\n0x10002fc7c: fcvtn v5.2S, v5.2D\n0x10002fc80: fcvtn2 v5.4S, v6.2D\n0x10002fc84: ldr d6, [x20, #0xa0]\n0x10002fc88: fcmp d6, d1\n0x10002fc8c: fcsel d7, d1, d6, gt\n0x10002fc90: adrp x9, 0x100086000\n0x10002fc94: ldr d16, [x9, #0x4e8]\n0x10002fc98: fmul d7, d7, d16\n0x10002fc9c: fcmp d6, #0.0\n0x10002fca0: mov x9, #-0x8000000000000000\n0x10002fca4: fmov d6, x9\n0x10002fca8: adrp x9, 0x100085000\n0x10002fcac: add x9, x9, #0xec0\n0x10002fcb0: ldr d16, x9, w8, , uxtw, #3\n0x10002fcb4: fcsel d6, d6, d7, mi\n0x10002fcb8: fmul d4, d4, d16\n0x10002fcbc: ldr d7, [x20, #0x78]\n0x10002fcc0: add x8, x20, #0xa8\n0x10002fcc4: ld1 v7, x8, \n0x10002fcc8: adrp x8, 0x100085000\n0x10002fccc: ldr q16, [x8, #0xed0]\n0x10002fcd0: fcmgt v16.2D, v16.2D, v7.2D\n0x10002fcd4: fcmgt v18.2D, v7.2D, v17.2D\n0x10002fcd8: bit v7.16B, v17.16B, v18.16B\n0x10002fcdc: adrp x8, 0x100085000\n0x10002fce0: ldr q17, [x8, #0xee0]\n0x10002fce4: fmul v7.2D, v7.2D, v17.2D\n0x10002fce8: adrp x8, 0x100085000\n0x10002fcec: ldr q17, [x8, #0xef0]\n0x10002fcf0: mov v6.D[0x1], v4.D[0x0]\n0x10002fcf4: mov v4.16B, v16.16B\n0x10002fcf8: bsl v4.16B, v17.16B, v7.16B\n0x10002fcfc: fmul v6.2D, v6.2D, v3.D[0x0]\n0x10002fd00: fmul v3.2D, v4.2D, v3.D[0x0]\n0x10002fd04: fcvtn v4.2S, v6.2D\n0x10002fd08: fcvtn2 v4.4S, v3.2D\n0x10002fd0c: stp q5, q4, [x19]\n0x10002fd10: ldr d3, [x20, #0xb0]\n0x10002fd14: fcmp d3, d1\n0x10002fd18: fcsel d1, d1, d3, gt\n0x10002fd1c: adrp x8, 0x100086000\n0x10002fd20: ldr d4, [x8, #0x1a0]\n0x10002fd24: fmul d1, d1, d4\n0x10002fd28: fcmp d3, #0.0\n0x10002fd2c: fcsel d0, d0, d1, mi\n0x10002fd30: fcvt s0, d0\n0x10002fd34: stp s0, s2, [x19, #0x20]\n0x10002fd38: ldp x29, x30, [sp, #0x20]\n0x10002fd3c: ldp x20, x19, [sp, #0x10]\n0x10002fd40: ldp d9, d8, [sp], #0x30\n0x10002fd44: ret","operation":"procedure_assembly","parameters":{"procedure":"0x10002fb74"},"predicate_type":"rea.analysis","provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"},"raw_result":{"limitations":[],"value":"0x10002fb74: stp d9, d8, [sp, #-0x30]!\n0x10002fb78: stp x20, x19, [sp, #0x10]\n0x10002fb7c: stp x29, x30, [sp, #0x20]\n0x10002fb80: add x29, sp, #0x20\n0x10002fb84: fmov d8, d0\n0x10002fb88: str xzr, [x8, #0x20]\n0x10002fb8c: movi v0.2D, #0x0\n0x10002fb90: stp q0, q0, [x8]\n0x10002fb94: cbz x0, 0x10002fd38\n0x10002fb98: mov x20, x0\n0x10002fb9c: mov x19, x8\n0x10002fba0: ldrb w8, [x0, #0x172]\n0x10002fba4: cmp w8, #0x1\n0x10002fba8: b.ne 0x10002fd38\n0x10002fbac: bl 0x1000500b0\n0x10002fbb0: cbz w0, 0x10002fd38\n0x10002fbb4: fmov d1, 0x3ff0000000000000\n0x10002fbb8: fcmp d8, d1\n0x10002fbbc: fcsel d2, d1, d8, gt\n0x10002fbc0: fcmp d8, #0.0\n0x10002fbc4: movi d0, #0x0\n0x10002fbc8: fcsel d2, d0, d2, mi\n0x10002fbcc: fcvt s2, d2\n0x10002fbd0: ldr d3, [x20, #0x88]\n0x10002fbd4: fcmp d3, d1\n0x10002fbd8: fcsel d4, d1, d3, gt\n0x10002fbdc: fmov d5, -0x4010000000000000\n0x10002fbe0: fcmp d3, d5\n0x10002fbe4: fcsel d4, d5, d4, mi\n0x10002fbe8: fcmp d4, #0.0\n0x10002fbec: cset w8, gt\n0x10002fbf0: fcvt d3, s2\n0x10002fbf4: ldp d5, d2, [x20, #0x68]\n0x10002fbf8: fcmp d2, d1\n0x10002fbfc: fcsel d6, d1, d2, gt\n0x10002fc00: adrp x9, 0x100086000\n0x10002fc04: ldr d7, [x9, #0xe0]\n0x10002fc08: fmul d6, d6, d7\n0x10002fc0c: fcmp d2, #0.0\n0x10002fc10: fcsel d2, d0, d6, mi\n0x10002fc14: fmul d2, d2, d3\n0x10002fc18: fcvt s2, d2\n0x10002fc1c: add x9, x20, #0x80\n0x10002fc20: ldr q6, [x20, #0x90]\n0x10002fc24: ld1 v5, x9, \n0x10002fc28: adrp x9, 0x100085000\n0x10002fc2c: ldr q7, [x9, #0xe80]\n0x10002fc30: fcmgt v7.2D, v7.2D, v5.2D\n0x10002fc34: fcmlt v16.2D, v6.2D\n0x10002fc38: fmov v17.2D, #0x3ff0000000000000\n0x10002fc3c: fcmgt v18.2D, v5.2D, v17.2D\n0x10002fc40: bit v5.16B, v17.16B, v18.16B\n0x10002fc44: fcmgt v18.2D, v6.2D, v17.2D\n0x10002fc48: bit v6.16B, v17.16B, v18.16B\n0x10002fc4c: adrp x9, 0x100085000\n0x10002fc50: ldr q18, [x9, #0xe90]\n0x10002fc54: fmul v5.2D, v5.2D, v18.2D\n0x10002fc58: adrp x9, 0x100085000\n0x10002fc5c: ldr q18, [x9, #0xea0]\n0x10002fc60: fmul v6.2D, v6.2D, v18.2D\n0x10002fc64: adrp x9, 0x100085000\n0x10002fc68: ldr q18, [x9, #0xeb0]\n0x10002fc6c: bit v5.16B, v18.16B, v7.16B\n0x10002fc70: bic v6.16B, v6.16B, v16.16B\n0x10002fc74: fmul v6.2D, v6.2D, v3.D[0x0]\n0x10002fc78: fmul v5.2D, v5.2D, v3.D[0x0]\n0x10002fc7c: fcvtn v5.2S, v5.2D\n0x10002fc80: fcvtn2 v5.4S, v6.2D\n0x10002fc84: ldr d6, [x20, #0xa0]\n0x10002fc88: fcmp d6, d1\n0x10002fc8c: fcsel d7, d1, d6, gt\n0x10002fc90: adrp x9, 0x100086000\n0x10002fc94: ldr d16, [x9, #0x4e8]\n0x10002fc98: fmul d7, d7, d16\n0x10002fc9c: fcmp d6, #0.0\n0x10002fca0: mov x9, #-0x8000000000000000\n0x10002fca4: fmov d6, x9\n0x10002fca8: adrp x9, 0x100085000\n0x10002fcac: add x9, x9, #0xec0\n0x10002fcb0: ldr d16, x9, w8, , uxtw, #3\n0x10002fcb4: fcsel d6, d6, d7, mi\n0x10002fcb8: fmul d4, d4, d16\n0x10002fcbc: ldr d7, [x20, #0x78]\n0x10002fcc0: add x8, x20, #0xa8\n0x10002fcc4: ld1 v7, x8, \n0x10002fcc8: adrp x8, 0x100085000\n0x10002fccc: ldr q16, [x8, #0xed0]\n0x10002fcd0: fcmgt v16.2D, v16.2D, v7.2D\n0x10002fcd4: fcmgt v18.2D, v7.2D, v17.2D\n0x10002fcd8: bit v7.16B, v17.16B, v18.16B\n0x10002fcdc: adrp x8, 0x100085000\n0x10002fce0: ldr q17, [x8, #0xee0]\n0x10002fce4: fmul v7.2D, v7.2D, v17.2D\n0x10002fce8: adrp x8, 0x100085000\n0x10002fcec: ldr q17, [x8, #0xef0]\n0x10002fcf0: mov v6.D[0x1], v4.D[0x0]\n0x10002fcf4: mov v4.16B, v16.16B\n0x10002fcf8: bsl v4.16B, v17.16B, v7.16B\n0x10002fcfc: fmul v6.2D, v6.2D, v3.D[0x0]\n0x10002fd00: fmul v3.2D, v4.2D, v3.D[0x0]\n0x10002fd04: fcvtn v4.2S, v6.2D\n0x10002fd08: fcvtn2 v4.4S, v3.2D\n0x10002fd0c: stp q5, q4, [x19]\n0x10002fd10: ldr d3, [x20, #0xb0]\n0x10002fd14: fcmp d3, d1\n0x10002fd18: fcsel d1, d1, d3, gt\n0x10002fd1c: adrp x8, 0x100086000\n0x10002fd20: ldr d4, [x8, #0x1a0]\n0x10002fd24: fmul d1, d1, d4\n0x10002fd28: fcmp d3, #0.0\n0x10002fd2c: fcsel d0, d0, d1, mi\n0x10002fd30: fcvt s0, d0\n0x10002fd34: stp s0, s2, [x19, #0x20]\n0x10002fd38: ldp x29, x30, [sp, #0x20]\n0x10002fd3c: ldp x20, x19, [sp, #0x10]\n0x10002fd40: ldp d9, d8, [sp], #0x30\n0x10002fd44: ret"},"subject":{"architecture":"arm64","digest":{"sha256":"afc71d0d21fd3c1ca95ca745ddafcd729e36ad95f17d8638009f2e45d9b7e8df"},"format":"mach-o","local_path":"/Users/dayu/Documents/PaintFCP/build/body-analysis/MSE18V2-arm64","name":"MSE18V2-arm64"}}],"scenarios":[],"unknowns":[]} \ No newline at end of file diff --git a/docs/analysis/vface-air.txt b/docs/analysis/vface-air.txt new file mode 100644 index 0000000..c754846 --- /dev/null +++ b/docs/analysis/vface-air.txt @@ -0,0 +1,71 @@ +MSE18V2 2.4.0 AIR excerpt; original complete-text line numbers. +SHA256: 1d5bd8d5a3917afbb3b4fe1c40b44480760614102685ac826e84ad2e30a450f9 + +3364: 158: ; preds = %109, %85 +3365: %159 = phi <2 x float> [ %155, %109 ], [ %86, %85 ] +3366: %160 = tail call fast float @air.fast_fabs.f32(float %5) #2 +3367: %161 = fcmp fast ogt float %160, 0x3EB0C6F7A0000000 +3368: br i1 %161, label %162, label %209 +3369: +3370: 162: ; preds = %158 +3371: %163 = bitcast [4 x <2 x i32>]* %17 to i8* +3372: call void @llvm.lifetime.start.p0i8(i64 32, i8* nonnull %163) #10 +3373: %164 = getelementptr inbounds [4 x <2 x i32>], [4 x <2 x i32>]* %17, i64 0, i64 0 +3374: store <2 x i32> , <2 x i32>* %164, align 8 +3375: %165 = getelementptr inbounds [4 x <2 x i32>], [4 x <2 x i32>]* %17, i64 0, i64 1 +3376: store <2 x i32> , <2 x i32>* %165, align 8 +3377: %166 = getelementptr inbounds [4 x <2 x i32>], [4 x <2 x i32>]* %17, i64 0, i64 2 +3378: store <2 x i32> , <2 x i32>* %166, align 8 +3379: %167 = getelementptr inbounds [4 x <2 x i32>], [4 x <2 x i32>]* %17, i64 0, i64 3 +3380: store <2 x i32> , <2 x i32>* %167, align 8 +3381: %168 = insertelement <2 x float> undef, float %5, i64 0 +3382: %169 = shufflevector <2 x float> %168, <2 x float> undef, <2 x i32> zeroinitializer +3383: br label %171 +3384: +3385: 170: ; preds = %205 +3386: call void @llvm.lifetime.end.p0i8(i64 32, i8* nonnull %163) #10 +3387: br label %209 +3388: +3389: 171: ; preds = %205, %162 +3390: %172 = phi <2 x float> [ %159, %162 ], [ %206, %205 ] +3391: %173 = phi i32 [ 0, %162 ], [ %207, %205 ] +3392: %174 = zext i32 %173 to i64 +3393: %175 = getelementptr inbounds [4 x <2 x i32>], [4 x <2 x i32>]* %17, i64 0, i64 %174 +3394: %176 = load <2 x i32>, <2 x i32>* %175, align 8 +3395: %177 = extractelement <2 x i32> %176, i64 0 +3396: %178 = add nsw i32 %177, %2 +3397: %179 = sext i32 %178 to i64 +3398: %180 = getelementptr inbounds %struct.MSSFaceThinPoint, %struct.MSSFaceThinPoint addrspace(2)* %1, i64 %179, i32 0 +3399: %181 = load <2 x float>, <2 x float> addrspace(2)* %180, align 8, !tbaa !74 +3400: %182 = extractelement <2 x i32> %176, i64 1 +3401: %183 = add nsw i32 %182, %2 +3402: %184 = sext i32 %183 to i64 +3403: %185 = getelementptr inbounds %struct.MSSFaceThinPoint, %struct.MSSFaceThinPoint addrspace(2)* %1, i64 %184, i32 0 +3404: %186 = load <2 x float>, <2 x float> addrspace(2)* %185, align 8, !tbaa !74 +3405: %187 = fsub fast <2 x float> %181, %186 +3406: %188 = tail call fast float @air.dot.v2f32(<2 x float> %187, <2 x float> %187) #2 +3407: %189 = tail call fast float @air.fast_sqrt.f32(float %188) #2 +3408: %190 = fcmp fast ugt float %189, 0x3F1A36E2E0000000 +3409: br i1 %190, label %191, label %205 +3410: +3411: 191: ; preds = %171 +3412: %192 = fsub fast <2 x float> %186, %181 +3413: %193 = fsub fast <2 x float> %172, %181 +3414: %194 = tail call fast float @air.dot.v2f32(<2 x float> %193, <2 x float> %193) #2 +3415: %195 = tail call fast float @air.fast_sqrt.f32(float %194) #2 +3416: %196 = fdiv fast float %195, %189 +3417: %197 = fsub fast float 1.000000e+00, %196 +3418: %198 = tail call fast float @air.fast_clamp.f32(float %197, float 0.000000e+00, float 1.000000e+00) #2 +3419: %199 = insertelement <2 x float> undef, float %198, i64 0 +3420: %200 = shufflevector <2 x float> %199, <2 x float> undef, <2 x i32> zeroinitializer +3421: %201 = fmul fast <2 x float> %192, %169 +3422: %202 = fmul fast <2 x float> %201, %200 +3423: %203 = fsub fast <2 x float> %172, %202 +3424: %204 = tail call fast <2 x float> @air.fast_clamp.v2f32(<2 x float> %203, <2 x float> zeroinitializer, <2 x float> ) #2 +3425: br label %205 +3426: +3427: 205: ; preds = %191, %171 +3428: %206 = phi <2 x float> [ %204, %191 ], [ %172, %171 ] +3429: %207 = add nuw nsw i32 %173, 1 +3430: %208 = icmp eq i32 %207, 4 +3431: br i1 %208, label %170, label %171, !llvm.loop !89 diff --git a/docs/analysis/vface-findings.json b/docs/analysis/vface-findings.json new file mode 100644 index 0000000..e723f34 --- /dev/null +++ b/docs/analysis/vface-findings.json @@ -0,0 +1,258 @@ +{ + "authority": "Static shipped Apple AIR and Mach-O byte/assembly inspection; target app and shaders not executed", + "sources": { + "metallib": "/Users/dayu/Downloads/MSE18V2.app/Contents/PlugIns/MSE18V2.pluginkit/Contents/Resources/default.metallib", + "metallib_sha256": "856849c45cea2c8f083eeba06a9e318c450f4a56694b438d1a5ba8ef752a0ee0", + "command": [ + "xcrun", + "metal-objdump", + "--metallib", + "--disassemble", + "/Users/dayu/Downloads/MSE18V2.app/Contents/PlugIns/MSE18V2.pluginkit/Contents/Resources/default.metallib" + ], + "air_sha256": "1d5bd8d5a3917afbb3b4fe1c40b44480760614102685ac826e84ad2e30a450f9", + "binary": "/Users/dayu/Downloads/MSE18V2.app/Contents/PlugIns/MSE18V2.pluginkit/Contents/MacOS/MSE18V2", + "binary_arm64_sha256": "afc71d0d21fd3c1ca95ca745ddafcd729e36ad95f17d8638009f2e45d9b7e8df", + "double065_file_offsets_in_arm64_slice": [ + 548512 + ] + }, + "new_version_vface": { + "struct_field": "MSSFaceShapeParams.vlian offset8; helper parameter5", + "render_params_field": "faceV double offset0x90", + "cpu_function": "0x10002fb74", + "cpu_coefficient_address": "0x100085ea0", + "cpu_coefficient": 0.065, + "cpu_ratio": "float(clamp(faceV,0,1)*0.065*double(float(clamp(commonStrength,0,1))))", + "pin_order": [ + [ + 69, + 39 + ], + [ + 65, + 35 + ], + [ + 70, + 26 + ], + [ + 64, + 34 + ] + ], + "pin_fields": [ + "origin", + "direction/radius anchor" + ], + "coordinate_system": "full-image normalized UV; q and points must share the same vertical axis", + "radius": "length(Panchor-Porigin), unscaled UV metric; skip radius<=0.0001", + "weight": "clamp(1-length(q-Porigin)/radius,0,1)", + "step": "q=clamp(q-(Panchor-Porigin)*ratio*weight,0,1)", + "sequential": true, + "ratio_threshold": 1e-06, + "shader_lines": { + "helper": 3159, + "vface_gate": 3364, + "pins": [ + 3374, + 3376, + 3378, + 3380 + ], + "loop": 3389, + "radius": 3405, + "offset": 3412, + "clamp": 3424, + "count": 3430, + "metadata": 4132 + }, + "order": "thin17 -> narrow16 -> V4 -> jaw6 -> cheek6 -> chin3 -> short -> smallHead -> bigEye", + "helper_copies": [ + { + "start_line": 3159, + "v_pins": [ + [ + 69, + 39 + ], + [ + 65, + 35 + ], + [ + 70, + 26 + ], + [ + 64, + 34 + ] + ], + "has_old_pixel_gain_constant": false + }, + { + "start_line": 6840, + "v_pins": [ + [ + 69, + 39 + ], + [ + 65, + 35 + ], + [ + 70, + 26 + ], + [ + 64, + 34 + ] + ], + "has_old_pixel_gain_constant": false + }, + { + "start_line": 8682, + "v_pins": [ + [ + 69, + 39 + ], + [ + 65, + 35 + ], + [ + 70, + 26 + ], + [ + 64, + 34 + ] + ], + "has_old_pixel_gain_constant": false + } + ] + }, + "version_difference": { + "old_air": "build/pose-analysis/face-thin.air.txt", + "old_gain_lines": [ + 592, + 633 + ], + "old_vface_lines": [ + 830, + 897 + ], + "old_cpu_coefficient_address": "0x10005f9a0", + "coefficient_unchanged": true, + "old_extra_gain": "clamp(280/max(length((P75-P59)*pixelSize),2*length((P67-(P74+P60)/2)*pixelSize),1),1,2.25)", + "new_extra_gain": "Absent from all3 new shader helper copies and the complete new AIR text; CPU helper is plain clamped-control*.065*commonStrength", + "anatomy": "Origin indices64,65,69,70 are lower contour; anchor indices26,34,35,39 are outer lips; eyes0..13 are not controls." + }, + "eye_support_probe": { + "authority": "Derived from existing local public-API Vision probe; no new image coordinates or original plug-in execution", + "settings": { + "vface": 1, + "common_strength": 1, + "coefficient": 0.065, + "pins": [ + [ + 69, + 39 + ], + [ + 65, + 35 + ], + [ + 70, + 26 + ], + [ + 64, + 34 + ] + ] + }, + "samples": [ + { + "seconds": 0, + "minimum_eye_distance_over_pin_radius": 2.86112178668839, + "max_eye_weight": 0, + "max_sequential_eye_displacement_pixels": 0 + }, + { + "seconds": 2, + "minimum_eye_distance_over_pin_radius": 2.7386373955689196, + "max_eye_weight": 0, + "max_sequential_eye_displacement_pixels": 0 + }, + { + "seconds": 4, + "minimum_eye_distance_over_pin_radius": 2.0723834235969316, + "max_eye_weight": 0, + "max_sequential_eye_displacement_pixels": 0 + }, + { + "seconds": 6, + "minimum_eye_distance_over_pin_radius": 2.7994186406360964, + "max_eye_weight": 0, + "max_sequential_eye_displacement_pixels": 0 + }, + { + "seconds": 8, + "minimum_eye_distance_over_pin_radius": 2.720538093620307, + "max_eye_weight": 0, + "max_sequential_eye_displacement_pixels": 0 + }, + { + "seconds": 10, + "minimum_eye_distance_over_pin_radius": 2.7459899006401245, + "max_eye_weight": 0, + "max_sequential_eye_displacement_pixels": 0 + }, + { + "seconds": 12, + "minimum_eye_distance_over_pin_radius": 2.8522571566241655, + "max_eye_weight": 0, + "max_sequential_eye_displacement_pixels": 0 + }, + { + "seconds": 14, + "minimum_eye_distance_over_pin_radius": 2.7849986024852744, + "max_eye_weight": 0, + "max_sequential_eye_displacement_pixels": 0 + }, + { + "seconds": 16, + "minimum_eye_distance_over_pin_radius": 2.86097776991491, + "max_eye_weight": 0, + "max_sequential_eye_displacement_pixels": 0 + }, + { + "seconds": 18, + "minimum_eye_distance_over_pin_radius": 2.4994391012873294, + "max_eye_weight": 0, + "max_sequential_eye_displacement_pixels": 0 + }, + { + "seconds": 20, + "minimum_eye_distance_over_pin_radius": 2.3076941598447127, + "max_eye_weight": 0, + "max_sequential_eye_displacement_pixels": 0 + } + ] + }, + "limits": [ + "No reference shader runtime or pixel comparison performed.", + "The commonStrength caller-source semantics are not fully traced; formula reports the helper input.", + "CPU prechecks/license/module gates are observed but are outside the independently authored geometry reconstruction.", + "Eye support results apply to the11 existing saved Vision samples, not every possible pose.", + "No product sources, app build, host session or Git were changed." + ] +} diff --git a/docs/verification/v1.3.1/fixed-core-tests.txt b/docs/verification/v1.3.1/fixed-core-tests.txt new file mode 100644 index 0000000..1521347 --- /dev/null +++ b/docs/verification/v1.3.1/fixed-core-tests.txt @@ -0,0 +1,5 @@ +Beauty renderer: 57 checks, 0 failures (Metal). +2026-10-22 16:24:20.836 face-tests[22582:8025448] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2 +Face processing: 116 checks passed +2026-10-22 16:24:34.977 slim-tests[22589:8025534] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2 +PASS: 1033 slim alignment checks; maximum UV oracle error 1.09828022e-05 diff --git a/docs/verification/v1.3.1/fixed-host-tests.txt b/docs/verification/v1.3.1/fixed-host-tests.txt new file mode 100644 index 0000000..56c65e2 --- /dev/null +++ b/docs/verification/v1.3.1/fixed-host-tests.txt @@ -0,0 +1,37 @@ +2026-10-22 16:21:55.726 host-tests[22385:8017721] 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 +Anatomy short-positive 0/0 max=2 mean=0.0011072 eye=0/0 chin=285953 inverted=0 +Anatomy short-positive 0/1 max=2 mean=0.0011072 eye=0/0 chin=285953 inverted=0 +Anatomy short-positive 1/0 max=2 mean=0.0011072 eye=0/0 chin=285953 inverted=0 +Anatomy short-positive 1/1 max=2 mean=0.0011072 eye=0/0 chin=285953 inverted=0 +Anatomy short-negative 0/0 max=2 mean=0.0008583 eye=0/0 chin=225648 inverted=0 +Anatomy short-negative 0/1 max=2 mean=0.0008583 eye=0/0 chin=225648 inverted=0 +Anatomy short-negative 1/0 max=2 mean=0.0008583 eye=0/0 chin=225648 inverted=0 +Anatomy short-negative 1/1 max=2 mean=0.0008583 eye=0/0 chin=225648 inverted=0 +Anatomy jaw 0/0 max=1 mean=0.0001974 eye=0/0 chin=12951 inverted=0 +Anatomy jaw 0/1 max=1 mean=0.0001974 eye=0/0 chin=12951 inverted=0 +Anatomy jaw 1/0 max=1 mean=0.0001974 eye=0/0 chin=12951 inverted=0 +Anatomy jaw 1/1 max=1 mean=0.0001974 eye=0/0 chin=12951 inverted=0 +Anatomy v-face 0/0 max=1 mean=0.0001240 eye=0/0 chin=39246 inverted=0 +Anatomy v-face 0/1 max=1 mean=0.0001240 eye=0/0 chin=39246 inverted=0 +Anatomy v-face 1/0 max=1 mean=0.0001240 eye=0/0 chin=39246 inverted=0 +Anatomy v-face 1/1 max=1 mean=0.0001240 eye=0/0 chin=39246 inverted=0 +Legacy fallback upright comparison: max=2 mean=0.0010357 +Host adapter: 370 checks, 0 failures diff --git a/docs/verification/v1.3.1/inspector-tests.txt b/docs/verification/v1.3.1/inspector-tests.txt new file mode 100644 index 0000000..aa77638 --- /dev/null +++ b/docs/verification/v1.3.1/inspector-tests.txt @@ -0,0 +1 @@ +Inspector: 227 checks, 0 failures diff --git a/docs/verification/v1.3.1/osc-tests.txt b/docs/verification/v1.3.1/osc-tests.txt new file mode 100644 index 0000000..36378c7 --- /dev/null +++ b/docs/verification/v1.3.1/osc-tests.txt @@ -0,0 +1,21 @@ +2026-10-22 16:24:26.444 osc-tests[22624:8025751] PGFaceOSC point-write desired=(0.25,0.75) setter=(0.25,0.75) readback=(0.25,0.75) mode=normalized verified=1 +2026-10-22 16:24:26.452 osc-tests[22624:8025751] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(80,180) surface=320x240 origin=0 +2026-10-22 16:24:26.887 osc-tests[22624:8025751] PGFaceOSC point-write desired=(0.8,0.3) setter=(1152,-432) readback=(0.8,0.3) mode=centered-pixels verified=1 +2026-10-22 16:24:26.887 osc-tests[22624:8025751] PGFaceOSC coordinates selection=(0.7,0.4) objectBounds=(-1920,-1080,3840,2160) canvas=(152,78) surface=320x240 origin=0 +2026-10-22 16:24:26.944 osc-tests[22624:8025751] PGFaceOSC point-write desired=(0.8,0.3) setter=(0.8,0.3) readback=(0.12345,0.87654) mode=normalized verified=0 +2026-10-22 16:24:26.944 osc-tests[22624:8025751] PGFaceOSC point-write desired=(0.75,0.25) setter=(0.75,0.25) readback=(0.75,0.25) mode=normalized verified=1 +2026-10-22 16:24:26.944 osc-tests[22624:8025751] PGFaceOSC coordinates selection=(0.75,0.25) objectBounds=(-100,-50,200,100) canvas=(160,60) surface=320x240 origin=0 +2026-10-22 16:24:26.994 osc-tests[22624:8025751] PGFaceOSC point-write desired=(0.625,0.375) setter=(0.625,0.375) readback=(0.625,0.375) mode=normalized verified=1 +2026-10-22 16:24:26.994 osc-tests[22624:8025751] PGFaceOSC coordinates selection=(0.625,0.375) objectBounds=(-100,-50,200,100) canvas=(240,60) surface=320x240 origin=0 +2026-10-22 16:24:27.043 osc-tests[22624:8025751] PGFaceOSC point-write desired=(0.25,0.75) setter=(0.25,0.75) readback=(0.25,0.75) mode=normalized verified=1 +2026-10-22 16:24:27.043 osc-tests[22624:8025751] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(144,180) surface=320x240 origin=0 +2026-10-22 16:24:27.094 osc-tests[22624:8025751] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1 +2026-10-22 16:24:27.094 osc-tests[22624:8025751] PGFaceOSC coordinates selection=(0,1) objectBounds=(-100,-50,200,100) canvas=(100,140) surface=320x240 origin=0 +2026-10-22 16:24:27.144 osc-tests[22624:8025751] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(nan,nan) surface=320x240 origin=0 +2026-10-22 16:24:27.194 osc-tests[22624:8025751] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1 +2026-10-22 16:24:27.194 osc-tests[22624:8025751] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(inf,nan) surface=320x240 origin=0 +2026-10-22 16:24:27.242 osc-tests[22624:8025751] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1 +2026-10-22 16:24:27.242 osc-tests[22624:8025751] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(80,nan) surface=320x240 origin=0 +2026-10-22 16:24:27.304 osc-tests[22624:8025751] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1 +2026-10-22 16:24:27.304 osc-tests[22624:8025751] 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 diff --git a/docs/verification/v1.3.1/real-pose-asan.txt b/docs/verification/v1.3.1/real-pose-asan.txt new file mode 100644 index 0000000..f4da969 --- /dev/null +++ b/docs/verification/v1.3.1/real-pose-asan.txt @@ -0,0 +1,2 @@ +2026-10-22 16:25:35.195 slim-tests[22704:8027100] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2 +PASS: 1561 slim alignment checks; maximum UV oracle error 1.09828022e-05 diff --git a/docs/verification/v1.3.1/regression.json b/docs/verification/v1.3.1/regression.json new file mode 100644 index 0000000..b15cb89 --- /dev/null +++ b/docs/verification/v1.3.1/regression.json @@ -0,0 +1,31 @@ +{ + "status": "passed", + "version": "1.3.1", + "build": "16", + "checks": { + "beauty": 57, + "face": 116, + "geometry": 1033, + "hostASan": 370, + "oscASan": 98, + "inspectorASan": 227 + }, + "total": 1901, + "extraRealPoseASan": { + "checks": 1561, + "inputs": "NASA astronaut plus 11 local user frames; frame images and full Vision points not uploaded", + "overlapsBaseline": true + }, + "maximumUVOracleError": 1.09828022e-05, + "isolatedV": { + "eyeChange": [ + 0, + 0 + ], + "lowerFaceChange": 39246, + "originsCompared": 4, + "uprightMax255": 1, + "uprightMean255": 0.000124 + }, + "nativeVisualQA": "pending user" +} diff --git a/docs/项目交接.md b/docs/项目交接.md index b8b3cf0..b77e87c 100644 --- a/docs/项目交接.md +++ b/docs/项目交接.md @@ -2,6 +2,8 @@ 更新时间:2026-10-22,Asia/Shanghai。项目根目录:`/Users/dayu/Documents/PaintFCP`。 +正在准备 **1.3.1(16)** 的 V 脸下颌修复;1901 项自动检查通过,正式安装尚未替换。 + 当前正式安装的 APP 为 **1.3.0(构建 15)**,已完成通用 Release 编译、正式签名、Apple 公证、票据验证及唯一正式路径安装。新增磨皮参数迁移与 1636 项自动检查已完成,原生效果视觉仍待用户实测。公开文章、PKG 与 GitHub Release 继续对应 **1.2.0**;本次未更新这些发行资料,也未覆盖模板或宣传图。 代码由 **OpenAI + Codex 全程编写,站长仅参与调试和测试**。文章和仓库须保留这个表述,不使用“站长原创代码”等说法。免费版本无需账号、激活码或付费授权;“自动注册”指系统插件注册。 @@ -84,18 +86,19 @@ Xcode 工程:`beatygril/beatygril.xcodeproj`。外层 scheme 为 `Wrapper Appl 本项目几何历史只取当前请求之前的近时记录,原版还存在近未来历史分支;冷启动、热缓存、乱序渲染、同尺寸换源和多人交叉仍有差异边界。旧离线分析字段保留兼容,但当前新面板的渲染统一使用实时自动跟随。 -### 已完成的四项局部算法 +### 已完成的五项局部算法 | 控制项 | 当前实现 | 防止回归的要点 | | --- | --- | --- | | 瘦脸 | 76 点、17 组轮廓到鼻部或瞳孔锚点的局部逆向采样 | 不恢复整脸框水平挤压;保留尺寸增益、组顺序和逐步 clamp | +| V 脸 | `69→39`、`65→35`、`70→26`、`64→34` 四组下颌到外唇局部位移 | 新版 2.4.0 使用 0.065 系数、无旧尺寸增益;两瞳孔保持不动 | | 下巴 | `68→38`、`67→37`、`66→36` 三组局部位移 | 正值上收、负值延伸,保留正负不同系数 | | 下颌骨 | 六组相邻轮廓半径的顺序局部位移 | 半径来自相邻轮廓点,方向锚点为 P29;不能同时求和替代逐步更新 | | 短脸 | P62 至 P72 的下半脸闭合区域,沿 P46−P48 鼻梁轴位移 | 正值收短、负值延长;不是太阳穴区域或脸框纵向压缩 | -这四项共享由实际图像像素尺寸计算的增益。完整合法 76 点缺失时,四项安全直通,不退回猜测的脸框场。图像与磨皮、肤色、牙齿、眼睛蒙版共用几何变形路径,避免效果与蒙版错位。 +此前四项沿用由实际图像像素尺寸计算的增益;V 脸对齐新版目标,不带该额外增益。完整合法 76 点缺失时,五项安全直通,不退回猜测的脸框场。图像与磨皮、肤色、牙齿、眼睛蒙版共用几何变形路径,避免效果与蒙版错位。 -**仍为近似实现的五项:大眼、窄脸、V 脸、颧骨、小头。** 原版完整链为 `thin → narrow → V → jaw → cheek → chin → short → small-head → eyes`,本项目尚未完全恢复整条组合链的数学及顺序。用户单项验收不代表所有参数组合逐像素等价。详细索引、系数和独立数值场验证见[下巴与坐标分析](analysis/下巴与坐标分析.md)、[短脸与下颌骨分析](analysis/短脸与下颌骨分析.md)。 +**仍为近似实现的四项:大眼、窄脸、颧骨、小头。** 原版完整链为 `thin → narrow → V → jaw → cheek → chin → short → small-head → eyes`,本项目尚未完全恢复整条组合链的数学及顺序。用户单项验收不代表所有参数组合逐像素等价。V 脸修复见 [下颌区域分析](analysis/V脸下颌区域修复.md)。详细索引、系数和独立数值场验证见[下巴与坐标分析](analysis/下巴与坐标分析.md)、[短脸与下颌骨分析](analysis/短脸与下颌骨分析.md)。 ### 坐标约定 diff --git a/docs/验证记录.md b/docs/验证记录.md index 198138d..378a8c9 100644 --- a/docs/验证记录.md +++ b/docs/验证记录.md @@ -138,3 +138,8 @@ Motion:独立普通视频项目,源PTS0、1920×1080/25fps/10秒,媒体通 按用户纠正后的截图需求,迁移连续五官保护、非肤色区保护和局部微细锐化;保留旧 bool、全局磨皮/锐化 ID 与旧工程行为。正式签名源码 `2f2c081980dc610ac810f8daf92da8966f310802`。1636 项通过:57 Beauty、116 Face、796 几何、341 宿主适配 ASan、98 OSC ASan、228 Inspector ASan(含 1 项可选 PNG 输出)。保护 0/50/100 像素响应、色彩覆盖隔离、透明度、v1 状态迁移、新控件归档/插值/输入/重置/旁路/松手前发布均有检查;离屏面板已查看。 通用 Release 正式签名、公证 Accepted(`ca96837f-bf33-44fa-8d58-799026ce5c7d`)、票据、双架构严格签名、安装身份、Gatekeeper、PluginKit 正式路径与临时副本清理通过。独立从 origin 克隆签名源码节点,205 个受跟踪文件逐字节匹配且 strict/full fsck 通过。本次没有原生新滑杆/效果视觉验收,也没有生成新 PKG 或更新文章;用户已保存并退出宿主后才执行安装。详见 `verification/v1.3.0/` 与 `analysis/新版磨皮参数迁移.md`。 + + +## 1.3.1(16)V 脸下颌区域修复(构建前) + +移除 V 脸的脸框近似宽带,改为新版目标的四组下颌到外唇 pin,单项映射为 clamp(V,0,1)×0.065,不叠加旧尺寸增益。无合法76点时直通,图像与四张蒙版同场变形。1901项自动检查通过:57 Beauty、116 Face、1033几何、370 Host ASan、98 OSC ASan、227 Inspector ASan。原失败复现、点位/CPU/AIR证据和未对齐范围见 analysis/V脸下颌区域修复.md。另对 NASA 人像和本机用户视频11帧运行1561项几何ASan专项(含重复基础用例,不相加),通过;用户帧和完整点坐标未上传。安装及用户视觉待完成,当前正式APP仍为1.3.0。 diff --git a/tests/face_tests.m b/tests/face_tests.m index 44ae9e4..e91f361 100644 --- a/tests/face_tests.m +++ b/tests/face_tests.m @@ -323,8 +323,8 @@ int main(int argc, const char *argv[]) { *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==2 || i==4 || i==6); - Check(warped != nil && (requiresPoints ? !changedPixels : changedPixels), requiresPoints ? @"recovered thin/chin/short/jaw skip a face with no indexed landmarks" : [@"geometry control changes pixels: " stringByAppendingString:names[i]]); + BOOL requiresPoints=(i==0 || i==2 || i==4 || i==5 || i==6); + Check(warped != nil && (requiresPoints ? !changedPixels : changedPixels), requiresPoints ? @"recovered thin/V/chin/short/jaw 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; diff --git a/tests/host_adapter_tests.m b/tests/host_adapter_tests.m index cb4ca5c..e151260 100644 --- a/tests/host_adapter_tests.m +++ b/tests/host_adapter_tests.m @@ -337,15 +337,16 @@ static void PortraitAnatomyTests(NSString *fixture, CIImage *portrait) { } lowerFace = CGRectInset(lowerFace, -fw*.15, -fh*.15); CGRect upsideDownFace = CGRectMake(box.origin.x*width, box.origin.y*height, fw, fh); - const UInt32 controls[] = {0, PGBigEye, PGChin, PGChin, PGShort, PGShort, PGJaw}; - const double values[] = {0, 1, 1, -1, 1, -1, 1}; - NSArray *names = @[@"default", @"big-eye", @"chin-positive", @"chin-negative", @"short-positive", @"short-negative", @"jaw"]; + const UInt32 controls[] = {0, PGBigEye, PGChin, PGChin, PGShort, PGShort, PGJaw, PGVFace}; + const double values[] = {0, 1, 1, -1, 1, -1, 1, 1}; + NSArray *names = @[@"default", @"big-eye", @"chin-positive", @"chin-negative", @"short-positive", @"short-negative", @"jaw", @"v-face"]; for (NSUInteger effect = 0; effect < names.count; effect++) { PGBeautySettings settings = PGBeautyDefaults(); if (effect == 1) settings.faceBigEye = 1; else if (effect == 2 || effect == 3) settings.faceChin = values[effect]; else if (effect == 4 || effect == 5) settings.faceShort = values[effect]; else if (effect == 6) settings.faceJaw = values[effect]; + else if (effect == 7) settings.faceV = 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; diff --git a/tests/slim_tests.m b/tests/slim_tests.m index fccbde2..4cbde14 100644 --- a/tests/slim_tests.m +++ b/tests/slim_tests.m @@ -28,6 +28,7 @@ static NSDictionary *Geometry(double yaw,double roll) { p[46]=@[@.5,@.68];p[47]=@[@.5,@.60];p[48]=@[@.5,@.52];p[49]=@[@.5,@.45];p[52]=@[@.5,@.42]; p[29]=@[@.5,@.39]; p[36]=@[@.54,@.36];p[37]=@[@.5,@.35];p[38]=@[@.46,@.36]; + p[26]=@[@.42,@.40];p[34]=@[@.58,@.40];p[35]=@[@.57,@.36];p[39]=@[@.43,@.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))]; @@ -79,7 +80,7 @@ static CGPoint ChinOracle(CGPoint pixel,NSDictionary *g,CGRect e,double thin,dou return CGPointMake(q.x,1-q.y); } -// Independent double-precision reconstruction of AIR's jaw and short-face +// Independent double-precision reconstruction of AIR's V, jaw and short-face // branches. This does not call or inspect the production kernel/parameter table. // Jaw's support uses the adjacent contour edge, not the much longer lip vector. // Short-face uses a filled lower-contour polygon and its distance envelope. @@ -108,6 +109,15 @@ static double ReferencePolygonWeight(CGPoint q,CGPoint p[76]) { static CGPoint JawShortOracle(CGPoint pixel,NSDictionary *g,CGRect e,PGBeautySettings s) { CGPoint p[76];for(NSUInteger i=0;i<76;i++)p[i]=ReadPoint(g[@"points76"][i]); double gain=ReferenceSizeGain(p,e);CGPoint q=Oracle(pixel,g,e,s.faceThin);q.y=1-q.y; + // 2.4.0 consumes the CPU's .065 V ratio directly, without the old size gain. + const unsigned int vOrigins[]={69,65,70,64},vAnchors[]={39,35,26,34}; + double vRatio=Clamp(s.faceV,0,1)*.065; + if(vRatio>1e-6)for(NSUInteger i=0;i<4;i++){ + CGPoint a=ReferencePoint(p,vOrigins[i]),b=ReferencePoint(p,vAnchors[i]); + double radius=hypot(b.x-a.x,b.y-a.y);if(radius<=.0001)continue; + double influence=Clamp(1-hypot(q.x-a.x,q.y-a.y)/radius,0,1); + q.x=Clamp(q.x-(b.x-a.x)*vRatio*influence,0,1);q.y=Clamp(q.y-(b.y-a.y)*vRatio*influence,0,1); + } const unsigned int origin[]={72,62,71,63,70,64},neighbour[]={71,63,70,64,71,63}; double jaw=Clamp(s.faceJaw,0,1)*.05*gain; if(fabs(jaw)>1e-6)for(NSUInteger i=0;i<6;i++){ @@ -251,6 +261,21 @@ static void JawShortAnatomicalChecks(CIContext *context,PGFaceProcessor *process } } } +static void VFaceAnatomicalChecks(CIContext *context,PGFaceProcessor *processor,NSDictionary *geometry,CGRect extent) { + CIImage *ramp=CoordinateImage(extent);PGBeautySettings settings=PGBeautyDefaults();settings.faceV=1; + CIImage *output=[processor warpImage:ramp geometry:@[geometry] settings:settings extent:extent]; + NSArray *points=geometry[@"points76"];double lowerMove=0; + for(NSNumber *index in @[@69,@65,@70,@64]) { + CGPoint motion=Displacement(context,ramp,output,ReadPoint(points[index.unsignedIntegerValue])); + lowerMove=fmax(lowerMove,hypot(motion.x,motion.y)); + } + Check(lowerMove>.1,@"V-face moves the actual lower-jaw contour"); + for(NSNumber *index in @[@6,@13]) { + CGPoint motion=Displacement(context,ramp,output,ReadPoint(points[index.unsignedIntegerValue])); + if(hypot(motion.x,motion.y)>=.002)fprintf(stderr,"V-face pupil %u moved %.6f pixels\n",index.unsignedIntValue,hypot(motion.x,motion.y)); + Check(hypot(motion.x,motion.y)<.002,@"isolated V-face leaves the physical pupils unchanged"); + } +} static void JawShortSupportChecks(CIContext *context,PGFaceProcessor *processor) { NSDictionary *g=Geometry(0,0);CGPoint points[76];for(NSUInteger i=0;i<76;i++)points[i]=ReadPoint(g[@"points76"][i]); CGPoint probe=CGPointMake((points[72].x+points[29].x)/2,(points[72].y+points[29].y)/2); @@ -267,16 +292,18 @@ static void JawShortSupportChecks(CIContext *context,PGFaceProcessor *processor) Check(ReferenceSizeGain(points,e)==2.25,@"small-face numerical case reaches the recovered gain cap"); NumericLocalCase(context,processor,e,small,settings,@"small face jaw max gain");settings.faceJaw=0;settings.faceShort=1; NumericLocalCase(context,processor,e,small,settings,@"small face short max gain"); + settings.faceShort=0;settings.faceV=1; + NumericLocalCase(context,processor,e,small,settings,@"small face V excludes legacy size gain"); for(NSUInteger i=0;i<76;i++)points[i]=ReadPoint(g[@"points76"][i]);Check(ReferenceSizeGain(points,CGRectMake(0,0,1600,1600))==1,@"large-face numerical case reaches gain floor"); // Four full process masks must use exactly the same new warp as the image. [processor setTrackingData:Archive(g) forTarget:0];settings=PGBeautyDefaults();settings.trackingEnabled=1; PGFaceFrame *neutral=[processor processImage:ramp settings:settings time:kCMTimeZero error:nil]; - for(NSUInteger mode=0;mode<3;mode++){ - PGBeautySettings shape=settings;shape.faceJaw=mode!=1?1:0;shape.faceShort=mode!=0?-.65:0; + for(NSUInteger mode=0;mode<4;mode++){ + PGBeautySettings shape=settings;shape.faceJaw=(mode==0||mode==2)?1:0;shape.faceShort=(mode==1||mode==2)?-.65:0;shape.faceV=mode==3?1:0; PGFaceFrame *frame=[processor processImage:ramp settings:shape time:kCMTimeZero error:nil]; - Check([Pixels(context,frame.image)isEqualToData:Pixels(context,[processor warpImage:ramp geometry:@[g] settings:shape extent:e])],@"process image uses the new common jaw/short field"); + Check([Pixels(context,frame.image)isEqualToData:Pixels(context,[processor warpImage:ramp geometry:@[g] settings:shape extent:e])],@"process image uses the common V/jaw/short field"); NSArray *before=@[neutral.skinMask,neutral.colorMask,neutral.teethMask,neutral.eyeMask],*after=@[frame.skinMask,frame.colorMask,frame.teethMask,frame.eyeMask]; - for(NSUInteger i=0;i<4;i++)Check([Pixels(context,after[i])isEqualToData:Pixels(context,[processor warpImage:before[i] geometry:@[g] settings:shape extent:e])],@"every image support mask shares the jaw/short coordinate field"); + for(NSUInteger i=0;i<4;i++)Check([Pixels(context,after[i])isEqualToData:Pixels(context,[processor warpImage:before[i] geometry:@[g] settings:shape extent:e])],@"every image support mask shares the V/jaw/short coordinate field"); } } static void IndependentImageLandmarkCheck(NSString *path,NSDictionary *geometry) { @@ -318,16 +345,21 @@ int main(int argc,const char *argv[]) {@autoreleasepool { 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"); - for(NSUInteger e=0;e<4;e++)for(NSUInteger y=0;y<3;y++)for(NSUInteger r=0;r<3;r++)for(NSUInteger mode=0;mode<3;mode++){ - PGBeautySettings local=PGBeautyDefaults();if(mode==0)local.faceJaw=1;else local.faceShort=mode==1?1:-1; + for(NSUInteger e=0;e<4;e++)for(NSUInteger y=0;y<3;y++)for(NSUInteger r=0;r<3;r++)for(NSUInteger mode=0;mode<4;mode++){ + PGBeautySettings local=PGBeautyDefaults();if(mode==0)local.faceJaw=1;else if(mode==3)local.faceV=1;else local.faceShort=mode==1?1:-1; double angle=y==0?0:(y==1?1.4:-1.4); NumericLocalCase(c,p,extents[e],Geometry(angle,roll[r]),local, [NSString stringWithFormat:@"local size%lu yaw%lu roll%lu mode%lu",(unsigned long)e,(unsigned long)y,(unsigned long)r,(unsigned long)mode]); + if(mode==3)VFaceAnatomicalChecks(c,p,Geometry(angle,roll[r]),extents[e]); } PGBeautySettings combined=PGBeautyDefaults();combined.faceThin=.7;combined.faceJaw=.8;combined.faceChin=-.6;combined.faceShort=.75; NumericLocalCase(c,p,extents[2],Geometry(.8,.45),combined,@"ordered thin jaw chin short positive"); combined.faceShort=-.75;combined.faceChin=.7; NumericLocalCase(c,p,extents[1],Geometry(-.8,-.45),combined,@"ordered thin jaw chin short negative"); + combined.faceV=.85; + NumericLocalCase(c,p,extents[3],Geometry(.6,.2),combined,@"thin V jaw chin short combined sequential order"); + PGBeautySettings vHalf=PGBeautyDefaults();vHalf.faceV=.5; + NumericLocalCase(c,p,extents[2],Geometry(-.6,-.4),vHalf,@"half V strength tilted portrait"); combined=PGBeautyDefaults();combined.faceJaw=.5;combined.faceShort=-.5; NumericLocalCase(c,p,extents[1],Geometry(0,0),combined,@"half signed short plus jaw"); JawShortSupportChecks(c,p); @@ -361,6 +393,8 @@ int main(int argc,const char *argv[]) {@autoreleasepool { Check([Pixels(c,[p processImage:image settings:shortOnly time:kCMTimeZero error:&error].image) isEqualToData:source],@"legacy/malformed indexed points skip recovered short-face field exactly"); PGBeautySettings jawOnly=s;jawOnly.faceThin=0;jawOnly.faceJaw=1; Check([Pixels(c,[p processImage:image settings:jawOnly time:kCMTimeZero error:&error].image) isEqualToData:source],@"legacy/malformed indexed points skip recovered jaw field exactly"); + PGBeautySettings vOnly=s;vOnly.faceThin=0;vOnly.faceV=1; + Check([Pixels(c,[p processImage:image settings:vOnly time:kCMTimeZero error:&error].image) isEqualToData:source],@"legacy/malformed indexed points skip recovered V-face without a guessed face-box band"); 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"); } @@ -385,6 +419,7 @@ int main(int argc,const char *argv[]) {@autoreleasepool { 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); JawShortAnatomicalChecks(c,real,f.deformationGeometry.firstObject,portrait.extent); + VFaceAnatomicalChecks(c,real,f.deformationGeometry.firstObject,portrait.extent); IndependentImageLandmarkCheck(@(argv[index]),f.deformationGeometry.firstObject); }} printf("PASS: %lu slim alignment checks; maximum UV oracle error %.9g\n",(unsigned long)checks,maxOracleError);return 0;