Align slimming with 76 indexed landmarks and 17 sequential local pins; pass 604 offline checks
这个提交包含在:
1 父节点
2a856c669a
当前提交
ec22f1b2cf
16 个文件已更改
+424
-39
没有与搜索条件相匹配的文件
@@ -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 副本。
|
工程从 `/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` 标签。
|
私有源码仓库:`https://dayu.media/git/dayu/paintfcp-fxplug.git`。恢复入口见 [RECOVERY.md](RECOVERY.md),当前验证记录见 [验证记录](docs/验证记录.md)。原始模板保存在 `baseline-20261021` 标签。
|
||||||
|
|
||||||
|
## 1.0.5 瘦脸修复
|
||||||
|
|
||||||
|
瘦脸现固定使用Vision revision 3的76点配置,以17组轮廓到鼻部/瞳孔锚点的局部逆向采样替代脸框水平挤压,包含原版尺寸增益与滑块强度映射。完整点阵贯通实时稳定与离线存储;旧分析记录没有完整76点时跳过瘦脸,重新分析可补齐。其他脸型项仍使用原有近似实现,不能将本次对齐扩张为全部脸型逐像素等价。
|
||||||
|
|
||||||
|
离屏验证新增224项:含48组脸部投影/尺寸/歪头与独立17-pin数值场对照、数据恢复和缺点保护;完整回归604项通过。Motion/FCP界面由用户自测。可运行 `python3 scripts/deliver.py --wait-for-hosts --user-tests`,签名公证后只读等待宿主自然退出,安装验证与清理无需操作宿主界面。
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# 大鱼人像美化 FCP 效果模板
|
# 大鱼人像美化 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字节。
|
黑屏修复恢复标准效果源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截图逐像素核验。
|
本次已经静态核验Source Media→clip→相对文件链存在、文件字节一致、48项发布仍解析、隐藏数据未发布和保存选择状态;修复后已在FCP独立验收项目新增实例,确认输入替换、实际人像和提亮画面正常;Mix=0逐字节直通由宿主适配器IOSurface回归测试覆盖,未将其混称为FCP截图逐像素核验。
|
||||||
|
|
||||||
|
1.0.5将瘦脸改为76点驱动的17组局部位移,供用户在Motion/FCP自测最大瘦脸与转脸。旧跟踪记录若没有全点阵,需重新分析后才有瘦脸,或关闭“使用跟踪数据”采用当前帧自动检测。
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.4",
|
"version": "1.0.5",
|
||||||
"sha256": "d234d504eba9e6d37b910b622a46c92458e658c5bbdad2caf5831c2a1463a3e2",
|
"sha256": "ba08de6916cd837b3733aa0c6fcb78f2e8224431d25e1310089ae67d18445f2d",
|
||||||
"filterUUID": "FB14B0E6-59B2-4836-916F-CD0C559E9241",
|
"filterUUID": "FB14B0E6-59B2-4836-916F-CD0C559E9241",
|
||||||
"publishedFunctionalControls": 46,
|
"publishedFunctionalControls": 46,
|
||||||
"informationalControls": 2,
|
"informationalControls": 2,
|
||||||
@@ -27,6 +27,6 @@
|
|||||||
"date": "2026-10-21",
|
"date": "2026-10-21",
|
||||||
"FCP": "Restored input-chain template shows supplied real video; effect parameters and forward analysis responded in independent QA project.",
|
"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.",
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
<flags>1</flags>
|
<flags>1</flags>
|
||||||
</template>
|
</template>
|
||||||
<build />
|
<build />
|
||||||
<description>大鱼人像美化 1.0.4:人像磨皮、肤色、脸型、五官保护和三目标人脸跟踪。模板中频磨皮默认为50%。</description>
|
<description>大鱼人像美化 1.0.5:人像磨皮、肤色、脸型、五官保护和三目标人脸跟踪。模板中频磨皮默认为50%。</description>
|
||||||
<scene>
|
<scene>
|
||||||
<sceneSettings>
|
<sceneSettings>
|
||||||
<width>1920</width>
|
<width>1920</width>
|
||||||
@@ -298,7 +298,7 @@
|
|||||||
<parameter name="Width" id="313" flags="8589934610" default="1" value="1920" />
|
<parameter name="Width" id="313" flags="8589934610" default="1" value="1920" />
|
||||||
<parameter name="Height" id="314" flags="8589934610" default="1" value="1080" />
|
<parameter name="Height" id="314" flags="8589934610" default="1" value="1080" />
|
||||||
</parameter>
|
</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" />
|
<timing in="0 1 1 0" out="299 30 1 0" offset="0 1 1 0" />
|
||||||
<baseFlags>8589934608</baseFlags>
|
<baseFlags>8589934608</baseFlags>
|
||||||
<parameter name="磨皮" id="100" flags="8589938704">
|
<parameter name="磨皮" id="100" flags="8589938704">
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>XPC!</string>
|
<string>XPC!</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.4</string>
|
<string>1.0.5</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>6</string>
|
<string>7</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string></string>
|
<string></string>
|
||||||
<key>PlugInKit</key>
|
<key>PlugInKit</key>
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
<key>uuid</key>
|
<key>uuid</key>
|
||||||
<string>FB14B0E6-59B2-4836-916F-CD0C559E9241</string>
|
<string>FB14B0E6-59B2-4836-916F-CD0C559E9241</string>
|
||||||
<key>version</key>
|
<key>version</key>
|
||||||
<string>1.0.4</string>
|
<string>1.0.5</string>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>className</key>
|
<key>className</key>
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
<key>uuid</key>
|
<key>uuid</key>
|
||||||
<string>E0E432C8-3D71-4ABA-B006-E7F519E06C87</string>
|
<string>E0E432C8-3D71-4ABA-B006-E7F519E06C87</string>
|
||||||
<key>version</key>
|
<key>version</key>
|
||||||
<string>1.0.4</string>
|
<string>1.0.5</string>
|
||||||
<key>protocolNames</key>
|
<key>protocolNames</key>
|
||||||
<array>
|
<array>
|
||||||
<string>FxOnScreenControl</string>
|
<string>FxOnScreenControl</string>
|
||||||
|
|||||||
@@ -6,6 +6,13 @@
|
|||||||
#include <float.h>
|
#include <float.h>
|
||||||
|
|
||||||
static NSString * const PGFaceErrorDomain = @"media.dayu.PaintFCP.Face";
|
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 ()
|
@interface PGFaceFrame ()
|
||||||
@property (nonatomic, copy, readwrite) NSArray<VNFaceObservation *> *faces;
|
@property (nonatomic, copy, readwrite) NSArray<VNFaceObservation *> *faces;
|
||||||
@@ -63,14 +70,42 @@ static NSArray *PGRegion(VNFaceLandmarkRegion2D *region, CGRect box) {
|
|||||||
}
|
}
|
||||||
return points;
|
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) {
|
static NSDictionary *PGGeometry(VNFaceObservation *observation) {
|
||||||
CGRect box = observation.boundingBox;
|
CGRect box = observation.boundingBox;
|
||||||
VNFaceLandmarks2D *lm = observation.landmarks;
|
VNFaceLandmarks2D *lm = observation.landmarks;
|
||||||
return @{@"box": PGRectArray(box),
|
NSMutableDictionary *geometry = [@{@"box": PGRectArray(box),
|
||||||
@"leftEye": PGRegion(lm.leftEye, box), @"rightEye": PGRegion(lm.rightEye, box),
|
@"leftEye": PGRegion(lm.leftEye, box), @"rightEye": PGRegion(lm.rightEye, box),
|
||||||
@"leftBrow": PGRegion(lm.leftEyebrow, box), @"rightBrow": PGRegion(lm.rightEyebrow, box),
|
@"leftBrow": PGRegion(lm.leftEyebrow, box), @"rightBrow": PGRegion(lm.rightEyebrow, box),
|
||||||
@"nose": PGRegion(lm.nose, box), @"lips": PGRegion(lm.outerLips, 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) {
|
static NSDictionary *PGSanitizedGeometry(id value) {
|
||||||
if (![value isKindOfClass:NSDictionary.class]) return nil;
|
if (![value isKindOfClass:NSDictionary.class]) return nil;
|
||||||
@@ -88,6 +123,10 @@ static NSDictionary *PGSanitizedGeometry(id value) {
|
|||||||
}
|
}
|
||||||
result[name] = points;
|
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];
|
return [result copy];
|
||||||
}
|
}
|
||||||
static CGRect PGRegionBounds(NSDictionary *geometry, NSString *name, CGRect fallback) {
|
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,
|
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);
|
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"]);
|
CGRect old=PGArrayRect(previous[@"box"]),box=PGArrayRect(current[@"box"]);
|
||||||
if(!PGValidRect(old) || !PGValidRect(box))return current;
|
if(!PGValidRect(old) || !PGValidRect(box))return current;
|
||||||
// Independent stabilization: translate/scale old landmarks into the 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];
|
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];
|
return [stable copy];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,7 +299,7 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
|
|||||||
else if(index>=0) {
|
else if(index>=0) {
|
||||||
NSDictionary *current=raw[(NSUInteger)index];
|
NSDictionary *current=raw[(NSUInteger)index];
|
||||||
double dt=reference?CMTimeGetSeconds(CMTimeSubtract(time,PGRecordTime(reference))):0;
|
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;}
|
} 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),
|
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];
|
@"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];
|
_lock = [[NSLock alloc] init];
|
||||||
_realtime = [PGRealtimeFaceTracker new];
|
_realtime = [PGRealtimeFaceTracker new];
|
||||||
_targets = [NSMutableArray arrayWithObjects:[NSMutableDictionary dictionary], [NSMutableDictionary dictionary], [NSMutableDictionary dictionary], nil];
|
_targets = [NSMutableArray arrayWithObjects:[NSMutableDictionary dictionary], [NSMutableDictionary dictionary], [NSMutableDictionary dictionary], nil];
|
||||||
// Analytic, feathered sampling deformation authored for this project.
|
// Local thinning reconstructs the recovered 17-pin coordinate math.
|
||||||
// Positive short-face shortens the lower face; positive chin lengthens it.
|
// The other eight controls retain this project's analytic approximation.
|
||||||
// This is an independent approximation, not the reference vendor's warp.
|
// Each pin changes the sampling coordinate, with only one image resample.
|
||||||
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) {"
|
NSMutableString *source = [NSMutableString stringWithString:
|
||||||
" vec2 p = destCoord(); vec2 n = (p-c)/size;"
|
@"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 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 lower = (1.0-smoothstep(-0.12,0.27,n.y))*body;"
|
||||||
" float sides = smoothstep(0.08,0.32,abs(n.x));"
|
" 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 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)));"
|
" 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 += (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);"
|
" 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));"
|
" vec2 dl = (p-le)/er; float wl = 1.0-smoothstep(0.0,1.0,length(dl));"
|
||||||
" p -= (p-le)*bigEye*0.33*wl*wl;"
|
" p -= (p-le)*bigEye*0.33*wl*wl;"
|
||||||
" vec2 dr = (p-re)/er; float wr = 1.0-smoothstep(0.0,1.0,length(dr));"
|
" 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 push
|
||||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||||
_warp = [CIWarpKernel kernelWithString:source];
|
_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)];
|
CIImage *prepared = [image imageByApplyingTransform:CGAffineTransformMakeTranslation(-extent.origin.x, -extent.origin.y)];
|
||||||
prepared = [prepared imageByApplyingTransform:CGAffineTransformMakeScale(scale, scale)];
|
prepared = [prepared imageByApplyingTransform:CGAffineTransformMakeScale(scale, scale)];
|
||||||
VNDetectFaceLandmarksRequest *request = [[VNDetectFaceLandmarksRequest alloc] init];
|
VNDetectFaceLandmarksRequest *request = [[VNDetectFaceLandmarksRequest alloc] init];
|
||||||
|
request.revision = VNDetectFaceLandmarksRequestRevision3;
|
||||||
|
request.constellation = VNRequestFaceLandmarksConstellation76Points;
|
||||||
// An image request handler is intentionally local to one invocation. A shared
|
// An image request handler is intentionally local to one invocation. A shared
|
||||||
// sequence handler would make parallel / reverse render order observable.
|
// sequence handler would make parallel / reverse render order observable.
|
||||||
VNImageRequestHandler *handler = [[VNImageRequestHandler alloc] initWithCIImage:prepared options:@{}];
|
VNImageRequestHandler *handler = [[VNImageRequestHandler alloc] initWithCIImage:prepared options:@{}];
|
||||||
@@ -418,14 +490,40 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
|
|||||||
CIImage *result = image;
|
CIImage *result = image;
|
||||||
for (NSDictionary *face in geometry) {
|
for (NSDictionary *face in geometry) {
|
||||||
CGRect box = PGArrayRect(face[@"box"]), pixels = PGToPixels(box, extent);
|
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 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));
|
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));
|
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)],
|
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.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];
|
CIImage *input = [result imageByClampingToExtent];
|
||||||
result = [_warp applyWithExtent:extent roiCallback:^CGRect(int index, CGRect rect) { (void)index; (void)rect; return extent; } inputImage:input arguments:arguments];
|
result = [_warp applyWithExtent:extent roiCallback:^CGRect(int index, CGRect rect) { (void)index; (void)rect; return extent; } inputImage:input arguments:arguments];
|
||||||
if (!result) return nil;
|
if (!result) return nil;
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.4</string>
|
<string>1.0.5</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>6</string>
|
<string>7</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
|||||||
@@ -75,3 +75,7 @@ REA 直接打开内嵌插件。原包为 arm64/x86_64 通用 Mach-O,Ghidra 无
|
|||||||
## 最大瘦脸与转脸差异(用户复测反馈)
|
## 最大瘦脸与转脸差异(用户复测反馈)
|
||||||
|
|
||||||
用户反馈跟踪正常,但最大瘦脸与原版不一致,侧脸出现异常变形。补充REA与Metal AIR分析确认,原版瘦脸以76点加中心点输入,执行17组轮廓到鼻部/瞳孔锚点的局部逆向采样;当前复刻采用矩形框水平对称场,两者没有对齐。已检查的GPU瘦脸路径没有显式yaw角度切换或侧脸衰减,不能将局部二维点自适应描述为完整3D姿态识别。详见 [脸型变形与转脸分析](脸型变形与转脸分析.md) 及 [结构报告](slim-pose-report.json)。本轮未修改安装版的变形代码。
|
用户反馈跟踪正常,但最大瘦脸与原版不一致,侧脸出现异常变形。补充REA与Metal AIR分析确认,原版瘦脸以76点加中心点输入,执行17组轮廓到鼻部/瞳孔锚点的局部逆向采样;当前复刻采用矩形框水平对称场,两者没有对齐。已检查的GPU瘦脸路径没有显式yaw角度切换或侧脸衰减,不能将局部二维点自适应描述为完整3D姿态识别。详见 [脸型变形与转脸分析](脸型变形与转脸分析.md) 及 [结构报告](slim-pose-report.json)。本轮未修改安装版的变形代码。
|
||||||
|
|
||||||
|
## 1.0.5 对齐实现
|
||||||
|
|
||||||
|
已根据上述证据将瘦脸本身改为76点/17组局部逆向采样,`.025 × sizeGain`映射、UV距离、每组clamp和顺序均由独立数值场对照。没有复制原版metallib或模型。全76点参与当前实时稳定并保存离线记录;缺失完整点阵时不再退回整框瘦脸。其他8类几何控制、完整77点稳定系数和快速多人重叠采样仍有既有差异。阶段验证见验证记录;原分析中“1.0.4尚未修复”是历史状态。
|
||||||
@@ -0,0 +1,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,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,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
|
||||||
+8
@@ -49,3 +49,11 @@ Motion:独立普通视频项目,源PTS0、1920×1080/25fps/10秒,媒体通
|
|||||||
后续静态REA和Apple Metal AIR分析确认:原版瘦脸以17组轮廓到鼻部/瞳孔锚点的局部位移驱动,当前瘦脸按框水平对称变形,两者的方向、范围与强度模型不同。独立Vision76点离线探针在同一视频11个采样时刻均检出1脸76点,含明显侧脸;它只证明关键点仍可获得,不代替原版宿主输出对照。探针使用Wall/Wextra编译成功并完成采样,没有执行原版APP、替换安装版或操作Motion/FCP。
|
后续静态REA和Apple Metal AIR分析确认:原版瘦脸以17组轮廓到鼻部/瞳孔锚点的局部位移驱动,当前瘦脸按框水平对称变形,两者的方向、范围与强度模型不同。独立Vision76点离线探针在同一视频11个采样时刻均检出1脸76点,含明显侧脸;它只证明关键点仍可获得,不代替原版宿主输出对照。探针使用Wall/Wextra编译成功并完成采样,没有执行原版APP、替换安装版或操作Motion/FCP。
|
||||||
|
|
||||||
分析报告见 [脸型变形与转脸分析](analysis/脸型变形与转脸分析.md)。当前安装1.0.4的变形问题尚未修复,本轮的诊断代码/证据/文档不改变已安装产品。
|
分析报告见 [脸型变形与转脸分析](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正式签名公证安装,Motion/FCP界面不操作、最终视觉由用户自测。默认单主要人脸对齐17-pin数学;快速多人仍沿用逐脸采样,重叠区不宣称与原版单fragment全脸序列等价。
|
||||||
+82
-11
@@ -2,8 +2,12 @@
|
|||||||
"""Release, notarize and install the unique FxPlug app under its delivery lock.
|
"""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.
|
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, shutil, subprocess, sys, time
|
||||||
root=pathlib.Path(__file__).resolve().parent.parent
|
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.')
|
||||||
|
args=parser.parse_args()
|
||||||
bundle_id='com.dayu.beatygril'
|
bundle_id='com.dayu.beatygril'
|
||||||
lock=pathlib.Path('/Users/dayu/.codex/app-delivery-locks')/(bundle_id+'.lock')
|
lock=pathlib.Path('/Users/dayu/.codex/app-delivery-locks')/(bundle_id+'.lock')
|
||||||
lock.parent.mkdir(parents=True,exist_ok=True)
|
lock.parent.mkdir(parents=True,exist_ok=True)
|
||||||
@@ -18,12 +22,57 @@ archive=logs/'notarization.zip'
|
|||||||
identity='Developer ID Application: zhong luo (B3W25FX5H5)'
|
identity='Developer ID Application: zhong luo (B3W25FX5H5)'
|
||||||
keychain='/Users/dayu/Library/Keychains/login.keychain-db'
|
keychain='/Users/dayu/Library/Keychains/login.keychain-db'
|
||||||
previous=logs/'previous-beatygril.app'
|
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):
|
def run(args, log=None):
|
||||||
if log:
|
if log:
|
||||||
with open(log,'w') as output:subprocess.run(args,cwd=root,stdout=output,stderr=subprocess.STDOUT,check=True)
|
with open(log,'w') as output:subprocess.run(args,cwd=root,stdout=output,stderr=subprocess.STDOUT,check=True)
|
||||||
return ''
|
return ''
|
||||||
return subprocess.run(args,cwd=root,text=True,capture_output=True,check=True).stdout
|
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 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:
|
try:
|
||||||
|
receipt['source']={'commit':run(['git','rev-parse','HEAD']).strip(),
|
||||||
|
'trackedWorktreeChanges':run(['git','status','--porcelain','--untracked-files=no']).splitlines()}
|
||||||
|
save_receipt('building')
|
||||||
identities=run(['security','find-identity','-v','-p','codesigning',keychain])
|
identities=run(['security','find-identity','-v','-p','codesigning',keychain])
|
||||||
if identity not in identities:raise RuntimeError('Valid Developer ID Application identity is unavailable')
|
if identity not in identities:raise RuntimeError('Valid Developer ID Application identity is unavailable')
|
||||||
print('Building universal Release…',flush=True)
|
print('Building universal Release…',flush=True)
|
||||||
@@ -34,28 +83,36 @@ try:
|
|||||||
run(['codesign','--force','--timestamp','--options','runtime','--sign',identity,str(plugin)])
|
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','--force','--timestamp','--options','runtime','--entitlements',str(root/'beatygril/beatygril/Wrapper Application/SandboxEntitlements.entitlements'),'--sign',identity,str(app)])
|
||||||
run(['codesign','--verify','--deep','--strict',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)])
|
run(['ditto','-c','-k','--keepParent',str(app),str(archive)])
|
||||||
print('Submitting signed app to Apple notarization…',flush=True)
|
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'])
|
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)
|
(logs/'notarization.json').write_text(response)
|
||||||
result=json.loads(response)
|
result=json.loads(response)
|
||||||
|
receipt['notarization']=result
|
||||||
if result.get('status')!='Accepted':
|
if result.get('status')!='Accepted':
|
||||||
if result.get('id'):
|
if result.get('id'):
|
||||||
(logs/'notarization-details.json').write_text(run(['xcrun','notarytool','log',result['id'],'--keychain-profile','codex-dayu-notary-B3W25FX5H5','--keychain',keychain]))
|
(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')
|
raise RuntimeError('Apple did not accept notarization; original installed app retained')
|
||||||
run(['xcrun','stapler','staple',str(app)])
|
run(['xcrun','stapler','staple',str(app)])
|
||||||
run(['xcrun','stapler','validate',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.
|
||||||
|
run(['pluginkit','-r',str(plugin)])
|
||||||
|
save_receipt('prepared',signingVerified=True,stapleValidated=True,stagingPluginUnregistered=True)
|
||||||
# A live host retains its old XPC/OSC connection. Replacing the bundle while
|
# A live host retains its old XPC/OSC connection. Replacing the bundle while
|
||||||
# that connection exists can deadlock host shutdown or select stale code.
|
# that connection exists can deadlock host shutdown or select stale code.
|
||||||
running=run(['ps','-axo','comm=']).splitlines()
|
await_host_exit()
|
||||||
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')
|
|
||||||
if installed.exists():
|
if installed.exists():
|
||||||
info=plistlib.loads((installed/'Contents/Info.plist').read_bytes())
|
require_product(installed)
|
||||||
if info.get('CFBundleIdentifier')!=bundle_id:raise RuntimeError('The installation path belongs to another product')
|
|
||||||
if previous.exists():raise RuntimeError('Previous recovery app already exists')
|
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))
|
shutil.move(str(installed),str(previous))
|
||||||
try:
|
try:
|
||||||
shutil.copytree(app,installed,symlinks=True)
|
shutil.copytree(app,installed,symlinks=True)
|
||||||
@@ -70,16 +127,30 @@ try:
|
|||||||
# Xcode may register its build product. Only the installed plugin should
|
# Xcode may register its build product. Only the installed plugin should
|
||||||
# be discoverable while native host validation is in progress.
|
# be discoverable while native host validation is in progress.
|
||||||
run(['pluginkit','-r',str(plugin)])
|
run(['pluginkit','-r',str(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:
|
except Exception:
|
||||||
if installed.exists():shutil.rmtree(installed)
|
if installed.exists():shutil.rmtree(installed)
|
||||||
if previous.exists():shutil.move(str(previous),str(installed))
|
if previous.exists():shutil.move(str(previous),str(installed))
|
||||||
raise
|
raise
|
||||||
print(json.dumps({'status':'installed','path':str(installed),'notarization':result}),flush=True)
|
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')
|
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.
|
# Only this task’s known generated product copies are removed.
|
||||||
if previous.exists():shutil.rmtree(previous)
|
removed=cleanup_known_products()
|
||||||
if archive.exists():archive.unlink()
|
save_receipt('installed_user_qa_pending' if args.user_tests else 'complete',removedGeneratedPaths=removed)
|
||||||
if staging.exists():shutil.rmtree(staging)
|
|
||||||
print('Delivery staging and notarization archive cleaned.',flush=True)
|
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:
|
finally:
|
||||||
(lock/'owner.json').unlink();lock.rmdir()
|
(lock/'owner.json').unlink();lock.rmdir()
|
||||||
@@ -9,3 +9,5 @@ xcrun clang "${common[@]}" "$plugin_dir/PGBeautyRenderer.m" tests/beauty_render_
|
|||||||
"$output_dir/beauty-tests"
|
"$output_dir/beauty-tests"
|
||||||
xcrun clang "${common[@]}" "$plugin_dir/PGFaceProcessor.m" tests/face_tests.m -o "$output_dir/face-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"
|
"$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 *left=Face(CGRectMake(.10,.25,.30,.50));
|
||||||
VNFaceObservation *right=Face(CGRectMake(.66,.30,.20,.40));
|
VNFaceObservation *right=Face(CGRectMake(.66,.30,.20,.40));
|
||||||
PGFaceFixture *p=[PGFaceFixture new];p.detections=@[left,right];
|
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];
|
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.deformationGeometry.count==1 && GeometryBox(automatic.deformationGeometry.firstObject).origin.x<.5,@"default realtime geometry chooses one area/center primary without analysis");
|
||||||
Check(automatic.faces.count==2 && automatic.faces[0]==left && automatic.faces[1]==right,@"default appearance retains both original Vision observations");
|
Check(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};
|
float *controls[] = {&changed.faceThin, &changed.faceBigEye, &changed.faceShort, &changed.faceNarrow, &changed.faceChin, &changed.faceV, &changed.faceJaw, &changed.faceCheekbone, &changed.faceSmallHead};
|
||||||
*controls[i] = 1;
|
*controls[i] = 1;
|
||||||
PGFaceFrame *warped = [processor processImage:image settings:changed time:kCMTimeZero error:&error];
|
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");
|
Check(CGRectEqualToRect(warped.image.extent, image.extent), @"warp preserves image extent");
|
||||||
}
|
}
|
||||||
settings.faceShort = 0.8;
|
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;
|
||||||
|
}}
|
||||||
在新工单中引用
屏蔽一个用户