Implement and test portrait beauty, face geometry and persistent tracking
这个提交包含在:
1 父节点
e2803e727f
当前提交
0310ca83ba
15 个文件已更改
+1203
-42
没有与搜索条件相匹配的文件
@@ -63,10 +63,11 @@
|
||||
"/Localized: Plugin/InfoPlist.strings",
|
||||
"/Localized: Plugin/Localizable.strings",
|
||||
Plugin/beatygrilPlugIn.m,
|
||||
Plugin/PGBeautyRenderer.m,
|
||||
Plugin/PGFaceProcessor.m,
|
||||
Plugin/PGFaceSelectOSC.m,
|
||||
Plugin/Info.plist,
|
||||
Plugin/main.m,
|
||||
Plugin/MetalDeviceCache.m,
|
||||
Plugin/TileableRemoteBrightness.metal,
|
||||
"Wrapper Application/Info.plist",
|
||||
);
|
||||
target = 6945FC8A3088B7220013E108 /* Wrapper Application */;
|
||||
@@ -248,11 +249,12 @@
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
6945FC933088B7220013E108 /* Copy and Code Sign FxPlug.framework */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
name = "Copy and Code Sign FxPlug.framework";
|
||||
shellPath = /bin/sh;
|
||||
shellScript = (
|
||||
"mkdir -p \"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}\"",
|
||||
"rsync --archive --links --whole-file --no-owner --no-group --checksum --exclude='Headers' \"/Library/Developer/SDKs/FxPlug.sdk/Library/Frameworks/FxPlug.framework/\" \"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FxPlug.framework/\"",
|
||||
"rsync --archive --links --whole-file --no-owner --no-group --checksum --exclude='Headers' \"/Library/Developer/Frameworks/FxPlug.framework/\" \"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FxPlug.framework/\"",
|
||||
"if [ \"$?\" -ne 0 ]; then echo \"error: rsync failed; couldn't embed FxPlug.framework\"; exit 1; fi",
|
||||
"codesign --force --timestamp --options runtime --sign \"${CODE_SIGN_IDENTITY}\" \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/FxPlug.framework\"",
|
||||
);
|
||||
@@ -260,11 +262,12 @@
|
||||
};
|
||||
6945FC943088B7220013E108 /* Copy and Code Sign PluginManager.framework */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
name = "Copy and Code Sign PluginManager.framework";
|
||||
shellPath = /bin/sh;
|
||||
shellScript = (
|
||||
"mkdir -p \"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}\"",
|
||||
"rsync --archive --links --whole-file --no-owner --no-group --checksum --exclude='Headers' \"/Library/Developer/SDKs/FxPlug.sdk/Library/Frameworks/PluginManager.framework/\" \"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PluginManager.framework/\"",
|
||||
"rsync --archive --links --whole-file --no-owner --no-group --checksum --exclude='Headers' \"/Library/Developer/Frameworks/PluginManager.framework/\" \"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PluginManager.framework/\"",
|
||||
"if [ \"$?\" -ne 0 ]; then echo \"error: rsync failed; couldn't embed PluginManager.framework\"; exit 1; fi",
|
||||
"codesign --force --timestamp --options runtime --sign \"${CODE_SIGN_IDENTITY}\" \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/PluginManager.framework\"",
|
||||
"",
|
||||
@@ -304,6 +307,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
||||
CODE_SIGN_IDENTITY = "Developer ID Application";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
DEVELOPMENT_TEAM = B3W25FX5H5;
|
||||
@@ -326,6 +330,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
||||
CODE_SIGN_IDENTITY = "Developer ID Application";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
DEVELOPMENT_TEAM = B3W25FX5H5;
|
||||
@@ -349,6 +354,7 @@
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "beatygril/Wrapper Application/SandboxEntitlements.entitlements";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
||||
CODE_SIGN_IDENTITY = "Developer ID Application";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
DEVELOPMENT_TEAM = B3W25FX5H5;
|
||||
@@ -365,6 +371,7 @@
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "beatygril/Wrapper Application/SandboxEntitlements.entitlements";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
||||
CODE_SIGN_IDENTITY = "Developer ID Application";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
DEVELOPMENT_TEAM = B3W25FX5H5;
|
||||
@@ -380,6 +387,8 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ADDITIONAL_SDKS = /Library/Developer/SDKs/FxPlug.sdk;
|
||||
ARCHS = "arm64 x86_64";
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
@@ -436,10 +445,9 @@
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_VERSION = 5;
|
||||
};
|
||||
@@ -449,6 +457,8 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ADDITIONAL_SDKS = /Library/Developer/SDKs/FxPlug.sdk;
|
||||
ARCHS = "arm64 x86_64";
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
@@ -499,7 +509,7 @@
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = macosx;
|
||||
|
||||
@@ -110,6 +110,6 @@
|
||||
</dict>
|
||||
</dict>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0</string>
|
||||
<string>13.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,19 @@
|
||||
#import <CoreImage/CoreImage.h>
|
||||
#import "PGBeautyTypes.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// Stateless, lazy Core Image graph. All masks use red=coverage and image coordinates.
|
||||
/// Caller owns face detection, geometric warping, tracking, and the CIContext/Metal device.
|
||||
/// showMask enables debugChannel 0=skin, 1=teeth, 2=eyes. invertMask affects each preview.
|
||||
/// If the caller blends geometry plus beauty against the original, pass settings.mix=1.
|
||||
@interface PGBeautyRenderer : NSObject
|
||||
- (nullable CIImage *)processImage:(CIImage *)image
|
||||
settings:(PGBeautySettings)settings
|
||||
skinMask:(nullable CIImage *)skinMask
|
||||
teethMask:(nullable CIImage *)teethMask
|
||||
eyeMask:(nullable CIImage *)eyeMask
|
||||
error:(NSError * _Nullable * _Nullable)error;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,242 @@
|
||||
#import "PGBeautyRenderer.h"
|
||||
#import <math.h>
|
||||
|
||||
// Original implementation reconstructed from observed processing stages, not vendor
|
||||
// resources. The learned segmentation model and LUTs are deliberately not embedded.
|
||||
static float PGClamp(float v, float lo, float hi) { return isfinite(v) ? fmaxf(lo, fminf(hi, v)) : lo; }
|
||||
static CIImage *PGBlur(CIImage *image, CGFloat radius, CGRect extent) {
|
||||
return [[[image imageByClampingToExtent] imageByApplyingFilter:@"CIGaussianBlur"
|
||||
withInputParameters:@{kCIInputRadiusKey:@(radius)}] imageByCroppingToRect:extent];
|
||||
}
|
||||
static CIColorKernel *PGKernel(NSString *body) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
return [CIColorKernel kernelWithString:body];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
static CIImage *PGApply(CIColorKernel *kernel, CGRect extent, NSArray *arguments) {
|
||||
return [kernel applyWithExtent:extent arguments:arguments];
|
||||
}
|
||||
static NSDictionary<NSString *, CIColorKernel *> *PGKernels(void) {
|
||||
static NSDictionary *kernels;
|
||||
static dispatch_once_t once;
|
||||
dispatch_once(&once, ^{
|
||||
NSMutableDictionary *k = [NSMutableDictionary dictionary];
|
||||
// The fallback mask observes the shipped G/B overlay > Gaussian high-pass >
|
||||
// repeated hard-light stages. Semantic skin support is independently estimated.
|
||||
k[@"overlay"] = PGKernel(@
|
||||
"kernel vec4 pgGB(__sample p) { vec4 q=unpremultiply(p);"
|
||||
"float g=clamp(q.g*0.5,0.0,1.0); float b=clamp(q.b*0.5,0.0,1.0);"
|
||||
"float v=2.0*g*b; return vec4(v,v,v,1.0); }");
|
||||
k[@"candidate"] = PGKernel(@
|
||||
"kernel vec4 pgCandidate(__sample p,__sample g,__sample b) {"
|
||||
"vec3 c=clamp(unpremultiply(p).rgb,0.0,1.0);"
|
||||
"float y=dot(c,vec3(0.299,0.587,0.114));"
|
||||
"float cb=(c.b-y)*0.564+0.5; float cr=(c.r-y)*0.713+0.5;"
|
||||
"float chroma=exp(-pow((cb-0.415)/0.115,2.0)-pow((cr-0.565)/0.135,2.0));"
|
||||
"float h=clamp(g.r-b.r+0.5,0.0,1.0);"
|
||||
"h=mix(2.0*h*h,1.0-2.0*(1.0-h)*(1.0-h),step(0.5,h));"
|
||||
"h=mix(2.0*h*h,1.0-2.0*(1.0-h)*(1.0-h),step(0.5,h));"
|
||||
"h=mix(2.0*h*h,1.0-2.0*(1.0-h)*(1.0-h),step(0.5,h));"
|
||||
"float band=clamp((h-0.29411765)*2.86516854,0.0,1.0);"
|
||||
"float edges=1.0-smoothstep(0.06,0.26,abs(band-0.59));"
|
||||
"float value=clamp(chroma*smoothstep(0.025,0.15,y)*(0.8+0.2*edges),0.0,1.0);"
|
||||
"return vec4(value,value,value,1.0); }");
|
||||
k[@"mask"] = PGKernel(@
|
||||
"kernel vec4 pgMask(__sample raw,__sample feather,__sample p,__sample b,__sample eyes,__sample teeth,"
|
||||
"float threshold,float softness,float edge,float protection,float invert) {"
|
||||
"float m=clamp(mix(raw.r,feather.r,softness),0.0,1.0);"
|
||||
"float width=0.025+0.24*softness;"
|
||||
"m=smoothstep(max(0.0,threshold-width),min(1.0,threshold+width),m);"
|
||||
"m=mix(m,1.0-m,invert);"
|
||||
"vec3 c=unpremultiply(p).rgb; vec3 l=unpremultiply(b).rgb;"
|
||||
"float structure=length(c-l);"
|
||||
"m*=1.0-edge*smoothstep(0.025,0.19,structure);"
|
||||
"float features=clamp(max(eyes.r,teeth.r),0.0,1.0);"
|
||||
"m*=1.0-protection*features;"
|
||||
"return vec4(m,m,m,1.0); }");
|
||||
// Independent frequency separation: texture and structure bands are compressed
|
||||
// in luminance, with feature/edge attenuation, keeping chroma and alpha stable.
|
||||
k[@"detail"] = PGKernel(@
|
||||
"kernel vec4 pgDetail(__sample p,__sample fine,__sample broad,float strength,float protect) {"
|
||||
"vec4 src=unpremultiply(p); vec3 c=src.rgb;"
|
||||
"vec3 t=unpremultiply(fine).rgb; vec3 b=unpremultiply(broad).rgb;"
|
||||
"vec3 w=vec3(0.299,0.587,0.114);"
|
||||
"float y=dot(c,w); float yf=dot(t,w); float yb=dot(b,w);"
|
||||
"float f=y-yf; float mid=yf-yb; float structure=length(c-b);"
|
||||
"float middle=smoothstep(0.04,0.22,y)*(1.0-smoothstep(0.82,0.98,y));"
|
||||
"float texture=smoothstep(0.001,0.010,abs(f));"
|
||||
"float edge=1.0-smoothstep(0.08,0.26,structure);"
|
||||
"float dark=(1.0-smoothstep(0.16,0.42,y))*smoothstep(0.0015,0.018,abs(f)+0.35*abs(y-yb));"
|
||||
"float light=smoothstep(0.70,0.96,y)*smoothstep(0.0025,0.024,abs(f));"
|
||||
"float ridge=smoothstep(0.10,0.30,structure)*smoothstep(0.002,0.020,abs(f)+0.85*abs(y-yb));"
|
||||
"float feature=max(dark*0.96,max(light*0.52,ridge*0.84));"
|
||||
"float gain=clamp(strength*middle*mix(0.56,0.88,texture),0.0,1.05);"
|
||||
"gain*=1.0-protect*feature*mix(0.42,0.82,texture);"
|
||||
"float delta=-gain*(f*mix(0.18,0.76,edge)+mid*mix(0.08,0.30,edge));"
|
||||
"float target=clamp(y+delta,0.0,1.0); float ratio=clamp(target/max(y,0.001),0.82,1.18);"
|
||||
"vec3 outColor=c*mix(1.0,ratio,0.88);"
|
||||
"outColor+=vec3((target-dot(outColor,w))*0.08);"
|
||||
"return premultiply(vec4(clamp(outColor,0.0,1.0),src.a)); }");
|
||||
k[@"maskedMix"] = PGKernel(@
|
||||
"kernel vec4 pgMaskedMix(__sample source,__sample effect,__sample mask,float amount) {"
|
||||
"vec4 s=unpremultiply(source); vec4 e=unpremultiply(effect);"
|
||||
"return premultiply(vec4(mix(s.rgb,e.rgb,clamp(mask.r*amount,0.0,1.0)),s.a)); }");
|
||||
k[@"even"] = PGKernel(@
|
||||
"kernel vec4 pgEven(__sample source,__sample broad,float amount) {"
|
||||
"vec4 s=unpremultiply(source); vec3 b=unpremultiply(broad).rgb;"
|
||||
"vec3 w=vec3(0.299,0.587,0.114); float y=dot(s.rgb,w); float yb=dot(b,w);"
|
||||
"vec3 baseChroma=s.rgb-vec3(y); vec3 smoothChroma=b-vec3(yb);"
|
||||
"float guard=1.0-smoothstep(0.12,0.35,length(s.rgb-b));"
|
||||
"vec3 c=vec3(y)+mix(baseChroma,smoothChroma,amount*0.78*guard);"
|
||||
"return premultiply(vec4(clamp(c,0.0,1.0),s.a)); }");
|
||||
// These six looks are newly authored color transforms, not the shipped LUTs.
|
||||
k[@"preset"] = PGKernel(@
|
||||
"kernel vec4 pgPreset(__sample source,float preset) {"
|
||||
"vec4 s=unpremultiply(source); vec3 c=s.rgb; float y=dot(c,vec3(0.299,0.587,0.114));"
|
||||
"if(preset<1.5){c=mix(vec3(y),c,0.91);c+=vec3(0.038,0.027,0.012)*(1.0-y);}"
|
||||
"else if(preset<2.5){c=mix(vec3(y),c,0.89);c+=vec3(0.003,0.020,0.038)*(1.0-y);}"
|
||||
"else if(preset<3.5){c=mix(vec3(y),c,1.04);c+=vec3(0.037,0.011,-0.018)*(1.0-y);}"
|
||||
"else if(preset<4.5){c=mix(vec3(y),c,0.94);c+=vec3(0.032,0.001,0.018)*(1.0-y);}"
|
||||
"else if(preset<5.5){c=pow(max(c,vec3(0.0)),vec3(1.08));c+=vec3(0.024,0.003,-0.018)*(1.0-y);}"
|
||||
"else{c=pow(max(c,vec3(0.0)),vec3(1.20));c=mix(vec3(dot(c,vec3(0.299,0.587,0.114))),c,1.06);"
|
||||
"c+=vec3(0.030,0.007,-0.022)*(1.0-y);}"
|
||||
"return premultiply(vec4(clamp(c,0.0,1.0),s.a)); }");
|
||||
k[@"teeth"] = PGKernel(@
|
||||
"kernel vec4 pgTeeth(__sample source,__sample mask,float amount) {"
|
||||
"vec4 s=unpremultiply(source);vec3 c=clamp(s.rgb,0.0,1.0);"
|
||||
"float y=dot(c,vec3(0.299,0.587,0.114));float hi=max(c.r,max(c.g,c.b));float lo=min(c.r,min(c.g,c.b));"
|
||||
"float sat=(hi-lo)/max(hi,0.0001);"
|
||||
"float gate=smoothstep(0.26,0.56,max(y,hi*0.86))*(1.0-smoothstep(0.52,0.86,sat));"
|
||||
"float weight=clamp(pow(max(0.0,smoothstep(0.08,0.92,mask.r)*gate*amount),0.90)*(1.02+0.18*amount),0.0,0.88);"
|
||||
"float yellow=max(0.0,(c.r+c.g)*0.5-c.b);"
|
||||
"vec3 neutral=mix(c,vec3(y),0.16+0.22*amount);"
|
||||
"neutral+=yellow*vec3(-0.070*amount,-0.028*amount,0.18+0.28*amount);"
|
||||
"neutral+=(vec3(1.0)-neutral)*(0.045+0.155*amount);"
|
||||
"return premultiply(vec4(mix(c,clamp(neutral,0.0,1.0),weight),s.a)); }");
|
||||
k[@"eyes"] = PGKernel(@
|
||||
"kernel vec4 pgEyes(__sample source,__sample mask,float amount) {"
|
||||
"vec4 s=unpremultiply(source);float y=dot(s.rgb,vec3(0.299,0.587,0.114));"
|
||||
"float weight=clamp(mask.r,0.0,1.0)*amount*smoothstep(0.08,0.32,y);"
|
||||
"vec3 c=s.rgb+(vec3(1.0)-s.rgb)*(0.18*weight);"
|
||||
"return premultiply(vec4(clamp(c,0.0,1.0),s.a)); }");
|
||||
k[@"debug"] = PGKernel(@
|
||||
"kernel vec4 pgDebug(__sample source,__sample mask,float invert) {"
|
||||
"vec4 s=unpremultiply(source);float m=clamp(mask.r,0.0,1.0);"
|
||||
"m=mix(m,1.0-m,invert);return premultiply(vec4(vec3(m),s.a)); }");
|
||||
kernels = [k copy];
|
||||
});
|
||||
return kernels;
|
||||
}
|
||||
|
||||
@implementation PGBeautyRenderer
|
||||
- (CIImage *)processImage:(CIImage *)image settings:(PGBeautySettings)s
|
||||
skinMask:(CIImage *)skinMask teethMask:(CIImage *)teethMask
|
||||
eyeMask:(CIImage *)eyeMask error:(NSError **)error {
|
||||
if (error) *error = nil;
|
||||
CGRect extent = image.extent;
|
||||
if (!image || CGRectIsEmpty(extent) || CGRectIsInfinite(extent) ||
|
||||
!isfinite(extent.origin.x) || !isfinite(extent.origin.y) ||
|
||||
!isfinite(extent.size.width) || !isfinite(extent.size.height)) {
|
||||
if (error) *error = [NSError errorWithDomain:@"PGBeautyRenderer" code:1
|
||||
userInfo:@{NSLocalizedDescriptionKey:@"The source image needs a finite nonempty extent."}];
|
||||
return nil;
|
||||
}
|
||||
float mix = PGClamp(s.mix, 0, 1);
|
||||
if (mix == 0 && !s.showMask) return image;
|
||||
NSDictionary *k = PGKernels();
|
||||
if (k.count != 10) {
|
||||
if (error) *error = [NSError errorWithDomain:@"PGBeautyRenderer" code:2
|
||||
userInfo:@{NSLocalizedDescriptionKey:@"The beauty image kernels could not be compiled."}];
|
||||
return nil;
|
||||
}
|
||||
@try {
|
||||
float amount = PGClamp(s.amount,0,1);
|
||||
float local = PGClamp(s.midtoneDetail,0,2)*0.5f;
|
||||
float global = PGClamp(s.highPassDetail,0,1);
|
||||
float brightness = PGClamp(s.skinBrightness,0,1);
|
||||
float hue = PGClamp(s.skinHue,-30,30);
|
||||
float saturation = PGClamp(s.skinSaturation,-100,100);
|
||||
float even = PGClamp(s.skinEvenTone,0,1);
|
||||
float sharpness = PGClamp(s.sharpness,0,2);
|
||||
float teeth = PGClamp(s.teethWhitening,0,1);
|
||||
float eyes = PGClamp(s.eyeBrightening,0,1);
|
||||
BOOL needsSkin = local*amount>0 || brightness>0 || hue!=0 || saturation!=0 || even>0 ||
|
||||
(s.skinTonePreset>0 && s.skinTonePreset<=6) || (s.showMask && s.debugChannel==0);
|
||||
CIImage *black = [[CIImage imageWithColor:[CIColor colorWithRed:0 green:0 blue:0 alpha:1]] imageByCroppingToRect:extent];
|
||||
CIImage *teethInput = teethMask ? [teethMask imageByCroppingToRect:extent] : black;
|
||||
CIImage *eyeInput = eyeMask ? [eyeMask imageByCroppingToRect:extent] : black;
|
||||
CIImage *raw = skinMask ? [skinMask imageByCroppingToRect:extent] : black;
|
||||
CIImage *mask = black;
|
||||
CGFloat radius = PGClamp(s.radius,1,40);
|
||||
if (needsSkin) {
|
||||
if (!skinMask) {
|
||||
CIImage *gb = PGApply(k[@"overlay"],extent,@[image]);
|
||||
raw = PGApply(k[@"candidate"],extent,@[image,gb,PGBlur(gb,radius,extent)]);
|
||||
}
|
||||
float soft = PGClamp(s.maskSoftness,0,1);
|
||||
float threshold = PGClamp(s.maskThreshold,0,255)/255.f;
|
||||
mask = PGApply(k[@"mask"],extent,@[raw,PGBlur(raw,0.35+soft*radius*0.35,extent),
|
||||
image,PGBlur(image,1.5,extent),eyeInput,teethInput,
|
||||
@(threshold),@(soft),@(PGClamp(s.maskEdgeProtect,0,1)),
|
||||
@(s.featureProtection ? 1 : 0),@(s.invertMask ? 1 : 0)]);
|
||||
}
|
||||
if (s.showMask) {
|
||||
// Popup values select coverage source; changing the popup alone must
|
||||
// never enable a preview. Feature masks retain their supplied coverage.
|
||||
CIImage *preview = s.debugChannel==1 ? teethInput : (s.debugChannel==2 ? eyeInput : mask);
|
||||
float invert = s.debugChannel==1 || s.debugChannel==2 ? (s.invertMask ? 1 : 0) : 0;
|
||||
return PGApply(k[@"debug"],extent,@[image,preview,@(invert)]);
|
||||
}
|
||||
CIImage *result = image;
|
||||
if (local*amount>0) {
|
||||
// The shipped helper derives both blur scales from the detail strength.
|
||||
CGFloat fine = fmin(7.4,2.275+3.15*pow(local,0.72)+0.95*local);
|
||||
CGFloat broad = fmin(16,(2.55+0.45*local)*fine+1.1+1.3*local);
|
||||
float compression = 0.12+2.4*powf(local,2.15)+1.25*local;
|
||||
CIImage *detail = PGApply(k[@"detail"],extent,@[result,PGBlur(result,fine,extent),
|
||||
PGBlur(result,broad,extent),@(compression),@(s.featureProtection ? 1 : 0)]);
|
||||
result = PGApply(k[@"maskedMix"],extent,@[result,detail,mask,@(amount)]);
|
||||
}
|
||||
CIImage *color = result;
|
||||
if (brightness>0) {
|
||||
float shoulder = PGClamp((brightness-0.56f)/0.44f,0,1);
|
||||
float gain = 0.88f*powf(brightness,1.24f)+0.12f*shoulder*shoulder*(3-2*shoulder);
|
||||
color = [color imageByApplyingFilter:@"CIExposureAdjust" withInputParameters:@{kCIInputEVKey:@(gain*0.95f)}];
|
||||
}
|
||||
if (hue!=0) color = [color imageByApplyingFilter:@"CIHueAdjust" withInputParameters:@{kCIInputAngleKey:@(hue*M_PI/180.0)}];
|
||||
if (saturation!=0) color = [color imageByApplyingFilter:@"CIColorControls" withInputParameters:@{kCIInputSaturationKey:@(1+saturation/100.f)}];
|
||||
if (even>0) color = PGApply(k[@"even"],extent,@[color,PGBlur(color,radius*1.8,extent),@(even)]);
|
||||
if (color!=result) result = PGApply(k[@"maskedMix"],extent,@[result,color,mask,@1]);
|
||||
if (sharpness*amount>0) result = [result imageByApplyingFilter:@"CISharpenLuminance"
|
||||
withInputParameters:@{kCIInputSharpnessKey:@(sharpness*amount)}];
|
||||
if (s.skinTonePreset>0 && s.skinTonePreset<=6) {
|
||||
CIImage *preset = PGApply(k[@"preset"],extent,@[result,@(s.skinTonePreset)]);
|
||||
result = PGApply(k[@"maskedMix"],extent,@[result,preset,mask,@1]);
|
||||
}
|
||||
if (global>0) {
|
||||
CGFloat fine = fmin(7.4,2.275+3.15*pow(global,0.72)+0.95*global);
|
||||
CGFloat broad = fmin(16,(2.55+0.45*global)*fine+1.1+1.3*global);
|
||||
float compression = 0.12+2.4*powf(global,2.15)+1.25*global;
|
||||
result = PGApply(k[@"detail"],extent,@[result,PGBlur(result,fine,extent),
|
||||
PGBlur(result,broad,extent),@(compression),@0]);
|
||||
}
|
||||
if (teeth>0 && teethMask) result = PGApply(k[@"teeth"],extent,@[result,teethInput,@(teeth)]);
|
||||
if (eyes>0 && eyeMask) result = PGApply(k[@"eyes"],extent,@[result,eyeInput,@(eyes)]);
|
||||
if (mix<1) {
|
||||
CIImage *white = [[CIImage imageWithColor:[CIColor colorWithRed:1 green:1 blue:1 alpha:1]] imageByCroppingToRect:extent];
|
||||
result = PGApply(k[@"maskedMix"],extent,@[image,result,white,@(mix)]);
|
||||
}
|
||||
if (!result) {
|
||||
if (error) *error = [NSError errorWithDomain:@"PGBeautyRenderer" code:3
|
||||
userInfo:@{NSLocalizedDescriptionKey:@"Core Image could not construct the beauty render graph."}];
|
||||
return nil;
|
||||
}
|
||||
return [result imageByCroppingToRect:extent];
|
||||
} @catch (NSException *exception) {
|
||||
if (error) *error = [NSError errorWithDomain:@"PGBeautyRenderer" code:4
|
||||
userInfo:@{NSLocalizedDescriptionKey:exception.reason ?: @"Invalid Core Image render state."}];
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
@end
|
||||
@@ -0,0 +1,37 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreImage/CoreImage.h>
|
||||
#import <CoreMedia/CoreMedia.h>
|
||||
#import <Vision/Vision.h>
|
||||
#import "PGBeautyTypes.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface PGFaceFrame : NSObject
|
||||
/// Vision observations in normalized, bottom-left image coordinates. Restored
|
||||
/// observations retain their bounding box; render geometry is stored separately.
|
||||
@property (nonatomic, copy, readonly) NSArray<VNFaceObservation *> *faces;
|
||||
@property (nonatomic, strong, readonly) CIImage *skinMask;
|
||||
@property (nonatomic, strong, readonly) CIImage *teethMask;
|
||||
@property (nonatomic, strong, readonly) CIImage *eyeMask;
|
||||
@property (nonatomic, strong, readonly) CIImage *image;
|
||||
/// Transparent face/target outlines, for compositing after beauty processing.
|
||||
@property (nonatomic, strong, readonly) CIImage *overlayImage;
|
||||
@end
|
||||
|
||||
@interface PGFaceProcessor : NSObject
|
||||
- (nullable PGFaceFrame *)processImage:(CIImage *)image
|
||||
settings:(PGBeautySettings)settings
|
||||
time:(CMTime)time
|
||||
error:(NSError * _Nullable * _Nullable)error;
|
||||
- (BOOL)analyzeImage:(CIImage *)image
|
||||
settings:(PGBeautySettings)settings
|
||||
time:(CMTime)time
|
||||
error:(NSError * _Nullable * _Nullable)error;
|
||||
- (void)resetTracking;
|
||||
/// Targets are zero-based (0, 1, 2). Data is a versioned property-list archive,
|
||||
/// independent of Vision implementation classes. Only analysis writes frames.
|
||||
- (nullable NSData *)trackingDataForTarget:(NSUInteger)target;
|
||||
- (void)setTrackingData:(nullable NSData *)data forTarget:(NSUInteger)target;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,402 @@
|
||||
// The source kernels remain available on the supported macOS 12+ range. This
|
||||
// keeps the processor self contained instead of requiring a separate metallib.
|
||||
#define CI_SILENCE_GL_DEPRECATION 1
|
||||
#import "PGFaceProcessor.h"
|
||||
#include <math.h>
|
||||
#include <float.h>
|
||||
|
||||
static NSString * const PGFaceErrorDomain = @"media.dayu.PaintFCP.Face";
|
||||
|
||||
@interface PGFaceFrame ()
|
||||
@property (nonatomic, copy, readwrite) NSArray<VNFaceObservation *> *faces;
|
||||
@property (nonatomic, strong, readwrite) CIImage *skinMask;
|
||||
@property (nonatomic, strong, readwrite) CIImage *teethMask;
|
||||
@property (nonatomic, strong, readwrite) CIImage *eyeMask;
|
||||
@property (nonatomic, strong, readwrite) CIImage *image;
|
||||
@property (nonatomic, strong, readwrite) CIImage *overlayImage;
|
||||
@end
|
||||
@implementation PGFaceFrame
|
||||
@end
|
||||
|
||||
// All geometry in stored records is normalized to the full source image, with
|
||||
// Y increasing upward. Pixel size, render scale and tile order are not persisted.
|
||||
static NSString *PGTimeKey(CMTime time) {
|
||||
if (!CMTIME_IS_NUMERIC(time) || time.timescale <= 0) return nil;
|
||||
int64_t a = time.value, b = time.timescale;
|
||||
// Unsigned magnitude avoids overflow for the minimum signed integer.
|
||||
uint64_t u = a < 0 ? (uint64_t)(-(a + 1)) + 1 : (uint64_t)a;
|
||||
uint64_t v = (uint64_t)b;
|
||||
while (v) { uint64_t r = u % v; u = v; v = r; }
|
||||
int64_t divisor = (int64_t)(u ?: 1);
|
||||
return [NSString stringWithFormat:@"%lld/%lld/%lld", (long long)(a / divisor),
|
||||
(long long)(b / divisor), (long long)time.epoch];
|
||||
}
|
||||
|
||||
static float PGClamp(float value, float low, float high) {
|
||||
return isfinite(value) ? fminf(high, fmaxf(low, value)) : 0;
|
||||
}
|
||||
static NSArray *PGRectArray(CGRect rect) {
|
||||
return @[@(rect.origin.x), @(rect.origin.y), @(rect.size.width), @(rect.size.height)];
|
||||
}
|
||||
static CGRect PGArrayRect(NSArray *array) {
|
||||
if (![array isKindOfClass:NSArray.class] || array.count != 4) return CGRectZero;
|
||||
for (id value in array) if (![value isKindOfClass:NSNumber.class] || !isfinite([value doubleValue])) return CGRectZero;
|
||||
return CGRectMake([array[0] doubleValue], [array[1] doubleValue], [array[2] doubleValue], [array[3] doubleValue]);
|
||||
}
|
||||
static BOOL PGValidRect(CGRect rect) {
|
||||
return rect.size.width > 0.001 && rect.size.height > 0.001 &&
|
||||
rect.origin.x >= -0.1 && rect.origin.y >= -0.1 && CGRectGetMaxX(rect) <= 1.1 && CGRectGetMaxY(rect) <= 1.1;
|
||||
}
|
||||
static CGPoint PGCenter(CGRect rect) { return CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect)); }
|
||||
static NSArray *PGRegion(VNFaceLandmarkRegion2D *region, CGRect box) {
|
||||
if (!region || region.pointCount == 0) return @[];
|
||||
NSMutableArray *points = [NSMutableArray arrayWithCapacity:region.pointCount];
|
||||
const CGPoint *p = region.normalizedPoints;
|
||||
if (!p) return @[];
|
||||
for (NSUInteger i = 0; i < MIN(region.pointCount, 256); i++) {
|
||||
if (isfinite(p[i].x) && isfinite(p[i].y)) {
|
||||
[points addObject:@[@(box.origin.x + p[i].x * box.size.width),
|
||||
@(box.origin.y + p[i].y * box.size.height)]];
|
||||
}
|
||||
}
|
||||
return points;
|
||||
}
|
||||
static NSDictionary *PGGeometry(VNFaceObservation *observation) {
|
||||
CGRect box = observation.boundingBox;
|
||||
VNFaceLandmarks2D *lm = observation.landmarks;
|
||||
return @{@"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)};
|
||||
}
|
||||
static NSDictionary *PGSanitizedGeometry(id value) {
|
||||
if (![value isKindOfClass:NSDictionary.class]) return nil;
|
||||
CGRect box = PGArrayRect(value[@"box"]);
|
||||
if (!PGValidRect(box)) return nil;
|
||||
NSMutableDictionary *result = [NSMutableDictionary dictionaryWithObject:PGRectArray(box) forKey:@"box"];
|
||||
for (NSString *name in @[@"leftEye", @"rightEye", @"leftBrow", @"rightBrow", @"nose", @"lips", @"mouth", @"contour"]) {
|
||||
id raw = value[name];
|
||||
if (![raw isKindOfClass:NSArray.class] || [raw count] > 256) return nil;
|
||||
NSMutableArray *points = [NSMutableArray array];
|
||||
for (id point in raw) {
|
||||
if (![point isKindOfClass:NSArray.class] || [point count] != 2) return nil;
|
||||
for (id coordinate in point) if (![coordinate isKindOfClass:NSNumber.class] || !isfinite([coordinate doubleValue]) || fabs([coordinate doubleValue]) > 2) return nil;
|
||||
[points addObject:[point copy]];
|
||||
}
|
||||
result[name] = points;
|
||||
}
|
||||
return [result copy];
|
||||
}
|
||||
static CGRect PGRegionBounds(NSDictionary *geometry, NSString *name, CGRect fallback) {
|
||||
NSArray *points = geometry[name];
|
||||
if (points.count == 0) return fallback;
|
||||
CGFloat minX = 2, minY = 2, maxX = -1, maxY = -1;
|
||||
for (NSArray *point in points) {
|
||||
CGFloat x = [point[0] doubleValue], y = [point[1] doubleValue];
|
||||
minX = MIN(minX, x); minY = MIN(minY, y); maxX = MAX(maxX, x); maxY = MAX(maxY, y);
|
||||
}
|
||||
return CGRectMake(minX, minY, MAX(0.001, maxX - minX), MAX(0.001, maxY - minY));
|
||||
}
|
||||
static CGRect PGInFace(CGRect face, CGFloat x, CGFloat y, CGFloat w, CGFloat h) {
|
||||
return CGRectMake(face.origin.x + x * face.size.width, face.origin.y + y * face.size.height,
|
||||
w * face.size.width, h * face.size.height);
|
||||
}
|
||||
static CGRect PGToPixels(CGRect normalized, CGRect extent) {
|
||||
return CGRectMake(extent.origin.x + normalized.origin.x * extent.size.width,
|
||||
extent.origin.y + normalized.origin.y * extent.size.height,
|
||||
normalized.size.width * extent.size.width, normalized.size.height * extent.size.height);
|
||||
}
|
||||
static CIVector *PGVector(CGPoint point) { return [CIVector vectorWithX:point.x Y:point.y]; }
|
||||
static CIImage *PGBlack(CGRect extent) {
|
||||
return [[CIImage imageWithColor:[CIColor colorWithRed:0 green:0 blue:0 alpha:1]] imageByCroppingToRect:extent];
|
||||
}
|
||||
static CIImage *PGTransparent(CGRect extent) {
|
||||
return [[CIImage imageWithColor:[CIColor colorWithRed:0 green:0 blue:0 alpha:0]] imageByCroppingToRect:extent];
|
||||
}
|
||||
static CIImage *PGMaximum(CIImage *a, CIImage *b) {
|
||||
return [a imageByApplyingFilter:@"CIMaximumCompositing" withInputParameters:@{kCIInputBackgroundImageKey:b}];
|
||||
}
|
||||
|
||||
@interface PGFaceProcessor () {
|
||||
NSLock *_lock;
|
||||
NSMutableArray<NSMutableDictionary<NSString *, NSDictionary *> *> *_targets;
|
||||
CIWarpKernel *_warp;
|
||||
CIColorKernel *_ellipse;
|
||||
CIColorKernel *_subtract;
|
||||
CIColorKernel *_box;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation PGFaceProcessor
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_lock = [[NSLock alloc] init];
|
||||
_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;"
|
||||
" float body = 1.0-smoothstep(0.38,0.76,length(n*vec2(1.0,0.82)));"
|
||||
" float lower = (1.0-smoothstep(-0.12,0.27,n.y))*body;"
|
||||
" float sides = smoothstep(0.08,0.32,abs(n.x));"
|
||||
" float j=(n.y+0.23)/0.22; float jawBand = exp(-j*j)*body;"
|
||||
" float ch=(n.y-0.02)/0.19; float cheekBand = exp(-ch*ch)*body;"
|
||||
" float head = 1.0-smoothstep(0.38,0.75+0.32*headRange,length(n*vec2(0.9,0.65)));"
|
||||
" p += (p-c)*smallHead*0.18*head;"
|
||||
" p.x += size.x*n.x*(narrow*0.30*body + thin*0.26*lower + 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; }";
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
_warp = [CIWarpKernel kernelWithString:source];
|
||||
_ellipse = [CIColorKernel kernelWithString:@"kernel vec4 pgEllipse(vec2 c, vec2 r, float inner) { float v=1.0-smoothstep(inner,1.0,length((destCoord()-c)/max(r,vec2(0.001)))); return vec4(v,v,v,1.0); }"];
|
||||
_subtract = [CIColorKernel kernelWithString:@"kernel vec4 pgSubtract(__sample a, __sample b) { float v=max(0.0,a.r-b.r); return vec4(v,v,v,1.0); }"];
|
||||
_box = [CIColorKernel kernelWithString:@"kernel vec4 pgBox(vec4 b, float width, vec4 color) { vec2 p=destCoord(); float inside=step(b.x,p.x)*step(b.y,p.y)*step(p.x,b.z)*step(p.y,b.w); float edge=min(min(abs(p.x-b.x),abs(p.x-b.z)),min(abs(p.y-b.y),abs(p.y-b.w))); float a=inside*(1.0-smoothstep(width,width+1.0,edge)); return color*a; }"];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSArray<VNFaceObservation *> *)observationsInImage:(CIImage *)image settings:(PGBeautySettings)settings error:(NSError **)error {
|
||||
CGRect extent = image.extent;
|
||||
CGFloat longest = MAX(extent.size.width, extent.size.height);
|
||||
CGFloat scale = MIN(1.0, (settings.fastMultiFace ? 720.0 : 1280.0) / longest);
|
||||
CIImage *prepared = [image imageByApplyingTransform:CGAffineTransformMakeTranslation(-extent.origin.x, -extent.origin.y)];
|
||||
prepared = [prepared imageByApplyingTransform:CGAffineTransformMakeScale(scale, scale)];
|
||||
VNDetectFaceLandmarksRequest *request = [[VNDetectFaceLandmarksRequest alloc] init];
|
||||
// An image request handler is intentionally local to one invocation. A shared
|
||||
// sequence handler would make parallel / reverse render order observable.
|
||||
VNImageRequestHandler *handler = [[VNImageRequestHandler alloc] initWithCIImage:prepared options:@{}];
|
||||
if (![handler performRequests:@[request] error:error]) return nil;
|
||||
NSArray<VNFaceObservation *> *faces = request.results ?: @[];
|
||||
return [faces sortedArrayUsingComparator:^NSComparisonResult(VNFaceObservation *a, VNFaceObservation *b) {
|
||||
CGRect x = a.boundingBox, y = b.boundingBox;
|
||||
if (CGRectGetMidX(x) != CGRectGetMidX(y)) return CGRectGetMidX(x) < CGRectGetMidX(y) ? NSOrderedAscending : NSOrderedDescending;
|
||||
if (CGRectGetMidY(x) != CGRectGetMidY(y)) return CGRectGetMidY(x) < CGRectGetMidY(y) ? NSOrderedAscending : NSOrderedDescending;
|
||||
return x.size.width > y.size.width ? NSOrderedAscending : NSOrderedDescending;
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSDictionary *)recordForTime:(CMTime)time target:(NSUInteger)target nearest:(BOOL)nearest {
|
||||
NSString *key = PGTimeKey(time);
|
||||
if (!key || target >= 3) return nil;
|
||||
[_lock lock];
|
||||
NSDictionary *record = _targets[target][key];
|
||||
if (nearest && [record[@"faces"] count] == 0) record = nil;
|
||||
if (!record && nearest) {
|
||||
// Pick a deterministic analyzed identity, retaining the last valid face
|
||||
// across lost detections. Only an explicit reset/reselection reacquires
|
||||
// a different identity; render ordering never updates this anchor.
|
||||
double distance = DBL_MAX;
|
||||
CMTime selectedTime = kCMTimeInvalid;
|
||||
for (NSDictionary *candidate in _targets[target].allValues) {
|
||||
if ([candidate[@"faces"] count] == 0) continue;
|
||||
CMTime stored = CMTimeMakeWithEpoch([candidate[@"value"] longLongValue], [candidate[@"scale"] intValue], [candidate[@"epoch"] longLongValue]);
|
||||
if (stored.epoch != time.epoch) continue;
|
||||
double delta = fabs(CMTimeGetSeconds(CMTimeSubtract(stored, time)));
|
||||
if (delta < distance || (delta == distance && CMTIME_IS_VALID(selectedTime) && CMTimeCompare(stored, selectedTime) < 0)) {
|
||||
record = candidate; distance = delta; selectedTime = stored;
|
||||
}
|
||||
}
|
||||
}
|
||||
[_lock unlock];
|
||||
return record;
|
||||
}
|
||||
|
||||
- (NSArray<NSDictionary *> *)geometryForObservations:(NSArray<VNFaceObservation *> *)observations
|
||||
settings:(PGBeautySettings)settings previous:(NSDictionary *)previous {
|
||||
BOOL selectOne = settings.singleFace || settings.trackingEnabled;
|
||||
if (!selectOne) {
|
||||
NSMutableArray *all = [NSMutableArray array];
|
||||
for (VNFaceObservation *face in observations) if (PGValidRect(face.boundingBox) && all.count < 16) [all addObject:PGGeometry(face)];
|
||||
return all;
|
||||
}
|
||||
CGPoint selection = CGPointMake(PGClamp(settings.selectionX, 0, 1), PGClamp(settings.selectionY, 0, 1));
|
||||
NSArray *previousFaces = previous[@"faces"];
|
||||
CGRect prior = previousFaces.count ? PGArrayRect(previousFaces.firstObject[@"box"]) : CGRectZero;
|
||||
BOOL guard = settings.faceJumpGuardRange > 0 && PGValidRect(prior);
|
||||
CGPoint reference = guard ? PGCenter(prior) : selection;
|
||||
VNFaceObservation *best = nil;
|
||||
double bestScore = DBL_MAX;
|
||||
for (VNFaceObservation *face in observations) {
|
||||
CGRect box = face.boundingBox;
|
||||
if (!PGValidRect(box)) continue;
|
||||
CGPoint center = PGCenter(box);
|
||||
double dx = (center.x - reference.x), dy = (center.y - reference.y);
|
||||
if (guard) {
|
||||
double radius = MAX(prior.size.width, prior.size.height) * PGClamp(settings.faceJumpGuardRange, 0.1, 8);
|
||||
double ratio = box.size.width / prior.size.width;
|
||||
if (hypot(dx, dy) > radius || ratio < 0.4 || ratio > 2.5) continue;
|
||||
}
|
||||
double score = dx * dx + dy * dy;
|
||||
if (!guard && CGRectContainsPoint(box, selection)) score -= 2;
|
||||
if (score < bestScore) { best = face; bestScore = score; }
|
||||
}
|
||||
return best ? @[PGGeometry(best)] : @[];
|
||||
}
|
||||
|
||||
- (BOOL)analyzeImage:(CIImage *)image settings:(PGBeautySettings)settings time:(CMTime)time error:(NSError **)error {
|
||||
NSString *key = PGTimeKey(time);
|
||||
if (!image || !key || CGRectIsInfinite(image.extent) || CGRectIsEmpty(image.extent)) {
|
||||
if (error) *error = [NSError errorWithDomain:PGFaceErrorDomain code:1 userInfo:@{NSLocalizedDescriptionKey:@"Face analysis requires a finite image and valid frame time."}];
|
||||
return NO;
|
||||
}
|
||||
NSArray *observations = [self observationsInImage:image settings:settings error:error];
|
||||
if (!observations) return NO;
|
||||
NSUInteger target = MIN((NSUInteger)settings.targetIndex, 2);
|
||||
NSDictionary *prior = [self recordForTime:time target:target nearest:YES];
|
||||
NSArray *geometry = [self geometryForObservations:observations settings:settings previous:prior];
|
||||
NSDictionary *record = @{@"value":@(time.value), @"scale":@(time.timescale), @"epoch":@(time.epoch), @"faces":geometry};
|
||||
[_lock lock];
|
||||
_targets[target][key] = record;
|
||||
[_lock unlock];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (CIImage *)ellipseInRect:(CGRect)rect extent:(CGRect)extent inner:(CGFloat)inner {
|
||||
return [_ellipse applyWithExtent:extent arguments:@[PGVector(PGCenter(rect)), [CIVector vectorWithX:rect.size.width * 0.5 Y:rect.size.height * 0.5], @(inner)]];
|
||||
}
|
||||
|
||||
- (CIImage *)warpImage:(CIImage *)image geometry:(NSArray<NSDictionary *> *)geometry settings:(PGBeautySettings)s extent:(CGRect)extent {
|
||||
if (!(s.faceThin || s.faceBigEye || s.faceShort || s.faceNarrow || s.faceChin || s.faceV || s.faceJaw || s.faceCheekbone || s.faceSmallHead)) return [image imageByCroppingToRect:extent];
|
||||
CIImage *result = image;
|
||||
for (NSDictionary *face in geometry) {
|
||||
CGRect box = PGArrayRect(face[@"box"]), pixels = PGToPixels(box, extent);
|
||||
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],
|
||||
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)),
|
||||
@(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))];
|
||||
CIImage *input = [result imageByClampingToExtent];
|
||||
result = [_warp applyWithExtent:extent roiCallback:^CGRect(int index, CGRect rect) { (void)index; (void)rect; return extent; } inputImage:input arguments:arguments];
|
||||
if (!result) return nil;
|
||||
}
|
||||
return [result imageByCroppingToRect:extent];
|
||||
}
|
||||
|
||||
- (PGFaceFrame *)processImage:(CIImage *)image settings:(PGBeautySettings)settings time:(CMTime)time error:(NSError **)error {
|
||||
CGRect extent = image.extent;
|
||||
if (!image || CGRectIsInfinite(extent) || CGRectIsEmpty(extent) || !_ellipse || !_warp || !_box || !_subtract) {
|
||||
if (error) *error = [NSError errorWithDomain:PGFaceErrorDomain code:2 userInfo:@{NSLocalizedDescriptionKey:@"Unable to initialize face processing for this image."}];
|
||||
return nil;
|
||||
}
|
||||
NSDictionary *record = settings.trackingEnabled ? [self recordForTime:time target:MIN((NSUInteger)settings.targetIndex, 2) nearest:NO] : nil;
|
||||
NSArray<NSDictionary *> *geometry;
|
||||
NSArray<VNFaceObservation *> *observations;
|
||||
if (record) {
|
||||
geometry = record[@"faces"];
|
||||
NSMutableArray *restored = [NSMutableArray array];
|
||||
for (NSDictionary *face in geometry) [restored addObject:[VNFaceObservation faceObservationWithRequestRevision:VNDetectFaceLandmarksRequestRevision3 boundingBox:PGArrayRect(face[@"box"]) roll:nil yaw:nil pitch:nil]];
|
||||
observations = restored;
|
||||
} else {
|
||||
NSArray *detected = [self observationsInImage:image settings:settings error:error];
|
||||
if (!detected) return nil;
|
||||
// A missing time may use a nearby analyzed identity as a guard, while
|
||||
// still detecting the current image. Rendering never appends to history.
|
||||
NSDictionary *anchor = settings.trackingEnabled ? [self recordForTime:time target:MIN((NSUInteger)settings.targetIndex, 2) nearest:YES] : nil;
|
||||
geometry = [self geometryForObservations:detected settings:settings previous:anchor];
|
||||
NSMutableArray *selected = [NSMutableArray array];
|
||||
for (NSDictionary *face in geometry) {
|
||||
CGRect box = PGArrayRect(face[@"box"]);
|
||||
for (VNFaceObservation *observation in detected) if (CGRectEqualToRect(observation.boundingBox, box)) { [selected addObject:observation]; break; }
|
||||
}
|
||||
observations = selected;
|
||||
}
|
||||
CIImage *skin = PGBlack(extent), *teeth = PGBlack(extent), *eyes = PGBlack(extent), *protection = PGBlack(extent), *overlay = PGTransparent(extent);
|
||||
for (NSDictionary *face in geometry) {
|
||||
CGRect box = PGArrayRect(face[@"box"]);
|
||||
CGRect skinRect = PGInFace(box, -0.10, -0.04, 1.20, 1.16);
|
||||
skin = PGMaximum(skin, [self ellipseInRect:PGToPixels(skinRect, extent) extent:extent inner:0.78]);
|
||||
NSArray *names = @[@"leftEye", @"rightEye", @"leftBrow", @"rightBrow", @"lips", @"nose"];
|
||||
NSArray<NSValue *> *fallbacks = @[[NSValue valueWithRect:PGInFace(box, 0.16, 0.53, 0.25, 0.15)],
|
||||
[NSValue valueWithRect:PGInFace(box, 0.59, 0.53, 0.25, 0.15)],
|
||||
[NSValue valueWithRect:PGInFace(box, 0.12, 0.72, 0.29, 0.08)],
|
||||
[NSValue valueWithRect:PGInFace(box, 0.59, 0.72, 0.29, 0.08)],
|
||||
[NSValue valueWithRect:PGInFace(box, 0.27, 0.16, 0.46, 0.18)],
|
||||
[NSValue valueWithRect:PGInFace(box, 0.39, 0.33, 0.22, 0.16)]];
|
||||
for (NSUInteger i = 0; i < names.count; i++) {
|
||||
CGRect region = PGToPixels(PGRegionBounds(face, names[i], fallbacks[i].rectValue), extent);
|
||||
CGRect expanded = CGRectInset(region, -region.size.width * 0.15, -region.size.height * 0.35);
|
||||
CIImage *mask = [self ellipseInRect:expanded extent:extent inner:0.45];
|
||||
protection = PGMaximum(protection, mask);
|
||||
if (i < 2) eyes = PGMaximum(eyes, [self ellipseInRect:region extent:extent inner:0.45]);
|
||||
}
|
||||
CGRect mouth = PGRegionBounds(face, @"mouth", PGInFace(box, 0.30, 0.20, 0.40, 0.08));
|
||||
teeth = PGMaximum(teeth, [self ellipseInRect:PGToPixels(mouth, extent) extent:extent inner:0.35]);
|
||||
if (settings.showFaceBoxes || settings.showTrackBox) {
|
||||
CGRect pixels = PGToPixels(box, extent);
|
||||
CGFloat inset = MAX(1, MIN(pixels.size.width, pixels.size.height) * 0.005);
|
||||
CIImage *outline = [_box applyWithExtent:extent arguments:@[[CIVector vectorWithX:CGRectGetMinX(pixels) Y:CGRectGetMinY(pixels) Z:CGRectGetMaxX(pixels) W:CGRectGetMaxY(pixels)], @(inset), [CIVector vectorWithX:0.12 Y:1.0 Z:0.35 W:0.95]]];
|
||||
overlay = [outline imageByCompositingOverImage:overlay];
|
||||
}
|
||||
}
|
||||
if (settings.featureProtection) skin = [_subtract applyWithExtent:extent arguments:@[skin, protection]];
|
||||
PGFaceFrame *frame = [[PGFaceFrame alloc] init];
|
||||
frame.faces = observations ?: @[];
|
||||
frame.image = [self warpImage:image geometry:geometry settings:settings extent:extent];
|
||||
frame.skinMask = [self warpImage:skin geometry:geometry settings:settings extent:extent];
|
||||
frame.teethMask = [self warpImage:teeth geometry:geometry settings:settings extent:extent];
|
||||
frame.eyeMask = [self warpImage:eyes geometry:geometry settings:settings extent:extent];
|
||||
frame.overlayImage = [overlay imageByCroppingToRect:extent];
|
||||
if (!frame.image || !frame.skinMask || !frame.teethMask || !frame.eyeMask) {
|
||||
if (error) *error = [NSError errorWithDomain:PGFaceErrorDomain code:3 userInfo:@{NSLocalizedDescriptionKey:@"Unable to apply face geometry."}];
|
||||
return nil;
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
- (void)resetTracking {
|
||||
[_lock lock];
|
||||
for (NSMutableDictionary *target in _targets) [target removeAllObjects];
|
||||
[_lock unlock];
|
||||
}
|
||||
|
||||
- (NSData *)trackingDataForTarget:(NSUInteger)target {
|
||||
if (target >= 3) return nil;
|
||||
[_lock lock];
|
||||
NSDictionary *frames = [_targets[target] copy];
|
||||
[_lock unlock];
|
||||
if (!frames.count) return nil;
|
||||
return [NSPropertyListSerialization dataWithPropertyList:@{@"version":@1, @"frames":frames} format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil];
|
||||
}
|
||||
|
||||
- (void)setTrackingData:(NSData *)data forTarget:(NSUInteger)target {
|
||||
if (target >= 3) return;
|
||||
NSMutableDictionary *frames = [NSMutableDictionary dictionary];
|
||||
if (data.length && data.length <= 128 * 1024 * 1024) {
|
||||
NSDictionary *archive = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListImmutable format:nil error:nil];
|
||||
if ([archive isKindOfClass:NSDictionary.class] && [archive[@"version"] isEqual:@1] && [archive[@"frames"] isKindOfClass:NSDictionary.class] && [archive[@"frames"] count] <= 250000) {
|
||||
for (id key in archive[@"frames"]) {
|
||||
id raw = archive[@"frames"][key];
|
||||
if (![key isKindOfClass:NSString.class] || ![raw isKindOfClass:NSDictionary.class]) continue;
|
||||
NSNumber *value = raw[@"value"], *scale = raw[@"scale"], *epoch = raw[@"epoch"];
|
||||
if (![value isKindOfClass:NSNumber.class] || ![scale isKindOfClass:NSNumber.class] || ![epoch isKindOfClass:NSNumber.class] || scale.intValue <= 0) continue;
|
||||
CMTime time = CMTimeMakeWithEpoch(value.longLongValue, scale.intValue, epoch.longLongValue);
|
||||
if (![key isEqualToString:PGTimeKey(time)] || ![raw[@"faces"] isKindOfClass:NSArray.class] || [raw[@"faces"] count] > 16) continue;
|
||||
NSMutableArray *faces = [NSMutableArray array];
|
||||
BOOL valid = YES;
|
||||
for (id geometry in raw[@"faces"]) {
|
||||
NSDictionary *face = PGSanitizedGeometry(geometry);
|
||||
if (!face) { valid = NO; break; }
|
||||
[faces addObject:face];
|
||||
}
|
||||
if (valid) frames[key] = @{@"value":value, @"scale":scale, @"epoch":epoch, @"faces":[faces copy]};
|
||||
}
|
||||
}
|
||||
}
|
||||
[_lock lock];
|
||||
_targets[target] = frames;
|
||||
[_lock unlock];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -46,7 +46,9 @@ static BOOL PGFail(NSError **error, NSString *message) {
|
||||
}
|
||||
static CIImage *PGImageFromTile(FxImageTile *tile) {
|
||||
if (!tile.ioSurface) return nil;
|
||||
NSDictionary *options = tile.colorSpace ? @{kCIImageColorSpace:(__bridge id)tile.colorSpace} : @{};
|
||||
// The host already supplies video-gamma RGB. Keep those numeric values in
|
||||
// every stage; implicit Core Image linearization changes mask thresholds.
|
||||
NSDictionary *options = @{kCIImageColorSpace:NSNull.null};
|
||||
CIImage *image = [CIImage imageWithIOSurface:(__bridge IOSurfaceRef)tile.ioSurface options:options];
|
||||
if (tile.imageOrigin == kFxImageOrigin_TOP_LEFT) {
|
||||
image = [image imageByApplyingTransform:CGAffineTransformMake(1,0,0,-1,0,image.extent.size.height)];
|
||||
@@ -58,11 +60,13 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
|
||||
@interface beatygrilPlugIn ()
|
||||
@property (strong) PGBeautyRenderer *beautyRenderer;
|
||||
@property (strong) PGFaceProcessor *faceProcessor;
|
||||
@property (strong) PGFaceProcessor *analysisFaceProcessor;
|
||||
@property (strong) NSMutableDictionary<NSNumber *,CIContext *> *contexts;
|
||||
@property PGBeautySettings analysisSettings;
|
||||
@property CMTime analysisTime;
|
||||
@property NSUInteger analyzedFrames;
|
||||
@property BOOL analyzing;
|
||||
@property BOOL analysisForward;
|
||||
@property (strong) NSArray<NSString *> *loadedTracks;
|
||||
@end
|
||||
|
||||
@@ -71,6 +75,7 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
|
||||
if ((self=[super init])) {
|
||||
_apiManager=manager;
|
||||
_beautyRenderer=[PGBeautyRenderer new]; _faceProcessor=[PGFaceProcessor new];
|
||||
_analysisFaceProcessor=[PGFaceProcessor new];
|
||||
_contexts=[NSMutableDictionary new]; _analysisSettings=PGBeautyDefaults();
|
||||
_analysisTime=kCMTimeZero;
|
||||
}
|
||||
@@ -178,7 +183,6 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
|
||||
if(!state) return PGFail(error,@"缺少渲染状态输出位置。");
|
||||
if(![_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)]) return PGFail(error,@"无法读取宿主参数。");
|
||||
PGBeautySettings s=[self settingsAtTime:time];
|
||||
@synchronized(self) {_analysisTime=time;}
|
||||
*state=[NSPropertyListSerialization dataWithPropertyList:@{@"settings":[NSData dataWithBytes:&s length:sizeof(s)],@"tracks":[self trackStrings]} format:NSPropertyListBinaryFormat_v1_0 options:0 error:error];
|
||||
return *state!=nil;
|
||||
}
|
||||
@@ -198,21 +202,21 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
|
||||
for(id<MTLDevice> candidate in MTLCopyAllDevices()) if(candidate.registryID==tile.deviceRegistryID) {device=candidate;break;}
|
||||
if(!device) device=MTLCreateSystemDefaultDevice();
|
||||
if(!device) return nil;
|
||||
_contexts[key]=[CIContext contextWithMTLDevice:device options:@{kCIContextCacheIntermediates:@NO}];
|
||||
_contexts[key]=[CIContext contextWithMTLDevice:device options:@{kCIContextCacheIntermediates:@NO,kCIContextWorkingColorSpace:NSNull.null,kCIContextOutputColorSpace:NSNull.null}];
|
||||
}
|
||||
return _contexts[key];
|
||||
}
|
||||
}
|
||||
- (BOOL)renderDestinationImage:(FxImageTile *)destination sourceImages:(NSArray<FxImageTile *> *)sources pluginState:(NSData *)state atTime:(CMTime)time error:(NSError **)error {
|
||||
@autoreleasepool {
|
||||
if(sources.count==0||!sources.firstObject.ioSurface||!destination.ioSurface||state.length>16*1024*1024) return PGFail(error,@"宿主传入的画面或状态无效。");
|
||||
if(sources.count==0||!sources.firstObject.ioSurface||!destination.ioSurface||state.length>320*1024*1024) return PGFail(error,@"宿主传入的画面或状态无效。");
|
||||
NSDictionary *plist=[NSPropertyListSerialization propertyListWithData:state options:NSPropertyListImmutable format:nil error:error];
|
||||
if(![plist isKindOfClass:NSDictionary.class]||![plist[@"settings"] isKindOfClass:NSData.class]||[plist[@"settings"] length]!=sizeof(PGBeautySettings)) return PGFail(error,@"渲染状态版本无效。");
|
||||
PGBeautySettings s;[plist[@"settings"] getBytes:&s length:sizeof(s)];
|
||||
if(s.version!=1) return PGFail(error,@"此版本不支持保存的渲染状态。");
|
||||
CIImage *input=PGImageFromTile(sources.firstObject); if(!input) return PGFail(error,@"无法读取 IOSurface。");
|
||||
CIContext *context=[self contextForTile:destination]; if(!context) return PGFail(error,@"没有可用的 Metal 图形设备。");
|
||||
CIImage *output=nil;
|
||||
CIImage *output=nil; CIImage *overlay=nil;
|
||||
@synchronized(_faceProcessor) {
|
||||
NSArray *tracks=plist[@"tracks"];
|
||||
if(![tracks isKindOfClass:NSArray.class]||tracks.count!=3) return PGFail(error,@"跟踪状态无效。");
|
||||
@@ -225,18 +229,21 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
|
||||
}
|
||||
PGFaceFrame *frame=[_faceProcessor processImage:input settings:s time:time error:error];
|
||||
if(!frame) return NO;
|
||||
output=[_beautyRenderer processImage:frame.image settings:s skinMask:frame.skinMask teethMask:frame.teethMask eyeMask:frame.eyeMask error:error];
|
||||
PGBeautySettings beauty=s; beauty.mix=1;
|
||||
output=[_beautyRenderer processImage:frame.image settings:beauty skinMask:frame.skinMask teethMask:frame.teethMask eyeMask:frame.eyeMask error:error];
|
||||
overlay=frame.overlayImage;
|
||||
}
|
||||
if(!output) return NO;
|
||||
// Blend from the original frame so zero mix also disables geometry.
|
||||
if(s.mix<1 && !s.showMask) {
|
||||
output=[output imageByApplyingFilter:@"CIDissolveTransition" withInputParameters:@{kCIInputTargetImageKey:input,kCIInputTimeKey:@(1-s.mix)}];
|
||||
}
|
||||
if(overlay && !s.showMask) output=[overlay imageByCompositingOverImage:output];
|
||||
FxRect b=destination.tilePixelBounds;
|
||||
output=[output imageByApplyingTransform:CGAffineTransformMakeTranslation(-b.left,-b.bottom)];
|
||||
size_t width=IOSurfaceGetWidth((__bridge IOSurfaceRef)destination.ioSurface),height=IOSurfaceGetHeight((__bridge IOSurfaceRef)destination.ioSurface);
|
||||
if(destination.imageOrigin==kFxImageOrigin_TOP_LEFT) output=[output imageByApplyingTransform:CGAffineTransformMake(1,0,0,-1,0,height)];
|
||||
@try {[context render:output toIOSurface:(__bridge IOSurfaceRef)destination.ioSurface bounds:CGRectMake(0,0,width,height) colorSpace:destination.colorSpace];}
|
||||
@try {[context render:output toIOSurface:(__bridge IOSurfaceRef)destination.ioSurface bounds:CGRectMake(0,0,width,height) colorSpace:nil];}
|
||||
@catch(NSException *exception) {return PGFail(error,exception.reason?:@"图像处理失败。");}
|
||||
return YES;
|
||||
}
|
||||
@@ -245,26 +252,49 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
|
||||
id<FxParameterSettingAPI_v5> api=[_apiManager apiForProtocol:@protocol(FxParameterSettingAPI_v5)];
|
||||
[api setStringParameterValue:status toParameter:PGStatus];
|
||||
}
|
||||
- (CMTime)currentHostTime {
|
||||
id<FxCustomParameterActionAPI_v4> api=[_apiManager apiForProtocol:@protocol(FxCustomParameterActionAPI_v4)];
|
||||
return api ? [api currentTime] : kCMTimeZero;
|
||||
}
|
||||
- (void)startAnalysisForward:(BOOL)forward {
|
||||
if(_analyzing) {[self setStatus:@"请先等待当前分析结束或取消分析。"];return;}
|
||||
NSError *error=nil;
|
||||
PGBeautySettings s=[self settingsAtTime:_analysisTime];
|
||||
@synchronized(_faceProcessor) {[_faceProcessor setTrackingData:nil forTarget:s.targetIndex];}
|
||||
_analysisSettings=s;_analysisSettings.trackingEnabled=1;_analysisSettings.singleFace=1;
|
||||
id<FxAnalysisAPI> api=[_apiManager apiForProtocol:@protocol(FxAnalysisAPI)];
|
||||
if(!api) {[self setStatus:@"当前宿主不提供帧分析接口。"];return;}
|
||||
_analysisTime=[self currentHostTime];
|
||||
PGBeautySettings s=[self settingsAtTime:_analysisTime];
|
||||
NSArray<NSString *> *tracks=[self trackStrings];
|
||||
// A separate processor owns the mutable analysis session. Render snapshots
|
||||
// cannot overwrite records being accumulated by the analyzer.
|
||||
_analysisFaceProcessor=[PGFaceProcessor new];
|
||||
[_analysisFaceProcessor setTrackingData:[[NSData alloc] initWithBase64EncodedString:tracks[s.targetIndex] options:0] forTarget:s.targetIndex];
|
||||
_analysisSettings=s;_analysisSettings.trackingEnabled=1;_analysisSettings.singleFace=1;
|
||||
_analysisForward=forward;_analyzing=YES;
|
||||
BOOL ok=forward?[api startForwardAnalysis:kFxAnalysisLocation_CPU error:&error]:[api startBackwardAnalysis:kFxAnalysisLocation_CPU error:&error];
|
||||
[self setStatus:ok?@"正在分析所选人脸…":(error.localizedDescription?:@"当前宿主不提供帧分析接口。")];
|
||||
if(!ok) _analyzing=NO;
|
||||
[self setStatus:ok?@"正在分析所选人脸…":(error.localizedDescription?:@"宿主未能开始帧分析。")];
|
||||
}
|
||||
- (void)analyzeForward {[self startAnalysisForward:YES];}
|
||||
- (void)analyzeBackward {[self startAnalysisForward:NO];}
|
||||
- (void)clearTracking {
|
||||
PGBeautySettings s=[self settingsAtTime:_analysisTime];
|
||||
PGBeautySettings s=[self settingsAtTime:[self currentHostTime]];
|
||||
@synchronized(_faceProcessor) {[_faceProcessor setTrackingData:nil forTarget:s.targetIndex];_loadedTracks=nil;}
|
||||
id<FxParameterSettingAPI_v5> api=[_apiManager apiForProtocol:@protocol(FxParameterSettingAPI_v5)];
|
||||
[api setStringParameterValue:@"" toParameter:PGTrackData1+s.targetIndex];
|
||||
[self setStatus:@"已清除当前目标的分析数据"];
|
||||
}
|
||||
- (BOOL)desiredAnalysisTimeRange:(CMTimeRange *)desired forInputWithTimeRange:(CMTimeRange)range error:(NSError **)error {
|
||||
if(!desired) return PGFail(error,@"缺少分析时间范围。");*desired=range;return YES;
|
||||
if(!desired) return PGFail(error,@"缺少分析时间范围。");
|
||||
*desired=range;
|
||||
if(CMTIMERANGE_IS_VALID(range) && CMTIME_IS_NUMERIC(_analysisTime) && CMTimeRangeContainsTime(range,_analysisTime)) {
|
||||
if(_analysisForward) *desired=CMTimeRangeFromTimeToTime(_analysisTime,CMTimeRangeGetEnd(range));
|
||||
else {
|
||||
CMTime last=CMTimeAdd(_analysisTime,CMTimeMake(1,MAX(1,_analysisTime.timescale)));
|
||||
if(CMTimeCompare(last,CMTimeRangeGetEnd(range))>0) last=CMTimeRangeGetEnd(range);
|
||||
*desired=CMTimeRangeFromTimeToTime(range.start,last);
|
||||
}
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
- (BOOL)setupAnalysisForTimeRange:(CMTimeRange)range frameDuration:(CMTime)duration error:(NSError **)error {
|
||||
_analyzedFrames=0;_analyzing=YES;return YES;
|
||||
@@ -272,14 +302,19 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
|
||||
- (BOOL)analyzeFrame:(FxImageTile *)frame atTime:(CMTime)time error:(NSError **)error {
|
||||
CIImage *image=PGImageFromTile(frame);if(!image) return PGFail(error,@"无法读取分析帧。");
|
||||
BOOL ok=NO;
|
||||
@synchronized(_faceProcessor) {ok=[_faceProcessor analyzeImage:image settings:_analysisSettings time:time error:error];}
|
||||
@synchronized(_analysisFaceProcessor) {ok=[_analysisFaceProcessor analyzeImage:image settings:_analysisSettings time:time error:error];}
|
||||
if(ok) _analyzedFrames++;
|
||||
return ok;
|
||||
}
|
||||
- (BOOL)cleanupAnalysis:(NSError **)error {
|
||||
id<FxParameterSettingAPI_v5> api=[_apiManager apiForProtocol:@protocol(FxParameterSettingAPI_v5)];
|
||||
NSData *data=nil;
|
||||
@synchronized(_faceProcessor) {data=[_faceProcessor trackingDataForTarget:_analysisSettings.targetIndex];_loadedTracks=nil;}
|
||||
@synchronized(_analysisFaceProcessor) {data=[_analysisFaceProcessor trackingDataForTarget:_analysisSettings.targetIndex];}
|
||||
@synchronized(_faceProcessor) {_loadedTracks=nil;}
|
||||
if(data.length>64*1024*1024) {
|
||||
_analyzing=NO;
|
||||
return PGFail(error,@"当前目标的跟踪数据超过 64 MB。请将长素材分段分析;原先保存的数据仍保留。");
|
||||
}
|
||||
if(data) {
|
||||
if(![api setStringParameterValue:[data base64EncodedStringWithOptions:0] toParameter:PGTrackData1+_analysisSettings.targetIndex]) return PGFail(error,@"无法将分析数据保存到工程。");
|
||||
[api setBoolValue:YES toParameter:PGTrackEnabled atTime:_analysisTime];
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
//
|
||||
// AppDelegate.m
|
||||
// beatygril
|
||||
//
|
||||
// Created by dayu on 2026/10/21.
|
||||
//
|
||||
|
||||
#import "AppDelegate.h"
|
||||
|
||||
@interface AppDelegate ()
|
||||
|
||||
@property (weak) IBOutlet NSWindow *window;
|
||||
@end
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
||||
// Insert code here to initialize your application
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
|
||||
self.window.title=@"大鱼人像美化";
|
||||
[self.window setContentSize:NSMakeSize(600,360)];
|
||||
NSView *content=[NSView new];self.window.contentView=content;
|
||||
NSStackView *stack=[NSStackView new];stack.orientation=NSUserInterfaceLayoutOrientationVertical;
|
||||
stack.alignment=NSLayoutAttributeLeading;stack.spacing=16;stack.translatesAutoresizingMaskIntoConstraints=NO;
|
||||
[content addSubview:stack];
|
||||
[NSLayoutConstraint activateConstraints:@[[stack.leadingAnchor constraintEqualToAnchor:content.leadingAnchor constant:32],[stack.trailingAnchor constraintEqualToAnchor:content.trailingAnchor constant:-32],[stack.topAnchor constraintEqualToAnchor:content.topAnchor constant:30]]];
|
||||
NSTextField *title=[NSTextField labelWithString:@"大鱼人像美化"];
|
||||
title.font=[NSFont systemFontOfSize:28 weight:NSFontWeightSemibold];[stack addArrangedSubview:title];
|
||||
NSTextField *subtitle=[NSTextField labelWithString:@"beatygril · FxPlug 4"];
|
||||
subtitle.textColor=NSColor.secondaryLabelColor;[stack addArrangedSubview:subtitle];
|
||||
NSTextField *description=[NSTextField wrappingLabelWithString:@"在 Motion 的滤镜资源库中选择「大鱼 → 大鱼人像美化」。Final Cut Pro 的效果浏览器中也可使用配套效果。\n\n磨皮、肤色、细节和脸型可分别开关。启用「只处理选中人脸」后,在检视器调整选脸位置,或直接点击画面选脸。选定目标后可向前、向后分析并将跟踪数据保存在工程中。"];
|
||||
description.font=[NSFont systemFontOfSize:13];[stack addArrangedSubview:description];
|
||||
NSStackView *buttons=[NSStackView new];buttons.orientation=NSUserInterfaceLayoutOrientationHorizontal;buttons.spacing=12;
|
||||
[buttons addArrangedSubview:[NSButton buttonWithTitle:@"打开 Motion" target:self action:@selector(openMotion:)]];
|
||||
[buttons addArrangedSubview:[NSButton buttonWithTitle:@"打开 Final Cut Pro" target:self action:@selector(openFinalCut:)]];
|
||||
[stack addArrangedSubview:buttons];
|
||||
NSTextField *note=[NSTextField wrappingLabelWithString:@"建议先调高中频磨皮,再按需调节肤色与脸型。肤色风格为本插件独立调色预设。"];
|
||||
note.textColor=NSColor.secondaryLabelColor;note.font=[NSFont systemFontOfSize:11];[stack addArrangedSubview:note];
|
||||
[self.window center];
|
||||
}
|
||||
|
||||
|
||||
- (void)applicationWillTerminate:(NSNotification *)aNotification {
|
||||
// Insert code here to tear down your application
|
||||
- (void)openMotion:(id)sender {[self openApplication:@"com.apple.motionapp"];}
|
||||
- (void)openFinalCut:(id)sender {[self openApplication:@"com.apple.FinalCut"];}
|
||||
- (void)openApplication:(NSString *)identifier {
|
||||
NSURL *url=[NSWorkspace.sharedWorkspace URLForApplicationWithBundleIdentifier:identifier];
|
||||
if(url) [NSWorkspace.sharedWorkspace openApplicationAtURL:url configuration:NSWorkspaceOpenConfiguration.configuration completionHandler:nil];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
在新工单中引用
屏蔽一个用户