Fix skin exposure mask islands and OSC coordinates; prepare 1.0.2

这个提交包含在:
dayu 于 2026-10-21 18:05:20 +08:00 提交
1 父节点 dd04a471e1
当前提交 d561537eed
19 个文件已更改
+364 -35

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

+4 -4
查看文件
@@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.0.1</string>
<string>1.0.2</string>
<key>CFBundleVersion</key>
<string>3</string>
<string>4</string>
<key>NSHumanReadableCopyright</key>
<string></string>
<key>PlugInKit</key>
@@ -72,7 +72,7 @@
<key>uuid</key>
<string>FB14B0E6-59B2-4836-916F-CD0C559E9241</string>
<key>version</key>
<string>1.0.1</string>
<string>1.0.2</string>
</dict>
<dict>
<key>className</key>
@@ -84,7 +84,7 @@
<key>uuid</key>
<string>E0E432C8-3D71-4ABA-B006-E7F519E06C87</string>
<key>version</key>
<string>1.0.1</string>
<string>1.0.2</string>
<key>protocolNames</key>
<array>
<string>FxOnScreenControl</string>
@@ -14,6 +14,14 @@ NS_ASSUME_NONNULL_BEGIN
teethMask:(nullable CIImage *)teethMask
eyeMask:(nullable CIImage *)eyeMask
error:(NSError * _Nullable * _Nullable)error;
/// Face-based callers supply uncut coverage for smoothly blended color changes.
- (nullable CIImage *)processImage:(CIImage *)image
settings:(PGBeautySettings)settings
skinMask:(nullable CIImage *)skinMask
colorMask:(nullable CIImage *)colorMask
teethMask:(nullable CIImage *)teethMask
eyeMask:(nullable CIImage *)eyeMask
error:(NSError * _Nullable * _Nullable)error;
@end
NS_ASSUME_NONNULL_END
+27 -3
查看文件
@@ -82,6 +82,13 @@ static NSDictionary<NSString *, CIColorKernel *> *PGKernels(void) {
"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;"
@@ -133,6 +140,11 @@ static NSDictionary<NSString *, CIColorKernel *> *PGKernels(void) {
- (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) ||
@@ -145,7 +157,7 @@ static NSDictionary<NSString *, CIColorKernel *> *PGKernels(void) {
float mix = PGClamp(s.mix, 0, 1);
if (mix == 0 && !s.showMask) return image;
NSDictionary *k = PGKernels();
if (k.count != 10) {
if (k.count != 11) {
if (error) *error = [NSError errorWithDomain:@"PGBeautyRenderer" code:2
userInfo:@{NSLocalizedDescriptionKey:@"The beauty image kernels could not be compiled."}];
return nil;
@@ -188,6 +200,18 @@ static NSDictionary<NSString *, CIColorKernel *> *PGKernels(void) {
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.
@@ -207,12 +231,12 @@ static NSDictionary<NSString *, CIColorKernel *> *PGKernels(void) {
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 (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,mask,@1]);
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);
@@ -11,6 +11,8 @@ NS_ASSUME_NONNULL_BEGIN
/// observations retain their bounding box; render geometry is stored separately.
@property (nonatomic, copy, readonly) NSArray<VNFaceObservation *> *faces;
@property (nonatomic, strong, readonly) CIImage *skinMask;
/// Continuous face support before eye/nose/lip detail-protection cutouts.
@property (nonatomic, strong, readonly) CIImage *colorMask;
@property (nonatomic, strong, readonly) CIImage *teethMask;
@property (nonatomic, strong, readonly) CIImage *eyeMask;
@property (nonatomic, strong, readonly) CIImage *image;
+4 -1
查看文件
@@ -10,6 +10,7 @@ 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 *colorMask;
@property (nonatomic, strong, readwrite) CIImage *teethMask;
@property (nonatomic, strong, readwrite) CIImage *eyeMask;
@property (nonatomic, strong, readwrite) CIImage *image;
@@ -340,15 +341,17 @@ static CIImage *PGMaximum(CIImage *a, CIImage *b) {
overlay = [outline imageByCompositingOverImage:overlay];
}
}
CIImage *colorCoverage=skin;
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.colorMask = [self warpImage:colorCoverage 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 (!frame.image || !frame.skinMask || !frame.colorMask || !frame.teethMask || !frame.eyeMask) {
if (error) *error = [NSError errorWithDomain:PGFaceErrorDomain code:3 userInfo:@{NSLocalizedDescriptionKey:@"Unable to apply face geometry."}];
return nil;
}
+49 -16
查看文件
@@ -2,9 +2,12 @@
#import <CoreImage/CoreImage.h>
#import <IOSurface/IOSurface.h>
#import "PGParameterIDs.h"
#include <math.h>
@interface PGFaceSelectOSC : NSObject <FxOnScreenControl_v4>
@property (assign) id<PROAPIAccessing> apiManager;
@property BOOL dragging;
@property BOOL undoOpen;
@end
@implementation PGFaceSelectOSC
- (instancetype)initWithAPIManager:(id<PROAPIAccessing>)api {
@@ -14,21 +17,33 @@
- (id<FxOnScreenControlAPI_v4>)oscAPI {return [_apiManager apiForProtocol:@protocol(FxOnScreenControlAPI_v4)];}
- (BOOL)selectionEnabledAtTime:(CMTime)time {
BOOL selected=NO;id<FxParameterRetrievalAPI_v6> api=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
[api getBoolValue:&selected fromParameter:PGSingleFace atTime:time];return selected;
return api && [api getBoolValue:&selected fromParameter:PGSingleFace atTime:time] && selected;
}
- (BOOL)validObjectBounds:(CGRect *)bounds {
id<FxOnScreenControlAPI_v4> api=[self oscAPI];
if(!api || !bounds) return NO;
CGRect b=[api objectBounds];
if(!isfinite(b.origin.x)||!isfinite(b.origin.y)||!isfinite(b.size.width)||!isfinite(b.size.height)||b.size.width<=0||b.size.height<=0) return NO;
*bounds=b;return YES;
}
- (void)drawOSCWithWidth:(NSInteger)width height:(NSInteger)height activePart:(NSInteger)part destinationImage:(FxImageTile *)tile atTime:(CMTime)time {
if(!tile.ioSurface||width<=0||height<=0) return;
CIImage *output=[[CIImage imageWithColor:CIColor.clearColor] imageByCroppingToRect:CGRectMake(0,0,width,height)];
if([self selectionEnabledAtTime:time]) {
size_t surfaceW=IOSurfaceGetWidth((__bridge IOSurfaceRef)tile.ioSurface),surfaceH=IOSurfaceGetHeight((__bridge IOSurfaceRef)tile.ioSurface);
if(!surfaceW||!surfaceH) return;
CIImage *output=[[CIImage imageWithColor:CIColor.clearColor] imageByCroppingToRect:CGRectMake(0,0,surfaceW,surfaceH)];
CGRect bounds=CGRectZero;
if([self selectionEnabledAtTime:time] && [self validObjectBounds:&bounds]) {
double x=.5,y=.5;
id<FxParameterRetrievalAPI_v6> params=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
[params getXValue:&x YValue:&y fromParameter:PGSelection atTime:time];
CGRect bounds=[[self oscAPI] objectBounds];double canvasX=width*.5,canvasY=height*.5;
[[self oscAPI] convertPointFromSpace:kFxDrawingCoordinates_OBJECT fromX:bounds.origin.x+x*bounds.size.width fromY:bounds.origin.y+y*bounds.size.height toSpace:kFxDrawingCoordinates_CANVAS toX:&canvasX toY:&canvasY];
CGFloat scale=MAX(1,[[self oscAPI] backingScaleFactor]);
size_t surfaceW=IOSurfaceGetWidth((__bridge IOSurfaceRef)tile.ioSurface),surfaceH=IOSurfaceGetHeight((__bridge IOSurfaceRef)tile.ioSurface);
CGFloat sx=(CGFloat)surfaceW/width,sy=(CGFloat)surfaceH/height;
canvasX*=sx;canvasY*=sy;CGFloat radius=10*scale;
BOOL valid=[params getXValue:&x YValue:&y fromParameter:PGSelection atTime:time] && isfinite(x) && isfinite(y);
double canvasX=NAN,canvasY=NAN;
if(valid) [[self oscAPI] convertPointFromSpace:kFxDrawingCoordinates_OBJECT fromX:bounds.origin.x+x*bounds.size.width fromY:bounds.origin.y+y*bounds.size.height toSpace:kFxDrawingCoordinates_CANVAS toX:&canvasX toY:&canvasY];
// CANVAS coordinates already address the OSC surface. Callback width and
// height may describe the effect image and must not rescale this point.
double backing=[[self oscAPI] backingScaleFactor];
CGFloat scale=isfinite(backing)?MAX(1,backing):1;
CGFloat radius=10*scale;
if(valid && isfinite(canvasX) && isfinite(canvasY)) {
CIColor *color=part?[CIColor colorWithRed:1 green:.8 blue:.2]:[CIColor colorWithRed:.1 green:.9 blue:.8];
CIImage *ring=[CIFilter filterWithName:@"CIRadialGradient" withInputParameters:@{@"inputCenter":[CIVector vectorWithX:canvasX Y:canvasY],@"inputRadius0":@(radius-2),@"inputRadius1":@(radius),@"inputColor0":color,@"inputColor1":CIColor.clearColor}].outputImage;
CIImage *hole=[CIFilter filterWithName:@"CIRadialGradient" withInputParameters:@{@"inputCenter":[CIVector vectorWithX:canvasX Y:canvasY],@"inputRadius0":@(radius-3),@"inputRadius1":@(radius-2),@"inputColor0":CIColor.blackColor,@"inputColor1":CIColor.whiteColor}].outputImage;
@@ -36,6 +51,7 @@
CIImage *v=[[CIImage imageWithColor:color] imageByCroppingToRect:CGRectMake(canvasX-scale,canvasY-16*scale,2*scale,32*scale)];
CIImage *h=[[CIImage imageWithColor:color] imageByCroppingToRect:CGRectMake(canvasX-16*scale,canvasY-scale,32*scale,2*scale)];
output=[[h imageByCompositingOverImage:v] imageByCompositingOverImage:ring];
}
}
CIContext *context=[CIContext contextWithOptions:@{kCIContextCacheIntermediates:@NO}];
size_t w=IOSurfaceGetWidth((__bridge IOSurfaceRef)tile.ioSurface),h=IOSurfaceGetHeight((__bridge IOSurfaceRef)tile.ioSurface);
@@ -43,21 +59,38 @@
[context render:output toIOSurface:(__bridge IOSurfaceRef)tile.ioSurface bounds:CGRectMake(0,0,w,h) colorSpace:tile.colorSpace];
}
- (void)hitTestOSCAtMousePositionX:(double)x mousePositionY:(double)y activePart:(NSInteger *)part atTime:(CMTime)time {
if(part) *part=([self selectionEnabledAtTime:time]&&CGRectContainsPoint([[self oscAPI] objectBounds],CGPointMake(x,y)))?1:0;
if(!part) return;*part=0;CGRect bounds=CGRectZero;
if(isfinite(x)&&isfinite(y)&&[self selectionEnabledAtTime:time]&&[self validObjectBounds:&bounds]&&CGRectContainsPoint(bounds,CGPointMake(x,y))) *part=1;
}
- (void)setSelectionX:(double)x y:(double)y time:(CMTime)time update:(BOOL *)update {
CGRect bounds=[[self oscAPI] objectBounds];if(bounds.size.width<=0||bounds.size.height<=0) return;
if(update) *update=NO;
CGRect bounds=CGRectZero;
if(!isfinite(x)||!isfinite(y)||![self validObjectBounds:&bounds]) return;
id<FxParameterSettingAPI_v5> api=[_apiManager apiForProtocol:@protocol(FxParameterSettingAPI_v5)];
BOOL ok=[api setXValue:MIN(1,MAX(0,(x-bounds.origin.x)/bounds.size.width)) YValue:MIN(1,MAX(0,(y-bounds.origin.y)/bounds.size.height)) toParameter:PGSelection atTime:time];
if(update) *update=ok;
}
- (void)mouseDownAtPositionX:(double)x positionY:(double)y activePart:(NSInteger)part modifiers:(FxModifierKeys)keys forceUpdate:(BOOL *)update atTime:(CMTime)time {
id<FxUndoAPI> undo=[_apiManager apiForProtocol:@protocol(FxUndoAPI)];[undo startUndoGroup:@"选择人脸"];
[self setSelectionX:x y:y time:time update:update];
if(update) *update=NO;
if(part!=1 || !isfinite(x) || !isfinite(y) || ![self selectionEnabledAtTime:time]) return;
CGRect bounds=CGRectZero;if(![self validObjectBounds:&bounds]) return;
id<FxUndoAPI> undo=[_apiManager apiForProtocol:@protocol(FxUndoAPI)];
if(_undoOpen) [undo endUndoGroup];
_undoOpen=[undo startUndoGroup:@"选择人脸"];
BOOL changed=NO;[self setSelectionX:x y:y time:time update:&changed];
_dragging=changed;
if(!changed && _undoOpen) {[undo endUndoGroup];_undoOpen=NO;}
if(update) *update=changed;
}
- (void)mouseDraggedAtPositionX:(double)x positionY:(double)y activePart:(NSInteger)part modifiers:(FxModifierKeys)keys forceUpdate:(BOOL *)update atTime:(CMTime)time {
if(update) *update=NO;
if(_dragging && part==1 && [self selectionEnabledAtTime:time]) [self setSelectionX:x y:y time:time update:update];
}
- (void)mouseDraggedAtPositionX:(double)x positionY:(double)y activePart:(NSInteger)part modifiers:(FxModifierKeys)keys forceUpdate:(BOOL *)update atTime:(CMTime)time {[self setSelectionX:x y:y time:time update:update];}
- (void)mouseUpAtPositionX:(double)x positionY:(double)y activePart:(NSInteger)part modifiers:(FxModifierKeys)keys forceUpdate:(BOOL *)update atTime:(CMTime)time {
[self setSelectionX:x y:y time:time update:update];id<FxUndoAPI> undo=[_apiManager apiForProtocol:@protocol(FxUndoAPI)];[undo endUndoGroup];
if(update) *update=NO;
if(_dragging && part==1 && [self selectionEnabledAtTime:time]) [self setSelectionX:x y:y time:time update:update];
_dragging=NO;
if(_undoOpen) {id<FxUndoAPI> undo=[_apiManager apiForProtocol:@protocol(FxUndoAPI)];[undo endUndoGroup];_undoOpen=NO;}
}
- (void)keyDownAtPositionX:(double)x positionY:(double)y keyPressed:(unsigned short)key modifiers:(FxModifierKeys)modifiers forceUpdate:(BOOL *)update didHandle:(BOOL *)handled atTime:(CMTime)time {if(handled)*handled=NO;if(update)*update=NO;}
- (void)keyUpAtPositionX:(double)x positionY:(double)y keyPressed:(unsigned short)key modifiers:(FxModifierKeys)modifiers forceUpdate:(BOOL *)update didHandle:(BOOL *)handled atTime:(CMTime)time {if(handled)*handled=NO;if(update)*update=NO;}
+1 -1
查看文件
@@ -250,7 +250,7 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
PGFaceFrame *frame=[_faceProcessor processImage:input settings:s time:time error:&stageError];
if(!frame) { os_log_error(PGRenderLog(), "face processing failed: %{public}@",stageError); if(error)*error=stageError; return NO; }
PGBeautySettings beauty=s; beauty.mix=1;
output=[_beautyRenderer processImage:frame.image settings:beauty skinMask:frame.skinMask teethMask:frame.teethMask eyeMask:frame.eyeMask error:&stageError];
output=[_beautyRenderer processImage:frame.image settings:beauty skinMask:frame.skinMask colorMask:frame.colorMask teethMask:frame.teethMask eyeMask:frame.eyeMask error:&stageError];
if(!output) { os_log_error(PGRenderLog(), "beauty processing failed: %{public}@",stageError); if(error)*error=stageError; return NO; }
overlay=frame.overlayImage;
}
@@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.1</string>
<string>1.0.2</string>
<key>CFBundleVersion</key>
<string>3</string>
<string>4</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>