5 次代码提交
共修改 30 个文件,包含 1962 行新增和 43 行删除

没有与搜索条件相匹配的文件

+6
查看文件
@@ -22,3 +22,9 @@ xcodebuild -project beatygril/beatygril.xcodeproj -scheme 'Wrapper Application'
工程从 `/Library/Developer/SDKs/FxPlug.sdk` 读取头文件与链接信息,从 `/Library/Developer/Frameworks` 嵌入可运行的 Apple 框架。构建默认使用团队 B3W25FX5H5 的 Developer ID Application 身份;正式交付使用 `python3 scripts/deliver.py` 完成公证、粘附和安装;安装前先关闭 Motion 与 FCP,脚本在检测到宿主仍运行时保留原安装版并停止。原生验收完成后继续脚本的清理阶段,清除临时注册与 APP 副本。
私有源码仓库:`https://dayu.media/git/dayu/paintfcp-fxplug.git`。恢复入口见 [RECOVERY.md](RECOVERY.md),当前验证记录见 [验证记录](docs/验证记录.md)。原始模板保存在 `baseline-20261021` 标签。
## 1.0.5 瘦脸修复
瘦脸现固定使用Vision revision 3的76点配置,以17组轮廓到鼻部/瞳孔锚点的局部逆向采样替代脸框水平挤压,包含原版尺寸增益与滑块强度映射。完整点阵贯通实时稳定与离线存储;旧分析记录没有完整76点时跳过瘦脸,重新分析可补齐。其他脸型项仍使用原有近似实现,不能将本次对齐扩张为全部脸型逐像素等价。
离屏验证新增224项:含48组脸部投影/尺寸/歪头与独立17-pin数值场对照、数据恢复和缺点保护;完整回归604项通过。Motion/FCP界面由用户自测。可运行 `python3 scripts/deliver.py --wait-for-hosts --user-tests`,签名公证后只读等待宿主自然退出,安装验证与清理无需操作宿主界面。
+13 -1
查看文件
@@ -12,7 +12,7 @@
```sh
git clone https://dayu.media/git/dayu/paintfcp-fxplug.git
cd paintfcp-fxplug
git switch --detach v1.0.4
git switch --detach v1.0.5
```
需要继续编辑时,基于该提交创建新分支。安装 Xcode 与 Apple FxPlug 4 SDK;盘点版本和构建入口见 README.md。SDK/framework 为 Apple 外部开发依赖,应重新安装匹配版本。证书、私钥和公证凭据需从独立加密备份恢复或重新申请;Git 不包含它们。
@@ -36,3 +36,15 @@ git switch --detach v1.0.4
FCP模板从 `Templates/大鱼人像美化/` 恢复,将 `.moef` 和 `Drop Zone.tiff` 一起放入 `~/Movies/Motion Templates.localized/Effects.localized/大鱼/大鱼人像美化/`;只放 `.moef` 会缺少标准效果源输入资源。正式APP仅安装在 `/Library/Plug-Ins/FxPlug/beatygril.app`,不是从工作树或构建目录运行。没有LFS或子模块,必要工程资源、NASA测试人像及配套模板资源均在Git中。
用户用于宿主验收的外置卷视频及build内的验收工程/截图属于测试素材与可重建产物,不属于产品构建依赖,也没有远程上传。原MSE18V2 APP与原版MS模板仅为只读参考,不作为运行依赖。远端恢复记录及实际宿主测试范围见 `docs/验证记录.md`。
## 瘦脸与侧脸问题的分析节点
`v1.0.4`之后的分析提交包含`docs/analysis/脸型变形与转脸分析.md`、`slim-pose-report.json`、REA证据包及独立Vision离线检测工具`scripts/inspect-face-geometry.m`。产品源码与安装版没有因该分析改变;已查明的问题仍存在于1.0.4。复现检测工具的依赖与命令见分析文档,用户视频、完整人脸坐标和帧图保留本地,不是产品恢复依赖。
## 1.0.5 当前恢复入口
当前发布标签为 `v1.0.5`,APP版本1.0.5、build7。签名构建的完整源码SHA为 `abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964`;最终标签还包含交付脚本恢复修正及验证记录,产品源码与该SHA一致。该节点已从独立远端克隆恢复,69文件及9项必要资源完整、389项核心测试通过。主工作区另通过604项完整回归及同一用户视频11帧的264项瘦脸ASan场景。
从最终标签恢复后,关闭Motion/FCP,运行 `python3 scripts/deliver.py --wait-for-hosts --user-tests` 可完成签名公证安装、只读宿主退出等待、静态发现及副本清理;它不会操作宿主界面。若上次因非产品错误在Accepted且stapled后停止,可加 `--resume-prepared`,脚本必须重新核对签名CDHash、版本、票据及产品源码未变才会继续。缺少原staging或回执时应完整重新构建,不能伪造回执。
1.0.5瘦脸需要完整76点;旧离线记录缺该字段时保留其余功能并跳过瘦脸。关闭使用跟踪数据可使用当前帧自动76点,或重新分析生成新记录。没有引入原版metallib或CoreML模型,运行只依赖本产品及Apple系统框架。最终安装、配套模板和扫描范围见验证记录。
+3 -1
查看文件
@@ -1,6 +1,6 @@
# 大鱼人像美化 FCP 效果模板
版本:1.0.4。滤镜 UUID:`FB14B0E6-59B2-4836-916F-CD0C559E9241`。模板必须与已安装的大鱼人像美化 FxPlug APP 配套使用。
版本:1.0.5。滤镜 UUID:`FB14B0E6-59B2-4836-916F-CD0C559E9241`。模板必须与已安装的大鱼人像美化 FxPlug APP 配套使用。
## 默认数值
@@ -31,3 +31,5 @@ Apple项目、群组、Effect Source和滤镜节点骨架来源于用户自有
黑屏修复恢复标准效果源clip/footage和Source Media关联,并随包加入标准`Drop Zone.tiff`,保留49个插件参数、48个发布入口与Type=3/Drop Zone=1。1.0.4同时包含独立的提亮蒙版与选脸坐标修复。该占位图从Apple Motion的Ozone.framework标准资源复制,实际运行只引用本模板内的相对副本。图像SHA256为 `a7f8e3a2a431dbb3ce9e777040bb55e701cfe9ab9632e8cbb10359732bc8692e`,420128字节。
本次已经静态核验Source Media→clip→相对文件链存在、文件字节一致、48项发布仍解析、隐藏数据未发布和保存选择状态;修复后已在FCP独立验收项目新增实例,确认输入替换、实际人像和提亮画面正常;Mix=0逐字节直通由宿主适配器IOSurface回归测试覆盖,未将其混称为FCP截图逐像素核验。
1.0.5将瘦脸改为76点驱动的17组局部位移,供用户在Motion/FCP自测最大瘦脸与转脸。旧跟踪记录若没有全点阵,需重新分析后才有瘦脸,或关闭“使用跟踪数据”采用当前帧自动检测。
+11 -3
查看文件
@@ -1,6 +1,6 @@
{
"version": "1.0.4",
"sha256": "d234d504eba9e6d37b910b622a46c92458e658c5bbdad2caf5831c2a1463a3e2",
"version": "1.0.5",
"sha256": "ba08de6916cd837b3733aa0c6fcb78f2e8224431d25e1310089ae67d18445f2d",
"filterUUID": "FB14B0E6-59B2-4836-916F-CD0C559E9241",
"publishedFunctionalControls": 46,
"informationalControls": 2,
@@ -27,6 +27,14 @@
"date": "2026-10-21",
"FCP": "Restored input-chain template shows supplied real video; effect parameters and forward analysis responded in independent QA project.",
"Motion": "1.0.4 installed formal app loaded the real-video QA project and rendered brightness/geometry with a primary box at different playhead positions; offline tracking disabled.",
"scope": "Original input-chain/brightness native regression passed; 1.0.4 FCP automatic-follow and explicit OSC native click tests remain pending because user took over Motion/FCP self-testing. Offline 380 checks passed; no exhaustive occlusion/identity equivalence claim."
"scope": "1.0.5 native visual QA is reserved for user self-testing; no Motion/FCP UI operations in the slimming repair. Historical input/brightness validation remains scoped to prior releases.",
"version105": "Formal 1.0.5 (7) signed/notarized/stapled/installed and PluginKit formal path verified. User closed hosts before replacement; no Motion/FCP UI operated. Native visual testing pending user."
},
"slimValidation": {
"offlineFullChecks": 604,
"slimChecks": 224,
"extraRealVideoASanChecks": 264,
"sampledVideoFrames": 11,
"maxUVOracleError": 1.09828022e-05
}
}
@@ -46,7 +46,7 @@
<flags>1</flags>
</template>
<build />
<description>大鱼人像美化 1.0.4:人像磨皮、肤色、脸型、五官保护和三目标人脸跟踪。模板中频磨皮默认为50%。</description>
<description>大鱼人像美化 1.0.5:人像磨皮、肤色、脸型、五官保护和三目标人脸跟踪。模板中频磨皮默认为50%。</description>
<scene>
<sceneSettings>
<width>1920</width>
@@ -298,7 +298,7 @@
<parameter name="Width" id="313" flags="8589934610" default="1" value="1920" />
<parameter name="Height" id="314" flags="8589934610" default="1" value="1080" />
</parameter>
<filter name="大鱼人像美化" id="510000005" factoryID="6" pluginUUID="FB14B0E6-59B2-4836-916F-CD0C559E9241" pluginVersion="1.0.4" pluginName="大鱼人像美化" pluginDynamicParams="0">
<filter name="大鱼人像美化" id="510000005" factoryID="6" pluginUUID="FB14B0E6-59B2-4836-916F-CD0C559E9241" pluginVersion="1.0.5" pluginName="大鱼人像美化" pluginDynamicParams="0">
<timing in="0 1 1 0" out="299 30 1 0" offset="0 1 1 0" />
<baseFlags>8589934608</baseFlags>
<parameter name="磨皮" id="100" flags="8589938704">
+4 -4
查看文件
@@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.0.4</string>
<string>1.0.5</string>
<key>CFBundleVersion</key>
<string>6</string>
<string>7</string>
<key>NSHumanReadableCopyright</key>
<string></string>
<key>PlugInKit</key>
@@ -72,7 +72,7 @@
<key>uuid</key>
<string>FB14B0E6-59B2-4836-916F-CD0C559E9241</string>
<key>version</key>
<string>1.0.4</string>
<string>1.0.5</string>
</dict>
<dict>
<key>className</key>
@@ -84,7 +84,7 @@
<key>uuid</key>
<string>E0E432C8-3D71-4ABA-B006-E7F519E06C87</string>
<key>version</key>
<string>1.0.4</string>
<string>1.0.5</string>
<key>protocolNames</key>
<array>
<string>FxOnScreenControl</string>
+112 -14
查看文件
@@ -6,6 +6,13 @@
#include <float.h>
static NSString * const PGFaceErrorDomain = @"media.dayu.PaintFCP.Face";
// Contour origin / internal anchor, in the reference's recovered pin order.
// The unused middle entry of each reference triplet is intentionally omitted.
static const uint8_t PGThinPins[17][2] = {
{75,6},{59,13},{74,47},{60,47},{73,47},{61,47},
{72,48},{62,48},{71,48},{63,48},{70,49},{64,49},
{69,49},{65,49},{68,52},{66,52},{67,47}
};
@interface PGFaceFrame ()
@property (nonatomic, copy, readwrite) NSArray<VNFaceObservation *> *faces;
@@ -63,14 +70,42 @@ static NSArray *PGRegion(VNFaceLandmarkRegion2D *region, CGRect box) {
}
return points;
}
// This optional field has a fixed topology. Never remove an invalid point and
// shift the remaining indices: the local pins refer to exact Vision 76 indices.
static NSArray *PGSanitizedPoints76(id value) {
if (![value isKindOfClass:NSArray.class] || [value count] != 76) return nil;
NSMutableArray *points = [NSMutableArray arrayWithCapacity:76];
for (id point in value) {
if (![point isKindOfClass:NSArray.class] || [point count] != 2) return nil;
for (id coordinate in point) {
if (![coordinate isKindOfClass:NSNumber.class] || !isfinite([coordinate doubleValue]) || fabs([coordinate doubleValue]) > 2) return nil;
}
[points addObject:[point copy]];
}
return [points copy];
}
static NSArray *PGFullPoints76(VNFaceLandmarkRegion2D *region, CGRect box) {
if (region.pointCount != 76 || !region.normalizedPoints) return nil;
const CGPoint *raw = region.normalizedPoints;
NSMutableArray *points = [NSMutableArray arrayWithCapacity:76];
for (NSUInteger i=0; i<76; i++) {
if (!isfinite(raw[i].x) || !isfinite(raw[i].y)) return nil;
[points addObject:@[@(box.origin.x+raw[i].x*box.size.width),
@(box.origin.y+raw[i].y*box.size.height)]];
}
return PGSanitizedPoints76(points);
}
static NSDictionary *PGGeometry(VNFaceObservation *observation) {
CGRect box = observation.boundingBox;
VNFaceLandmarks2D *lm = observation.landmarks;
return @{@"box": PGRectArray(box),
NSMutableDictionary *geometry = [@{@"box": PGRectArray(box),
@"leftEye": PGRegion(lm.leftEye, box), @"rightEye": PGRegion(lm.rightEye, box),
@"leftBrow": PGRegion(lm.leftEyebrow, box), @"rightBrow": PGRegion(lm.rightEyebrow, box),
@"nose": PGRegion(lm.nose, box), @"lips": PGRegion(lm.outerLips, box),
@"mouth": PGRegion(lm.innerLips, box), @"contour": PGRegion(lm.faceContour, box)};
@"mouth": PGRegion(lm.innerLips, box), @"contour": PGRegion(lm.faceContour, box)} mutableCopy];
NSArray *points = PGFullPoints76(lm.allPoints, box);
if (points) geometry[@"points76"] = points;
return [geometry copy];
}
static NSDictionary *PGSanitizedGeometry(id value) {
if (![value isKindOfClass:NSDictionary.class]) return nil;
@@ -88,6 +123,10 @@ static NSDictionary *PGSanitizedGeometry(id value) {
}
result[name] = points;
}
// Old archives and partially detected faces remain usable by the other
// controls. Missing or malformed full topology only disables local thinning.
NSArray *points76 = PGSanitizedPoints76(value[@"points76"]);
if (points76) result[@"points76"] = points76;
return [result copy];
}
static CGRect PGRegionBounds(NSDictionary *geometry, NSString *name, CGRect fallback) {
@@ -133,7 +172,7 @@ static CGPoint PGRemapPoint(NSArray *point, CGRect old, CGRect current) {
return CGPointMake(current.origin.x+([point[0] doubleValue]-old.origin.x)*current.size.width/old.size.width,
current.origin.y+([point[1] doubleValue]-old.origin.y)*current.size.height/old.size.height);
}
static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previous, double dt) {
static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previous, double dt, BOOL stabilizePoints76) {
CGRect old=PGArrayRect(previous[@"box"]),box=PGArrayRect(current[@"box"]);
if(!PGValidRect(old) || !PGValidRect(box))return current;
// Independent stabilization: translate/scale old landmarks into the current
@@ -160,6 +199,26 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
}
stable[name]=[points copy];
}
NSArray *current76 = PGSanitizedPoints76(current[@"points76"]);
NSArray *previous76 = PGSanitizedPoints76(previous[@"points76"]);
if (current76) {
if (stabilizePoints76 && previous76) {
NSMutableArray *points = [NSMutableArray arrayWithCapacity:76];
for (NSUInteger i=0; i<76; i++) {
CGPoint p = PGRemapPoint(previous76[i], old, box);
if (!axisHold) {
p.x += ([current76[i][0] doubleValue]-p.x)*weight;
p.y += ([current76[i][1] doubleValue]-p.y)*weight;
}
[points addObject:@[@(p.x),@(p.y)]];
}
stable[@"points76"] = [points copy];
} else {
// Reacquired detections and changes from/to incomplete topology do
// not borrow a held face's old projected pose or invent missing points.
stable[@"points76"] = current76;
}
}
return [stable copy];
}
@@ -240,7 +299,7 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
else if(index>=0) {
NSDictionary *current=raw[(NSUInteger)index];
double dt=reference?CMTimeGetSeconds(CMTimeSubtract(time,PGRecordTime(reference))):0;
output=@[previous?PGStableGeometry(current,previous,dt):current];
output=@[previous?PGStableGeometry(current,previous,dt,![reference[@"held"] boolValue]):current];
} else {output=prior?:@[];held=output.count>0;}
NSMutableDictionary *record=[@{@"context":context,@"scope":scope,@"key":key?:@"invalid",@"value":@(time.value),@"scale":@(time.timescale),@"epoch":@(time.epoch),
@"seedX":@(seedX),@"seedY":@(seedY),@"raw":raw,@"output":output,@"index":@(index),@"held":@(held),@"referenceKey":reference[@"key"]?:@""} mutableCopy];
@@ -284,11 +343,22 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
_lock = [[NSLock alloc] init];
_realtime = [PGRealtimeFaceTracker new];
_targets = [NSMutableArray arrayWithObjects:[NSMutableDictionary dictionary], [NSMutableDictionary dictionary], [NSMutableDictionary dictionary], nil];
// Analytic, feathered sampling deformation authored for this project.
// Positive short-face shortens the lower face; positive chin lengthens it.
// This is an independent approximation, not the reference vendor's warp.
NSString *source = @"kernel vec2 pgFaceWarp(vec2 c, vec2 size, vec2 le, vec2 re, vec2 er, float thin, float bigEye, float shortFace, float narrow, float chin, float vFace, float jaw, float cheek, float smallHead, float headRange) {"
" vec2 p = destCoord(); vec2 n = (p-c)/size;"
// Local thinning reconstructs the recovered 17-pin coordinate math.
// The other eight controls retain this project's analytic approximation.
// Each pin changes the sampling coordinate, with only one image resample.
NSMutableString *source = [NSMutableString stringWithString:
@"vec2 pgThinPin(vec2 q, vec4 control, float ratio) {"
" vec2 direction=control.zw-control.xy; float radius=length(direction);"
" if(radius<=0.0001) return q;"
" float weight=clamp(1.0-length(q-control.xy)/radius,0.0,1.0);"
" return clamp(q-direction*ratio*weight,vec2(0.0),vec2(1.0)); }"
" kernel vec2 pgFaceWarp(vec2 c, vec2 size, vec2 le, vec2 re, vec2 er, float thinRatio, float bigEye, float shortFace, float narrow, float chin, float vFace, float jaw, float cheek, float smallHead, float headRange, vec2 imageOrigin, vec2 imageSize"];
for (NSUInteger i=0; i<17; i++) [source appendFormat:@", vec4 pin%lu",(unsigned long)i];
[source appendString:@") { vec2 p=destCoord();"
" if(thinRatio>0.000001) { vec2 q=(p-imageOrigin)/imageSize; q.y=1.0-q.y;"];
for (NSUInteger i=0; i<17; i++) [source appendFormat:@" q=pgThinPin(q,pin%lu,thinRatio);",(unsigned long)i];
[source appendString:@" q.y=1.0-q.y; p=imageOrigin+q*imageSize; }"
" vec2 n = (p-c)/size;"
" float body = 1.0-smoothstep(0.38,0.76,length(n*vec2(1.0,0.82)));"
" float lower = (1.0-smoothstep(-0.12,0.27,n.y))*body;"
" float sides = smoothstep(0.08,0.32,abs(n.x));"
@@ -296,12 +366,12 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
" float ch=(n.y-0.02)/0.19; float cheekBand = exp(-ch*ch)*body;"
" float head = 1.0-smoothstep(0.38,0.75+0.32*headRange,length(n*vec2(0.9,0.65)));"
" p += (p-c)*smallHead*0.18*head;"
" p.x += size.x*n.x*(narrow*0.30*body + thin*0.26*lower + vFace*0.24*jawBand + jaw*0.20*jawBand + cheek*0.18*cheekBand)*sides;"
" p.x += size.x*n.x*(narrow*0.30*body + vFace*0.24*jawBand + jaw*0.20*jawBand + cheek*0.18*cheekBand)*sides;"
" float cn=n.x/0.30; p.y += size.y*(-shortFace*0.16*lower + chin*0.13*exp(-cn*cn)*jawBand - vFace*0.025*lower);"
" vec2 dl = (p-le)/er; float wl = 1.0-smoothstep(0.0,1.0,length(dl));"
" p -= (p-le)*bigEye*0.33*wl*wl;"
" vec2 dr = (p-re)/er; float wr = 1.0-smoothstep(0.0,1.0,length(dr));"
" p -= (p-re)*bigEye*0.33*wr*wr; return p; }";
" p -= (p-re)*bigEye*0.33*wr*wr; return p; }"];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
_warp = [CIWarpKernel kernelWithString:source];
@@ -320,6 +390,8 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
CIImage *prepared = [image imageByApplyingTransform:CGAffineTransformMakeTranslation(-extent.origin.x, -extent.origin.y)];
prepared = [prepared imageByApplyingTransform:CGAffineTransformMakeScale(scale, scale)];
VNDetectFaceLandmarksRequest *request = [[VNDetectFaceLandmarksRequest alloc] init];
request.revision = VNDetectFaceLandmarksRequestRevision3;
request.constellation = VNRequestFaceLandmarksConstellation76Points;
// An image request handler is intentionally local to one invocation. A shared
// sequence handler would make parallel / reverse render order observable.
VNImageRequestHandler *handler = [[VNImageRequestHandler alloc] initWithCIImage:prepared options:@{}];
@@ -418,14 +490,40 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
CIImage *result = image;
for (NSDictionary *face in geometry) {
CGRect box = PGArrayRect(face[@"box"]), pixels = PGToPixels(box, extent);
NSArray *points76 = PGSanitizedPoints76(face[@"points76"]);
float thinRatio = 0;
if (points76) {
double width = hypot(([points76[75][0] doubleValue]-[points76[59][0] doubleValue])*extent.size.width,
([points76[75][1] doubleValue]-[points76[59][1] doubleValue])*extent.size.height);
double midX = ([points76[74][0] doubleValue]+[points76[60][0] doubleValue])*.5;
double midY = ([points76[74][1] doubleValue]+[points76[60][1] doubleValue])*.5;
double height = 2*hypot(([points76[67][0] doubleValue]-midX)*extent.size.width,
([points76[67][1] doubleValue]-midY)*extent.size.height);
double gain = fmin(2.25,fmax(1,280/fmax(1,fmax(width,height))));
thinRatio = PGClamp(s.faceThin,0,1)*.025*gain;
}
BOOL otherControls = s.faceBigEye || s.faceShort || s.faceNarrow || s.faceChin || s.faceV || s.faceJaw || s.faceCheekbone || s.faceSmallHead;
// No full topology means no guessed bbox thinning. In particular, legacy
// archives with thinning alone remain an exact image/mask pass-through.
if (thinRatio <= .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));
NSArray *arguments = @[PGVector(PGCenter(pixels)), [CIVector vectorWithX:pixels.size.width Y:pixels.size.height],
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)],
@(PGClamp(s.faceThin, 0, 1)), @(PGClamp(s.faceBigEye, 0, 1)), @(PGClamp(s.faceShort, -1, 1)),
@(thinRatio), @(PGClamp(s.faceBigEye, 0, 1)), @(PGClamp(s.faceShort, -1, 1)),
@(PGClamp(s.faceNarrow, -1, 1)), @(PGClamp(s.faceChin, -1, 1)), @(PGClamp(s.faceV, 0, 1)),
@(PGClamp(s.faceJaw, 0, 1)), @(PGClamp(s.faceCheekbone, 0, 1)), @(PGClamp(s.faceSmallHead, 0, 1)), @(PGClamp(s.faceSmallHeadRange, 0, 1))];
@(PGClamp(s.faceJaw, 0, 1)), @(PGClamp(s.faceCheekbone, 0, 1)), @(PGClamp(s.faceSmallHead, 0, 1)), @(PGClamp(s.faceSmallHeadRange, 0, 1)),
[CIVector vectorWithX:extent.origin.x Y:extent.origin.y], [CIVector vectorWithX:extent.size.width Y:extent.size.height]] mutableCopy];
for (NSUInteger i=0; i<17; i++) {
double ox=0,oy=0,ax=0,ay=0;
if (points76) {
NSArray *origin=points76[PGThinPins[i][0]],*anchor=points76[PGThinPins[i][1]];
ox=[origin[0] doubleValue];oy=1-[origin[1] doubleValue];
ax=[anchor[0] doubleValue];ay=1-[anchor[1] doubleValue];
}
[arguments addObject:[CIVector vectorWithX:ox Y:oy Z:ax W:ay]];
}
CIImage *input = [result imageByClampingToExtent];
result = [_warp applyWithExtent:extent roiCallback:^CGRect(int index, CGRect rect) { (void)index; (void)rect; return extent; } inputImage:input arguments:arguments];
if (!result) return nil;
@@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.4</string>
<string>1.0.5</string>
<key>CFBundleVersion</key>
<string>6</string>
<string>7</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
@@ -71,3 +71,11 @@ REA 直接打开内嵌插件。原包为 arm64/x86_64 通用 Mach-O,Ghidra 无
本机原生观察:Motion中使用同一蓝发视频,原版UI默认多人关闭(单人)、防跳180%;启用检测框并调瘦脸/大眼后,播放时自动更新人脸框,没有先执行离线分析。此观察只覆盖该单人片段,不能替代多人遮挡与所有参数对照。
1.0.4已在Motion实际加载与不同播放头位置观察默认自动跟随输出;用户随后接管Motion/FCP自测,FCP该版自动跟随及显式OSC原生点击尚未完成自动验收。已完成范围与剩余验证见 [验证记录](../验证记录.md)。
## 最大瘦脸与转脸差异(用户复测反馈)
用户反馈跟踪正常,但最大瘦脸与原版不一致,侧脸出现异常变形。补充REA与Metal AIR分析确认,原版瘦脸以76点加中心点输入,执行17组轮廓到鼻部/瞳孔锚点的局部逆向采样;当前复刻采用矩形框水平对称场,两者没有对齐。已检查的GPU瘦脸路径没有显式yaw角度切换或侧脸衰减,不能将局部二维点自适应描述为完整3D姿态识别。详见 [脸型变形与转脸分析](脸型变形与转脸分析.md) 及 [结构报告](slim-pose-report.json)。本轮未修改安装版的变形代码。
## 1.0.5 对齐实现
已根据上述证据将瘦脸本身改为76点/17组局部逆向采样,`.025 × sizeGain`映射、UV距离、每组clamp和顺序均由独立数值场对照。没有复制原版metallib或模型。全76点参与当前实时稳定并保存离线记录;缺失完整点阵时不再退回整框瘦脸。其他8类几何控制、完整77点稳定系数和快速多人重叠采样仍有既有差异。阶段验证见验证记录;原分析中“1.0.4尚未修复”是历史状态。
+1
查看文件
@@ -0,0 +1 @@
{"artifacts":[{"architecture":"arm64","digest":{"sha256":"f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb"},"format":"mach-o"}],"captures":[],"environments":[],"providers":[{"id":"ghidra","name":"Ghidra","version":"12.1.4"}],"records":[{"analysis_profile":{"digest":"20565088524a2d0359b9b96f46ac48e679d774c7b0092c56abc06de8fdbec11c","parameters":{"analyzer_preset":"ghidra-default","annotation_policy":"atomic-function-entry-metadata-v1","architecture":"arm64","available_architectures":["arm64"],"compiler_spec_id":"auto-default","decompiler_jump_loads":true,"function_body_evidence":"complete-inclusive-ranges-v1","function_boundary_observations":"ghidra-terminal-call-limitations-v1","function_references":"complete-body-and-entry-reference-manager-v2","import_mode":"ephemeral-source-immutable","instruction_flow_evidence":"decoded-return-pcode-v1","jump_table_evidence":"typed-case-default-blocks-v1","language_id":"auto-from-header","load_image_observations":"source-mappings-entry-context-v2","loader":"auto-from-header","location_resolution":"explicit-address-exact-entry-symbol-first-v3","no_return_repair":"returning-imports-decoded-return-v2","process_launch":"inspected-jvm-launch-support-v1","string_inventory_evidence":"defined-data-coverage-v1","target_format":"mach-o","target_kind":"executable"},"provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"}},"authority":"shipped-artifact","confidence":"observed","environment":null,"evidence_id":"ev_02f734a0d72f1b7b77a3112d1de297b9e99ea43d5377349e6eebb71ab017d650","evidence_links":[],"limitations":["The session serves operations only after Ghidra reports default auto-analysis complete; incomplete analysis does not expose partial results.","The imported Program and temporary project are ephemeral and deleted on close. Annotation operations edit session database metadata; original executable bytes are never written.","Cancelling an active Ghidra request terminates its ephemeral database and discards session annotations. The selected target remains open; the next provider query imports the original artifact again.","Default-space addresses use lowercase 0x-prefixed hexadecimal; other address spaces use <percent-encoded-space>:0x<hex>.","Pseudocode is Ghidra decompiler output, not original source and not text-equivalent to Hopper output.","External functions and functions without an analyzable body return null; other decompiler failures remain explicit.","Ghidra Listing reports a terminal call at 0x100016b84 to 0x10005c880 objc_stub::setUsesCPUOnly: (external=false, thunk=false, hasNoReturn=false); fallthrough is excluded by Ghidra's flow model. The reason for the terminal flow is not established by this observation.","macOS sessions require a matching executable Ghidra native decompiler; REA checks for it but does not build native components or change Gatekeeper quarantine state."],"locations":[],"normalized_result":"\nvoid FUN_100016a3c(ID param_1,ID param_2,ID param_3)\n\n{\n undefined8 uVar1;\n int iVar2;\n ID IVar3;\n ID IVar4;\n ID IVar5;\n \n if (param_1 == 0) {\n return;\n }\n if ((param_2 == 0) || (IVar3 = objc_stub::revision(param_2), IVar3 == 0)) {\n IVar5 = VNDetectFaceLandmarksRequest::supportedRevisions\n ((ID)&_OBJC_CLASS___VNDetectFaceLandmarksRequest);\n IVar3 = 3;\n IVar4 = VNDetectFaceLandmarksRequest::containsIndex_(IVar5,3);\n if ((IVar4 & 1) == 0) {\n IVar5 = VNDetectFaceLandmarksRequest::containsIndex_(IVar5,2);\n IVar3 = 1;\n if ((int)IVar5 != 0) {\n IVar3 = 2;\n }\n }\n }\n else {\n IVar3 = objc_stub::revision(param_2);\n }\n IVar5 = VNDetectFaceLandmarksRequest::supportedRevisions\n ((ID)&_OBJC_CLASS___VNDetectFaceLandmarksRequest);\n IVar5 = VNDetectFaceLandmarksRequest::containsIndex_(IVar5,IVar3);\n if ((IVar5 & 1) == 0) {\n IVar3 = VNDetectFaceLandmarksRequest::supportedRevisions\n ((ID)&_OBJC_CLASS___VNDetectFaceLandmarksRequest);\n IVar5 = VNDetectFaceLandmarksRequest::containsIndex_(IVar3,3);\n if ((int)IVar5 == 0) {\n IVar3 = VNDetectFaceLandmarksRequest::containsIndex_(IVar3,2);\n uVar1 = 1;\n if ((int)IVar3 != 0) {\n uVar1 = 2;\n }\n objc_stub::setRevision_(param_1,uVar1);\n goto LAB_100016b4c;\n }\n objc_stub::setRevision_(param_1,3);\n }\n else {\n objc_stub::setRevision_(param_1,IVar3);\n if (IVar3 != 3) goto LAB_100016b4c;\n }\n IVar3 = VNDetectFaceLandmarksRequest::revision_supportsConstellation_\n ((ID)&_OBJC_CLASS___VNDetectFaceLandmarksRequest,3,2);\n if ((int)IVar3 != 0) {\n objc_stub::setConstellation_(param_1,2);\n }\nLAB_100016b4c:\n IVar3 = objc_stub::count(param_3);\n iVar2 = 0;\n if (IVar3 != 0) {\n IVar3 = objc_stub::setInputFaceObservations_(param_1,param_3);\n iVar2 = (int)IVar3;\n }\n FUN_10003b084();\n if (iVar2 == 0) {\n return;\n }\n objc_stub::setUsesCPUOnly_(param_1,1Line truncated
+401
查看文件
@@ -0,0 +1,401 @@
{
"schema": 1,
"date": "2026-10-21",
"request": "最大瘦脸差异与人物转脸时的变形机制",
"reference": {
"plugin_sha256": "6c5d74bc844a5c5f745718b48ea10ec6635d08aa08a5b6dc6e62653980849f8a",
"arm64_sha256": "f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb",
"metallib_sha256": "4e8c77db8be94ae169337a2e646407f69520b63afb248faa653e0e068237aaae"
},
"air_observation": {
"authority": "static Apple metal-objdump AIR disassembly; not observed shader execution",
"command": "xcrun metal-objdump --metallib --disassemble-symbols=faceThinFragmentShader <reference default.metallib>",
"full_output_sha256": "f97705b88f5491057df2117aef457ffca84e11020574cd57742b0670db4484d8",
"full_output_bytes": 274496,
"local_output": "build/pose-analysis/face-thin.air.txt",
"module": "faceThinFragmentShader",
"module_metallib_offset": "0x5d5e",
"function": "mssFaceShapePinCoordinate",
"active_branch": "faceShapeParamsActive selects Shape helper; inactive branch alone selects legacy Thin helper",
"local_pin_triplets": [
[
75,
74,
6
],
[
59,
60,
13
],
[
74,
73,
47
],
[
60,
61,
47
],
[
73,
72,
47
],
[
61,
62,
47
],
[
72,
71,
48
],
[
62,
63,
48
],
[
71,
70,
48
],
[
63,
64,
48
],
[
70,
69,
49
],
[
64,
65,
49
],
[
69,
68,
49
],
[
65,
66,
49
],
[
68,
67,
52
],
[
66,
67,
52
],
[
67,
65,
47
]
],
"triplet_usage": "first=origin, third=anchor; second unused in thin loop",
"thin_cpu_scale": 0.025,
"size_gain": "clamp(280/max(length((P75-P59)*imageSize),2*length((P67-(P74+P60)/2)*imageSize),1),1,2.25)",
"effective_pin_gain": "clamp(UI thin,0,1) * strengthMultiplier * 0.025 * size_gain",
"radius": "length(origin-anchor) in normalized image UV",
"falloff": "clamp(1-length(currentSampleCoordinate-origin)/radius,0,1)",
"inverse_sample_step": "q = clamp(q-(anchor-origin)*effective_pin_gain*falloff,0,1)",
"iteration": "17 sequential updates; not 17 independent offsets evaluated at one original coordinate",
"bounded_negative_findings": [
"No yaw/roll/pitch input to the checked GPU helpers",
"No explicit side-view angle gate in the checked thin branch",
"No explicit eye/nose restore mask in the checked thin branch; each single pin has zero weight at its own anchor"
],
"limits": [
"This is a 2D local landmark deformation, not proof of 3D head pose estimation",
"Static paths are not a capture of the exact runtime branch used in the supplied screenshots",
"Other contour controls include fixed horizontal offsets; not all controls share rotation-equivariant behavior"
]
},
"vision_probe": {
"authority": "Independent public Apple Vision / AVFoundation invocation; no reference app or plug-in executed",
"script": "scripts/inspect-face-geometry.m",
"revision": 3,
"constellation": 76,
"maximum_dimension": 1280,
"coordinate_system": "full-image normalized, y up",
"region_indices": {
"contour": [
59,
60,
61,
62,
63,
64,
65,
66,
67,
68,
69,
70,
71,
72,
73,
74,
75
],
"innerLips": [
42,
40,
43,
45,
41,
44
],
"leftEye": [
0,
4,
5,
1,
3,
2
],
"leftPupil": [
6
],
"nose": [
47,
56,
54,
53,
52,
51,
50,
55
],
"noseCrest": [
46,
47,
48,
49,
58,
57
],
"outerLips": [
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
26
],
"rightEye": [
7,
11,
12,
8,
10,
9
],
"rightPupil": [
13
]
},
"frames": [
{
"time": 0,
"face_count": 1,
"all_point_counts": [
76
],
"yaw_radians": [
0
],
"pitch_radians": [
null
]
},
{
"time": 2,
"face_count": 1,
"all_point_counts": [
76
],
"yaw_radians": [
0.7853981852531433
],
"pitch_radians": [
null
]
},
{
"time": 4,
"face_count": 1,
"all_point_counts": [
76
],
"yaw_radians": [
1.5707963705062866
],
"pitch_radians": [
null
]
},
{
"time": 6,
"face_count": 1,
"all_point_counts": [
76
],
"yaw_radians": [
0
],
"pitch_radians": [
null
]
},
{
"time": 8,
"face_count": 1,
"all_point_counts": [
76
],
"yaw_radians": [
0
],
"pitch_radians": [
null
]
},
{
"time": 10,
"face_count": 1,
"all_point_counts": [
76
],
"yaw_radians": [
0
],
"pitch_radians": [
null
]
},
{
"time": 12,
"face_count": 1,
"all_point_counts": [
76
],
"yaw_radians": [
0
],
"pitch_radians": [
null
]
},
{
"time": 14,
"face_count": 1,
"all_point_counts": [
76
],
"yaw_radians": [
0
],
"pitch_radians": [
null
]
},
{
"time": 16,
"face_count": 1,
"all_point_counts": [
76
],
"yaw_radians": [
0
],
"pitch_radians": [
null
]
},
{
"time": 18,
"face_count": 1,
"all_point_counts": [
76
],
"yaw_radians": [
1.5707963705062866
],
"pitch_radians": [
null
]
},
{
"time": 20,
"face_count": 1,
"all_point_counts": [
76
],
"yaw_radians": [
0.7853981852531433
],
"pitch_radians": [
null
]
}
],
"result": "All 11 sampled frames returned one face and 76 points; 4s/18s reported yaw approx pi/2; pitch unavailable",
"privacy": "Source frames, complete landmarks and source path remain ignored under build/pose-analysis/vision and are not included in Git"
},
"reconstruction_baseline": {
"commit": "e03e460a9b3e5ad489aad24095485ba10ceb987d",
"installed_version": "1.0.4 (6)",
"root_causes": [
"thin deformation uses bounding box x/y/size rather than saved contour/nose points",
"horizontal symmetric inverse warp is applied at full strength for any valid box",
"no contour/landmark completeness guard or pose metadata preservation",
"fixed coefficients do not match reference local-pin scaling"
],
"direction_check": "Positive thin samples outward and moves image features inward; the sign is not reversed",
"test_gap": "Existing geometry tests establish pixel changes, extent and persistence, not reference likeness or profile shape preservation"
},
"checklist": {
"why_maximum_slim_differs": "answered: different geometric model, locality and effective strength",
"how_profile_is_handled": "answered within checked active thin path: projected 2D landmarks, local directions/radii; no established explicit yaw gate",
"full_native_visual_equivalence": "unverified; screenshots are not a synchronized isolated-parameter capture",
"product_fix": "not changed by this analysis; installed 1.0.4 retains the reported issue"
},
"host_boundary": "User controls Motion/FCP; no UI interaction, host shutdown, app install or plug-in replacement in this analysis.",
"followup_implementation": {
"version": "1.0.5 (7)",
"signed_source_commit": "abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964",
"status": "signed_notarized_installed_user_visual_qa_pending",
"landmarks": "explicit revision3 / 76 constellation, points76 persisted and stabilized",
"warp": "17 sequential UV pins, .025 * pixel-size gain, original thin bbox term removed",
"offline_checks": 604,
"extra_real_video_asan_checks": 264,
"real_video_frames": 11,
"max_uv_oracle_error": 1.09828022e-05,
"limitations": [
"Other eight controls retain prior approximations",
"Fast multi-face overlapping resampling order is not equivalent to the original single fragment",
"Full native Motion/FCP visual comparison reserved for user"
]
}
}
@@ -0,0 +1,84 @@
# 最大瘦脸与转脸变形分析
2026-10-21。用户已反馈自动跟随正常;本次检查四张实测图指出的脸型差异。当前安装版仍为1.0.4(6),本次没有修改产品变形代码、替换APP或操作Motion/FCP。
## 结论
我们的瘦脸按矩形脸框建立左右对称的水平收缩场;原版主要瘦脸路径根据完整二维面部关键点,顺序执行17组局部位移。两者的作用范围、方向和强度定义均不同。侧脸继续使用正面矩形场,是当前鼻尖、下巴和邻近区域异常变形的直接原因;跟踪框跟对人不能解决这个问题。
原版已检查的瘦脸路径没有读取yaw/roll/pitch后切换侧脸算法,也没有专门的侧脸角度衰减。它每帧获得随姿态变化的二维轮廓与鼻部位置,让局部收缩方向和作用半径随这些点变化。这属于二维关键点自适应,不能表述成已恢复完整3D姿态估计。
## 原版证据链
只读目标与上一轮一致:插件原件SHA-256为`6c5d74bc844a5c5f745718b48ea10ec6635d08aa08a5b6dc6e62653980849f8a`;arm64分析切片为`f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb`。本次开始后重新计算原件摘要,未发生变化。Metal库摘要为`4e8c77db8be94ae169337a2e646407f69520b63afb248faa653e0e068237aaae`。
|步骤|直接静态观察|REA Evidence|
|---|---|---|
|检测设置|`0x100016a3c`优先配置Vision revision 3及76点constellation;不支持时存在旧revision回退|`ev_02f734a0d72f1b7b77a3112d1de297b9e99ea43d5377349e6eebb71ab017d650`|
|当前帧检测|`cachedFaceObservations…`发起`VNDetectFaceLandmarksRequest`,另有重试与缓存|`ev_61ea8c6e7117f18e47fe6cf820c9ec0a59865dedaf3d9c09813be0a5bd7a79bc`|
|有效点筛选|`0x1000230cc`要求至少76点;`0x100022c70`转换76点并添加第77个中心点;`0x100017cd8`检查有限数值、空间范围及轮廓分布|`ev_63bb7c8fb07dbe456027f4d4e6be52118338b91ab39dbf9ed6620de9090bc260`、`ev_5f27f174c793c94cd45219d6c405e6b54b96e2650c7462ccc5eeaea14f2604c3`、`ev_1c62b4dab3cf8b661eb5c31b034a0b3cadf9f1f6cb512aace8b3b0838bc74b5c`|
|输入几何|`0x1000274d0`打包77个float2,616字节,转换到输出纹理坐标并检查越界点/轮廓尺度|`ev_8a33a7bb27a1a966f9e4ee26380501f1cb009d7d4665ed93b73bd3f630436556`|
|参数缩放|`0x1000279f4`将瘦脸滑块值乘0.025及渲染强度,写入十float脸型结构的第一项|`ev_449e1fc8883b8afe27240f672f98f300d4132ac4994db5e1590a4f7ca4e21278`|
|GPU连接|`FxMTLDeviceCacheItem::initWithDevice:pixelFormat:`将`faceThinFragmentShader`接入瘦脸pipeline;`0x100027d38/0x10002807c`上传点阵、数量及脸型参数到该pipeline|`ev_1520c2d83845004994f2be940c8c5ea3f4a62daf2d8e3e06278f381ffca1a010`、`ev_dc4487de4891c4a663dab370f2e207799bec7e2ad305a98a1ed579e5de6ef263`、`ev_7d2cc9ff3bf8ed3ceea2d91ffb7c2c47ad45337a792f101c1d989272f37023b1`|
完整REA记录见[证据包](rea-slim-pose.json)。伪代码是Ghidra生成的静态分析结果,寄存器参数与CGRect返回类型部分恢复不完整;此处没有将其当作原始源码或实际执行记录。
## Metal瘦脸计算
用本机Apple工具对只读`default.metallib`输出AIR中间代码。它不是字符串猜测:`faceThinFragmentShader`在`faceShapeParamsActive`为真时调用`mssFaceShapePinCoordinate`,只有inactive分支调用旧`mssFaceThinPinCoordinate`。开启瘦脸后第一项脸型参数非零,因此可建立Shape分支的静态路径。原版截图当时具体走哪条宿主渲染分支没有运行捕获。
Shape helper中的瘦脸部分有17组点操作,每组实际读取origin和anchor,三元表中的第二项在这一循环中未使用。origin均为59–75号脸轮廓点,anchor为6、13、47、48、49、52号点;独立Vision区域对应检查确认,6/13是左右瞳孔,47/48/49/52位于鼻部或鼻梁。不能把所有锚点都叫下颌点或鼻部点。
单组逆向采样的结构为:
```text
r = distance(origin, anchor)
w = clamp(1 - distance(q, origin) / r, 0, 1)
q = clamp(q - (anchor - origin) * effectiveStrength * w, 0, 1)
```
17组顺序更新同一个采样坐标q,不能将其视为一次整脸等比缩放。这里的距离在整幅纹理归一化UV中计算;它并非完整按像素各向同性的旋转不变模型。单组在其anchor处权重为零,但其他组仍可能影响该处,不能说整个鼻部或所有五官被锁死。
有效强度另含尺寸补偿:`k=clamp(280/max(轮廓两端像素距离, 下巴到上侧轮廓中点的像素距离×2, 1), 1, 2.25)`,单pin系数约为`滑块×0.025×k×渲染强度`。它对小脸加大效果,**不是侧脸质量衰减**。最大单组系数0.05625也不是最终整脸缩窄百分比。
AIR结构摘要、全部17组索引、尺寸公式、输出摘要和复现命令在[机器可读报告](slim-pose-report.json)。完整AIR文本留在本地`build/pose-analysis/face-thin.air.txt`,未把原版metallib或原版程序加入产品或仓库。
## 为什么转脸时更自然
已证实的机制是:轮廓投影变化→局部origin、anchor间向量变化→各处的收缩方向、半径随之变化;并非先证明“人物转头90度”,再换一套固定对称瘦脸公式。另有上一轮已分析的时间点阵稳定、有效性检查和失检参考保持,这些可减少跳动;它们与瘦脸位移模型是两个层面。
本次检查的GPU瘦脸分支没有yaw/roll/pitch输入、左右脸对称比门限、侧脸强度开关或末端五官恢复蒙版。原生函数名检索也未发现`objc_stub::yaw/roll/pitch` getter;这是有界负面结果,不能据此宣称整个APP绝无任何姿态相关逻辑。其他脸型项仍有固定画面X方向位移,不能把瘦脸的局部点特性扩张成所有控制都具有相同的转头适应性。
## 同一素材的独立验证
运行独立的[离线检测工具](../../scripts/inspect-face-geometry.m),从用户视频在0、2、4、6、8、10、12、14、16、18、20秒各取一帧。请求使用Vision revision 3 / 76点,图像最长边1280。全部11帧均返回1张脸、76点;4秒和18秒的Vision yaw约为π/2,pitch均不可用。
这证明明显侧脸仍可以获得完整二维点阵,也校验了59–75号点确为轮廓、6/13号为瞳孔、47/48/49/52号为鼻部相关点。yaw为检测器返回值,不能当作实测头部角度。工具没有调用或执行原版滤镜,不是原版宿主像素等价测试;本机Vision版本、检测尺寸与宿主缓存可能使点坐标与原版当帧不同。Vision本身报告yaw,也不能说明原版瘦脸实际使用该属性。
用户素材、截图、导出的帧及完整人脸坐标均只保留在被忽略的本地build目录,没有上传远程仓库。入库的报告仅含汇总计数、区域索引和角度信息。
工具复现:
```sh
xcrun clang -fobjc-arc -Wall -Wextra \
-framework Foundation -framework AVFoundation -framework Vision \
-framework ImageIO -framework CoreGraphics -framework CoreMedia \
scripts/inspect-face-geometry.m -o /tmp/inspect-face-geometry
/tmp/inspect-face-geometry '<用户视频路径>' '<本地输出目录>/geometry.json' 0 2 4 6 8 10 12 14 16 18 20
```
## 当前实现差异与修复落点
`PGFaceProcessor.m:290–305`的瘦脸只用框中心/宽高,按画面水平轴同时处理左右;`warpImage:geometry:settings:extent:`额外读取的双眼仅用于大眼。已有`contour/nose/lips`未参与瘦脸,缺失轮廓时仍会按有效框足额变形。pose信息也没有贯通提取、净化、实时稳定和离线存储。
正值采样向外、视觉向内收的符号本身正确;简单反号无法修复。当前公式在框高35%处的测试位置将脸框两边缘间距缩小约20.2%,作用域还可延伸到框外。这个局部计算是当前公式的解析检查,不代表与原版截图的实测宽度差。
后续修正应让瘦脸直接使用稳定后的轮廓和鼻部控制点,按局部方向/半径位移,并校准尺寸补偿和滑块映射。若另加显式姿态或质量衰减,应标记为本项目设计,不能当作已证实的原版算法。完整点阵必须贯通实时与持久化路径,不能只在检测入口新增字段后被后续步骤丢弃。
现有测试只能证明各几何参数改变像素、范围保持和跟踪恢复一致,尚未验证最大瘦脸外观或侧脸形状。修复验证至少应包括同帧正面/半侧/全侧、向左右转脸、歪头、轮廓缺失、正逆播放、100%瘦脸与多个脸型项叠加,和原版在隔离相同其他参数后的对照。
本轮完成机制分析及独立检测,未宣称已修复安装版,也未将用户的“跟踪没问题”扩张为其他未覆盖的原生验收通过。
## 后续修复状态
1.0.5已实现并安装76点/17组局部瘦脸,旧的矩形框瘦脸已移除。上文“安装版未修复”为本次分析开始时1.0.4的历史状态。新增独立数值场与真实素材回归,签名公证及安装记录见 `../验证记录.md`。Motion/FCP最终视觉由用户自测,其他8类脸型控制与快速多人重叠的既有近似边界仍保留。
+5
查看文件
@@ -0,0 +1,5 @@
Beauty renderer: 49 checks, 0 failures (Metal).
2026-10-21 20:18:32.623 face-tests[6394:5408005] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
Face processing: 116 checks passed
2026-10-21 20:18:36.942 slim-tests[6433:5408137] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
PASS: 224 slim alignment checks; maximum UV oracle error 1.09828022e-05
@@ -0,0 +1,49 @@
{
"status": "installed_user_qa_pending",
"bundleIdentifier": "com.dayu.beatygril",
"installationPath": "/Library/Plug-Ins/FxPlug/beatygril.app",
"options": {
"waitForHosts": true,
"userTests": true,
"resumePrepared": true
},
"nativeEffectQA": {
"status": "pending_user",
"performedByScript": false
},
"source": {
"commit": "abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964",
"trackedWorktreeChanges": []
},
"updatedAt": "2026-10-21T12:23:23.275614+00:00",
"version": "1.0.5",
"build": "7",
"stagingSignature": "Executable=/private/tmp/paintfcp-build-20261021/Build/Products/Release/beatygril.app/Contents/MacOS/beatygril\nIdentifier=com.dayu.beatygril\nFormat=app bundle with Mach-O universal (x86_64 arm64)\nCodeDirectory v=20500 size=478 flags=0x10000(runtime) hashes=4+7 location=embedded\nHash type=sha256 size=32\nCandidateCDHash sha256=573debde15f2628c7bc7c6ed9a4cdeabd33f7518\nCandidateCDHashFull sha256=573debde15f2628c7bc7c6ed9a4cdeabd33f7518762fa036f842d998dd623720\nHash choices=sha256\nCMSDigest=573debde15f2628c7bc7c6ed9a4cdeabd33f7518762fa036f842d998dd623720\nCMSDigestType=2\nCDHash=573debde15f2628c7bc7c6ed9a4cdeabd33f7518\nSignature size=8968\nAuthority=Developer ID Application: zhong luo (B3W25FX5H5)\nAuthority=Developer ID Certification Authority\nAuthority=Apple Root CA\nTimestamp=Oct 21, 2026 at 20:20:14\nInfo.plist entries=22\nTeamIdentifier=B3W25FX5H5\nRuntime Version=27.0.0\nSealed Resources version=2 rules=13 files=4\nInternal requirements count=1 size=180\nTotal signatures=1\nChosen signature=1\n",
"notarization": {
"id": "7d7034f8-7389-410f-ad8e-9e722408c0fc",
"status": "Accepted",
"message": "Processing complete"
},
"resumedBy": {
"commit": "a6551c334fd1a0883259700b802174955c78376a",
"trackedWorktreeChanges": []
},
"signingVerified": true,
"stapleValidated": true,
"stagingPluginUnregistered": true,
"runningHosts": [],
"installedSignature": "Executable=/Library/Plug-Ins/FxPlug/beatygril.app/Contents/MacOS/beatygril\nIdentifier=com.dayu.beatygril\nFormat=app bundle with Mach-O universal (x86_64 arm64)\nCodeDirectory v=20500 size=478 flags=0x10000(runtime) hashes=4+7 location=embedded\nHash type=sha256 size=32\nCandidateCDHash sha256=573debde15f2628c7bc7c6ed9a4cdeabd33f7518\nCandidateCDHashFull sha256=573debde15f2628c7bc7c6ed9a4cdeabd33f7518762fa036f842d998dd623720\nHash choices=sha256\nCMSDigest=573debde15f2628c7bc7c6ed9a4cdeabd33f7518762fa036f842d998dd623720\nCMSDigestType=2\nCDHash=573debde15f2628c7bc7c6ed9a4cdeabd33f7518\nSignature size=8968\nAuthority=Developer ID Application: zhong luo (B3W25FX5H5)\nAuthority=Developer ID Certification Authority\nAuthority=Apple Root CA\nTimestamp=Oct 21, 2026 at 20:20:14\nNotarization Ticket=stapled\nInfo.plist entries=22\nTeamIdentifier=B3W25FX5H5\nRuntime Version=27.0.0\nSealed Resources version=2 rules=13 files=4\nInternal requirements count=1 size=180\nTotal signatures=1\nChosen signature=1\n",
"installedSignatureVerified": true,
"installedStapleValidated": true,
"gatekeeperAccepted": true,
"pluginDiscovery": {
"bundleIdentifier": "com.dayu.beatygril.fxplug",
"formalPath": "/Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit",
"output": " com.dayu.beatygril.fxplug(1.1)\tF419A538-F395-459D-9984-AC46EAB5E83D\t2026-10-21 12:23:23 +0000\t/Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit\n (1 plug-in)\n"
},
"removedGeneratedPaths": [
"/Users/dayu/Documents/PaintFCP/build/delivery/previous-beatygril.app",
"/tmp/paintfcp-build-20261021",
"/Users/dayu/Documents/PaintFCP/build/delivery/notarization.zip"
]
}
+4
查看文件
@@ -0,0 +1,4 @@
2026-10-21 20:18:33.844 host-tests[6427:5408121] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
Default IOSurface maximum byte difference: 0
Alternate destination tag maximum byte difference: 0
Host adapter: 117 checks, 0 failures
@@ -0,0 +1,2 @@
com.dayu.beatygril.fxplug(1.1) F419A538-F395-459D-9984-AC46EAB5E83D 2026-10-21 12:23:23 +0000 /Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit
(1 plug-in)
@@ -0,0 +1,24 @@
Executable=/Library/Plug-Ins/FxPlug/beatygril.app/Contents/MacOS/beatygril
Identifier=com.dayu.beatygril
Format=app bundle with Mach-O universal (x86_64 arm64)
CodeDirectory v=20500 size=478 flags=0x10000(runtime) hashes=4+7 location=embedded
Hash type=sha256 size=32
CandidateCDHash sha256=573debde15f2628c7bc7c6ed9a4cdeabd33f7518
CandidateCDHashFull sha256=573debde15f2628c7bc7c6ed9a4cdeabd33f7518762fa036f842d998dd623720
Hash choices=sha256
CMSDigest=573debde15f2628c7bc7c6ed9a4cdeabd33f7518762fa036f842d998dd623720
CMSDigestType=2
CDHash=573debde15f2628c7bc7c6ed9a4cdeabd33f7518
Signature size=8968
Authority=Developer ID Application: zhong luo (B3W25FX5H5)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
Timestamp=Oct 21, 2026 at 20:20:14
Notarization Ticket=stapled
Info.plist entries=22
TeamIdentifier=B3W25FX5H5
Runtime Version=27.0.0
Sealed Resources version=2 rules=13 files=4
Internal requirements count=1 size=180
Total signatures=1
Chosen signature=1
@@ -0,0 +1,32 @@
{
"version": "1.0.5",
"build": "7",
"uiOperations": 0,
"template": {
"path": "/Users/dayu/Movies/Motion Templates.localized/Effects.localized/大鱼/大鱼人像美化/大鱼人像美化.moef",
"sha256": "ba08de6916cd837b3733aa0c6fcb78f2e8224431d25e1310089ae67d18445f2d",
"resourceVerified": true
},
"codesign": "",
"xcrun": "Processing: /Library/Plug-Ins/FxPlug/beatygril.app\nThe validate action worked!",
"spctl": "/Library/Plug-Ins/FxPlug/beatygril.app: accepted\nsource=Notarized Developer ID",
"Contents/MacOS/beatygril": "x86_64 arm64",
"Contents/PlugIns/beatygril XPC Service.pluginkit/Contents/MacOS/beatygril XPC Service": "x86_64 arm64",
"duplicateInventory": {
"scopes": [
"/Users/dayu/Documents/PaintFCP",
"/Users/dayu/Library/Developer/Xcode/DerivedData",
"/Users/dayu/Desktop",
"/Users/dayu/Downloads",
"/Applications",
"/Library/Plug-Ins/FxPlug",
"/Users/dayu/Library/Plug-Ins/FxPlug"
],
"matchingOuterApps": [
"/Library/Plug-Ins/FxPlug/beatygril.app"
],
"errors": []
},
"discovery": " com.dayu.beatygril.fxplug(1.1)\tF419A538-F395-459D-9984-AC46EAB5E83D\t2026-10-21 12:23:23 +0000\t/Library/Plug-Ins/FxPlug/beatygril.app/Contents/PlugIns/beatygril XPC Service.pluginkit\n (1 plug-in)\n",
"status": "installed_verified_user_visual_tests_pending"
}
@@ -0,0 +1 @@
{"id":"7d7034f8-7389-410f-ad8e-9e722408c0fc","status":"Accepted","message":"Processing complete"}
+21
查看文件
@@ -0,0 +1,21 @@
2026-10-21 20:18:34.451 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.25,0.75) setter=(0.25,0.75) readback=(0.25,0.75) mode=normalized verified=1
2026-10-21 20:18:34.466 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(80,180) surface=320x240 origin=0
2026-10-21 20:18:34.897 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.8,0.3) setter=(1152,-432) readback=(0.8,0.3) mode=centered-pixels verified=1
2026-10-21 20:18:34.897 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0.7,0.4) objectBounds=(-1920,-1080,3840,2160) canvas=(152,78) surface=320x240 origin=0
2026-10-21 20:18:34.948 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.8,0.3) setter=(0.8,0.3) readback=(0.12345,0.87654) mode=normalized verified=0
2026-10-21 20:18:34.948 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.75,0.25) setter=(0.75,0.25) readback=(0.75,0.25) mode=normalized verified=1
2026-10-21 20:18:34.948 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0.75,0.25) objectBounds=(-100,-50,200,100) canvas=(160,60) surface=320x240 origin=0
2026-10-21 20:18:35.005 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.625,0.375) setter=(0.625,0.375) readback=(0.625,0.375) mode=normalized verified=1
2026-10-21 20:18:35.005 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0.625,0.375) objectBounds=(-100,-50,200,100) canvas=(240,60) surface=320x240 origin=0
2026-10-21 20:18:35.057 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.25,0.75) setter=(0.25,0.75) readback=(0.25,0.75) mode=normalized verified=1
2026-10-21 20:18:35.057 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(144,180) surface=320x240 origin=0
2026-10-21 20:18:35.108 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
2026-10-21 20:18:35.108 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0,1) objectBounds=(-100,-50,200,100) canvas=(100,140) surface=320x240 origin=0
2026-10-21 20:18:35.167 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(nan,nan) surface=320x240 origin=0
2026-10-21 20:18:35.217 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
2026-10-21 20:18:35.217 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(inf,nan) surface=320x240 origin=0
2026-10-21 20:18:35.267 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
2026-10-21 20:18:35.267 osc-tests[6459:5408235] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(80,nan) surface=320x240 origin=0
2026-10-21 20:18:35.316 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
2026-10-21 20:18:35.316 osc-tests[6459:5408235] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
OSC adapter: 98 checks, 0 failures
@@ -0,0 +1,11 @@
Independent remote recovery test
Target: abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964
Command: zsh scripts/test.sh
Exit code: 0
Beauty renderer: 49 checks, 0 failures (Metal).
Face processing: 116 checks passed
PASS: 224 slim alignment checks; maximum UV oracle error 1.09828022e-05
2026-10-21 20:20:38.246 face-tests[7122:5413459] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
2026-10-21 20:20:40.227 slim-tests[7129:5413512] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
@@ -0,0 +1,740 @@
{
"target": "abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964",
"remote": "https://dayu.media/git/dayu/paintfcp-fxplug.git",
"startedAt": "2026-10-21T12:20:32.822716+00:00",
"temporaryClone": "/var/folders/t4/kbc10sqs43b0rjf3nfh51nmw0000gn/T/paintfcp-recovery-abe97ed-um2yk1vm/clone",
"scope": "Independent source recovery plus scripts/test.sh only; no APP build, host execution/UI, installation, root source changes, commit, host/OSC ASan or delivery-lock operation.",
"cloneHead": "abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964",
"files": {
"count": 69,
"allByteBlobAndModeEqual": true,
"entries": [
{
"path": ".gitignore",
"mode": "100644",
"kind": "blob",
"blob": "d2bca6e38076bb5f7574cc7781eadef27b41d9ff",
"bytes": 490,
"sha256": "34e63260d343569ad4d5a1ffd6c15647da5a7da2b8940dc93f6d987c7fec474e",
"byteBlobAndModeEqual": true
},
{
"path": "README.md",
"mode": "100644",
"kind": "blob",
"blob": "d0282233a2a630f2a00a7bd1e3342ba4f3a0791a",
"bytes": 3167,
"sha256": "3f1d5c727a335ef862837db92a57ba684424e802cbdee4d64f6bdae069e43076",
"byteBlobAndModeEqual": true
},
{
"path": "RECOVERY.md",
"mode": "100644",
"kind": "blob",
"blob": "d97c7731d0a3d349c72982ad156a581ef6a03a87",
"bytes": 4183,
"sha256": "2f310d4b5f4df318b8b522963c3f1bbe35c1e2a3b3ed85a112d59fc10205c36a",
"byteBlobAndModeEqual": true
},
{
"path": "Templates/大鱼人像美化/Drop Zone.tiff",
"mode": "100644",
"kind": "blob",
"blob": "6d15c98bc221f999514f197ff67a7145836e6a38",
"bytes": 420128,
"sha256": "a7f8e3a2a431dbb3ce9e777040bb55e701cfe9ab9632e8cbb10359732bc8692e",
"byteBlobAndModeEqual": true
},
{
"path": "Templates/大鱼人像美化/PARAMETERS.json",
"mode": "100644",
"kind": "blob",
"blob": "df2c01f370e6ee639d2950774fe8020742802c17",
"bytes": 9815,
"sha256": "bb561b0dfff90223f0925a60ec05b28518e22163a6bf7922c4efc6d155ddbabb",
"byteBlobAndModeEqual": true
},
{
"path": "Templates/大鱼人像美化/README.md",
"mode": "100644",
"kind": "blob",
"blob": "46cf59f6b4500b32c5a203a63a79a491a2202cdf",
"bytes": 4420,
"sha256": "c57ef81f43271d566a0488385d5f6b8e492142bae0860049bcbf943206b0c567",
"byteBlobAndModeEqual": true
},
{
"path": "Templates/大鱼人像美化/VALIDATION.json",
"mode": "100644",
"kind": "blob",
"blob": "63aa4f2ce4e1e72ce8b255d5f98fe209e2354c09",
"bytes": 1872,
"sha256": "558ac5ff3bee28e6d3df41cc5ffd9a43cd51fca2fd001fff8641260c327e351d",
"byteBlobAndModeEqual": true
},
{
"path": "Templates/大鱼人像美化/大鱼人像美化.moef",
"mode": "100644",
"kind": "blob",
"blob": "f68569a62b399cf7ad1ee2b33427372fc9b6b913",
"bytes": 22996,
"sha256": "ba08de6916cd837b3733aa0c6fcb78f2e8224431d25e1310089ae67d18445f2d",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril.xcodeproj/project.pbxproj",
"mode": "100644",
"kind": "blob",
"blob": "db5251bf3c337b865621e771a4b28690214f1c8e",
"bytes": 22986,
"sha256": "e9b9be100e53003f6bba96658d3a36078bb607236958e43aec6ed59f8c73ce32",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
"mode": "100644",
"kind": "blob",
"blob": "919434a6254f0e9651f402737811be6634a03e9c",
"bytes": 135,
"sha256": "7f3b00b5c3fdb45242d7b87e1e5c4e25d1fa8129a16c94295ecc4e8ea2235c5f",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/Info.plist",
"mode": "100644",
"kind": "blob",
"blob": "95542fa8c27e6770b9266389f0aa34027053e31f",
"bytes": 3030,
"sha256": "865f1775fb476ff1f22556141b41b25d3569c05e4aa1d2450cccdcb06de6aa4c",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/PGBeautyRenderer.h",
"mode": "100644",
"kind": "blob",
"blob": "a0a6965c837072e6d837a98b701d59b1cc139655",
"bytes": 1410,
"sha256": "4e9b663c47261e4feceb7ab38c25171b65f36c14417d82a8b7c49ee3201492d3",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/PGBeautyRenderer.m",
"mode": "100644",
"kind": "blob",
"blob": "4c9fc6fbb1dc1f2c4048a16435f554cae9f2ca74",
"bytes": 17132,
"sha256": "00b52b1f22c1e875f137ee4ea90f50d6caa38db277bebd0da538d0ba7e66072b",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/PGBeautyTypes.h",
"mode": "100644",
"kind": "blob",
"blob": "e1cd3ef21f37b831cdaa7d9f52211cafaf243994",
"bytes": 1149,
"sha256": "6ea8005accc22f85fe8be1b73bf6e89a5b37ffd6e97460d4f6dd69d55eaf33cc",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/PGFaceProcessor.h",
"mode": "100644",
"kind": "blob",
"blob": "0b861830a6e9652d81ccfe62f2ce758febee449f",
"bytes": 2178,
"sha256": "a3448bd75fd278c9702f079f55637ba2c26bc820bdbb81f49fb3a3d0e2a4aba5",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/PGFaceProcessor.m",
"mode": "100644",
"kind": "blob",
"blob": "08c1e85455da07203e4ee30a5410fb10cb9e78c7",
"bytes": 42670,
"sha256": "445aac56a8ff7332c3b94c6ad5b37b9e412af757995a8332ae2e70066d348d7e",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/PGFaceSelectOSC.m",
"mode": "100644",
"kind": "blob",
"blob": "ae0aa7ce1c2de355b2c1cb890314a0de689df81a",
"bytes": 11630,
"sha256": "d5e1dd1db6825730803ddc3f21b588cd548cbbacc60760aa45ad9e2feedae307",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/PGParameterIDs.h",
"mode": "100644",
"kind": "blob",
"blob": "4cc0e20af1e659f1e8e666fabe01003c06cfd885",
"bytes": 932,
"sha256": "8fdfa2021131317ce09532d27fd6f34fb0b505f7b17c561819336ad2a06c42cc",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/beatygrilPlugIn.h",
"mode": "100644",
"kind": "blob",
"blob": "12dca07a744c064d5d474e0294304ca29055a077",
"bytes": 347,
"sha256": "4f00716328c9237b79cc58153ba47d8ac8ddc1786939cb206bd37b01e3b6397e",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/beatygrilPlugIn.m",
"mode": "100644",
"kind": "blob",
"blob": "3f148956b32770c7db5b0df97700fd96f7e20025",
"bytes": 24193,
"sha256": "b8f4326ed3eb2de74770e4ea266d09ebe4aea70428a4511f0ec501a0cd89b247",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/en.lproj/InfoPlist.strings",
"mode": "100644",
"kind": "blob",
"blob": "9a5f27348a39c8e3ee50cffc2fb359c78a1319c8",
"bytes": 85,
"sha256": "1b1cd5b74690bd5a74cf2b0a0c068bbc2c3462c5e9fe5ca3e61541bcbd451a7a",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/en.lproj/Localizable.strings",
"mode": "100644",
"kind": "blob",
"blob": "7f52f3dd719cb4f94e24867e36114cb061ef0b92",
"bytes": 132,
"sha256": "b3036a2339b97ebf264516e1e534351d1ceb64f45942595508f4d67d07b07769",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Plugin/main.m",
"mode": "100644",
"kind": "blob",
"blob": "34d50d3f8cdf438d3508d0aa94e3f2d20e45f962",
"bytes": 114,
"sha256": "5b812ddb1ef171fe075e4faf13d761935ca73ecbba1be0bd9d4802a64a7759cd",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Wrapper Application/AppDelegate.h",
"mode": "100644",
"kind": "blob",
"blob": "f3f8e19cfe057114c2289ff37c01c2e6e1aa7ed0",
"bytes": 167,
"sha256": "3fc6516be93176f90c2deb17332c9cf98e670458a33fbad4fe7b8f367ec9494f",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Wrapper Application/AppDelegate.m",
"mode": "100644",
"kind": "blob",
"blob": "1cb56f073356dd30add33415117bf4c24fdc4ef0",
"bytes": 3003,
"sha256": "acef6ade6591c039cd8b3e603f83d6d06f64da9709a0385b73c8588e824fe6de",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Wrapper Application/Base.lproj/MainMenu.xib",
"mode": "100644",
"kind": "blob",
"blob": "57d03a50e249d189c5fa0a4602c5ec846f06612d",
"bytes": 50614,
"sha256": "7f0945bc5903a658550b712267870edecbdf1b73ac83c71f1358468f40c1ab1d",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Wrapper Application/Info.plist",
"mode": "100644",
"kind": "blob",
"blob": "a1fe6cdcfe8e41f2de4ee07525e192e52723d760",
"bytes": 1084,
"sha256": "7b3ff213b68dda774306a96835cf4743e3998e23dd7ac4b20cbdb3cf709d7b8b",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Wrapper Application/SandboxEntitlements.entitlements",
"mode": "100644",
"kind": "blob",
"blob": "852fa1a4728ae4789e3bca55dd07caef3b41f2a5",
"bytes": 240,
"sha256": "b746c74b201e6af4e0864fbd1f4f629f96156a7ec4b7208bc995d0a5089bb592",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Wrapper Application/en.lproj/Credits.rtf",
"mode": "100644",
"kind": "blob",
"blob": "da779d0f6bec11568066e347a01ccef0308488b2",
"bytes": 264,
"sha256": "ec846e198df85720e8443abb810334411b5690386f3062f90208c23b8575f28a",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Wrapper Application/en.lproj/InfoPlist.strings",
"mode": "100644",
"kind": "blob",
"blob": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"byteBlobAndModeEqual": true
},
{
"path": "beatygril/beatygril/Wrapper Application/main.m",
"mode": "100644",
"kind": "blob",
"blob": "99ad4384792f9b2050e0755c07522c0dab9b4b86",
"bytes": 112,
"sha256": "5c3dc97a53d0379af0e0ce75c5541f33c47f2da115d2d0018cfba003d185a63d",
"byteBlobAndModeEqual": true
},
{
"path": "docs/analysis/REA-分析与复刻说明.md",
"mode": "100644",
"kind": "blob",
"blob": "6581570f9d140ef8d0e00f19d9285d44a6ae3813",
"bytes": 10519,
"sha256": "ee64e260fd9261856047096570b8fed308284b3a02cd0dd892f3ceda6a5482ab",
"byteBlobAndModeEqual": true
},
{
"path": "docs/analysis/auto-follow-report.json",
"mode": "100644",
"kind": "blob",
"blob": "e60d6d055a513ab8e9c21d2b39460c9d0698e1d3",
"bytes": 15665,
"sha256": "1a0c91efcaee058f331f6a7446bea8794af1dcf9120f5c2585f249b188d038e3",
"byteBlobAndModeEqual": true
},
{
"path": "docs/analysis/beauty-preview.png",
"mode": "100644",
"kind": "blob",
"blob": "35ace472cb42629b01b3e05660cb7be8b9711fd7",
"bytes": 617622,
"sha256": "37aa97855915a49ff1f225a9e1fa9d8bcbed0be5f4109b6835ba4db17f9113f6",
"byteBlobAndModeEqual": true
},
{
"path": "docs/analysis/face-preview.png",
"mode": "100644",
"kind": "blob",
"blob": "9a5bab3ad706b95ac32875352d16d6580dcd48ab",
"bytes": 713552,
"sha256": "6cc662cbb2b554a0dd8c926d94a0d819506ac76710d9c09c2d3d7f808fbf686b",
"byteBlobAndModeEqual": true
},
{
"path": "docs/analysis/rea-auto-tracking.json",
"mode": "100644",
"kind": "blob",
"blob": "2f5738f934f0ce89780cf4535c7257b54a397bef",
"bytes": 2883488,
"sha256": "dc1007f3798fade5e454bb2ef0b58e4cfe2bb0256948c364070fd9af70a97377",
"byteBlobAndModeEqual": true
},
{
"path": "docs/analysis/rea-host.json",
"mode": "100644",
"kind": "blob",
"blob": "9de443173a74668f0a2dced3426dae20549a16dd",
"bytes": 10111,
"sha256": "141866459948b19d5378de2f6534de333c795e317416f2ac6c9fdc86c94dd61a",
"byteBlobAndModeEqual": true
},
{
"path": "docs/analysis/rea-plugin.json",
"mode": "100644",
"kind": "blob",
"blob": "79bcafbcfeda694ed466d95f81f30eb622a95a0b",
"bytes": 343165,
"sha256": "dda6e450915e1054653c28addfed5254c678dccf14ee66643d508e7087c1f0c1",
"byteBlobAndModeEqual": true
},
{
"path": "docs/analysis/rea-slim-pose.json",
"mode": "100644",
"kind": "blob",
"blob": "249eb3678b20c378e0d73239e0949cc86c2af436",
"bytes": 343127,
"sha256": "5f4a2dfe5796ce72bb3d022a85a42cd5ed0eceb0cd0655041812126ebec7933a",
"byteBlobAndModeEqual": true
},
{
"path": "docs/analysis/reference-template-parameters.json",
"mode": "100644",
"kind": "blob",
"blob": "9947e2ff182a146bc4c9120dfff7eacc8cca9c0c",
"bytes": 1608,
"sha256": "026e47e8ecbd9a967d8b8aabddac2a370479a43b9e1a7a7728b5b01a5002f006",
"byteBlobAndModeEqual": true
},
{
"path": "docs/analysis/slim-pose-report.json",
"mode": "100644",
"kind": "blob",
"blob": "755e0dfcb6eef016e932415ad8c391b97f8de575",
"bytes": 8724,
"sha256": "512d22abc2b08860adb64c38efa0f8e3474b45e3dbd19cb81406b1d9e7060f06",
"byteBlobAndModeEqual": true
},
{
"path": "docs/analysis/脸型变形与转脸分析.md",
"mode": "100644",
"kind": "blob",
"blob": "b98271ebd17b745d2e16e6357578eac6a757c1c1",
"bytes": 8789,
"sha256": "0121d022e4ba430b9fd62b9a13da5b84d03866eb0d541e64e4da3204730c03cf",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/installed-signature.txt",
"mode": "100644",
"kind": "blob",
"blob": "78fc46fd6aee5443af9eeb53a97502d0251cf2a9",
"bytes": 988,
"sha256": "add93ef0fa60cd32ed766c965e6f2869f56b4da65030e774e76d133b02ef7aec",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/live-face-asan.txt",
"mode": "100644",
"kind": "blob",
"blob": "b92d565bec7a49444b638437b4ae8e7400ce90f6",
"bytes": 219,
"sha256": "b956e0fe7a3fccdd8eff8788807c0aaf60da56aa1cb153087f25234cc156c27d",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/notarization.json",
"mode": "100644",
"kind": "blob",
"blob": "86d17c6738b9e0aa6f92583ffc798c2e6b6bece1",
"bytes": 98,
"sha256": "91b4f5e239a50467bde83b8c40c1c1809f85cd4eb4b1f1091e1b2197c11a79b6",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/recovery-e03e460-beauty-face.txt",
"mode": "100644",
"kind": "blob",
"blob": "2c6f0b5e7f7dedf9571d6da6ce67303bf34faedc",
"bytes": 298,
"sha256": "45698c80fdfc68c2dc68be59ba03380aa6e8194731d4858dc009f827dd06e226",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/recovery-e03e460-host-asan.txt",
"mode": "100644",
"kind": "blob",
"blob": "7eeed088f0dad0564aa166232aedb9ae47a56dd0",
"bytes": 380,
"sha256": "65d0226f95e882702484849997fde558fae7af418e9243702f789b1e2fcacf14",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/recovery-e03e460-osc-asan.txt",
"mode": "100644",
"kind": "blob",
"blob": "7abde29290bfea1e475af387899fb3160a60cc95",
"bytes": 3336,
"sha256": "2e326d2fc0eb5d2cdd9f07e2318dfd17a41d346a82f383b05b51f416ffdacdb3",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/recovery-e03e460-xcode-list.txt",
"mode": "100644",
"kind": "blob",
"blob": "e90278ec1aedf6a1176682ec1037e49ba2f588c1",
"bytes": 570,
"sha256": "ff9ffb300caac840f4e64f97f24712e319af9bbe4cee1dbbd79b0a5caa1432d1",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/recovery-e03e460.json",
"mode": "100644",
"kind": "blob",
"blob": "ecd24b6f35c893c3637dc4e268d0acabb4d09967",
"bytes": 25360,
"sha256": "559839e989fb4fa9589847632975a6d4d8548c00e6ad5b87bdba736bb3e1daf3",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/recovery-e03e460.txt",
"mode": "100644",
"kind": "blob",
"blob": "ed81f4abdeada50c1afb2ac866d3cc79fd6579d9",
"bytes": 846,
"sha256": "3931beb81cf475ce1c44e5b90c8e9101abb1a7ba20b657d72090b6e7662530cc",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/release-receipt.json",
"mode": "100644",
"kind": "blob",
"blob": "e53f21de9e8ea68a40f435cf9e3d1a0d95499bde",
"bytes": 2454,
"sha256": "07ba4f5bbba101a49d4579c0b79fbce0dad70a1e4b5887e61e052741c49486ee",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/v1.0.5/core-tests.txt",
"mode": "100644",
"kind": "blob",
"blob": "76d29d91672ec631950b2f3ada1db39db4c877ac",
"bytes": 427,
"sha256": "408910aeedcf0155bc31ef43ca6e8994d9348f8fbdc0c25eb8a43ce89965e081",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/v1.0.5/host-tests.txt",
"mode": "100644",
"kind": "blob",
"blob": "3e1c1c6b7c104995a2656e142671759634352525",
"bytes": 271,
"sha256": "c727d9d56a0b46a4cdbc79d24eeedc589e5984b38144f28ccf017f4d65f504f7",
"byteBlobAndModeEqual": true
},
{
"path": "docs/verification/v1.0.5/osc-tests.txt",
"mode": "100644",
"kind": "blob",
"blob": "c9c618896ca3ee0c2a943979103ab3b42335c7e0",
"bytes": 3229,
"sha256": "5e443009bc39635d01a1f6276b48170fe9da73c088646425c6ed140d87ba4518",
"byteBlobAndModeEqual": true
},
{
"path": "docs/验证记录.md",
"mode": "100644",
"kind": "blob",
"blob": "2d2d97b0e13e876bc8ddf65aedf671cbeead8b6b",
"bytes": 9154,
"sha256": "8ece9ba47c81980a781ab00b73c961bd32c1acec2e09811f348375e162e29661",
"byteBlobAndModeEqual": true
},
{
"path": "scripts/checkpoint.py",
"mode": "100644",
"kind": "blob",
"blob": "8248d1732aceed377d28cdae02873057f442474b",
"bytes": 2218,
"sha256": "3c27b0e603dd2d94512f611cec97bd9afe5809aea0f5a4bb213995d2e23366f8",
"byteBlobAndModeEqual": true
},
{
"path": "scripts/deliver.py",
"mode": "100755",
"kind": "blob",
"blob": "b20e2149f84307afd6198df71f967f4facb444bb",
"bytes": 10848,
"sha256": "dc5df519aaabc695adddff8e9087dcb6c6e9861036ab0efccf242a5fe12e0b78",
"byteBlobAndModeEqual": true
},
{
"path": "scripts/inspect-face-geometry.m",
"mode": "100644",
"kind": "blob",
"blob": "d075cf7c8c9a23b9bd01bbcbc31da89fea84d5d3",
"bytes": 6613,
"sha256": "a2c54e7297f9f94e9c56c0b29f9ce607756705f2005c1f13e10996bc26c24b66",
"byteBlobAndModeEqual": true
},
{
"path": "scripts/test-host.sh",
"mode": "100755",
"kind": "blob",
"blob": "ef9c7f6436e3d1babbf272e9d329d65ec6c395cb",
"bytes": 1129,
"sha256": "7f55ed6de05e6cc59884fb7c96062676bd086f4ac745ae847511eacb12316467",
"byteBlobAndModeEqual": true
},
{
"path": "scripts/test-osc.sh",
"mode": "100755",
"kind": "blob",
"blob": "ffaed0b8d449eef6acf2fb352d9fea10fc1649fd",
"bytes": 941,
"sha256": "ed405cd214ce252641074d0ef0a12c4cc0faf3e1869fe92716a98fff7f9ac517",
"byteBlobAndModeEqual": true
},
{
"path": "scripts/test.sh",
"mode": "100755",
"kind": "blob",
"blob": "f13ba656929fe34b43c143bdfeb4a208c44c7428",
"bytes": 932,
"sha256": "3ca9cb120b452a0c402debbe5d3399fa4996e0ea0882253470791494815d530d",
"byteBlobAndModeEqual": true
},
{
"path": "tests/beauty_render_tests.m",
"mode": "100644",
"kind": "blob",
"blob": "24050fb3a2630693253202ae1525c280f4f144eb",
"bytes": 16870,
"sha256": "b04eb2f0d78faec13bbdd2a3286cc74c46c2f536358c7c1c3d5d03c54daa868c",
"byteBlobAndModeEqual": true
},
{
"path": "tests/face_tests.m",
"mode": "100644",
"kind": "blob",
"blob": "9fe21ddd021d1a2517fa8b75d7a65532f6fefedb",
"bytes": 33180,
"sha256": "0350abe9f206fc371f697c993f5dea420d512259f16947c7a1167569843b215a",
"byteBlobAndModeEqual": true
},
{
"path": "tests/fixtures/ATTRIBUTION.txt",
"mode": "100644",
"kind": "blob",
"blob": "6e439ab97256952b0acd1eef0e1c1b4ab3ea015c",
"bytes": 498,
"sha256": "8823ad8aed1a5c1bdda93946e4b5ed24cd542e996a2b8087517cf62f7f4f2a84",
"byteBlobAndModeEqual": true
},
{
"path": "tests/fixtures/astronaut.png",
"mode": "100644",
"kind": "blob",
"blob": "834cda0012478c5edc8d43bade96d315dedeaab4",
"bytes": 791555,
"sha256": "88431cd9653ccd539741b555fb0a46b61558b301d4110412b5bc28b5e3ea6cb5",
"byteBlobAndModeEqual": true
},
{
"path": "tests/host_adapter_tests.m",
"mode": "100644",
"kind": "blob",
"blob": "8091c1523eb0b05295c391e20637c931d681e049",
"bytes": 30363,
"sha256": "77131c770222d3b404204fb04bf47cf943885eeff6acfc7498d59feb28eb1e73",
"byteBlobAndModeEqual": true
},
{
"path": "tests/osc_tests.m",
"mode": "100644",
"kind": "blob",
"blob": "66fff5f78f8e0ba08026e53375b184a460a29162",
"bytes": 22495,
"sha256": "a3199147e32facd63373b53f77840408411b04436fb1dd42e2fb71023c8f2f8a",
"byteBlobAndModeEqual": true
},
{
"path": "tests/slim_tests.m",
"mode": "100644",
"kind": "blob",
"blob": "3f7654f14e642f72ee57c2a5c3e92df39fe7346a",
"bytes": 12759,
"sha256": "701b52d7f803b44680586c215e5a351e4dc67661e5ea1a56ca5038567cd34cd6",
"byteBlobAndModeEqual": true
}
]
},
"requiredResources": [
{
"path": "Templates/大鱼人像美化/大鱼人像美化.moef",
"exists": true,
"bytes": 22996
},
{
"path": "Templates/大鱼人像美化/Drop Zone.tiff",
"exists": true,
"bytes": 420128
},
{
"path": "beatygril/beatygril/Wrapper Application/Base.lproj/MainMenu.xib",
"exists": true,
"bytes": 50614
},
{
"path": "beatygril/beatygril/Wrapper Application/en.lproj/Credits.rtf",
"exists": true,
"bytes": 264
},
{
"path": "beatygril/beatygril/Wrapper Application/en.lproj/InfoPlist.strings",
"exists": true,
"bytes": 0
},
{
"path": "beatygril/beatygril/Plugin/en.lproj/InfoPlist.strings",
"exists": true,
"bytes": 85
},
{
"path": "beatygril/beatygril/Plugin/en.lproj/Localizable.strings",
"exists": true,
"bytes": 132
},
{
"path": "tests/fixtures/astronaut.png",
"exists": true,
"bytes": 791555
},
{
"path": "tests/fixtures/ATTRIBUTION.txt",
"exists": true,
"bytes": 498
}
],
"gitFsck": {
"args": [
"git",
"fsck",
"--full",
"--strict"
],
"returncode": 0,
"stdout": "",
"stderr": ""
},
"submodules": {
"args": [
"git",
"submodule",
"status",
"--recursive"
],
"returncode": 0,
"stdout": "",
"stderr": ""
},
"lfsPointers": [],
"remoteMain": {
"args": [
"git",
"ls-remote",
"origin",
"refs/heads/main"
],
"returncode": 0,
"stdout": "abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964\trefs/heads/main\n",
"stderr": ""
},
"processorUnchangedFromEc22": {
"commit": "ec22f1b2cf541d02e7ffbd5cc4edbc4d0cd97311",
"equal": true
},
"test": {
"args": [
"zsh",
"scripts/test.sh"
],
"returncode": 0,
"stdout": "Beauty renderer: 49 checks, 0 failures (Metal).\nFace processing: 116 checks passed\nPASS: 224 slim alignment checks; maximum UV oracle error 1.09828022e-05\n",
"stderr": "2026-10-21 20:20:38.246 face-tests[7122:5413459] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2\n2026-10-21 20:20:40.227 slim-tests[7129:5413512] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2\n",
"elapsedSeconds": 3.384
},
"testCounts": {
"beauty": 49,
"face": 116,
"slim": 224
},
"testTotal": 389,
"cloneTrackedWorktreeClean": true,
"passed": true,
"temporaryCloneRemoved": true,
"completedAt": "2026-10-21T12:20:40.364542+00:00"
}
@@ -0,0 +1,9 @@
独立恢复验收:通过
源码节点:abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964
远程:https://dayu.media/git/dayu/paintfcp-fxplug.git
远程 main 与节点一致。
全部 69 个跟踪文件逐 byte/blob/模式一致;9 项必要资源完整。
git fsck --full --strict 通过;无 LFS 指针、无子模块。
独立 clone 内 scripts/test.sh:Beauty 49、Face 116、Slim 224;共 389 检查通过。
未重复 Host/OSC ASan;未构建 APP、启动宿主、操作 UI、安装、修改源码、提交或操作交付锁。
本次临时 clone 已清理。
@@ -0,0 +1,2 @@
2026-10-21 20:20:01.427 slim-asan[6677:5410585] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
PASS: 264 slim alignment checks; maximum UV oracle error 1.09828022e-05
+21 -1
查看文件
@@ -22,7 +22,7 @@ Motion:独立普通视频项目,源PTS0、1920×1080/25fps/10秒,媒体通
用户给出的MS超级美颜V2.moef共发布15个入口,没有跟踪、前向或后向分析控件。没有额外分析按钮不影响原版默认自动跟随;活跃render路径与Motion运行均已确认这一点。复刻版的三目标离线分析与持久化属于独立扩展,应与默认实时跟随分开理解。
六种肤色风格采用自写调色变换,脸型使用基于Vision关键点的近似局部反向采样;未复制原版私有LUT或算法资源。没有完成原版逐像素对齐、全部脸型强度主观比对、多人交叉遮挡、所有宿主版本及Intel硬件验收。源码层保存/恢复测试不代替两宿主所有工程关闭重开场景。
六种肤色风格采用自写调色变换,脸型当前使用Vision脸框场与眼部点位置的近似反向采样;瘦脸本身没有使用下颌轮廓点。未复制原版私有LUT或算法资源。没有完成原版逐像素对齐、全部脸型强度主观比对、多人交叉遮挡、所有宿主版本及Intel硬件验收。源码层保存/恢复测试不代替两宿主所有工程关闭重开场景。
## 自动化与恢复
@@ -41,3 +41,23 @@ Motion:独立普通视频项目,源PTS0、1920×1080/25fps/10秒,媒体通
原版完整数学系数未恢复,本版使用自有平滑公式与past-only时间策略;当前检测框未做原版77点网格级稳定,没有真正sourceID,不能声称任意同尺寸换源或首次乱序完全一致。原生1.0.4:Motion成功加载正式路径插件,实际视频在不同播放头位置正常显示提亮、脸型和黄色主要人脸框;项目未执行离线分析,“使用跟踪数据”读取为0。用户随后明确接管Motion/FCP进行自测,自动化已停止,因此尚未完成1.0.4的FCP自动跟随、显式OSC点击回写及两宿主复杂场景验收。FCP黑屏输入链与提亮此前已原生复测,不能把它们混称为1.0.4所有自动跟随场景通过。详见分析说明的具体边界。
代码节点已从远端独立克隆恢复,51项受跟踪文件与9项必要资源逐字节/对象/文件模式一致;380项检查、严格fsck与Xcode工程读取全部通过,无LFS或子模块。验收报告见 [独立恢复摘要](verification/recovery-e03e460.txt) 与 [完整记录](verification/recovery-e03e460.json)。最终文档标签另作独立克隆核验,源码构建依赖没有因文档更新改变。
## 用户复测与瘦脸专项分析
用户反馈“跟踪没问题”,同时提供最大瘦脸与侧脸变形对照。记录为用户实测反馈,不扩张为显式OSC点击、两宿主复杂场景或全部脸型验收通过。
后续静态REA和Apple Metal AIR分析确认:原版瘦脸以17组轮廓到鼻部/瞳孔锚点的局部位移驱动,当前瘦脸按框水平对称变形,两者的方向、范围与强度模型不同。独立Vision76点离线探针在同一视频11个采样时刻均检出1脸76点,含明显侧脸;它只证明关键点仍可获得,不代替原版宿主输出对照。探针使用Wall/Wextra编译成功并完成采样,没有执行原版APP、替换安装版或操作Motion/FCP。
分析报告见 [脸型变形与转脸分析](analysis/脸型变形与转脸分析.md)。当前安装1.0.4的变形问题尚未修复,本轮的诊断代码/证据/文档不改变已安装产品。
## 1.0.5 瘦脸修复
已将最大瘦脸改为76点、17组局部位移;明确revision3/76点,保留图像及四类蒙版共用变形,旧档案缺完整点阵时只跳过瘦脸。新增224项数值与数据回归:48组尺寸/左右侧脸/歪头投影、非零原点、0/50/100%强度、独立double逆向坐标场、旧档案/坏点保护、失检重获及真实Vision76点与存储回放。最大UV坐标误差1.09828022e-5。
完整回归604项通过(渲染49、人脸116、瘦脸224、宿主适配117、OSC98);宿主适配与OSC启用ASan。独立审查另用程序生成的UV图与1×1读取验证36点,最大误差1.12695e-7。位图数值测试曾发现测试输入行方向与CI坐标不一致,修正测试桥接后通过,生产算法未因此反改。
1.0.5(7)已完成通用Release编译、Developer ID逐层签名、Apple公证Accepted(7d7034f8-7389-410f-ad8e-9e722408c0fc)、票据粘附/验证、Gatekeeper、安装身份与正式路径注册验证。用户保存退出宿主后安装;本轮没有操作或启动Motion/FCP界面,最终视觉由用户自测。默认单主要人脸对齐17-pin数学;快速多人仍沿用逐脸采样,重叠区不宣称与原版单fragment全脸序列等价。
额外用用户视频11帧(含4秒及18秒明显侧脸)运行264项瘦脸ASan检查,通过且未发现内存错误;此数包含重复的220项基础数值/数据检查,不与604简单相加。独立恢复从远程abe97ed0cbd5a2b5ffda6ebdb486d3bff9170964克隆,69文件/9资源和模式一致、strict fsck通过,clone内389项Metal/人脸/瘦脸测试通过。签名构建源码就是该节点;后续交付脚本补充了已公证产物校验恢复与重复注销兼容,产品源码未再变化。
正式安装APP仅位于 `/Library/Plug-Ins/FxPlug/beatygril.app`;配套1.0.5模板及占位资源已同步并逐字节核验。临时构建、旧版APP备份、公证ZIP已清理;项目、DerivedData、桌面、下载、Applications及系统/用户FxPlug目录扫描只发现该正式APP,无扫描错误。原生效果验收状态为用户自测待完成。票据、发现、恢复与检测报告见 `docs/verification/v1.0.5/`。
+117 -13
查看文件
@@ -2,8 +2,13 @@
"""Release, notarize and install the unique FxPlug app under its delivery lock.
The final prompt keeps the lock held while native-host validation is performed.
"""
import json, os, pathlib, plistlib, shutil, subprocess, sys, time
import argparse, datetime, json, os, pathlib, plistlib, re, shutil, subprocess, sys, time
root=pathlib.Path(__file__).resolve().parent.parent
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--wait-for-hosts',action='store_true',help='After notarization, retain the delivery lock and wait for Motion/FCP to exit naturally before installing; never control or quit either host.')
parser.add_argument('--user-tests',action='store_true',help='After non-UI installed-app verification, record native effect QA as pending user testing and clean known generated copies without the interactive validation prompt.')
parser.add_argument('--resume-prepared',action='store_true',help='Resume an already Accepted and stapled staged app after checking its recorded signature, version and unchanged product source.')
args=parser.parse_args()
bundle_id='com.dayu.beatygril'
lock=pathlib.Path('/Users/dayu/.codex/app-delivery-locks')/(bundle_id+'.lock')
lock.parent.mkdir(parents=True,exist_ok=True)
@@ -18,44 +23,129 @@ archive=logs/'notarization.zip'
identity='Developer ID Application: zhong luo (B3W25FX5H5)'
keychain='/Users/dayu/Library/Keychains/login.keychain-db'
previous=logs/'previous-beatygril.app'
receipt_path=logs/'delivery-receipt.json'
receipt={'status':'preparing','bundleIdentifier':bundle_id,'installationPath':str(installed),
'options':{'waitForHosts':args.wait_for_hosts,'userTests':args.user_tests},
'nativeEffectQA':{'status':'pending_user' if args.user_tests else 'pending_operator','performedByScript':False}}
def run(args, log=None):
if log:
with open(log,'w') as output:subprocess.run(args,cwd=root,stdout=output,stderr=subprocess.STDOUT,check=True)
return ''
return subprocess.run(args,cwd=root,text=True,capture_output=True,check=True).stdout
def save_receipt(status,**fields):
receipt.update(fields);receipt['status']=status
receipt['updatedAt']=datetime.datetime.now(datetime.timezone.utc).isoformat()
receipt_path.write_text(json.dumps(receipt,ensure_ascii=False,indent=2)+'\n')
def running_hosts():
# Read process state only. Match executable suffixes so renamed installation
# roots do not accidentally allow replacing a live host's plug-in connection.
suffixes=('/Motion.app/Contents/MacOS/Motion','/Final Cut Pro.app/Contents/MacOS/Final Cut Pro')
found=[]
for line in run(['ps','-axo','pid=,comm=']).splitlines():
columns=line.strip().split(None,1)
if len(columns)==2 and columns[1].endswith(suffixes):found.append({'pid':int(columns[0]),'executable':columns[1]})
return found
def await_host_exit():
active=running_hosts()
if active and not args.wait_for_hosts:
raise RuntimeError('Close Motion and Final Cut Pro before installing; installed app retained')
if active:
save_receipt('waiting_for_host_exit',runningHosts=active)
print('HOST_EXIT_REQUIRED: Motion/Final Cut Pro must exit naturally before installation. The delivery lock remains held; no host/UI action will be taken.',flush=True)
while active:
time.sleep(3)
active=running_hosts()
print('HOSTS_EXITED: continuing with installation under the same delivery lock.',flush=True)
save_receipt('ready_to_install',runningHosts=[])
def require_product(path):
info=plistlib.loads((path/'Contents/Info.plist').read_bytes())
if info.get('CFBundleIdentifier')!=bundle_id:raise RuntimeError('Generated/installed app identity mismatch: '+str(path))
return info
def unregister_generated_plugin(plugin):
# Removing an already absent registration can return 1. Establish absence
# by discovery rather than turning that idempotent state into a failure.
removed=subprocess.run(['pluginkit','-r',str(plugin)],capture_output=True,text=True)
info=plistlib.loads((plugin/'Contents/Info.plist').read_bytes())
discovered=run(['pluginkit','-m','-A','-D','-v','-i',info['CFBundleIdentifier']])
if str(plugin) in discovered:raise RuntimeError('Generated plug-in is still discoverable: '+removed.stderr)
def cleanup_known_products():
removed=[]
if previous.exists():
require_product(previous);shutil.rmtree(previous);removed.append(str(previous))
if staging.exists():
if app.exists():require_product(app)
shutil.rmtree(staging);removed.append(str(staging))
if archive.exists():archive.unlink();removed.append(str(archive))
return removed
try:
delivery_source={'commit':run(['git','rev-parse','HEAD']).strip(),
'trackedWorktreeChanges':run(['git','status','--porcelain','--untracked-files=no']).splitlines()}
plugin=app/'Contents/PlugIns/beatygril XPC Service.pluginkit'
if args.resume_prepared:
old_receipt=json.loads(receipt_path.read_text())
result=old_receipt.get('notarization',{})
if result.get('status')!='Accepted':raise RuntimeError('No Accepted staged notarization to resume')
info=require_product(app)
run(['codesign','--verify','--deep','--strict',str(app)])
run(['xcrun','stapler','validate',str(app)])
signature=subprocess.run(['codesign','-dvvv',str(app)],capture_output=True,text=True,check=True).stderr
expected=re.search(r'^CDHash=(.+)$',old_receipt.get('stagingSignature',''),re.M)
actual=re.search(r'^CDHash=(.+)$',signature,re.M)
if not expected or not actual or expected.group(1)!=actual.group(1):raise RuntimeError('Prepared signature hash changed')
if info.get('CFBundleShortVersionString')!=old_receipt.get('version') or info.get('CFBundleVersion')!=old_receipt.get('build'):raise RuntimeError('Prepared version changed')
source_commit=old_receipt.get('source',{}).get('commit')
if not source_commit or run(['git','diff',source_commit,'--','beatygril']).strip():raise RuntimeError('Product source changed since prepared build')
receipt.update(old_receipt);receipt.pop('failure',None)
receipt['resumedBy']=delivery_source
receipt['options']={'waitForHosts':args.wait_for_hosts,'userTests':args.user_tests,'resumePrepared':True}
receipt['nativeEffectQA']={'status':'pending_user' if args.user_tests else 'pending_operator','performedByScript':False}
save_receipt('resuming_prepared')
print('Previously Accepted and stapled Release verified; resuming installation.',flush=True)
else:
receipt['source']=delivery_source
save_receipt('building')
identities=run(['security','find-identity','-v','-p','codesigning',keychain])
if identity not in identities:raise RuntimeError('Valid Developer ID Application identity is unavailable')
print('Building universal Release…',flush=True)
run(['xcodebuild','-project','beatygril/beatygril.xcodeproj','-scheme','Wrapper Application','-configuration','Release','-derivedDataPath',str(staging),'-destination','generic/platform=macOS','build'],logs/'build.log')
plugin=app/'Contents/PlugIns/beatygril XPC Service.pluginkit'
# Xcode can register a fresh build. Remove this known generated path before
# lengthy signing/notarization while a user may still be testing the old app.
unregister_generated_plugin(plugin)
for framework in sorted((plugin/'Contents/Frameworks').glob('*.framework')):
run(['codesign','--force','--timestamp','--options','runtime','--sign',identity,str(framework)])
run(['codesign','--force','--timestamp','--options','runtime','--sign',identity,str(plugin)])
run(['codesign','--force','--timestamp','--options','runtime','--entitlements',str(root/'beatygril/beatygril/Wrapper Application/SandboxEntitlements.entitlements'),'--sign',identity,str(app)])
run(['codesign','--verify','--deep','--strict',str(app)])
info=require_product(app)
receipt['version']=info.get('CFBundleShortVersionString');receipt['build']=info.get('CFBundleVersion')
receipt['stagingSignature']=subprocess.run(['codesign','-dvvv',str(app)],capture_output=True,text=True,check=True).stderr
run(['ditto','-c','-k','--keepParent',str(app),str(archive)])
print('Submitting signed app to Apple notarization…',flush=True)
response=run(['xcrun','notarytool','submit',str(archive),'--keychain-profile','codex-dayu-notary-B3W25FX5H5','--keychain',keychain,'--wait','--output-format','json'])
(logs/'notarization.json').write_text(response)
result=json.loads(response)
receipt['notarization']=result
if result.get('status')!='Accepted':
if result.get('id'):
(logs/'notarization-details.json').write_text(run(['xcrun','notarytool','log',result['id'],'--keychain-profile','codex-dayu-notary-B3W25FX5H5','--keychain',keychain]))
raise RuntimeError('Apple did not accept notarization; original installed app retained')
run(['xcrun','stapler','staple',str(app)])
run(['xcrun','stapler','validate',str(app)])
# Keep a prepared build out of host discovery while the user still has the
# old installed version open. This removes only this known staging path.
unregister_generated_plugin(plugin)
save_receipt('prepared',signingVerified=True,stapleValidated=True,stagingPluginUnregistered=True)
# A live host retains its old XPC/OSC connection. Replacing the bundle while
# that connection exists can deadlock host shutdown or select stale code.
running=run(['ps','-axo','comm=']).splitlines()
hosts={'/Applications/Motion.app/Contents/MacOS/Motion',
'/Applications/Final Cut Pro.app/Contents/MacOS/Final Cut Pro'}
if hosts.intersection(line.strip() for line in running):
raise RuntimeError('Close Motion and Final Cut Pro before installing; installed app retained')
await_host_exit()
if installed.exists():
info=plistlib.loads((installed/'Contents/Info.plist').read_bytes())
if info.get('CFBundleIdentifier')!=bundle_id:raise RuntimeError('The installation path belongs to another product')
require_product(installed)
if previous.exists():raise RuntimeError('Previous recovery app already exists')
# Recheck immediately before touching the installed bundle. Waiting mode does
# not grant permission for a hot replacement if a host was opened again.
await_host_exit()
save_receipt('installing')
if installed.exists():
shutil.move(str(installed),str(previous))
try:
shutil.copytree(app,installed,symlinks=True)
@@ -69,17 +159,31 @@ try:
run(['pluginkit','-a',str(installed/'Contents/PlugIns/beatygril XPC Service.pluginkit')])
# Xcode may register its build product. Only the installed plugin should
# be discoverable while native host validation is in progress.
run(['pluginkit','-r',str(plugin)])
unregister_generated_plugin(plugin)
installed_plugin=installed/'Contents/PlugIns/beatygril XPC Service.pluginkit'
plugin_info=plistlib.loads((installed_plugin/'Contents/Info.plist').read_bytes())
discovered=run(['pluginkit','-m','-A','-D','-v','-i',plugin_info['CFBundleIdentifier']])
if str(installed_plugin) not in discovered:raise RuntimeError('PluginKit did not report the formal installed plug-in path')
(logs/'installed-discovery.txt').write_text(discovered)
save_receipt('installed',installedSignature=signature,installedSignatureVerified=True,
installedStapleValidated=True,gatekeeperAccepted=True,
pluginDiscovery={'bundleIdentifier':plugin_info['CFBundleIdentifier'],'formalPath':str(installed_plugin),'output':discovered})
except Exception:
if installed.exists():shutil.rmtree(installed)
if previous.exists():shutil.move(str(previous),str(installed))
raise
print(json.dumps({'status':'installed','path':str(installed),'notarization':result}),flush=True)
if args.user_tests:
print('USER_TESTS_PENDING: installed identity, ticket, Gatekeeper and formal-path discovery verified. Native effect QA is left to the user; no host/UI operation was performed.',flush=True)
else:
input('HOST_VALIDATION_PENDING: press Return only after host verification and duplicate inventory.\n')
receipt['nativeEffectQA']={'status':'operator_confirmed','performedByScript':False}
# Only this task’s known generated product copies are removed.
if previous.exists():shutil.rmtree(previous)
if archive.exists():archive.unlink()
if staging.exists():shutil.rmtree(staging)
removed=cleanup_known_products()
save_receipt('installed_user_qa_pending' if args.user_tests else 'complete',removedGeneratedPaths=removed)
print('Delivery staging and notarization archive cleaned.',flush=True)
except BaseException as error:
save_receipt('failed',failure={'type':type(error).__name__,'message':str(error)})
raise
finally:
(lock/'owner.json').unlink();lock.rmdir()
+109
查看文件
@@ -0,0 +1,109 @@
// Offline diagnostic only. It uses Apple's public Vision API, never a host UI
// or the reference plug-in. User media and generated observations stay local.
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
#import <Vision/Vision.h>
#import <ImageIO/ImageIO.h>
#include <math.h>
static NSArray *PGProbePoint(CGPoint p) { return @[@(p.x), @(p.y)]; }
static NSArray *RegionIndices(VNFaceLandmarkRegion2D *region, VNFaceLandmarkRegion2D *all) {
NSMutableArray *indices = [NSMutableArray array];
for (NSUInteger i=0; i<region.pointCount; i++) {
CGPoint p=region.normalizedPoints[i];
double best=INFINITY; NSUInteger index=NSNotFound;
for (NSUInteger j=0; j<all.pointCount; j++) {
CGPoint q=all.normalizedPoints[j];
double d=hypot(p.x-q.x,p.y-q.y);
if (d<best) { best=d; index=j; }
}
// Preserve an unmatched result instead of inventing a point identity.
[indices addObject:best<1e-6 ? @(index) : NSNull.null];
}
return indices;
}
int main(int argc, const char *argv[]) { @autoreleasepool {
if (argc<4) {
fprintf(stderr,"usage: inspect-face-geometry <video> <output.json> <seconds>...\n");
return 2;
}
NSURL *source=[NSURL fileURLWithPath:@(argv[1])];
NSURL *output=[NSURL fileURLWithPath:@(argv[2])];
if (![NSFileManager.defaultManager fileExistsAtPath:source.path]) return 2;
NSError *error=nil;
if (![NSFileManager.defaultManager createDirectoryAtURL:[output URLByDeletingLastPathComponent]
withIntermediateDirectories:YES attributes:nil error:&error]) {
fprintf(stderr,"%s\n",error.description.UTF8String); return 1;
}
AVURLAsset *asset=[AVURLAsset URLAssetWithURL:source options:nil];
AVAssetImageGenerator *generator=[[AVAssetImageGenerator alloc] initWithAsset:asset];
generator.appliesPreferredTrackTransform=YES;
generator.maximumSize=CGSizeMake(1280,1280);
generator.requestedTimeToleranceBefore=kCMTimeZero;
generator.requestedTimeToleranceAfter=kCMTimeZero;
NSMutableArray *frames=[NSMutableArray array];
for (int a=3; a<argc; a++) { @autoreleasepool {
char *end=NULL; double seconds=strtod(argv[a],&end);
if (!end || end==argv[a] || *end || !isfinite(seconds) || seconds<0) return 2;
CMTime actual=kCMTimeInvalid;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
CGImageRef image=[generator copyCGImageAtTime:CMTimeMakeWithSeconds(seconds,60000)
actualTime:&actual error:&error];
#pragma clang diagnostic pop
if (!image) { fprintf(stderr,"%s\n",error.description.UTF8String); return 1; }
VNDetectFaceLandmarksRequest *request=[VNDetectFaceLandmarksRequest new];
if (![VNDetectFaceLandmarksRequest.supportedRevisions containsIndex:VNDetectFaceLandmarksRequestRevision3] ||
![VNDetectFaceLandmarksRequest revision:VNDetectFaceLandmarksRequestRevision3
supportsConstellation:VNRequestFaceLandmarksConstellation76Points]) {
CGImageRelease(image); fprintf(stderr,"Vision revision 3 / 76 points unavailable\n"); return 1;
}
request.revision=VNDetectFaceLandmarksRequestRevision3;
request.constellation=VNRequestFaceLandmarksConstellation76Points;
VNImageRequestHandler *handler=[[VNImageRequestHandler alloc] initWithCGImage:image options:@{}];
if (![handler performRequests:@[request] error:&error]) {
CGImageRelease(image); fprintf(stderr,"%s\n",error.description.UTF8String); return 1;
}
NSMutableArray *faces=[NSMutableArray array];
for (VNFaceObservation *face in request.results) {
VNFaceLandmarks2D *lm=face.landmarks;
VNFaceLandmarkRegion2D *all=lm.allPoints;
CGRect box=face.boundingBox;
NSMutableArray *points=[NSMutableArray array];
for (NSUInteger i=0; i<all.pointCount; i++) {
CGPoint p=all.normalizedPoints[i];
[points addObject:PGProbePoint(CGPointMake(box.origin.x+p.x*box.size.width,
box.origin.y+p.y*box.size.height))];
}
NSDictionary *regions=@{@"contour":RegionIndices(lm.faceContour,all),
@"leftEye":RegionIndices(lm.leftEye,all), @"rightEye":RegionIndices(lm.rightEye,all),
@"leftPupil":RegionIndices(lm.leftPupil,all), @"rightPupil":RegionIndices(lm.rightPupil,all),
@"nose":RegionIndices(lm.nose,all), @"noseCrest":RegionIndices(lm.noseCrest,all),
@"outerLips":RegionIndices(lm.outerLips,all), @"innerLips":RegionIndices(lm.innerLips,all)};
[faces addObject:@{@"box":@[@(box.origin.x),@(box.origin.y),@(box.size.width),@(box.size.height)],
@"confidence":@(face.confidence), @"yaw":face.yaw?:NSNull.null,
@"roll":face.roll?:NSNull.null, @"pitch":face.pitch?:NSNull.null,
@"allPoints":points, @"regions":regions}];
}
NSString *name=[NSString stringWithFormat:@"frame-%0.3f.png",seconds];
NSURL *png=[[output URLByDeletingLastPathComponent] URLByAppendingPathComponent:name];
CGImageDestinationRef destination=CGImageDestinationCreateWithURL((__bridge CFURLRef)png,CFSTR("public.png"),1,NULL);
if (!destination) { CGImageRelease(image); return 1; }
CGImageDestinationAddImage(destination,image,NULL);
BOOL saved=CGImageDestinationFinalize(destination); CFRelease(destination);
[frames addObject:@{@"requestedSeconds":@(seconds), @"actualSeconds":@(CMTimeGetSeconds(actual)),
@"width":@(CGImageGetWidth(image)), @"height":@(CGImageGetHeight(image)),
@"image":name, @"faces":faces}];
CGImageRelease(image); if (!saved) return 1;
}}
NSDictionary *result=@{@"schema":@1,@"source":source.path,@"coordinateSystem":@"full-image normalized, y up",
@"authority":@"independent Apple Vision probe, not reference plug-in execution",
@"requestRevision":@3,@"constellation":@76,@"frames":frames};
NSData *json=[NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted|NSJSONWritingSortedKeys error:&error];
if (!json || ![json writeToURL:output options:NSDataWritingAtomic error:&error]) {
fprintf(stderr,"%s\n",error.description.UTF8String); return 1;
}
printf("Saved %lu independent frame observations.\n",(unsigned long)frames.count);
return 0;
}}
+2
查看文件
@@ -9,3 +9,5 @@ xcrun clang "${common[@]}" "$plugin_dir/PGBeautyRenderer.m" tests/beauty_render_
"$output_dir/beauty-tests"
xcrun clang "${common[@]}" "$plugin_dir/PGFaceProcessor.m" tests/face_tests.m -o "$output_dir/face-tests"
"$output_dir/face-tests" "$PWD/tests/fixtures/astronaut.png"
xcrun clang "${common[@]}" "$plugin_dir/PGFaceProcessor.m" tests/slim_tests.m -o "$output_dir/slim-tests"
"$output_dir/slim-tests" "$PWD/tests/fixtures/astronaut.png"
+3 -2
查看文件
@@ -88,7 +88,7 @@ static void RealtimeChecks(CIContext *context,CIImage *image) {
VNFaceObservation *left=Face(CGRectMake(.10,.25,.30,.50));
VNFaceObservation *right=Face(CGRectMake(.66,.30,.20,.40));
PGFaceFixture *p=[PGFaceFixture new];p.detections=@[left,right];
s.faceThin=.8;
s.faceSmallHead=.8;
PGFaceFrame *automatic=[p processImage:image settings:s time:kCMTimeZero error:&error];
Check(automatic.deformationGeometry.count==1 && GeometryBox(automatic.deformationGeometry.firstObject).origin.x<.5,@"default realtime geometry chooses one area/center primary without analysis");
Check(automatic.faces.count==2 && automatic.faces[0]==left && automatic.faces[1]==right,@"default appearance retains both original Vision observations");
@@ -322,7 +322,8 @@ int main(int argc, const char *argv[]) {
float *controls[] = {&changed.faceThin, &changed.faceBigEye, &changed.faceShort, &changed.faceNarrow, &changed.faceChin, &changed.faceV, &changed.faceJaw, &changed.faceCheekbone, &changed.faceSmallHead};
*controls[i] = 1;
PGFaceFrame *warped = [processor processImage:image settings:changed time:kCMTimeZero error:&error];
Check(warped != nil && ![Pixels(context, warped.image) isEqualToData:base], [@"geometry control changes pixels: " stringByAppendingString:names[i]]);
BOOL changedPixels=![Pixels(context, warped.image) isEqualToData:base];
Check(warped != nil && (i==0 ? !changedPixels : changedPixels), i==0 ? @"thin safely skips 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;
+163
查看文件
@@ -0,0 +1,163 @@
#import <Foundation/Foundation.h>
#import <CoreImage/CoreImage.h>
#import <Metal/Metal.h>
#import "PGFaceProcessor.h"
#include <math.h>
@interface PGFaceProcessor (SlimTest)
- (CIImage *)warpImage:(CIImage *)image geometry:(NSArray<NSDictionary *> *)geometry settings:(PGBeautySettings)s extent:(CGRect)extent;
@end
@interface PGRealtimeFaceTracker : NSObject
- (NSDictionary *)resultForGeometry:(NSArray *)raw settings:(PGBeautySettings)s time:(CMTime)time extent:(CGRect)extent;
@end
static NSUInteger checks=0;
static double maxOracleError=0;
static void Check(BOOL ok,NSString *message) {checks++;if(!ok){fprintf(stderr,"FAIL: %s\n",message.UTF8String);exit(1);}}
static double Clamp(double x,double a,double b) {return fmin(b,fmax(a,x));}
static CGPoint ReadPoint(NSArray *a) {return CGPointMake([a[0] doubleValue],[a[1] doubleValue]);}
static NSArray *WritePoint(CGPoint p) {return @[@(p.x),@(p.y)];}
// Authored 2D projections, with the documented 76-point layout. They are not
// extracted vendor assets or private face coordinates. Positive yaw projects
// the nose toward the right while the silhouette becomes narrower.
static NSDictionary *Geometry(double yaw,double roll) {
NSMutableArray *p=[NSMutableArray new];
for(NSUInteger i=0;i<76;i++)[p addObject:@[@.5,@.5]];
p[6]=@[@.34,@.65];p[13]=@[@.66,@.65];
p[47]=@[@.5,@.60];p[48]=@[@.5,@.52];p[49]=@[@.5,@.45];p[52]=@[@.5,@.42];
for(NSUInteger i=0;i<17;i++) {
double t=M_PI+M_PI*i/16;
p[59+i]=@[@(.5+.29*cos(t)),@(.65+.43*sin(t))];
}
for(NSUInteger i=0;i<76;i++) {
CGPoint a=ReadPoint(p[i]);
double depth=(i==47 || i==48 || i==49 || i==52)?.18:.03;
double x=(a.x-.5)*cos(yaw)+depth*sin(yaw),y=a.y-.5;
p[i]=WritePoint(CGPointMake(.5+x*cos(roll)-y*sin(roll),.5+x*sin(roll)+y*cos(roll)));
}
NSMutableDictionary *g=[NSMutableDictionary dictionaryWithDictionary:@{@"box":@[@.15,@.15,@.7,@.6],@"points76":p}];
for(NSString *key in @[@"leftEye",@"rightEye",@"leftBrow",@"rightBrow",@"nose",@"lips",@"mouth",@"contour"])g[key]=@[];
return g;
}
// Independent scalar oracle from the recorded AIR specification. Its explicit
// y-down conversion checks the Core Image implementation's coordinate bridge.
static CGPoint Oracle(CGPoint pixel,NSDictionary *g,CGRect extent,double thin) {
static const unsigned int origins[17]={75,59,74,60,73,61,72,62,71,63,70,64,69,65,68,66,67};
static const unsigned int anchors[17]={6,13,47,47,47,47,48,48,48,48,49,49,49,49,52,52,47};
NSArray *raw=g[@"points76"];CGPoint p[76];
for(NSUInteger i=0;i<76;i++){p[i]=ReadPoint(raw[i]);p[i].y=1-p[i].y;}
double width=hypot((p[75].x-p[59].x)*extent.size.width,(p[75].y-p[59].y)*extent.size.height);
double height=hypot((p[67].x-(p[74].x+p[60].x)/2)*extent.size.width,(p[67].y-(p[74].y+p[60].y)/2)*extent.size.height);
double gain=Clamp(280/fmax(1,fmax(width,2*height)),1,2.25)*.025*Clamp(thin,0,1);
CGPoint q=CGPointMake((pixel.x-extent.origin.x)/extent.size.width,1-(pixel.y-extent.origin.y)/extent.size.height);
for(NSUInteger i=0;i<17;i++) {
CGPoint a=p[origins[i]],b=p[anchors[i]];
double r=hypot(a.x-b.x,a.y-b.y);
if(r<=.0001)continue;
double w=Clamp(1-hypot(q.x-a.x,q.y-a.y)/r,0,1);
q.x=Clamp(q.x-(b.x-a.x)*gain*w,0,1);q.y=Clamp(q.y-(b.y-a.y)*gain*w,0,1);
}
return CGPointMake(q.x,1-q.y);
}
static CIImage *CoordinateImage(CGRect extent) {
NSUInteger w=extent.size.width,h=extent.size.height;
NSMutableData *d=[NSMutableData dataWithLength:w*h*4*sizeof(float)];float *p=d.mutableBytes;
for(NSUInteger y=0;y<h;y++)for(NSUInteger x=0;x<w;x++) {
NSUInteger k=(y*w+x)*4;p[k]=(x+.5)/w;p[k+1]=(y+.5)/h;p[k+2]=.25;p[k+3]=1;
}
return [[CIImage imageWithBitmapData:d bytesPerRow:w*4*sizeof(float) size:extent.size format:kCIFormatRGBAf colorSpace:nil]
imageByApplyingTransform:CGAffineTransformMakeTranslation(extent.origin.x,extent.origin.y)];
}
static NSData *Pixels(CIContext *c,CIImage *image) {
CGRect e=image.extent;NSUInteger w=e.size.width,h=e.size.height;
NSMutableData *d=[NSMutableData dataWithLength:w*h*4*sizeof(float)];
[c render:image toBitmap:d.mutableBytes rowBytes:w*4*sizeof(float) bounds:e format:kCIFormatRGBAf colorSpace:nil];return d;
}
static NSData *Archive(NSDictionary *geometry) {
return [NSPropertyListSerialization dataWithPropertyList:@{@"version":@1,@"frames":@{@"0/1/0":@{@"value":@0,@"scale":@1,@"epoch":@0,@"faces":@[geometry]}}} format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil];
}
static void NumericCase(CIContext *c,PGFaceProcessor *p,CGRect e,NSDictionary *g,double amount,NSString *name) {
PGBeautySettings s=PGBeautyDefaults();s.faceThin=amount;
CIImage *input=CoordinateImage(e),*output=[p warpImage:input geometry:@[g] settings:s extent:e];
Check(output!=nil && CGRectEqualToRect(output.extent,e),[name stringByAppendingString:@" preserves extent"]);
NSData *data=Pixels(c,output);const float *pixels=data.bytes;double worst=0,maxMove=0;
NSUInteger worstX=0,worstY=0;CGPoint worstExpected=CGPointZero,worstActual=CGPointZero;
NSUInteger w=e.size.width,h=e.size.height;
for(NSUInteger y=3;y<h-3;y+=7)for(NSUInteger x=3;x<w-3;x+=7) {
// Bitmap rows are top first, while destCoord / geometry are y up.
CGPoint q=Oracle(CGPointMake(e.origin.x+x+.5,e.origin.y+h-y-.5),g,e,amount);
double ex=Clamp(q.x,.5/w,1-.5/w),ey=Clamp(1-q.y,.5/h,1-.5/h);
NSUInteger k=(y*w+x)*4;
double difference=fmax(fabs(pixels[k]-ex),fabs(pixels[k+1]-ey));
if(difference>worst){worst=difference;worstX=x;worstY=y;worstExpected=CGPointMake(ex,ey);worstActual=CGPointMake(pixels[k],pixels[k+1]);}
maxMove=fmax(maxMove,hypot(ex-(x+.5)/w,ey-(y+.5)/h));
}
if(worst>0.00006)fprintf(stderr,"%s oracle max error=%g at %lu,%lu expected=(%g,%g) actual=(%g,%g)\n",name.UTF8String,worst,(unsigned long)worstX,(unsigned long)worstY,worstExpected.x,worstExpected.y,worstActual.x,worstActual.y);
maxOracleError=fmax(maxOracleError,worst);
Check(worst<0.00006,[name stringByAppendingString:@" GPU inverse field matches independent 17-pin oracle"]);
Check(amount==0 || maxMove>.0001,[name stringByAppendingString:@" exercises a nonzero displacement"]);
NSUInteger corner=4*((h-3)*w+(w-3));
Check(fabs(pixels[corner]-(w-2.5)/w)<1e-6 && fabs(pixels[corner+1]-(h-2.5)/h)<1e-6,[name stringByAppendingString:@" preserves distant background"]);
}
int main(int argc,const char *argv[]) {@autoreleasepool {
CIContext *c=[CIContext contextWithMTLDevice:MTLCreateSystemDefaultDevice() options:@{kCIContextWorkingColorSpace:NSNull.null,kCIContextOutputColorSpace:NSNull.null}];
PGFaceProcessor *p=[PGFaceProcessor new];
CGRect extents[]={CGRectMake(0,0,240,240),CGRectMake(0,0,320,180),CGRectMake(-17,29,180,320),CGRectMake(0,0,640,360)};
double yaw[]={0,.65,1.40,-1.40},roll[]={0,.45,-.50};
NumericCase(c,p,extents[1],Geometry(0,0),0,@"zero strength");
for(NSUInteger e=0;e<4;e++)for(NSUInteger y=0;y<4;y++)for(NSUInteger r=0;r<3;r++) {
NSString *name=[NSString stringWithFormat:@"size%lu yaw%lu roll%lu",(unsigned long)e,(unsigned long)y,(unsigned long)r];
NumericCase(c,p,extents[e],Geometry(yaw[y],roll[r]),1,name);
}
NumericCase(c,p,extents[1],Geometry(.6,.2),.5,@"half strength");
CIImage *image=CoordinateImage(extents[1]);NSData *source=Pixels(c,image);
PGBeautySettings s=PGBeautyDefaults();s.faceThin=1;s.trackingEnabled=1;NSError *error=nil;
NSDictionary *g=Geometry(.9,.15);[p setTrackingData:Archive(g) forTarget:0];
PGFaceFrame *frame=[p processImage:image settings:s time:kCMTimeZero error:&error];
Check(frame!=nil && [frame.deformationGeometry[0][@"points76"] count]==76,@"offline restoration retains all 76 indexed points");
PGFaceProcessor *restored=[PGFaceProcessor new];[restored setTrackingData:[p trackingDataForTarget:0] forTarget:0];
Check([Pixels(c,[restored processImage:image settings:s time:kCMTimeZero error:&error].image) isEqualToData:Pixels(c,frame.image)],@"76-point geometry survives archive round trip with identical pixels");
NSData *imageWarp=Pixels(c,[p warpImage:frame.colorMask geometry:@[] settings:s extent:image.extent]);
Check(imageWarp.length==source.length,@"warped support masks retain dimensions");
// Apply the same coordinate field to an independent scalar mask ramp.
CIImage *mask=[image imageByApplyingFilter:@"CIColorMatrix" withInputParameters:@{@"inputRVector":[CIVector vectorWithX:1 Y:0 Z:0 W:0],@"inputGVector":[CIVector vectorWithX:1 Y:0 Z:0 W:0],@"inputBVector":[CIVector vectorWithX:1 Y:0 Z:0 W:0]}];
NSData *maskData=Pixels(c,[p warpImage:mask geometry:@[g] settings:s extent:image.extent]),*frameData=Pixels(c,frame.image);
const float *m=maskData.bytes,*v=frameData.bytes;
BOOL masksAgree=YES;for(NSUInteger i=0;i<source.length/sizeof(float);i+=4)if(fabs(m[i]-v[i])>1e-6){masksAgree=NO;break;}
Check(masksAgree,@"scalar support masks and RGB use the same source coordinates");
for(NSUInteger mode=0;mode<4;mode++) {
NSMutableDictionary *bad=[g mutableCopy];
if(mode==0)[bad removeObjectForKey:@"points76"];
if(mode==1)bad[@"points76"]=[g[@"points76"] subarrayWithRange:NSMakeRange(0,75)];
if(mode==2){NSMutableArray *a=[g[@"points76"] mutableCopy];a[65]=@[@(NAN),@.5];bad[@"points76"]=a;}
if(mode==3){NSMutableArray *a=[g[@"points76"] mutableCopy];a[65]=@[@9,@.5];bad[@"points76"]=a;}
[p setTrackingData:Archive(bad) forTarget:0];frame=[p processImage:image settings:s time:kCMTimeZero error:&error];
Check(frame!=nil && frame.deformationGeometry.count==1,@"legacy/malformed indexed points retain the valid face record");
Check([Pixels(c,frame.image) isEqualToData:source],@"legacy/malformed indexed points never trigger a guessed symmetric thin warp");
PGBeautySettings other=s;other.faceNarrow=.5;
Check(![Pixels(c,[p processImage:image settings:other time:kCMTimeZero error:&error].image) isEqualToData:source],@"other controls remain available in legacy archives");
}
PGRealtimeFaceTracker *tracker=[PGRealtimeFaceTracker new];PGBeautySettings live=PGBeautyDefaults();
NSDictionary *a=[tracker resultForGeometry:@[Geometry(0,0)] settings:live time:kCMTimeZero extent:image.extent];
NSDictionary *b=[tracker resultForGeometry:@[Geometry(.2,0)] settings:live time:CMTimeMake(1,25) extent:image.extent];
Check([a[@"output"][0][@"points76"] count]==76 && [b[@"output"][0][@"points76"] count]==76,@"realtime stabilization retains the complete indexed point layout");
Check([b isEqual:[tracker resultForGeometry:@[Geometry(.2,0)] settings:live time:CMTimeMake(1,25) extent:image.extent]],@"repeated rendering does not smooth the 76-point layout again");
NSDictionary *held=[tracker resultForGeometry:@[] settings:live time:CMTimeMake(2,25) extent:image.extent];
Check([held[@"output"][0][@"points76"] isEqual:b[@"output"][0][@"points76"]],@"missing detection holds the complete point layout");
NSDictionary *turned=Geometry(1.4,0);
NSDictionary *back=[tracker resultForGeometry:@[turned] settings:live time:CMTimeMake(3,25) extent:image.extent];
Check([back[@"output"][0][@"points76"] isEqual:turned[@"points76"]],@"reacquired side-face points do not blend with a held frontal pose");
for(int index=1;index<argc;index++) {@autoreleasepool {
CIImage *portrait=[CIImage imageWithContentsOfURL:[NSURL fileURLWithPath:@(argv[index])] options:@{kCIImageColorSpace:NSNull.null}];
PGFaceProcessor *real=[PGFaceProcessor new];PGBeautySettings rs=PGBeautyDefaults();rs.faceThin=1;
PGFaceFrame *f=[real processImage:portrait settings:rs time:kCMTimeZero error:&error];
Check(f!=nil && f.faces.count>0 && [f.deformationGeometry[0][@"points76"] count]==76,@"real Vision detection supplies exactly 76 points to thin deformation");
Check(![Pixels(c,f.image) isEqualToData:Pixels(c,portrait)],@"maximum thin changes a real detected portrait");
Check([real analyzeImage:portrait settings:rs time:kCMTimeZero error:&error],@"real pose analysis saves the complete geometry");
PGFaceProcessor *copy=[PGFaceProcessor new];[copy setTrackingData:[real trackingDataForTarget:0] forTarget:0];rs.trackingEnabled=1;
Check([Pixels(c,[copy processImage:portrait settings:rs time:kCMTimeZero error:&error].image) isEqualToData:Pixels(c,f.image)],@"real pose replays from persisted 76 points with identical pixels");
}}
printf("PASS: %lu slim alignment checks; maximum UV oracle error %.9g\n",(unsigned long)checks,maxOracleError);return 0;
}}