文件
paintfcp-fxplug/beatygril/beatygril/Plugin/PGBeautyRenderer.m
T

267 行
17 KiB
Objective-C

#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[@"colorCoverage"] = PGKernel(@
"kernel vec4 pgColorCoverage(__sample coverage,__sample image,float invert) {"
"vec3 c=unpremultiply(image).rgb; 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 likelihood=exp(-pow((cb-0.415)/0.115,2.0)-pow((cr-0.565)/0.135,2.0));"
"float v=clamp(coverage.r,0.0,1.0)*smoothstep(0.05,0.60,likelihood);"
"v=mix(v,1.0-v,invert); return vec4(v,v,v,1.0); }");
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 {
return [self processImage:image settings:s skinMask:skinMask colorMask:nil teethMask:teethMask eyeMask:eyeMask error:error];
}
- (CIImage *)processImage:(CIImage *)image settings:(PGBeautySettings)s
skinMask:(CIImage *)skinMask colorMask:(CIImage *)colorMask
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 != 11) {
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)]);
}
// Detail protection may contain small eye, brow, nose and lip cutouts.
// Exposure and color must not reuse those holes or sharpen a face ellipse
// with the detail threshold; that creates visible rings and islands.
CIImage *toneMask=mask;
BOOL changesColor=brightness>0 || hue!=0 || saturation!=0 || even>0 ||
(s.skinTonePreset>0 && s.skinTonePreset<=6);
if(colorMask && changesColor) {
CGFloat feather=MAX(3,MIN(extent.size.width,extent.size.height)*0.03);
CIImage *support=[colorMask imageByCroppingToRect:extent];
toneMask=PGApply(k[@"colorCoverage"],extent,@[PGBlur(support,feather,extent),
PGBlur(image,MAX(1,feather*0.15),extent),@(s.invertMask?1:0)]);
}
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,toneMask,@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,toneMask,@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