Align 2.4.0 pin tracking and pupil-based eyes; add sequence and target-field regressions
这个提交包含在:
1 父节点
524f536a53
当前提交
75e7f4a00d
26 个文件已更改
+16839
-87
没有与搜索条件相匹配的文件
@@ -0,0 +1,251 @@
|
||||
// Contract tests for the 2.4.0 pin-style reconstruction. The source is imported
|
||||
// once so the quality gate and actual tracker can be exercised without a host,
|
||||
// real portrait, private Vision construction, or reference binary execution.
|
||||
#import "../FCP-BeautyGirl/FCP-BeautyGirl/Plugin/PGFaceProcessor.m"
|
||||
#import "tracking_fixture.h"
|
||||
|
||||
static NSUInteger checks=0,failures=0;
|
||||
static void Check(BOOL ok,NSString *message){checks++;if(!ok){failures++;fprintf(stderr,"FAIL: %s\n",message.UTF8String);}}
|
||||
static BOOL EqualPoints(NSArray *a,NSArray *b,double tolerance){
|
||||
if(a.count!=76||b.count!=76)return NO;
|
||||
for(NSUInteger i=0;i<76;i++)for(NSUInteger j=0;j<2;j++)if(fabs([a[i][j] doubleValue]-[b[i][j] doubleValue])>tolerance)return NO;
|
||||
return YES;
|
||||
}
|
||||
static NSDictionary *First(NSDictionary *result){return [result[@"output"] firstObject];}
|
||||
static NSDictionary *Live(PGRealtimeFaceTracker *t,NSArray *g,PGBeautySettings s,int64_t tick){return[t resultForGeometry:g settings:s time:CMTimeMake(tick,1000) extent:CGRectMake(0,0,256,256)];}
|
||||
static NSArray *MappedPoints(NSDictionary *g,CGRect box){
|
||||
CGRect old=PGArrayRect(g[@"box"]);NSMutableArray *a=[NSMutableArray new];
|
||||
for(NSArray *point in g[@"points76"]){CGPoint p=PGRemapPoint(point,old,box);[a addObject:@[@(p.x),@(p.y)]];}return a;
|
||||
}
|
||||
static NSDictionary *Without76(NSDictionary *g){NSMutableDictionary *bad=[g mutableCopy];[bad removeObjectForKey:@"points76"];return bad;}
|
||||
static NSDictionary *WithPoints(NSDictionary *g,void (^change)(NSMutableArray *)){
|
||||
NSMutableDictionary *bad=[g mutableCopy];NSMutableArray *p=[g[@"points76"] mutableCopy];change(p);bad[@"points76"]=p;return bad;
|
||||
}
|
||||
|
||||
@interface PGScriptedTracker : PGRealtimeFaceTracker
|
||||
@property CGRect proposedBox;
|
||||
@property NSUInteger followCalls;
|
||||
@property NSUInteger seedCalls;
|
||||
@end
|
||||
@implementation PGScriptedTracker
|
||||
- (instancetype)init{if((self=[super init]))_proposedBox=CGRectNull;return self;}
|
||||
- (CGRect)trackedBoxForImage:(CIImage *)image time:(CMTime)time seedBox:(CGRect)seed {
|
||||
if(PGValidRect(seed)){self.seedCalls++;return seed;}
|
||||
self.followCalls++;return self.proposedBox;
|
||||
}
|
||||
@end
|
||||
|
||||
@interface PGCountingDetector : PGFaceProcessor
|
||||
@property (nonatomic,copy) NSArray<VNFaceObservation *> *suppliedFaces;
|
||||
@property NSUInteger calls;
|
||||
@property BOOL fail;
|
||||
@end
|
||||
@implementation PGCountingDetector
|
||||
- (NSArray<VNFaceObservation *> *)observationsInImage:(CIImage *)image settings:(PGBeautySettings)s error:(NSError **)error {
|
||||
self.calls++;if(self.fail){if(error)*error=[NSError errorWithDomain:@"synthetic-detection" code:1 userInfo:nil];return nil;}return self.suppliedFaces?:@[];
|
||||
}
|
||||
@end
|
||||
@interface PGScriptedOutputTracker : PGRealtimeFaceTracker
|
||||
@property (nonatomic,copy) NSDictionary *scriptedOutput;
|
||||
@end
|
||||
@implementation PGScriptedOutputTracker
|
||||
- (NSDictionary *)resultForGeometry:(NSArray *)raw settings:(PGBeautySettings)s time:(CMTime)time extent:(CGRect)extent image:(CIImage *)image {return self.scriptedOutput;}
|
||||
@end
|
||||
|
||||
// Objective-C message-compatible test doubles, never supplied to Vision. They
|
||||
// expose only the public read-only observation/region selectors used by the
|
||||
// production geometry converter, backed entirely by our authored fixture.
|
||||
@interface PGAuthoredRegion : NSObject
|
||||
@property (nonatomic,strong) NSData *storage;
|
||||
@property NSUInteger pointCount;
|
||||
- (const CGPoint *)normalizedPoints;
|
||||
@end
|
||||
@implementation PGAuthoredRegion
|
||||
- (const CGPoint *)normalizedPoints{return self.storage.bytes;}
|
||||
@end
|
||||
@interface PGAuthoredLandmarks : NSObject
|
||||
@property (nonatomic,strong) PGAuthoredRegion *allPoints,*leftEye,*rightEye,*leftEyebrow,*rightEyebrow,*nose,*outerLips,*innerLips,*faceContour;
|
||||
@property VNConfidence confidence;
|
||||
@end
|
||||
@implementation PGAuthoredLandmarks @end
|
||||
@interface PGAuthoredObservation : NSObject
|
||||
@property CGRect boundingBox;
|
||||
@property (nonatomic,strong) PGAuthoredLandmarks *landmarks;
|
||||
@property VNConfidence confidence;
|
||||
@end
|
||||
@implementation PGAuthoredObservation @end
|
||||
static PGAuthoredRegion *AuthoredRegion(NSArray *points,CGRect box){
|
||||
NSMutableData *data=[NSMutableData dataWithLength:points.count*sizeof(CGPoint)];CGPoint *p=data.mutableBytes;
|
||||
for(NSUInteger i=0;i<points.count;i++)p[i]=PGLocalPoint(CGPointMake([points[i][0] doubleValue],[points[i][1] doubleValue]),box);
|
||||
PGAuthoredRegion *region=[PGAuthoredRegion new];region.storage=data;region.pointCount=points.count;return region;
|
||||
}
|
||||
static PGAuthoredObservation *AuthoredObservation(NSDictionary *g){
|
||||
CGRect box=PGArrayRect(g[@"box"]);PGAuthoredLandmarks *l=[PGAuthoredLandmarks new];l.confidence=.99;
|
||||
l.allPoints=AuthoredRegion(g[@"points76"],box);l.leftEye=AuthoredRegion(g[@"leftEye"],box);l.rightEye=AuthoredRegion(g[@"rightEye"],box);
|
||||
l.leftEyebrow=AuthoredRegion(g[@"leftBrow"],box);l.rightEyebrow=AuthoredRegion(g[@"rightBrow"],box);l.nose=AuthoredRegion(g[@"nose"],box);
|
||||
l.outerLips=AuthoredRegion(g[@"lips"],box);l.innerLips=AuthoredRegion(g[@"mouth"],box);l.faceContour=AuthoredRegion(g[@"contour"],box);
|
||||
PGAuthoredObservation *o=[PGAuthoredObservation new];o.boundingBox=box;o.landmarks=l;o.confidence=.99;return o;
|
||||
}
|
||||
|
||||
static void QualityAndAcceptance(void){
|
||||
CGRect box=CGRectMake(.15,.12,.70,.78);NSDictionary *good=PGTrackingTestGeometry(box,0);
|
||||
Check(PGGeometryQualityReason(good).length==0,@"authored public-layout geometry passes the recovered quality gate");
|
||||
NSMutableDictionary *legacy=[good mutableCopy];[legacy removeObjectForKey:@"landmarkConfidence"];
|
||||
Check(PGGeometryQualityReason(legacy).length==0,@"missing confidence metadata keeps authored/legacy geometry usable");
|
||||
NSMutableDictionary *low=[good mutableCopy];low[@"landmarkConfidence"]=@.2;
|
||||
NSMutableDictionary *shortContour=[good mutableCopy];shortContour[@"contour"]=[good[@"contour"] subarrayWithRange:NSMakeRange(0,8)];
|
||||
NSMutableDictionary *eyes=[good mutableCopy];eyes[@"rightEye"]=eyes[@"leftEye"];
|
||||
NSMutableDictionary *mouth=[good mutableCopy];mouth[@"lips"]=mouth[@"leftEye"];
|
||||
NSMutableDictionary *nose=[good mutableCopy];nose[@"nose"]=@[@[@.5,@.99]];
|
||||
NSArray *badCases=@[Without76(good),WithPoints(good,^(NSMutableArray *p){p[65]=@[@(NAN),@.5];}),WithPoints(good,^(NSMutableArray *p){p[65]=@[@3,@.5];}),low,shortContour,eyes,mouth,nose];
|
||||
PGBeautySettings s=PGBeautyDefaults();s.faceThin=1;
|
||||
for(NSUInteger i=0;i<badCases.count;i++){
|
||||
NSDictionary *bad=badCases[i];
|
||||
Check(PGGeometryQualityReason(bad).length>0,[NSString stringWithFormat:@"quality rejects malformed case %lu",(unsigned long)i]);
|
||||
PGRealtimeFaceTracker *t=[PGRealtimeFaceTracker new];Live(t,@[good],s,0);NSDictionary *r=Live(t,@[bad],s,40);
|
||||
Check([r[@"held"] boolValue] && EqualPoints(First(r)[@"points76"],good[@"points76"],1e-12),[NSString stringWithFormat:@"malformed case %lu holds the last accepted complete shape",(unsigned long)i]);
|
||||
PGRealtimeFaceTracker *cold=[PGRealtimeFaceTracker new];NSDictionary *initial=Live(cold,@[bad],s,0);
|
||||
Check([First(initial)[@"points76"] count]==0,[NSString stringWithFormat:@"cold malformed case %lu never invents topology",(unsigned long)i]);
|
||||
}
|
||||
for(NSNumber *fps in @[@25,@30,@60,@120]){
|
||||
PGRealtimeFaceTracker *t=[PGRealtimeFaceTracker new];Live(t,@[good],s,0);NSDictionary *next=PGTrackingTestGeometry(box,.002);
|
||||
NSDictionary *r=[t resultForGeometry:@[next] settings:s time:CMTimeMake(1,fps.intValue) extent:CGRectMake(0,0,256,256)];
|
||||
Check(![r[@"held"] boolValue] && EqualPoints(First(r)[@"points76"],next[@"points76"],0),[NSString stringWithFormat:@"accepted %ldfps points are copied exactly without legacy temporal blending",(long)fps.integerValue]);
|
||||
}
|
||||
PGRealtimeFaceTracker *t=[PGRealtimeFaceTracker new];Live(t,@[good],s,0);NSDictionary *middle=Live(t,@[Without76(good)],s,40),*back=Live(t,@[good],s,80);
|
||||
Check([middle[@"held"] boolValue] && [First(middle)[@"points76"] count]==76 && EqualPoints(First(back)[@"points76"],good[@"points76"],0),@"76 to missing76 to76 never disables the geometric field");
|
||||
NSDictionary *jumpy=PGTrackingTestGeometry(box,.34);
|
||||
Check(PGGeometryQualityReason(jumpy).length==0,@"large coherent local jump remains structurally valid before temporal compatibility checks");
|
||||
PGRealtimeFaceTracker *jump=[PGRealtimeFaceTracker new];Live(jump,@[good],s,0);NSDictionary *rejected=Live(jump,@[jumpy],s,40);
|
||||
Check([rejected[@"held"] boolValue] && EqualPoints(First(rejected)[@"points76"],good[@"points76"],1e-12),@"a large finite median jump cannot poison accepted shape memory");
|
||||
NSDictionary *axisReverse=WithPoints(good,^(NSMutableArray *p){for(NSUInteger i=66;i<=68;i++)p[i]=@[p[i][0],@(2*CGRectGetMidY(box)-[p[i][1] doubleValue])];});
|
||||
Check(PGGeometryAxisFlip(axisReverse,good),@"weighted lower-contour to bbox-center axis detects a vertical sign reversal");
|
||||
NSMutableDictionary *eyeReverse=[good mutableCopy];eyeReverse[@"leftEye"]=good[@"rightEye"];eyeReverse[@"rightEye"]=good[@"leftEye"];
|
||||
Check(!PGGeometryAxisFlip(eyeReverse,good),@"an isolated eye-order change is not the recovered lower-contour axis test");
|
||||
NSDictionary *fewOutliers=WithPoints(good,^(NSMutableArray *p){for(NSUInteger i=0;i<6;i++)p[i]=@[@([p[i][0] doubleValue]+.55),p[i][1]];});
|
||||
NSDictionary *manyOutliers=WithPoints(good,^(NSMutableArray *p){for(NSUInteger i=0;i<12;i++)p[i]=@[@([p[i][0] doubleValue]+.55),p[i][1]];});
|
||||
Check(PGGeometryTransitionReason(fewOutliers,good).length==0,@"the temporal quantile gate does not treat a few outliers as a large median or P90 jump");
|
||||
Check(PGGeometryTransitionReason(manyOutliers,good).length>0,@"P90 rejects a substantial outlier group even when the median remains zero");
|
||||
}
|
||||
|
||||
static void SequenceAndCache(void){
|
||||
PGBeautySettings s=PGBeautyDefaults();s.faceThin=1;CGRect box=CGRectMake(.15,.12,.70,.78);
|
||||
NSDictionary *good=PGTrackingTestGeometry(box,0),*changed=PGTrackingTestGeometry(box,.003);CIImage *image=[[CIImage imageWithColor:[CIColor colorWithRed:.5 green:.5 blue:.5]] imageByCroppingToRect:CGRectMake(0,0,256,256)];
|
||||
PGScriptedTracker *t=[PGScriptedTracker new];NSDictionary *first=[t resultForGeometry:@[good] settings:s time:kCMTimeZero extent:image.extent image:image];
|
||||
Check(t.seedCalls>0,@"first accepted face seeds the continuous object-box tracker");
|
||||
CGRect tracked=CGRectMake(.17,.13,.66,.74);t.proposedBox=tracked;
|
||||
NSDictionary *hold=[t resultForGeometry:@[Without76(good)] settings:s time:CMTimeMake(1,25) extent:image.extent image:image];
|
||||
Check(t.followCalls>0 && [hold[@"held"] boolValue],@"a partial detection asks the continuous sequence tracker for a box");
|
||||
Check(CGRectEqualToRect(PGArrayRect(First(hold)[@"box"]),tracked) && EqualPoints(First(hold)[@"points76"],MappedPoints(good,tracked),1e-12),@"held complete points follow the tracked box with exact affine remapping");
|
||||
NSUInteger calls=t.followCalls+t.seedCalls;
|
||||
NSDictionary *repeat=[t resultForGeometry:@[changed] settings:s time:CMTimeMake(2,50) extent:image.extent image:image];
|
||||
Check([repeat isEqual:hold] && t.followCalls+t.seedCalls==calls,@"same rational frame freezes its output and does not advance Vision again when raw changes");
|
||||
t.proposedBox=CGRectNull;NSDictionary *reacquired=[t resultForGeometry:@[changed] settings:s time:CMTimeMake(2,25) extent:image.extent image:image];
|
||||
Check(![reacquired[@"held"] boolValue] && EqualPoints(First(reacquired)[@"points76"],changed[@"points76"],0),@"a good reacquired detection replaces held shape exactly");
|
||||
NSDictionary *miss=Live(t,@[],s,120);Check([miss[@"held"] boolValue] && [First(miss)[@"points76"] count]==76,@"nearby complete detection loss preserves complete accepted geometry");
|
||||
PGRealtimeFaceTracker *cold=[PGRealtimeFaceTracker new];NSDictionary *empty=Live(cold,@[],s,40);Live(cold,@[good],s,0);
|
||||
Check([empty isEqual:Live(cold,@[changed],s,40)],@"same-frame freeze is stable when an earlier request arrives later");
|
||||
PGRealtimeFaceTracker *past=[PGRealtimeFaceTracker new];Live(past,@[good],s,1000);NSDictionary *rewind=Live(past,@[],s,960);
|
||||
Check([rewind[@"output"] count]==0,@"uncached reverse request cannot inherit a future accepted face");
|
||||
PGRealtimeFaceTracker *scope=[PGRealtimeFaceTracker new];Live(scope,@[good],s,0);
|
||||
Check([[scope resultForGeometry:@[] settings:s time:CMTimeMakeWithEpoch(40,1000,9) extent:image.extent][@"output"] count]==0,@"CMTime epoch changes isolate accepted geometry");
|
||||
Check([[scope resultForGeometry:@[] settings:s time:CMTimeMake(40,1000) extent:CGRectMake(10,0,256,256)][@"output"] count]==0,@"image origin changes isolate accepted geometry");
|
||||
PGBeautySettings multi=s;multi.fastMultiFace=1;NSDictionary *many=Live(scope,@[good,Without76(changed)],multi,40);
|
||||
Check([many[@"output"] count]==2,@"fast-multi retains its independent legacy raw-geometry contract");
|
||||
Check([Live(scope,@[],multi,80)[@"output"] count]==0,@"fast-multi cannot inherit the default primary-face hold");
|
||||
NSDictionary *defaultAgain=Live(scope,@[],s,80);Check([First(defaultAgain)[@"points76"] count]==76,@"fast-multi requests do not overwrite default accepted memory");
|
||||
[scope reset];Check([Live(scope,@[],s,120)[@"output"] count]==0,@"reset removes both output history and accepted shape state");
|
||||
Check(EqualPoints(First(first)[@"points76"],good[@"points76"],0),@"the first sequence frame preserves every authored point");
|
||||
}
|
||||
|
||||
// Exercise the real VNSequenceRequestHandler/VNTrackObjectRequest on a textured
|
||||
// synthetic rectangle. This verifies API wiring; it is not portrait accuracy or
|
||||
// target parity. No face detector or media file is required.
|
||||
static CIImage *ObjectFrame(NSUInteger frame){
|
||||
NSMutableData *d=[NSMutableData dataWithLength:256*256*4];uint8_t *p=d.mutableBytes;NSInteger dx=(NSInteger)frame*2;
|
||||
for(NSInteger y=0;y<256;y++)for(NSInteger x=0;x<256;x++){
|
||||
NSUInteger i=4*(y*256+x);NSInteger u=x-76-dx,v=y-51;BOOL inside=u>=0&&u<90&&v>=0&&v<128;
|
||||
uint8_t texture=inside?(uint8_t)(35+(u*37+v*53+((u/7+v/9)%2)*83)%210):8;
|
||||
p[i]=texture;p[i+1]=inside?(uint8_t)(50+(u*17+v*13)%180):12;p[i+2]=inside?(uint8_t)(40+(u*11+v*29)%200):16;p[i+3]=255;
|
||||
}
|
||||
return[CIImage imageWithBitmapData:d bytesPerRow:1024 size:CGSizeMake(256,256) format:kCIFormatRGBA8 colorSpace:NULL];
|
||||
}
|
||||
static double MaskPeak(CIContext *context,CIImage *image){
|
||||
CGRect extent=image.extent;NSUInteger width=extent.size.width,height=extent.size.height;
|
||||
NSMutableData *d=[NSMutableData dataWithLength:width*height*4*sizeof(float)];
|
||||
[context render:image toBitmap:d.mutableBytes rowBytes:width*4*sizeof(float) bounds:extent format:kCIFormatRGBAf colorSpace:NULL];
|
||||
const float *p=d.bytes;double peak=0;for(NSUInteger i=0;i<width*height;i++)peak=MAX(peak,p[4*i]);return peak;
|
||||
}
|
||||
static void DetectionCacheAndHeldMasks(void){
|
||||
CIImage *image=ObjectFrame(0);PGBeautySettings s=PGBeautyDefaults();PGCountingDetector *p=[PGCountingDetector new];
|
||||
VNFaceObservation *face=[VNFaceObservation faceObservationWithRequestRevision:VNDetectFaceLandmarksRequestRevision3 boundingBox:CGRectMake(.15,.12,.7,.78) roll:nil yaw:nil pitch:nil];
|
||||
p.suppliedFaces=@[face];NSArray *first=[p cachedObservationsInImage:image settings:s time:CMTimeMake(1,25) error:nil];p.suppliedFaces=@[];
|
||||
NSArray *again=[p cachedObservationsInImage:image settings:s time:CMTimeMake(2,50) error:nil];
|
||||
Check(first.count==1 && [again isEqual:first] && p.calls==1,@"equivalent same-frame requests reuse detection before running Vision again");
|
||||
s.faceThin=1;Check([[p cachedObservationsInImage:image settings:s time:CMTimeMake(1,25) error:nil] isEqual:first] && p.calls==1,@"shape parameter redraws do not invalidate same-frame face detection");
|
||||
NSArray *empty=[p cachedObservationsInImage:image settings:s time:CMTimeMake(2,25) error:nil];p.suppliedFaces=@[face];
|
||||
Check(empty.count==0 && [[p cachedObservationsInImage:image settings:s time:CMTimeMake(2,25) error:nil] count]==0 && p.calls==2,@"successful empty detection is frozen for redraws rather than toggling appearance");
|
||||
[p cachedObservationsInImage:image settings:s time:CMTimeMakeWithEpoch(2,25,8) error:nil];Check(p.calls==3,@"detection cache distinguishes CMTime epochs");
|
||||
CIImage *offset=[image imageByApplyingTransform:CGAffineTransformMakeTranslation(10,20)];[p cachedObservationsInImage:offset settings:s time:CMTimeMake(2,25) error:nil];
|
||||
Check(p.calls==4,@"detection cache distinguishes source extent origin");
|
||||
s.fastMultiFace=1;[p cachedObservationsInImage:image settings:s time:CMTimeMake(2,25) error:nil];Check(p.calls==5,@"different detection-resolution modes cannot share cached observations");
|
||||
[p resetTracking];[p cachedObservationsInImage:image settings:s time:CMTimeMake(2,25) error:nil];Check(p.calls==6,@"reset also clears observation cache entries");
|
||||
PGCountingDetector *retry=[PGCountingDetector new];retry.fail=YES;Check([retry cachedObservationsInImage:image settings:s time:kCMTimeZero error:nil]==nil,@"transient detector error remains an error rather than a successful empty result");
|
||||
retry.fail=NO;retry.suppliedFaces=@[face];Check([[retry cachedObservationsInImage:image settings:s time:kCMTimeZero error:nil] count]==1 && retry.calls==2,@"failed detections remain retryable at the exact same frame");
|
||||
PGCountingDetector *bounded=[PGCountingDetector new];for(NSUInteger i=0;i<160;i++)[bounded cachedObservationsInImage:image settings:s time:CMTimeMake(i,25) error:nil];
|
||||
NSUInteger before=bounded.calls;[bounded cachedObservationsInImage:image settings:s time:CMTimeMake(159,25) error:nil];
|
||||
Check(bounded.calls==before,@"recent detection remains cached after a long authored sequence");
|
||||
[bounded cachedObservationsInImage:image settings:s time:kCMTimeZero error:nil];Check(bounded.calls==before+1,@"observation history is bounded and old frames can be recomputed");
|
||||
// Inject only the tracker output to exercise processImage's real held-mask
|
||||
// integration. The detector reports no person; all geometry is authored.
|
||||
PGCountingDetector *maskProcessor=[PGCountingDetector new];PGScriptedOutputTracker *held=[PGScriptedOutputTracker new];
|
||||
NSDictionary *g=PGTrackingTestGeometry(CGRectMake(.15,.12,.7,.78),0);held.scriptedOutput=@{@"output":@[g],@"held":@YES,@"index":@(-1)};
|
||||
[maskProcessor setValue:held forKey:@"realtime"];s=PGBeautyDefaults();s.faceThin=1;
|
||||
PGFaceFrame *frame=[maskProcessor processImage:image settings:s time:kCMTimeZero error:nil];
|
||||
CIContext *context=[CIContext contextWithOptions:nil];float colorPixel[4]={0},skinPixel[4]={0};CGRect cheek=CGRectMake(88,130,1,1);
|
||||
[context render:frame.colorMask toBitmap:colorPixel rowBytes:sizeof(colorPixel) bounds:cheek format:kCIFormatRGBAf colorSpace:NULL];
|
||||
[context render:frame.skinMask toBitmap:skinPixel rowBytes:sizeof(skinPixel) bounds:cheek format:kCIFormatRGBAf colorSpace:NULL];
|
||||
Check(frame.deformationGeometry.count==1 && colorPixel[0]>.5 && skinPixel[0]>.25,@"default held geometry preserves skin and color support through complete detection loss");
|
||||
Check(MaskPeak(context,frame.teethMask)>.5 && MaskPeak(context,frame.eyeMask)>.5,@"held default geometry retains nonzero teeth and eye support after the shared warp");
|
||||
PGCountingDetector *errorProcessor=[PGCountingDetector new];PGScriptedTracker *objectTracker=[PGScriptedTracker new];
|
||||
[errorProcessor setValue:objectTracker forKey:@"realtime"];errorProcessor.suppliedFaces=(NSArray<VNFaceObservation *> *)@[AuthoredObservation(g)];
|
||||
PGFaceFrame *valid=[errorProcessor processImage:image settings:s time:kCMTimeZero error:nil];
|
||||
Check(valid.deformationGeometry.count==1 && [valid.deformationGeometry[0][@"points76"] count]==76,@"public-selector authored observations seed actual processImage accepted geometry");
|
||||
errorProcessor.fail=YES;objectTracker.proposedBox=PGArrayRect(g[@"box"]);NSError *stageError=nil;
|
||||
PGFaceFrame *failureHold=[errorProcessor processImage:image settings:s time:CMTimeMake(1,25) error:&stageError];
|
||||
Check(failureHold!=nil && failureHold.deformationGeometry.count==1 && [failureHold.deformationGeometry[0][@"points76"] count]==76,@"a realtime detector API error returns a frame with the last accepted complete shape");
|
||||
Check(failureHold!=nil && MaskPeak(context,failureHold.skinMask)>.5 && MaskPeak(context,failureHold.colorMask)>.5 && MaskPeak(context,failureHold.teethMask)>.5 && MaskPeak(context,failureHold.eyeMask)>.5,@"realtime detector errors keep all held beauty masks instead of flashing them off");
|
||||
NSDictionary *recovered=PGTrackingTestGeometry(PGArrayRect(g[@"box"]),.01);
|
||||
errorProcessor.fail=NO;errorProcessor.suppliedFaces=(NSArray<VNFaceObservation *> *)@[AuthoredObservation(recovered)];
|
||||
PGFaceFrame *sameTime=[errorProcessor processImage:image settings:s time:CMTimeMake(1,25) error:nil];
|
||||
Check(EqualPoints(sameTime.deformationGeometry.firstObject[@"points76"],recovered[@"points76"],1e-12),@"same-time recovery replaces a transient error hold instead of replaying its frozen geometry");
|
||||
PGCountingDetector *coldError=[PGCountingDetector new];coldError.fail=YES;
|
||||
[coldError processImage:image settings:s time:kCMTimeZero error:nil];coldError.fail=NO;
|
||||
coldError.suppliedFaces=(NSArray<VNFaceObservation *> *)@[AuthoredObservation(g)];
|
||||
PGFaceFrame *acquired=[coldError processImage:image settings:s time:kCMTimeZero error:nil];
|
||||
Check(acquired.deformationGeometry.count==1&&coldError.calls==2,@"a cold API failure cannot freeze the exact frame without geometry after a successful retry");
|
||||
errorProcessor.fail=YES;
|
||||
[[errorProcessor valueForKey:@"observationCache"] removeAllObjects];
|
||||
PGFaceFrame *cachedFailure=[errorProcessor processImage:image settings:s time:kCMTimeZero error:nil];
|
||||
Check(cachedFailure.deformationGeometry.count==1&&MaskPeak(context,cachedFailure.skinMask)>.5&&MaskPeak(context,cachedFailure.colorMask)>.5&&MaskPeak(context,cachedFailure.eyeMask)>.5&&MaskPeak(context,cachedFailure.teethMask)>.5,@"an API error after detection-cache eviction preserves masks even when geometry hits an older successful snapshot");
|
||||
PGBeautySettings offline=s;offline.trackingEnabled=1;stageError=nil;
|
||||
Check([errorProcessor processImage:image settings:offline time:CMTimeMake(2,25) error:&stageError]==nil && stageError!=nil,@"explicit offline tracking still reports detector errors rather than silently holding live geometry");
|
||||
}
|
||||
static void VisionSequence(void){
|
||||
PGRealtimeFaceTracker *t=[PGRealtimeFaceTracker new];CGRect seed=CGRectMake(76./256,51./256,90./256,128./256);
|
||||
CGRect start=[t trackedBoxForImage:ObjectFrame(0) time:kCMTimeZero seedBox:seed];Check(PGValidRect(start),@"real sequence tracker accepts an authored textured object seed");
|
||||
for(NSUInteger i=1;i<=5;i++){
|
||||
CGRect tracked=[t trackedBoxForImage:ObjectFrame(i) time:CMTimeMake(i,25) seedBox:CGRectNull];
|
||||
Check(PGValidRect(tracked) && fabs(CGRectGetMidX(tracked)-(CGRectGetMidX(seed)+2.*i/256))<.07 && fabs(CGRectGetMidY(tracked)-CGRectGetMidY(seed))<.07,@"real sequence tracker follows a known synthetic translation without losing the box");
|
||||
CGRect again=[t trackedBoxForImage:ObjectFrame(i) time:CMTimeMake(i,25) seedBox:CGRectNull];Check(CGRectEqualToRect(tracked,again),@"real sequence same-frame reuse does not advance tracking twice");
|
||||
}
|
||||
CGRect rewind=[t trackedBoxForImage:ObjectFrame(3) time:CMTimeMake(3,25) seedBox:CGRectNull];
|
||||
Check(!PGValidRect(rewind),@"a real sequence rewind cannot return a future tracked box without a fresh seed");
|
||||
CGRect reseed=CGRectOffset(seed,6./256,0);CGRect restarted=[t trackedBoxForImage:ObjectFrame(3) time:CMTimeMake(3,25) seedBox:reseed];
|
||||
Check(CGRectEqualToRect(reseed,restarted),@"a rewind with an explicit box restarts the real Vision sequence at that seed");
|
||||
PGRealtimeFaceTracker *gap=[PGRealtimeFaceTracker new];
|
||||
[gap trackedBoxForImage:ObjectFrame(0) time:kCMTimeZero seedBox:seed];
|
||||
Check(!PGValidRect([gap trackedBoxForImage:ObjectFrame(8) time:CMTimeMake(8,25) seedBox:CGRectNull]),@"a gap over .2 seconds invalidates an unseeded continuous object request");
|
||||
Check(CGRectEqualToRect([gap trackedBoxForImage:ObjectFrame(9) time:CMTimeMake(9,25) seedBox:CGRectNull],seed),@"after gap invalidation the next unseeded frame follows the recovered short lost-box hold path");
|
||||
}
|
||||
int main(void){@autoreleasepool{QualityAndAcceptance();SequenceAndCache();DetectionCacheAndHeldMasks();VisionSequence();printf("%s: %lu tracking sequence checks (%lu failures); synthetic data only\n",failures?"FAIL":"PASS",(unsigned long)checks,(unsigned long)failures);return failures?1:0;}}
|
||||
在新工单中引用
屏蔽一个用户