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;