#import #import #import #import "PGFaceProcessor.h" #include @interface PGFaceProcessor (SlimTest) - (CIImage *)warpImage:(CIImage *)image geometry:(NSArray *)geometry settings:(PGBeautySettings)s extent:(CGRect)extent; @end @interface PGRealtimeFaceTracker : NSObject - (NSDictionary *)resultForGeometry:(NSArray *)raw settings:(PGBeautySettings)s time:(CMTime)time extent:(CGRect)extent; @end static NSUInteger checks=0; static double maxOracleError=0; static void Check(BOOL ok,NSString *message) {checks++;if(!ok){fprintf(stderr,"FAIL: %s\n",message.UTF8String);exit(1);}} static double Clamp(double x,double a,double b) {return fmin(b,fmax(a,x));} static CGPoint ReadPoint(NSArray *a) {return CGPointMake([a[0] doubleValue],[a[1] doubleValue]);} static NSArray *WritePoint(CGPoint p) {return @[@(p.x),@(p.y)];} // Authored 2D projections, with the documented 76-point layout. They are not // extracted vendor assets or private face coordinates. Positive yaw projects // the nose toward the right while the silhouette becomes narrower. static NSDictionary *Geometry(double yaw,double roll) { NSMutableArray *p=[NSMutableArray new]; for(NSUInteger i=0;i<76;i++)[p addObject:@[@.5,@.5]]; p[6]=@[@.34,@.65];p[13]=@[@.66,@.65]; p[47]=@[@.5,@.60];p[48]=@[@.5,@.52];p[49]=@[@.5,@.45];p[52]=@[@.5,@.42]; p[36]=@[@.54,@.36];p[37]=@[@.5,@.35];p[38]=@[@.46,@.36]; for(NSUInteger i=0;i<17;i++) { double t=M_PI+M_PI*i/16; p[59+i]=@[@(.5+.29*cos(t)),@(.65+.43*sin(t))]; } for(NSUInteger i=0;i<76;i++) { CGPoint a=ReadPoint(p[i]); double depth=(i==47 || i==48 || i==49 || i==52)?.18:.03; double x=(a.x-.5)*cos(yaw)+depth*sin(yaw),y=a.y-.5; p[i]=WritePoint(CGPointMake(.5+x*cos(roll)-y*sin(roll),.5+x*sin(roll)+y*cos(roll))); } NSMutableDictionary *g=[NSMutableDictionary dictionaryWithDictionary:@{@"box":@[@.15,@.15,@.7,@.6],@"points76":p}]; for(NSString *key in @[@"leftEye",@"rightEye",@"leftBrow",@"rightBrow",@"nose",@"lips",@"mouth",@"contour"])g[key]=@[]; return g; } // Independent scalar oracle from the recorded AIR specification. Its explicit // y-down conversion checks the Core Image implementation's coordinate bridge. static CGPoint Oracle(CGPoint pixel,NSDictionary *g,CGRect extent,double thin) { static const unsigned int origins[17]={75,59,74,60,73,61,72,62,71,63,70,64,69,65,68,66,67}; static const unsigned int anchors[17]={6,13,47,47,47,47,48,48,48,48,49,49,49,49,52,52,47}; NSArray *raw=g[@"points76"];CGPoint p[76]; for(NSUInteger i=0;i<76;i++){p[i]=ReadPoint(raw[i]);p[i].y=1-p[i].y;} double width=hypot((p[75].x-p[59].x)*extent.size.width,(p[75].y-p[59].y)*extent.size.height); double height=hypot((p[67].x-(p[74].x+p[60].x)/2)*extent.size.width,(p[67].y-(p[74].y+p[60].y)/2)*extent.size.height); double gain=Clamp(280/fmax(1,fmax(width,2*height)),1,2.25)*.025*Clamp(thin,0,1); CGPoint q=CGPointMake((pixel.x-extent.origin.x)/extent.size.width,1-(pixel.y-extent.origin.y)/extent.size.height); for(NSUInteger i=0;i<17;i++) { CGPoint a=p[origins[i]],b=p[anchors[i]]; double r=hypot(a.x-b.x,a.y-b.y); if(r<=.0001)continue; double w=Clamp(1-hypot(q.x-a.x,q.y-a.y)/r,0,1); q.x=Clamp(q.x-(b.x-a.x)*gain*w,0,1);q.y=Clamp(q.y-(b.y-a.y)*gain*w,0,1); } return CGPointMake(q.x,1-q.y); } static CGPoint ChinOracle(CGPoint pixel,NSDictionary *g,CGRect e,double thin,double chin) { CGPoint q=Oracle(pixel,g,e,thin);q.y=1-q.y; NSArray *raw=g[@"points76"];CGPoint points[76]; for(NSUInteger i=0;i<76;i++){points[i]=ReadPoint(raw[i]);points[i].y=1-points[i].y;} double width=hypot((points[75].x-points[59].x)*e.size.width,(points[75].y-points[59].y)*e.size.height); double height=2*hypot((points[67].x-(points[74].x+points[60].x)/2)*e.size.width,(points[67].y-(points[74].y+points[60].y)/2)*e.size.height); double ratio=Clamp(chin,-1,1)*(chin>0?.04:.02)*Clamp(280/fmax(1,fmax(width,height)),1,2.25); for(NSUInteger i=0;i<3;i++) { CGPoint a=points[68-i],b=points[38-i];double r=hypot(a.x-b.x,a.y-b.y); if(r<=.0001)continue; double w=Clamp(1-hypot(q.x-a.x,q.y-a.y)/r,0,1); q.x=Clamp(q.x-(b.x-a.x)*ratio*w,0,1);q.y=Clamp(q.y-(b.y-a.y)*ratio*w,0,1); } return CGPointMake(q.x,1-q.y); } static CIImage *CoordinateImage(CGRect extent) { NSUInteger w=extent.size.width,h=extent.size.height; NSMutableData *d=[NSMutableData dataWithLength:w*h*4*sizeof(float)];float *p=d.mutableBytes; for(NSUInteger y=0;yworst){worst=difference;worstX=x;worstY=y;worstExpected=CGPointMake(ex,ey);worstActual=CGPointMake(pixels[k],pixels[k+1]);} maxMove=fmax(maxMove,hypot(ex-(x+.5)/w,ey-(y+.5)/h)); } if(worst>0.00006)fprintf(stderr,"%s oracle max error=%g at %lu,%lu expected=(%g,%g) actual=(%g,%g)\n",name.UTF8String,worst,(unsigned long)worstX,(unsigned long)worstY,worstExpected.x,worstExpected.y,worstActual.x,worstActual.y); maxOracleError=fmax(maxOracleError,worst); Check(worst<0.00006,[name stringByAppendingString:@" GPU field matches the independent thin/chin oracle"]); Check((amount==0 && chin==0) || maxMove>.0001,[name stringByAppendingString:@" exercises a nonzero displacement"]); NSUInteger corner=4*((h-3)*w+(w-3)); Check(fabs(pixels[corner]-(w-2.5)/w)<1e-6 && fabs(pixels[corner+1]-(h-2.5)/h)<1e-6,[name stringByAppendingString:@" preserves distant background"]); } static void NumericCase(CIContext *c,PGFaceProcessor *p,CGRect e,NSDictionary *g,double amount,NSString *name) { NumericSettingsCase(c,p,e,g,amount,0,name); } static CGPoint Displacement(CIContext *c,CIImage *ramp,CIImage *warped,CGPoint uv) { CGRect e=ramp.extent;CGRect point=CGRectMake(floor(e.origin.x+uv.x*e.size.width),floor(e.origin.y+uv.y*e.size.height),1,1); float a[4],b[4]; [c render:ramp toBitmap:a rowBytes:sizeof(a) bounds:point format:kCIFormatRGBAf colorSpace:nil]; [c render:warped toBitmap:b rowBytes:sizeof(b) bounds:point format:kCIFormatRGBAf colorSpace:nil]; return CGPointMake((b[0]-a[0])*e.size.width,-(b[1]-a[1])*e.size.height); } static void AnatomicalChecks(CIContext *c,PGFaceProcessor *p,NSDictionary *g,CGRect e) { CIImage *ramp=CoordinateImage(e);NSArray *points=g[@"points76"]; CGPoint chin=ReadPoint(points[67]),lip=ReadPoint(points[37]); double ax=(lip.x-chin.x)*e.size.width,ay=(lip.y-chin.y)*e.size.height; for(int sign=-1;sign<=1;sign+=2) { PGBeautySettings s=PGBeautyDefaults();s.faceChin=sign; CIImage *out=[p warpImage:ramp geometry:@[g] settings:s extent:e]; CGPoint d=Displacement(c,ramp,out,chin); Check(hypot(d.x,d.y)>.05,@"chin control moves the detected physical chin landmark"); Check(sign*(d.x*ax+d.y*ay)<0,@"signed chin direction follows chin-to-lip anchor instead of a fixed image Y axis"); BOOL outside=YES,eyesUnchanged=YES; for(NSNumber *index in @[@6,@13]) { CGPoint eye=ReadPoint(points[index.unsignedIntegerValue]); for(NSUInteger i=0;i<3;i++) { CGPoint a=ReadPoint(points[68-i]),b=ReadPoint(points[38-i]); if(hypot(eye.x-a.x,eye.y-a.y)<=hypot(a.x-b.x,a.y-b.y)+.003)outside=NO; } CGPoint shift=Displacement(c,ramp,out,eye);eyesUnchanged &= hypot(shift.x,shift.y)<.002; } Check(outside && eyesUnchanged,@"both physical pupils outside chin supports remain unchanged"); } } int main(int argc,const char *argv[]) {@autoreleasepool { CIContext *c=[CIContext contextWithMTLDevice:MTLCreateSystemDefaultDevice() options:@{kCIContextWorkingColorSpace:NSNull.null,kCIContextOutputColorSpace:NSNull.null}]; PGFaceProcessor *p=[PGFaceProcessor new]; CGRect extents[]={CGRectMake(0,0,240,240),CGRectMake(0,0,320,180),CGRectMake(-17,29,180,320),CGRectMake(0,0,640,360)}; double yaw[]={0,.65,1.40,-1.40},roll[]={0,.45,-.50}; NumericCase(c,p,extents[1],Geometry(0,0),0,@"zero strength"); for(NSUInteger e=0;e<4;e++)for(NSUInteger y=0;y<4;y++)for(NSUInteger r=0;r<3;r++) { NSString *name=[NSString stringWithFormat:@"size%lu yaw%lu roll%lu",(unsigned long)e,(unsigned long)y,(unsigned long)r]; NumericCase(c,p,extents[e],Geometry(yaw[y],roll[r]),1,name); } NumericCase(c,p,extents[1],Geometry(.6,.2),.5,@"half strength"); for(NSUInteger e=0;e<3;e++)for(NSUInteger y=0;y<3;y++)for(NSUInteger r=0;r<2;r++)for(int sign=-1;sign<=1;sign+=2) { double angle=y==0?0:(y==1?1.4:-1.4); NumericSettingsCase(c,p,extents[e],Geometry(angle,r?.45:0),0,sign, [NSString stringWithFormat:@"chin size%lu yaw%lu roll%lu sign%d",(unsigned long)e,(unsigned long)y,(unsigned long)r,sign]); } NumericSettingsCase(c,p,extents[1],Geometry(.4,.2),.8,1,@"thin plus positive chin"); NumericSettingsCase(c,p,extents[2],Geometry(-.6,-.2),1,-1,@"thin plus negative chin"); NumericSettingsCase(c,p,extents[0],Geometry(0,0),0,.5,@"half positive chin"); NumericSettingsCase(c,p,extents[0],Geometry(0,0),0,-.5,@"half negative chin"); CIImage *image=CoordinateImage(extents[1]);NSData *source=Pixels(c,image); PGBeautySettings s=PGBeautyDefaults();s.faceThin=1;s.trackingEnabled=1;NSError *error=nil; NSDictionary *g=Geometry(.9,.15);[p setTrackingData:Archive(g) forTarget:0]; PGFaceFrame *frame=[p processImage:image settings:s time:kCMTimeZero error:&error]; Check(frame!=nil && [frame.deformationGeometry[0][@"points76"] count]==76,@"offline restoration retains all 76 indexed points"); PGFaceProcessor *restored=[PGFaceProcessor new];[restored setTrackingData:[p trackingDataForTarget:0] forTarget:0]; Check([Pixels(c,[restored processImage:image settings:s time:kCMTimeZero error:&error].image) isEqualToData:Pixels(c,frame.image)],@"76-point geometry survives archive round trip with identical pixels"); NSData *imageWarp=Pixels(c,[p warpImage:frame.colorMask geometry:@[] settings:s extent:image.extent]); Check(imageWarp.length==source.length,@"warped support masks retain dimensions"); // Apply the same coordinate field to an independent scalar mask ramp. CIImage *mask=[image imageByApplyingFilter:@"CIColorMatrix" withInputParameters:@{@"inputRVector":[CIVector vectorWithX:1 Y:0 Z:0 W:0],@"inputGVector":[CIVector vectorWithX:1 Y:0 Z:0 W:0],@"inputBVector":[CIVector vectorWithX:1 Y:0 Z:0 W:0]}]; NSData *maskData=Pixels(c,[p warpImage:mask geometry:@[g] settings:s extent:image.extent]),*frameData=Pixels(c,frame.image); const float *m=maskData.bytes,*v=frameData.bytes; BOOL masksAgree=YES;for(NSUInteger i=0;i1e-6){masksAgree=NO;break;} Check(masksAgree,@"scalar support masks and RGB use the same source coordinates"); for(NSUInteger mode=0;mode<4;mode++) { NSMutableDictionary *bad=[g mutableCopy]; if(mode==0)[bad removeObjectForKey:@"points76"]; if(mode==1)bad[@"points76"]=[g[@"points76"] subarrayWithRange:NSMakeRange(0,75)]; if(mode==2){NSMutableArray *a=[g[@"points76"] mutableCopy];a[65]=@[@(NAN),@.5];bad[@"points76"]=a;} if(mode==3){NSMutableArray *a=[g[@"points76"] mutableCopy];a[65]=@[@9,@.5];bad[@"points76"]=a;} [p setTrackingData:Archive(bad) forTarget:0];frame=[p processImage:image settings:s time:kCMTimeZero error:&error]; Check(frame!=nil && frame.deformationGeometry.count==1,@"legacy/malformed indexed points retain the valid face record"); Check([Pixels(c,frame.image) isEqualToData:source],@"legacy/malformed indexed points never trigger a guessed symmetric thin warp"); PGBeautySettings chinOnly=s;chinOnly.faceThin=0;chinOnly.faceChin=1; Check([Pixels(c,[p processImage:image settings:chinOnly time:kCMTimeZero error:&error].image) isEqualToData:source],@"legacy/malformed indexed points never trigger guessed bbox chin deformation"); PGBeautySettings other=s;other.faceNarrow=.5; Check(![Pixels(c,[p processImage:image settings:other time:kCMTimeZero error:&error].image) isEqualToData:source],@"other controls remain available in legacy archives"); } PGRealtimeFaceTracker *tracker=[PGRealtimeFaceTracker new];PGBeautySettings live=PGBeautyDefaults(); NSDictionary *a=[tracker resultForGeometry:@[Geometry(0,0)] settings:live time:kCMTimeZero extent:image.extent]; NSDictionary *b=[tracker resultForGeometry:@[Geometry(.2,0)] settings:live time:CMTimeMake(1,25) extent:image.extent]; Check([a[@"output"][0][@"points76"] count]==76 && [b[@"output"][0][@"points76"] count]==76,@"realtime stabilization retains the complete indexed point layout"); Check([b isEqual:[tracker resultForGeometry:@[Geometry(.2,0)] settings:live time:CMTimeMake(1,25) extent:image.extent]],@"repeated rendering does not smooth the 76-point layout again"); NSDictionary *held=[tracker resultForGeometry:@[] settings:live time:CMTimeMake(2,25) extent:image.extent]; Check([held[@"output"][0][@"points76"] isEqual:b[@"output"][0][@"points76"]],@"missing detection holds the complete point layout"); NSDictionary *turned=Geometry(1.4,0); NSDictionary *back=[tracker resultForGeometry:@[turned] settings:live time:CMTimeMake(3,25) extent:image.extent]; Check([back[@"output"][0][@"points76"] isEqual:turned[@"points76"]],@"reacquired side-face points do not blend with a held frontal pose"); for(int index=1;index0 && [f.deformationGeometry[0][@"points76"] count]==76,@"real Vision detection supplies exactly 76 points to thin deformation"); Check(![Pixels(c,f.image) isEqualToData:Pixels(c,portrait)],@"maximum thin changes a real detected portrait"); Check([real analyzeImage:portrait settings:rs time:kCMTimeZero error:&error],@"real pose analysis saves the complete geometry"); PGFaceProcessor *copy=[PGFaceProcessor new];[copy setTrackingData:[real trackingDataForTarget:0] forTarget:0];rs.trackingEnabled=1; Check([Pixels(c,[copy processImage:portrait settings:rs time:kCMTimeZero error:&error].image) isEqualToData:Pixels(c,f.image)],@"real pose replays from persisted 76 points with identical pixels"); AnatomicalChecks(c,real,f.deformationGeometry.firstObject,portrait.extent); }} printf("PASS: %lu slim alignment checks; maximum UV oracle error %.9g\n",(unsigned long)checks,maxOracleError);return 0; }}