Restore recovered short-face and jaw fields; prepare 1.1.2 with 1535 regressions passed
这个提交包含在:
1 父节点
2d3edcf0a2
当前提交
183bf0e4b2
23 个文件已更改
+998
-46
没有与搜索条件相匹配的文件
+3
-3
@@ -323,14 +323,14 @@ int main(int argc, const char *argv[]) {
|
||||
*controls[i] = 1;
|
||||
PGFaceFrame *warped = [processor processImage:image settings:changed time:kCMTimeZero error:&error];
|
||||
BOOL changedPixels=![Pixels(context, warped.image) isEqualToData:base];
|
||||
BOOL requiresPoints=(i==0 || i==4);
|
||||
Check(warped != nil && (requiresPoints ? !changedPixels : changedPixels), requiresPoints ? @"thin/chin safely skip a face with no indexed landmarks" : [@"geometry control changes pixels: " stringByAppendingString:names[i]]);
|
||||
BOOL requiresPoints=(i==0 || i==2 || i==4 || i==6);
|
||||
Check(warped != nil && (requiresPoints ? !changedPixels : changedPixels), requiresPoints ? @"recovered thin/chin/short/jaw skip a face with no indexed landmarks" : [@"geometry control changes pixels: " stringByAppendingString:names[i]]);
|
||||
Check(CGRectEqualToRect(warped.image.extent, image.extent), @"warp preserves image extent");
|
||||
}
|
||||
settings.faceShort = 0.8;
|
||||
NSData *positive = Pixels(context, [processor processImage:image settings:settings time:kCMTimeZero error:&error].image);
|
||||
settings.faceShort = -0.8;
|
||||
Check(![positive isEqualToData:Pixels(context, [processor processImage:image settings:settings time:kCMTimeZero error:&error].image)], @"signed short-face control has distinct directions");
|
||||
Check([positive isEqualToData:base] && [positive isEqualToData:Pixels(context, [processor processImage:image settings:settings time:kCMTimeZero error:&error].image)], @"both signed short-face values safely skip missing76; direction is tested with full geometry");
|
||||
settings = PGBeautyDefaults(); settings.showFaceBoxes = 1;
|
||||
frame = [processor processImage:image settings:settings time:kCMTimeZero error:&error];
|
||||
Check(Channel(context, frame.overlayImage, 26, 64, 3) > 0, @"face outlines provide separate final-composite overlay");
|
||||
|
||||
+18
-6
@@ -328,14 +328,24 @@ static void PortraitAnatomyTests(NSString *fixture, CIImage *portrait) {
|
||||
CGRect leftEye = CGRectMake(left.x-fw*.24, left.y-fh*.17, fw*.48, fh*.34);
|
||||
CGRect rightEye = CGRectMake(right.x-fw*.24, right.y-fh*.17, fw*.48, fh*.34);
|
||||
CGRect chinArea = CGRectMake(chin.x-fw*.30, chin.y-fh*.22, fw*.60, fh*.44);
|
||||
CGRect lowerFace = CGRectNull;
|
||||
for (NSUInteger point = 62; point <= 72; point++) {
|
||||
CGPoint p = face.landmarks.allPoints.normalizedPoints[point];
|
||||
CGFloat x = (box.origin.x+p.x*box.size.width)*width;
|
||||
CGFloat y = (1-box.origin.y-p.y*box.size.height)*height;
|
||||
lowerFace = CGRectUnion(lowerFace, CGRectMake(x, y, 1, 1));
|
||||
}
|
||||
lowerFace = CGRectInset(lowerFace, -fw*.15, -fh*.15);
|
||||
CGRect upsideDownFace = CGRectMake(box.origin.x*width, box.origin.y*height, fw, fh);
|
||||
const UInt32 controls[] = {0, PGBigEye, PGChin, PGChin};
|
||||
const double values[] = {0, 1, 1, -1};
|
||||
NSArray<NSString *> *names = @[@"default", @"big-eye", @"chin-positive", @"chin-negative"];
|
||||
for (NSUInteger effect = 0; effect < 4; effect++) {
|
||||
const UInt32 controls[] = {0, PGBigEye, PGChin, PGChin, PGShort, PGShort, PGJaw};
|
||||
const double values[] = {0, 1, 1, -1, 1, -1, 1};
|
||||
NSArray<NSString *> *names = @[@"default", @"big-eye", @"chin-positive", @"chin-negative", @"short-positive", @"short-negative", @"jaw"];
|
||||
for (NSUInteger effect = 0; effect < names.count; effect++) {
|
||||
PGBeautySettings settings = PGBeautyDefaults();
|
||||
if (effect == 1) settings.faceBigEye = 1;
|
||||
else if (effect > 1) settings.faceChin = values[effect];
|
||||
else if (effect == 2 || effect == 3) settings.faceChin = values[effect];
|
||||
else if (effect == 4 || effect == 5) settings.faceShort = values[effect];
|
||||
else if (effect == 6) settings.faceJaw = values[effect];
|
||||
NSData *expected = ImageBytes(context, ProcessedReference(portrait, settings, kCMTimeZero, nil));
|
||||
Check(expected.length == canonical.length, [names[effect] stringByAppendingString:@" independent upright face/beauty chain renders"]);
|
||||
if (expected.length != canonical.length) continue;
|
||||
@@ -361,9 +371,11 @@ static void PortraitAnatomyTests(NSString *fixture, CIImage *portrait) {
|
||||
Check(MaximumDifference(actual,originBaseline) == 0, [label stringByAppendingString:@" is byte-identical across coordinate tags"]);
|
||||
double lm = ChangeMass(canonical, actual, width, leftEye), rm = ChangeMass(canonical, actual, width, rightEye);
|
||||
double cm = ChangeMass(canonical, actual, width, chinArea), um = ChangeMass(canonical, actual, width, upsideDownFace);
|
||||
double lowerMass = ChangeMass(canonical, actual, width, lowerFace);
|
||||
BOOL located = effect == 0 ? MaximumDifference(canonical, actual) <= 1 :
|
||||
(effect == 1 ? lm > 100 && rm > 100 && cm < (lm+rm)*.01 && um == 0 :
|
||||
cm > 100 && cm > 4*(lm+rm) && um == 0);
|
||||
(effect < 4 ? cm > 100 && cm > 4*(lm+rm) && um == 0 :
|
||||
lowerMass > 100 && lowerMass > 4*(lm+rm) && um == 0));
|
||||
Check(located, [label stringByAppendingString:effect == 1 ? @" changes the two real eye regions" : (effect ? @" changes the real chin rather than eyes or mirrored anatomy" : @" preserves the independently decoded photo")]);
|
||||
printf("Anatomy %-13s %lu/%lu max=%lu mean=%.7f eye=%.0f/%.0f chin=%.0f inverted=%.0f\n", names[effect].UTF8String,
|
||||
(unsigned long)sourceTop, (unsigned long)destinationTop, (unsigned long)difference, MeanDifference(actual,expected), lm, rm, cm, um);
|
||||
|
||||
+170
-2
@@ -1,6 +1,7 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreImage/CoreImage.h>
|
||||
#import <Metal/Metal.h>
|
||||
#import <ImageIO/ImageIO.h>
|
||||
#import "PGFaceProcessor.h"
|
||||
#include <math.h>
|
||||
|
||||
@@ -24,7 +25,8 @@ 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[46]=@[@.5,@.68];p[47]=@[@.5,@.60];p[48]=@[@.5,@.52];p[49]=@[@.5,@.45];p[52]=@[@.5,@.42];
|
||||
p[29]=@[@.5,@.39];
|
||||
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;
|
||||
@@ -32,7 +34,7 @@ static NSDictionary *Geometry(double yaw,double roll) {
|
||||
}
|
||||
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 depth=(i==46 || 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)));
|
||||
}
|
||||
@@ -76,6 +78,58 @@ static CGPoint ChinOracle(CGPoint pixel,NSDictionary *g,CGRect e,double thin,dou
|
||||
}
|
||||
return CGPointMake(q.x,1-q.y);
|
||||
}
|
||||
|
||||
// Independent double-precision reconstruction of AIR's jaw and short-face
|
||||
// branches. This does not call or inspect the production kernel/parameter table.
|
||||
// Jaw's support uses the adjacent contour edge, not the much longer lip vector.
|
||||
// Short-face uses a filled lower-contour polygon and its distance envelope.
|
||||
static double ReferenceSizeGain(CGPoint p[76],CGRect e) {
|
||||
double span=hypot((p[75].x-p[59].x)*e.size.width,(p[75].y-p[59].y)*e.size.height);
|
||||
double vertical=2*hypot((p[67].x-(p[74].x+p[60].x)/2)*e.size.width,(p[67].y-(p[74].y+p[60].y)/2)*e.size.height);
|
||||
return Clamp(280/fmax(1,fmax(span,vertical)),1,2.25);
|
||||
}
|
||||
static CGPoint ReferencePoint(CGPoint p[76],NSUInteger index) {return CGPointMake(p[index].x,1-p[index].y);}
|
||||
static double ReferencePolygonWeight(CGPoint q,CGPoint p[76]) {
|
||||
double radius=hypot(p[46].x-p[48].x,p[46].y-p[48].y);
|
||||
if(radius<=.0001)return 0;
|
||||
BOOL inside=NO;double minimum=10;
|
||||
for(NSUInteger i=0;i<11;i++){
|
||||
CGPoint a=ReferencePoint(p,62+i),b=ReferencePoint(p,62+(i+1)%11);
|
||||
if((a.y>q.y)!=(b.y>q.y)){
|
||||
double divisor=b.y-a.y;if(fabs(divisor)<=1e-6)divisor=divisor<0?-1e-6:1e-6;
|
||||
if(q.x<a.x+(q.y-a.y)*(b.x-a.x)/divisor)inside=!inside;
|
||||
}
|
||||
double dx=b.x-a.x,dy=b.y-a.y,squared=dx*dx+dy*dy;
|
||||
double t=squared>1e-6?Clamp(((q.x-a.x)*dx+(q.y-a.y)*dy)/squared,0,1):0;
|
||||
minimum=fmin(minimum,hypot(q.x-a.x-t*dx,q.y-a.y-t*dy));
|
||||
}
|
||||
return inside?1:Clamp(1-minimum/radius,0,1);
|
||||
}
|
||||
static CGPoint JawShortOracle(CGPoint pixel,NSDictionary *g,CGRect e,PGBeautySettings s) {
|
||||
CGPoint p[76];for(NSUInteger i=0;i<76;i++)p[i]=ReadPoint(g[@"points76"][i]);
|
||||
double gain=ReferenceSizeGain(p,e);CGPoint q=Oracle(pixel,g,e,s.faceThin);q.y=1-q.y;
|
||||
const unsigned int origin[]={72,62,71,63,70,64},neighbour[]={71,63,70,64,71,63};
|
||||
double jaw=Clamp(s.faceJaw,0,1)*.05*gain;
|
||||
if(fabs(jaw)>1e-6)for(NSUInteger i=0;i<6;i++){
|
||||
CGPoint a=ReferencePoint(p,origin[i]),b=ReferencePoint(p,neighbour[i]),target=ReferencePoint(p,29);
|
||||
double radius=hypot(b.x-a.x,b.y-a.y);if(radius<=.0001)continue;
|
||||
double influence=Clamp(1-hypot(q.x-a.x,q.y-a.y)/radius,0,1);
|
||||
q.x=Clamp(q.x-(target.x-a.x)*jaw*influence,0,1);q.y=Clamp(q.y-(target.y-a.y)*jaw*influence,0,1);
|
||||
}
|
||||
double chin=Clamp(s.faceChin,-1,1)*(s.faceChin>0?.04:.02)*gain;
|
||||
if(fabs(chin)>1e-6)for(NSUInteger i=0;i<3;i++){
|
||||
CGPoint a=ReferencePoint(p,68-i),target=ReferencePoint(p,38-i);double radius=hypot(target.x-a.x,target.y-a.y);
|
||||
if(radius<=.0001)continue;double influence=Clamp(1-hypot(q.x-a.x,q.y-a.y)/radius,0,1);
|
||||
q.x=Clamp(q.x-(target.x-a.x)*chin*influence,0,1);q.y=Clamp(q.y-(target.y-a.y)*chin*influence,0,1);
|
||||
}
|
||||
double shortRatio=Clamp(s.faceShort,-1,1)*.25*gain;
|
||||
if(fabs(shortRatio)>1e-6){
|
||||
double influence=ReferencePolygonWeight(q,p);
|
||||
q.x=Clamp(q.x-(p[46].x-p[48].x)*shortRatio*influence,0,1);
|
||||
q.y=Clamp(q.y-(p[48].y-p[46].y)*shortRatio*influence,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;
|
||||
@@ -119,6 +173,22 @@ static void NumericSettingsCase(CIContext *c,PGFaceProcessor *p,CGRect e,NSDicti
|
||||
static void NumericCase(CIContext *c,PGFaceProcessor *p,CGRect e,NSDictionary *g,double amount,NSString *name) {
|
||||
NumericSettingsCase(c,p,e,g,amount,0,name);
|
||||
}
|
||||
static void NumericLocalCase(CIContext *c,PGFaceProcessor *processor,CGRect extent,NSDictionary *geometry,PGBeautySettings settings,NSString *name) {
|
||||
CIImage *input=CoordinateImage(extent),*output=[processor warpImage:input geometry:@[geometry] settings:settings extent:extent];
|
||||
Check(output!=nil && CGRectEqualToRect(output.extent,extent),[name stringByAppendingString:@" extent"]);
|
||||
NSData *data=Pixels(c,output);const float *field=data.bytes;NSUInteger width=extent.size.width,height=extent.size.height;
|
||||
double maximum=0,movement=0;
|
||||
for(NSUInteger row=3;row<height-3;row+=5)for(NSUInteger column=3;column<width-3;column+=5){
|
||||
CGPoint source=JawShortOracle(CGPointMake(extent.origin.x+column+.5,extent.origin.y+height-row-.5),geometry,extent,settings);
|
||||
double x=Clamp(source.x,.5/width,1-.5/width),y=Clamp(1-source.y,.5/height,1-.5/height);NSUInteger k=4*(row*width+column);
|
||||
maximum=fmax(maximum,fmax(fabs(field[k]-x),fabs(field[k+1]-y)));
|
||||
movement=fmax(movement,hypot(x-(column+.5)/width,y-(row+.5)/height));
|
||||
}
|
||||
maxOracleError=fmax(maxOracleError,maximum);
|
||||
if(maximum>=.00006)fprintf(stderr,"%s jaw/short oracle error %.9g\n",name.UTF8String,maximum);
|
||||
Check(maximum<.00006,[name stringByAppendingString:@" actual GPU field vs independently decoded AIR double oracle"]);
|
||||
Check(movement>.0001,[name stringByAppendingString:@" has nonzero covered samples"]);
|
||||
}
|
||||
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];
|
||||
@@ -148,6 +218,85 @@ static void AnatomicalChecks(CIContext *c,PGFaceProcessor *p,NSDictionary *g,CGR
|
||||
Check(outside && eyesUnchanged,@"both physical pupils outside chin supports remain unchanged");
|
||||
}
|
||||
}
|
||||
static void JawShortAnatomicalChecks(CIContext *context,PGFaceProcessor *processor,NSDictionary *geometry,CGRect extent) {
|
||||
CGPoint points[76];for(NSUInteger i=0;i<76;i++)points[i]=ReadPoint(geometry[@"points76"][i]);
|
||||
CIImage *ramp=CoordinateImage(extent);PGBeautySettings settings=PGBeautyDefaults();settings.faceJaw=1;
|
||||
CIImage *jaw=[processor warpImage:ramp geometry:@[geometry] settings:settings extent:extent];
|
||||
double jawMove=0;NSUInteger origin[]={72,62,71,63,70,64},neighbour[]={71,63,70,64,71,63};
|
||||
for(NSUInteger i=0;i<6;i++){
|
||||
CGPoint movement=Displacement(context,ramp,jaw,points[origin[i]]);jawMove=fmax(jawMove,hypot(movement.x,movement.y));
|
||||
double x=(points[29].x-points[origin[i]].x)*extent.size.width,y=(points[29].y-points[origin[i]].y)*extent.size.height;
|
||||
Check(movement.x*x+movement.y*y<.001,@"jaw inverse sampling moves opposite the physical contour-to-lip direction");
|
||||
}
|
||||
Check(jawMove>.1,@"jaw control acts on a detected lower-jaw contour, not a guessed temple band");
|
||||
// These are anatomical regions reported by public Vision, not positions
|
||||
// picked by the production support formula: pupils and upper contour ends.
|
||||
for(NSNumber *index in @[@6,@13,@59,@75]){
|
||||
CGPoint point=points[index.unsignedIntegerValue];BOOL clear=YES;
|
||||
for(NSUInteger i=0;i<6;i++)if(hypot(point.x-points[origin[i]].x,point.y-points[origin[i]].y)<hypot(points[origin[i]].x-points[neighbour[i]].x,points[origin[i]].y-points[neighbour[i]].y)+.004)clear=NO;
|
||||
CGPoint movement=Displacement(context,ramp,jaw,point);
|
||||
Check(clear&&hypot(movement.x,movement.y)<.002,@"physical pupils/upper-contour temple positions outside jaw supports stay unchanged");
|
||||
}
|
||||
CGPoint lower=CGPointZero;for(NSUInteger i=62;i<=72;i++){lower.x+=points[i].x/11;lower.y+=points[i].y/11;}
|
||||
Check(ReferencePolygonWeight(CGPointMake(lower.x,1-lower.y),points)>.99,@"public lower-contour centroid lies inside the short-face region");
|
||||
double axisX=(points[46].x-points[48].x)*extent.size.width,axisY=(points[46].y-points[48].y)*extent.size.height;
|
||||
for(int sign=-1;sign<=1;sign+=2){
|
||||
settings=PGBeautyDefaults();settings.faceShort=sign;CIImage *shortFace=[processor warpImage:ramp geometry:@[geometry] settings:settings extent:extent];
|
||||
CGPoint displacement=Displacement(context,ramp,shortFace,lower);
|
||||
Check(hypot(displacement.x,displacement.y)>.1,@"short-face changes the physical lower face");
|
||||
Check(sign*(displacement.x*axisX+displacement.y*axisY)<0,@"short-face signed source sampling follows the nose-crest axis; positive visible motion shortens upward");
|
||||
for(NSNumber *index in @[@6,@13,@59,@75]){
|
||||
CGPoint point=points[index.unsignedIntegerValue],motion=Displacement(context,ramp,shortFace,point);
|
||||
Check(ReferencePolygonWeight(CGPointMake(point.x,1-point.y),points)==0 && hypot(motion.x,motion.y)<.002,@"physical pupil/temple beyond short-face distance envelope is unaffected");
|
||||
}
|
||||
}
|
||||
}
|
||||
static void JawShortSupportChecks(CIContext *context,PGFaceProcessor *processor) {
|
||||
NSDictionary *g=Geometry(0,0);CGPoint points[76];for(NSUInteger i=0;i<76;i++)points[i]=ReadPoint(g[@"points76"][i]);
|
||||
CGPoint probe=CGPointMake((points[72].x+points[29].x)/2,(points[72].y+points[29].y)/2);
|
||||
const unsigned int origins[]={72,62,71,63,70,64},neighbours[]={71,63,70,64,71,63};BOOL outside=YES;
|
||||
for(NSUInteger i=0;i<6;i++)if(hypot(probe.x-points[origins[i]].x,probe.y-points[origins[i]].y)<=hypot(points[origins[i]].x-points[neighbours[i]].x,points[origins[i]].y-points[neighbours[i]].y)+.005)outside=NO;
|
||||
Check(outside,@"radius discriminator lies beyond all adjacent-contour jaw radii");
|
||||
Check(hypot(probe.x-points[72].x,probe.y-points[72].y)<.8*hypot(points[29].x-points[72].x,points[29].y-points[72].y),@"same point would be inside the incorrect lip-distance jaw radius");
|
||||
CGRect e=CGRectMake(-19,23,640,360);CIImage *ramp=CoordinateImage(e);PGBeautySettings settings=PGBeautyDefaults();settings.faceJaw=1;
|
||||
CIImage *out=[processor warpImage:ramp geometry:@[g] settings:settings extent:e];CGPoint d=Displacement(context,ramp,out,probe);
|
||||
Check(hypot(d.x,d.y)<.002,@"jaw uses neighbouring-contour support rather than the lip-anchor distance");
|
||||
NSMutableDictionary *small=[g mutableCopy];NSMutableArray *scaled=[NSMutableArray new];
|
||||
for(NSArray *point in g[@"points76"])[scaled addObject:@[@(.5+([point[0]doubleValue]-.5)*.18),@(.5+([point[1]doubleValue]-.5)*.18)]];small[@"points76"]=scaled;
|
||||
for(NSUInteger i=0;i<76;i++)points[i]=ReadPoint(scaled[i]);
|
||||
Check(ReferenceSizeGain(points,e)==2.25,@"small-face numerical case reaches the recovered gain cap");
|
||||
NumericLocalCase(context,processor,e,small,settings,@"small face jaw max gain");settings.faceJaw=0;settings.faceShort=1;
|
||||
NumericLocalCase(context,processor,e,small,settings,@"small face short max gain");
|
||||
for(NSUInteger i=0;i<76;i++)points[i]=ReadPoint(g[@"points76"][i]);Check(ReferenceSizeGain(points,CGRectMake(0,0,1600,1600))==1,@"large-face numerical case reaches gain floor");
|
||||
// Four full process masks must use exactly the same new warp as the image.
|
||||
[processor setTrackingData:Archive(g) forTarget:0];settings=PGBeautyDefaults();settings.trackingEnabled=1;
|
||||
PGFaceFrame *neutral=[processor processImage:ramp settings:settings time:kCMTimeZero error:nil];
|
||||
for(NSUInteger mode=0;mode<3;mode++){
|
||||
PGBeautySettings shape=settings;shape.faceJaw=mode!=1?1:0;shape.faceShort=mode!=0?-.65:0;
|
||||
PGFaceFrame *frame=[processor processImage:ramp settings:shape time:kCMTimeZero error:nil];
|
||||
Check([Pixels(context,frame.image)isEqualToData:Pixels(context,[processor warpImage:ramp geometry:@[g] settings:shape extent:e])],@"process image uses the new common jaw/short field");
|
||||
NSArray *before=@[neutral.skinMask,neutral.colorMask,neutral.teethMask,neutral.eyeMask],*after=@[frame.skinMask,frame.colorMask,frame.teethMask,frame.eyeMask];
|
||||
for(NSUInteger i=0;i<4;i++)Check([Pixels(context,after[i])isEqualToData:Pixels(context,[processor warpImage:before[i] geometry:@[g] settings:shape extent:e])],@"every image support mask shares the jaw/short coordinate field");
|
||||
}
|
||||
}
|
||||
static void IndependentImageLandmarkCheck(NSString *path,NSDictionary *geometry) {
|
||||
CGImageSourceRef source=CGImageSourceCreateWithURL((__bridge CFURLRef)[NSURL fileURLWithPath:path],NULL);
|
||||
CGImageRef image=source?CGImageSourceCreateImageAtIndex(source,0,NULL):NULL;if(source)CFRelease(source);
|
||||
VNDetectFaceLandmarksRequest *request=[VNDetectFaceLandmarksRequest new];request.revision=VNDetectFaceLandmarksRequestRevision3;request.constellation=VNRequestFaceLandmarksConstellation76Points;
|
||||
NSError *error=nil;VNImageRequestHandler *handler=image?[[VNImageRequestHandler alloc]initWithCGImage:image options:@{}]:nil;
|
||||
BOOL ok=[handler performRequests:@[request] error:&error];if(image)CGImageRelease(image);
|
||||
VNFaceObservation *face=request.results.firstObject;VNFaceLandmarkRegion2D *all=face.landmarks.allPoints;
|
||||
Check(ok && !error && all.pointCount==76,@"independent public CGImage detection provides76 for physical PNG orientation check");
|
||||
for(NSNumber *index in @[@6,@13,@29,@46,@48,@59,@62,@67,@72,@75]){
|
||||
CGPoint local=all.normalizedPoints[index.unsignedIntegerValue];
|
||||
CGPoint full=CGPointMake(face.boundingBox.origin.x+local.x*face.boundingBox.size.width,face.boundingBox.origin.y+local.y*face.boundingBox.size.height);
|
||||
CGPoint product=ReadPoint(geometry[@"points76"][index.unsignedIntegerValue]);
|
||||
// Separate CGImage and CIImage Vision requests have small conversion
|
||||
// differences. A Y flip, reordered topology or bbox-local point cannot
|
||||
// satisfy this tolerance. Physical PNG top-row is (1-y)*height.
|
||||
Check(hypot(full.x-product.x,full.y-product.y)<.025,@"independent CGImage anatomy agrees on pupils, lips, crest and lower contour in the PNG coordinate system");
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc,const char *argv[]) {@autoreleasepool {
|
||||
CIContext *c=[CIContext contextWithMTLDevice:MTLCreateSystemDefaultDevice() options:@{kCIContextWorkingColorSpace:NSNull.null,kCIContextOutputColorSpace:NSNull.null}];
|
||||
@@ -169,6 +318,19 @@ int main(int argc,const char *argv[]) {@autoreleasepool {
|
||||
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");
|
||||
for(NSUInteger e=0;e<4;e++)for(NSUInteger y=0;y<3;y++)for(NSUInteger r=0;r<3;r++)for(NSUInteger mode=0;mode<3;mode++){
|
||||
PGBeautySettings local=PGBeautyDefaults();if(mode==0)local.faceJaw=1;else local.faceShort=mode==1?1:-1;
|
||||
double angle=y==0?0:(y==1?1.4:-1.4);
|
||||
NumericLocalCase(c,p,extents[e],Geometry(angle,roll[r]),local,
|
||||
[NSString stringWithFormat:@"local size%lu yaw%lu roll%lu mode%lu",(unsigned long)e,(unsigned long)y,(unsigned long)r,(unsigned long)mode]);
|
||||
}
|
||||
PGBeautySettings combined=PGBeautyDefaults();combined.faceThin=.7;combined.faceJaw=.8;combined.faceChin=-.6;combined.faceShort=.75;
|
||||
NumericLocalCase(c,p,extents[2],Geometry(.8,.45),combined,@"ordered thin jaw chin short positive");
|
||||
combined.faceShort=-.75;combined.faceChin=.7;
|
||||
NumericLocalCase(c,p,extents[1],Geometry(-.8,-.45),combined,@"ordered thin jaw chin short negative");
|
||||
combined=PGBeautyDefaults();combined.faceJaw=.5;combined.faceShort=-.5;
|
||||
NumericLocalCase(c,p,extents[1],Geometry(0,0),combined,@"half signed short plus jaw");
|
||||
JawShortSupportChecks(c,p);
|
||||
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];
|
||||
@@ -195,6 +357,10 @@ int main(int argc,const char *argv[]) {@autoreleasepool {
|
||||
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 shortOnly=s;shortOnly.faceThin=0;shortOnly.faceShort=-1;
|
||||
Check([Pixels(c,[p processImage:image settings:shortOnly time:kCMTimeZero error:&error].image) isEqualToData:source],@"legacy/malformed indexed points skip recovered short-face field exactly");
|
||||
PGBeautySettings jawOnly=s;jawOnly.faceThin=0;jawOnly.faceJaw=1;
|
||||
Check([Pixels(c,[p processImage:image settings:jawOnly time:kCMTimeZero error:&error].image) isEqualToData:source],@"legacy/malformed indexed points skip recovered jaw field exactly");
|
||||
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");
|
||||
}
|
||||
@@ -218,6 +384,8 @@ int main(int argc,const char *argv[]) {@autoreleasepool {
|
||||
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);
|
||||
JawShortAnatomicalChecks(c,real,f.deformationGeometry.firstObject,portrait.extent);
|
||||
IndependentImageLandmarkCheck(@(argv[index]),f.deformationGeometry.firstObject);
|
||||
}}
|
||||
printf("PASS: %lu slim alignment checks; maximum UV oracle error %.9g\n",(unsigned long)checks,maxOracleError);return 0;
|
||||
}}
|
||||
在新工单中引用
屏蔽一个用户