Align 2.4.0 pin tracking and pupil-based eyes; add sequence and target-field regressions

这个提交包含在:
dayu 于 2026-10-23 00:09:32 +08:00 提交
1 父节点 524f536a53
当前提交 75e7f4a00d
26 个文件已更改
+16839 -87

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

+36 -32
查看文件
@@ -5,9 +5,10 @@
#import "PGFaceProcessor.h"
#import <dispatch/dispatch.h>
#include <math.h>
#import "tracking_fixture.h"
// Exercise the independent selector with authored landmark geometry, without
// requiring Vision to manufacture a particular jitter or eye-axis failure.
// requiring Vision to manufacture a particular jitter or invalid topology.
@interface PGRealtimeFaceTracker : NSObject
- (NSDictionary *)resultForGeometry:(NSArray<NSDictionary *> *)raw settings:(PGBeautySettings)settings time:(CMTime)time extent:(CGRect)extent;
- (NSDictionary *)cacheStatistics;
@@ -18,6 +19,7 @@
// tested independently of the separate public-domain NASA portrait fixture.
@interface PGFaceProcessor (TestDetection)
- (NSArray<VNFaceObservation *> *)observationsInImage:(CIImage *)image settings:(PGBeautySettings)settings error:(NSError **)error;
- (NSArray<VNFaceObservation *> *)cachedObservationsInImage:(CIImage *)image settings:(PGBeautySettings)settings time:(CMTime)time error:(NSError **)error;
@end
@interface PGFaceFixture : PGFaceProcessor
@property (nonatomic, copy) NSArray<VNFaceObservation *> *detections;
@@ -28,6 +30,12 @@
self.detectionCount++;
return self.detections ?: @[];
}
// These tests change an injected detector result while reusing one tiny image.
// Keep that fixture boundary explicit; tracking_tests exercises the real frame
// detection cache separately, including same-time changed detector responses.
- (NSArray<VNFaceObservation *> *)cachedObservationsInImage:(CIImage *)image settings:(PGBeautySettings)settings time:(CMTime)time error:(NSError **)error {
return [self observationsInImage:image settings:settings error:error];
}
@end
static NSUInteger PGChecks = 0;
@@ -69,17 +77,17 @@ static CIImage *Gradient(void) {
}
static NSDictionary *Geometry(CGRect box,double jitter) {
NSMutableDictionary *g=[NSMutableDictionary dictionaryWithObject:@[@(box.origin.x),@(box.origin.y),@(box.size.width),@(box.size.height)] forKey:@"box"];
for(NSString *name in @[@"leftEye",@"rightEye",@"leftBrow",@"rightBrow",@"nose",@"lips",@"mouth",@"contour"]) {
double x=[name isEqual:@"rightEye"]?.7:.3;
g[name]=@[@[@(box.origin.x+(x+jitter)*box.size.width),@(box.origin.y+.65*box.size.height)]];
}
return [g copy];
return PGTrackingTestGeometry(box,jitter);
}
static CGRect GeometryBox(NSDictionary *g) {
NSArray *b=g[@"box"];return b.count==4?CGRectMake([b[0] doubleValue],[b[1] doubleValue],[b[2] doubleValue],[b[3] doubleValue]):CGRectZero;
}
static double EyeX(NSDictionary *g) {return [g[@"leftEye"][0][0] doubleValue];}
static BOOL SameCompleteShape(NSDictionary *a,NSDictionary *b) {
NSArray *x=a[@"points76"],*y=b[@"points76"];if(x.count!=76||y.count!=76)return NO;
for(NSUInteger i=0;i<76;i++)for(NSUInteger j=0;j<2;j++)if(fabs([x[i][j] doubleValue]-[y[i][j] doubleValue])>1e-12)return NO;
return YES;
}
static NSDictionary *Live(PGRealtimeFaceTracker *tracker,NSArray *g,PGBeautySettings s,int64_t tick) {
return [tracker resultForGeometry:g settings:s time:CMTimeMake(tick,1000) extent:CGRectMake(0,0,128,128)];
}
@@ -90,7 +98,7 @@ static void RealtimeChecks(CIContext *context,CIImage *image) {
PGFaceFixture *p=[PGFaceFixture new];p.detections=@[left,right];
s.faceSmallHead=.8;
PGFaceFrame *automatic=[p processImage:image settings:s time:kCMTimeZero error:&error];
Check(automatic.deformationGeometry.count==1 && GeometryBox(automatic.deformationGeometry.firstObject).origin.x<.5,@"default realtime geometry chooses one area/center primary without analysis");
Check(automatic.deformationGeometry.count==0,@"bbox-only fixture cannot seed default deformation without complete valid landmarks");
Check(automatic.faces.count==2 && automatic.faces[0]==left && automatic.faces[1]==right,@"default appearance retains both original Vision observations");
Check(Channel(context,automatic.teethMask,97,51,0)>20 && Channel(context,automatic.eyeMask,92,69,0)>20,@"default teeth and eye support covers the non-primary face");
CGRect rightCrop=CGRectMake(84,38,30,52);
@@ -102,18 +110,10 @@ static void RealtimeChecks(CIContext *context,CIImage *image) {
Check([p trackingDataForTarget:0]==nil && [p trackingDataForTarget:1]==nil && [p trackingDataForTarget:2]==nil,@"realtime processing never creates persisted analysis records");
p.detections=@[Face(CGRectMake(.15,.25,.30,.50)),Face(CGRectMake(.43,.005,.56,.99))];
PGFaceFrame *moving=[p processImage:image settings:s time:CMTimeMake(1,25) error:&error];
Check(GeometryBox(moving.deformationGeometry.firstObject).origin.x<.3,@"moving primary stays selected when a much larger stranger appears outside its rectangular guard");
Check(moving.deformationGeometry.count==0 && moving.faces.count==2,@"successive bbox-only observations retain appearance coverage without inventing a shape");
PGFaceFixture *cold=[PGFaceFixture new];cold.detections=p.detections;
PGFaceFrame *coldFrame=[cold processImage:image settings:s time:CMTimeMake(1,25) error:&error];
Check(GeometryBox(coldFrame.deformationGeometry.firstObject).origin.x>.4,@"cold initialization explicitly differs from warm identity association when a new primary dominates");
PGBeautySettings manual=s;manual.singleFace=1;manual.selectionX=.78;manual.selectionY=.5;
p.detections=@[left,right];
PGFaceFrame *chosen=[p processImage:image settings:manual time:CMTimeMake(2,25) error:&error];
Check(chosen.faces.count==1 && chosen.faces.firstObject==right,@"manual selection retains its original observation rather than matching a smoothed box");
Check(GeometryBox(chosen.deformationGeometry.firstObject).origin.x>.5 && Channel(context,chosen.colorMask,26,54,0)==0,@"explicit single-face UX narrows appearance to the selected target");
manual.selectionX=.25;
chosen=[p processImage:image settings:manual time:CMTimeMake(2,25) error:&error];
Check(chosen.faces.firstObject==left,@"manual reselection at the same exact time immediately selects the other face");
Check(coldFrame.deformationGeometry.count==0,@"cold initialization also rejects otherwise valid boxes with missing topology");
PGRealtimeFaceTracker *t=[PGRealtimeFaceTracker new];
NSDictionary *a=Geometry(CGRectMake(.1,.25,.3,.5),0),*b=Geometry(CGRectMake(.66,.3,.2,.4),0);
@@ -126,13 +126,15 @@ static void RealtimeChecks(CIContext *context,CIImage *image) {
Check([r[@"index"] integerValue]==0 && [r[@"referenceKey"] isEqual:@"0/1/0"],@"live association uses the previous frame rather than recomputing the primary score");
NSDictionary *outside=Geometry(CGRectMake(.60,.30,.10,.10),0);
r=Live(t,@[outside],s,80);
Check([r[@"held"] boolValue] && [r[@"output"][0] isEqual:Live(t,@[aMoved,large],s,40)[@"output"][0]],@"no guard candidate retains the previous stable geometry at full strength");
Check([r[@"held"] boolValue] && SameCompleteShape(r[@"output"][0],aMoved),@"no guard candidate retains the previous accepted geometry at full strength");
r=Live(t,@[],s,120);
Check([r[@"held"] boolValue] && [r[@"referenceKey"] isEqual:@"2/25/0"],@"empty detections hold geometry while advancing output time");
for(int64_t tick=160;tick<=2160;tick+=40)r=Live(t,@[],s,tick);
Check([r[@"held"] boolValue] && [r[@"output"] count]==1,@"continuous close held frames can retain geometry beyond .35 seconds");
r=Live(t,@[],s,3000);
Check([r[@"output"] count]==0 && [r[@"referenceKey"] length]==0,@"a distant seek without a nearby past reference cold-starts without held geometry");
Check([r[@"held"] boolValue] && [r[@"acceptedKey"] isEqual:@"1/25/0"],@"forward holds retain the last accepted detection time separately from output time");
[t reset];r=Live(t,@[],s,3000);
Check([r[@"output"] count]==0 && [r[@"referenceKey"] length]==0,@"an explicit reset clears held geometry even at a previously rendered time");
r=Live(t,@[b],s,4000);
Check(![r[@"held"] boolValue] && [r[@"index"] integerValue]==0,@"a far seek with a current face cold-acquires that face");
@@ -154,23 +156,21 @@ static void RealtimeChecks(CIContext *context,CIImage *image) {
NSDictionary *g0=Geometry(box,0),*jitter=Geometry(box,.06);
Live(smooth,@[g0],s,0);
NSDictionary *smoothed=Live(smooth,@[jitter],s,10);
double expected=EyeX(g0)+(EyeX(jitter)-EyeX(g0))*.3;
Check(fabs(EyeX(smoothed[@"output"][0])-expected)<1e-9,@"small-dt landmark smoothing uses a time-aware current weight");
Check(fabs(EyeX(smoothed[@"output"][0])-EyeX(g0))<fabs(EyeX(jitter)-EyeX(g0)),@"landmark jitter amplitude is reduced");
Check([smoothed isEqual:Live(smooth,@[jitter],s,10)] && [[smooth cacheStatistics][@"frames"] unsignedIntegerValue]==2,@"same exact time and raw geometry returns an identical cached result without another smoothing step");
Check(fabs(EyeX(smoothed[@"output"][0])-EyeX(jitter))<1e-9,@"pin-style accepts a qualified current shape without arbitrary temporal blending");
Check([smoothed[@"output"][0][@"points76"] isEqual:jitter[@"points76"]],@"all accepted indexed points remain exactly current rather than partially blended");
Check([smoothed isEqual:Live(smooth,@[jitter],s,10)] && [[smooth cacheStatistics][@"frames"] unsignedIntegerValue]==2,@"same exact frame returns an identical frozen result without another tracking step");
NSDictionary *equivalent=[smooth resultForGeometry:@[jitter] settings:s time:CMTimeMake(1,100) extent:CGRectMake(0,0,128,128)];
Check([equivalent isEqual:smoothed],@"equivalent rational CMTime representations share one realtime result");
NSDictionary *changed=Geometry(box,.08);
NSDictionary *refreshed=Live(smooth,@[changed],s,10);
Check(fabs(EyeX(refreshed[@"output"][0])-(EyeX(g0)+(EyeX(changed)-EyeX(g0))*.3))<1e-9,@"changed raw detections at the same time recompute from the earlier frame rather than compound smoothing");
NSMutableDictionary *flipped=[changed mutableCopy];flipped[@"leftEye"]=changed[@"rightEye"];flipped[@"rightEye"]=changed[@"leftEye"];
NSDictionary *axis=Live(smooth,@[flipped],s,20);
Check(fabs(EyeX(axis[@"output"][0])-EyeX(refreshed[@"output"][0]))<1e-9,@"eye-axis reversal holds remapped old landmarks instead of flipping the warp");
Check([refreshed isEqual:smoothed],@"same-frame changed raw observations reuse the previously committed output");
NSMutableDictionary *partial=[changed mutableCopy];[partial removeObjectForKey:@"points76"];
NSDictionary *heldShape=Live(smooth,@[partial],s,20);
Check([heldShape[@"held"] boolValue] && SameCompleteShape(heldShape[@"output"][0],smoothed[@"output"][0]),@"partial topology preserves the last accepted complete shape");
NSDictionary *translated=Geometry(CGRectOffset(box,.1,0),.08);
NSDictionary *translatedResult=Live(smooth,@[translated],s,60);
Check(fabs(GeometryBox(translatedResult[@"output"][0]).origin.x-.3)<1e-9,@"normal translation uses the current detection box without accumulated box lag");
double mappedEye=EyeX(axis[@"output"][0])+.1;
Check(fabs(EyeX(translatedResult[@"output"][0])-(mappedEye+.9*(EyeX(translated)-mappedEye)))<1e-9,@"old landmarks remap into the current moving box before blending");
Check([translatedResult[@"output"][0][@"points76"] isEqual:translated[@"points76"]],@"qualified translating reacquisition adopts the current complete shape exactly");
Check([smoothed[@"output"] isEqual:Live(smooth,@[jitter],s,10)[@"output"]],@"repeated rewind to a cached time reproduces its original stable geometry");
PGRealtimeFaceTracker *seeds=[PGRealtimeFaceTracker new];
@@ -206,7 +206,7 @@ static void RealtimeChecks(CIContext *context,CIImage *image) {
NSDictionary *past=Live(order,@[a],s,0);
Check([past[@"referenceKey"] length]==0 && [past[@"output"][0] isEqual:a],@"uncached rewind never borrows geometry from a future frame");
r=Live(order,@[Geometry(CGRectMake(.64,.3,.2,.4),0)],s,1040);
Check([r[@"referenceKey"] isEqual:future[@"key"]],@"late older frames do not replace the newer live reference head");
Check([r[@"referenceKey"] isEqual:past[@"key"]],@"an uncached rewind starts a new forward reference instead of borrowing a cached future head");
NSDictionary *nearRewind=Live(order,@[a],s,980);
Check([nearRewind[@"referenceKey"] length]==0,@"uncached small rewind also refuses a closer future reference");
NSDictionary *epoch=[order resultForGeometry:@[] settings:s time:CMTimeMakeWithEpoch(1080,1000,9) extent:CGRectMake(0,0,128,128)];
@@ -323,8 +323,12 @@ 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];
Check(warped != nil && !changedPixels && warped.deformationGeometry.count==0,[NSString stringWithFormat:@"default pin-style rejects bbox-only topology for %@",names[i]]);
changed.fastMultiFace=1;
PGFaceFrame *legacyWarp=[processor processImage:image settings:changed time:kCMTimeZero error:&error];
BOOL legacyChangedPixels=![Pixels(context,legacyWarp.image) isEqualToData:base];
BOOL requiresPoints=(i==0 || i==2 || i==4 || i==5 || i==6);
Check(warped != nil && (requiresPoints ? !changedPixels : changedPixels), requiresPoints ? @"recovered thin/V/chin/short/jaw skip a face with no indexed landmarks" : [@"geometry control changes pixels: " stringByAppendingString:names[i]]);
Check(legacyWarp != nil && (requiresPoints ? !legacyChangedPixels : legacyChangedPixels), requiresPoints ? @"fast-multi recovered thin/V/chin/short/jaw skip a face with no indexed landmarks" : [@"fast-multi preserves legacy bbox geometry control: " stringByAppendingString:names[i]]);
Check(CGRectEqualToRect(warped.image.extent, image.extent), @"warp preserves image extent");
}
settings.faceShort = 0.8;