WIP: align the 2.4.0 detector, pin quality gates and sequence box tracker; investigate remaining clip motion
这个提交包含在:
1 父节点
79710f17b3
当前提交
524f536a53
5 个文件已更改
+8061
-136
没有与搜索条件相匹配的文件
@@ -90,7 +90,7 @@ static NSArray *PGSanitizedPoints76(id value) {
|
|||||||
return [points copy];
|
return [points copy];
|
||||||
}
|
}
|
||||||
static NSArray *PGFullPoints76(VNFaceLandmarkRegion2D *region, CGRect box) {
|
static NSArray *PGFullPoints76(VNFaceLandmarkRegion2D *region, CGRect box) {
|
||||||
if (region.pointCount != 76 || !region.normalizedPoints) return nil;
|
if (region.pointCount < 76 || !region.normalizedPoints) return nil;
|
||||||
const CGPoint *raw = region.normalizedPoints;
|
const CGPoint *raw = region.normalizedPoints;
|
||||||
NSMutableArray *points = [NSMutableArray arrayWithCapacity:76];
|
NSMutableArray *points = [NSMutableArray arrayWithCapacity:76];
|
||||||
for (NSUInteger i=0; i<76; i++) {
|
for (NSUInteger i=0; i<76; i++) {
|
||||||
@@ -110,6 +110,7 @@ static NSDictionary *PGGeometry(VNFaceObservation *observation) {
|
|||||||
@"mouth": PGRegion(lm.innerLips, box), @"contour": PGRegion(lm.faceContour, box)} mutableCopy];
|
@"mouth": PGRegion(lm.innerLips, box), @"contour": PGRegion(lm.faceContour, box)} mutableCopy];
|
||||||
NSArray *points = PGFullPoints76(lm.allPoints, box);
|
NSArray *points = PGFullPoints76(lm.allPoints, box);
|
||||||
if (points) geometry[@"points76"] = points;
|
if (points) geometry[@"points76"] = points;
|
||||||
|
geometry[@"landmarkConfidence"] = @(lm ? lm.confidence : 0);
|
||||||
return [geometry copy];
|
return [geometry copy];
|
||||||
}
|
}
|
||||||
static NSDictionary *PGSanitizedGeometry(id value) {
|
static NSDictionary *PGSanitizedGeometry(id value) {
|
||||||
@@ -133,6 +134,7 @@ static NSDictionary *PGSanitizedGeometry(id value) {
|
|||||||
// thin/chin/jaw/short fields without discarding otherwise usable old records.
|
// thin/chin/jaw/short fields without discarding otherwise usable old records.
|
||||||
NSArray *points76 = PGSanitizedPoints76(value[@"points76"]);
|
NSArray *points76 = PGSanitizedPoints76(value[@"points76"]);
|
||||||
if (points76) result[@"points76"] = points76;
|
if (points76) result[@"points76"] = points76;
|
||||||
|
if ([value[@"landmarkConfidence"] isKindOfClass:NSNumber.class]) result[@"landmarkConfidence"] = value[@"landmarkConfidence"];
|
||||||
return [result copy];
|
return [result copy];
|
||||||
}
|
}
|
||||||
static CGRect PGRegionBounds(NSDictionary *geometry, NSString *name, CGRect fallback) {
|
static CGRect PGRegionBounds(NSDictionary *geometry, NSString *name, CGRect fallback) {
|
||||||
@@ -178,154 +180,259 @@ static CGPoint PGRemapPoint(NSArray *point, CGRect old, CGRect current) {
|
|||||||
return CGPointMake(current.origin.x+([point[0] doubleValue]-old.origin.x)*current.size.width/old.size.width,
|
return CGPointMake(current.origin.x+([point[0] doubleValue]-old.origin.x)*current.size.width/old.size.width,
|
||||||
current.origin.y+([point[1] doubleValue]-old.origin.y)*current.size.height/old.size.height);
|
current.origin.y+([point[1] doubleValue]-old.origin.y)*current.size.height/old.size.height);
|
||||||
}
|
}
|
||||||
static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previous, double dt, BOOL stabilizePoints76) {
|
// 2.4.0's default pin path accepts good points without a temporal blend.
|
||||||
CGRect old=PGArrayRect(previous[@"box"]),box=PGArrayRect(current[@"box"]);
|
// Geometry remains in our existing bottom-left coordinate convention; distances,
|
||||||
if(!PGValidRect(old) || !PGValidRect(box))return current;
|
// dot products and the two-axis sign test are invariant to the reference's Y flip.
|
||||||
// Independent stabilization: translate/scale old landmarks into the current
|
static CGPoint PGPoint(NSArray *p) { return CGPointMake([p[0] doubleValue], [p[1] doubleValue]); }
|
||||||
// detection box first, so normal head motion does not accumulate box lag.
|
static CGPoint PGMeanPoints(NSArray *points, NSUInteger first, NSUInteger count) {
|
||||||
double weight=.9*fmin(1,fmax(0,dt/.03));
|
if (!count || first+count>points.count) return CGPointMake(NAN,NAN);
|
||||||
CGPoint oldLE=PGCenter(PGRegionBounds(previous,@"leftEye",PGInFace(old,.16,.53,.25,.15)));
|
CGPoint sum=CGPointZero;
|
||||||
CGPoint oldRE=PGCenter(PGRegionBounds(previous,@"rightEye",PGInFace(old,.59,.53,.25,.15)));
|
for(NSUInteger i=first;i<first+count;i++) {CGPoint p=PGPoint(points[i]);sum.x+=p.x;sum.y+=p.y;}
|
||||||
CGPoint newLE=PGCenter(PGRegionBounds(current,@"leftEye",PGInFace(box,.16,.53,.25,.15)));
|
return CGPointMake(sum.x/count,sum.y/count);
|
||||||
CGPoint newRE=PGCenter(PGRegionBounds(current,@"rightEye",PGInFace(box,.59,.53,.25,.15)));
|
}
|
||||||
double ox=oldRE.x-oldLE.x,oy=oldRE.y-oldLE.y,nx=newRE.x-newLE.x,ny=newRE.y-newLE.y;
|
static CGPoint PGLocalPoint(CGPoint p,CGRect box) {
|
||||||
double oldLength=hypot(ox,oy),newLength=hypot(nx,ny);
|
return CGPointMake((p.x-box.origin.x)/box.size.width,(p.y-box.origin.y)/box.size.height);
|
||||||
BOOL axisHold=oldLength>MAX(.003,.055*MIN(old.size.height,box.size.height)) &&
|
}
|
||||||
newLength>MAX(.003,.055*MIN(old.size.height,box.size.height)) &&
|
static NSString *PGGeometryQualityReason(NSDictionary *g) {
|
||||||
(ox*nx+oy*ny)/(oldLength*newLength)<-.18;
|
CGRect box=PGArrayRect(g[@"box"]);NSArray *points=PGSanitizedPoints76(g[@"points76"]),*contour=g[@"contour"];
|
||||||
NSMutableDictionary *stable=[NSMutableDictionary dictionaryWithObject:current[@"box"] forKey:@"box"];
|
if(!PGValidRect(box)||points.count!=76)return @"incomplete76";
|
||||||
for(NSString *name in PGGeometryRegions()) {
|
if(contour.count<=10)return @"contour";
|
||||||
NSArray *a=previous[name],*b=current[name];
|
double confidence=g[@"landmarkConfidence"]?[g[@"landmarkConfidence"] doubleValue]:1;
|
||||||
if(!a.count || (!axisHold && a.count!=b.count)) {stable[name]=b;continue;}
|
if(!isfinite(confidence)||confidence<.55)return @"confidence";
|
||||||
NSMutableArray *points=[NSMutableArray arrayWithCapacity:a.count];
|
for(NSArray *point in points) {
|
||||||
for(NSUInteger i=0;i<a.count;i++) {
|
CGPoint p=PGLocalPoint(PGPoint(point),box);
|
||||||
CGPoint p=PGRemapPoint(a[i],old,box);
|
if(!isfinite(p.x)||!isfinite(p.y)||p.x<-.35||p.x>1.35||p.y<-.35||p.y>1.35)return @"localBounds";
|
||||||
if(!axisHold) {p.x+=( [b[i][0] doubleValue]-p.x)*weight;p.y+=([b[i][1] doubleValue]-p.y)*weight;}
|
|
||||||
[points addObject:@[@(p.x),@(p.y)]];
|
|
||||||
}
|
}
|
||||||
stable[name]=[points copy];
|
CGPoint centers[5];NSArray *names=@[@"leftEye",@"rightEye",@"nose",@"lips"];
|
||||||
|
for(NSUInteger i=0;i<4;i++) {
|
||||||
|
NSArray *region=g[names[i]];if(!region.count)return @"missingRegion";
|
||||||
|
centers[i]=PGLocalPoint(PGMeanPoints(region,0,region.count),box);
|
||||||
|
if(!isfinite(centers[i].x)||!isfinite(centers[i].y))return @"regionBounds";
|
||||||
}
|
}
|
||||||
NSArray *current76 = PGSanitizedPoints76(current[@"points76"]);
|
centers[4]=PGLocalPoint(PGMeanPoints(contour,contour.count/2-1,3),box);
|
||||||
NSArray *previous76 = PGSanitizedPoints76(previous[@"points76"]);
|
CGPoint left=centers[0],right=centers[1],nose=centers[2],mouth=centers[3],chin=centers[4];
|
||||||
if (current76) {
|
double dx=right.x-left.x,dy=right.y-left.y,length=hypot(dx,dy);
|
||||||
if (stabilizePoints76 && previous76) {
|
if(!isfinite(length)||length<.1)return @"eyeDistance";
|
||||||
NSMutableArray *points = [NSMutableArray arrayWithCapacity:76];
|
dx/=length;dy/=length;
|
||||||
for (NSUInteger i=0; i<76; i++) {
|
CGPoint eye=CGPointMake((left.x+right.x)*.5,(left.y+right.y)*.5);
|
||||||
CGPoint p = PGRemapPoint(previous76[i], old, box);
|
double vx=-dy,vy=dx;
|
||||||
if (!axisHold) {
|
if(vx*(mouth.x-eye.x)+vy*(mouth.y-eye.y)<0){vx=-vx;vy=-vy;}
|
||||||
p.x += ([current76[i][0] doubleValue]-p.x)*weight;
|
double mouthProjection=vx*(mouth.x-eye.x)+vy*(mouth.y-eye.y);
|
||||||
p.y += ([current76[i][1] doubleValue]-p.y)*weight;
|
double noseProjection=vx*(nose.x-eye.x)+vy*(nose.y-eye.y);
|
||||||
|
double chinProjection=vx*(chin.x-eye.x)+vy*(chin.y-eye.y);
|
||||||
|
double lateral=dx*(chin.x-eye.x)+dy*(chin.y-eye.y);
|
||||||
|
if(!isfinite(chinProjection)||mouthProjection<.08||chinProjection-mouthProjection<.06||chinProjection<.2)return @"anatomy";
|
||||||
|
if(noseProjection<-.12||noseProjection>mouthProjection+.16||fabs(lateral)>length*.5+.3)return @"anatomy";
|
||||||
|
return @"";
|
||||||
|
}
|
||||||
|
static double PGBoxIoU(CGRect a,CGRect b) {
|
||||||
|
CGRect intersection=CGRectIntersection(a,b);double area=CGRectIsNull(intersection)?0:intersection.size.width*intersection.size.height;
|
||||||
|
double total=a.size.width*a.size.height+b.size.width*b.size.height-area;return total>0?area/total:0;
|
||||||
|
}
|
||||||
|
static BOOL PGCompatibleBoxes(CGRect a,CGRect b) {
|
||||||
|
if(!PGValidRect(a)||!PGValidRect(b))return NO;
|
||||||
|
CGPoint x=PGCenter(a),y=PGCenter(b);
|
||||||
|
return hypot(x.x-y.x,x.y-y.y)<=MAX(.04,.82*MAX(b.size.width,b.size.height))||PGBoxIoU(a,b)>=.08;
|
||||||
|
}
|
||||||
|
static CGPoint PGChinAxis(NSDictionary *g) {
|
||||||
|
NSArray *p=g[@"points76"];if(p.count!=76)return CGPointMake(NAN,NAN);
|
||||||
|
CGPoint a=PGPoint(p[66]),b=PGPoint(p[67]),c=PGPoint(p[68]),center=PGCenter(PGArrayRect(g[@"box"]));
|
||||||
|
return CGPointMake((a.x+1.65*b.x+c.x)/3.65-center.x,(a.y+1.65*b.y+c.y)/3.65-center.y);
|
||||||
|
}
|
||||||
|
static BOOL PGGeometryAxisFlip(NSDictionary *current,NSDictionary *previous) {
|
||||||
|
CGRect a=PGArrayRect(current[@"box"]),b=PGArrayRect(previous[@"box"]);
|
||||||
|
if(!PGCompatibleBoxes(a,b))return NO;
|
||||||
|
CGPoint x=PGChinAxis(current),y=PGChinAxis(previous);double lx=hypot(x.x,x.y),ly=hypot(y.x,y.y);
|
||||||
|
double threshold=MAX(.003,.055*MIN(a.size.height,b.size.height));
|
||||||
|
if(!isfinite(lx)||!isfinite(ly)||lx<.0008||ly<.0008||lx<threshold||ly<threshold)return NO;
|
||||||
|
if(x.y*y.y>=0||fabs(x.y)<.35*threshold||fabs(y.y)<.35*threshold)return NO;
|
||||||
|
double cosine=fmax(-1,fmin(1,(x.x*y.x+x.y*y.y)/(lx*ly)));
|
||||||
|
return cosine<=-.18||fabs(x.y-y.y)>=threshold;
|
||||||
|
}
|
||||||
|
static NSDictionary *PGRemappedGeometry(NSDictionary *previous,CGRect box) {
|
||||||
|
CGRect old=PGArrayRect(previous[@"box"]);if(!PGValidRect(old)||!PGValidRect(box))return previous;
|
||||||
|
NSMutableDictionary *result=[previous mutableCopy];result[@"box"]=PGRectArray(box);
|
||||||
|
NSMutableArray *names=[PGGeometryRegions() mutableCopy];[names addObject:@"points76"];
|
||||||
|
for(NSString *name in names) {
|
||||||
|
NSMutableArray *points=[NSMutableArray new];
|
||||||
|
for(NSArray *point in previous[name]) {CGPoint p=PGRemapPoint(point,old,box);[points addObject:@[@(p.x),@(p.y)]];}
|
||||||
|
if(points.count)result[name]=points;
|
||||||
}
|
}
|
||||||
[points addObject:@[@(p.x),@(p.y)]];
|
return result;
|
||||||
|
}
|
||||||
|
// Reference hold validation uses the 76 points plus the box center. Keep our
|
||||||
|
// persisted topology at 76 and reconstruct the center rather than changing archives.
|
||||||
|
static BOOL PGValidHeldGeometry(NSDictionary *g) {
|
||||||
|
CGRect box=PGArrayRect(g[@"box"]);NSArray *p=PGSanitizedPoints76(g[@"points76"]);
|
||||||
|
if(!PGValidRect(box)||p.count!=76)return NO;
|
||||||
|
CGRect expanded=CGRectInset(box,-.4*box.size.width,-.4*box.size.height);
|
||||||
|
double minX=CGRectGetMidX(box),maxX=minX,minY=CGRectGetMidY(box),maxY=minY;
|
||||||
|
double contourMinX=DBL_MAX,contourMaxX=-DBL_MAX,contourMinY=DBL_MAX,contourMaxY=-DBL_MAX,left=0,mid=0,right=0;
|
||||||
|
for(NSUInteger i=0;i<76;i++) {
|
||||||
|
CGPoint point=PGPoint(p[i]);
|
||||||
|
if(point.x<-.25||point.x>1.25||point.y<-.25||point.y>1.25)return NO;
|
||||||
|
minX=MIN(minX,point.x);maxX=MAX(maxX,point.x);minY=MIN(minY,point.y);maxY=MAX(maxY,point.y);
|
||||||
|
if(i<59)continue;
|
||||||
|
if(!CGRectContainsPoint(expanded,point))return NO;
|
||||||
|
contourMinX=MIN(contourMinX,point.x);contourMaxX=MAX(contourMaxX,point.x);
|
||||||
|
contourMinY=MIN(contourMinY,point.y);contourMaxY=MAX(contourMaxY,point.y);
|
||||||
|
if(i<66)left+=point.x;else if(i<=68)mid+=point.x;else right+=point.x;
|
||||||
}
|
}
|
||||||
stable[@"points76"] = [points copy];
|
double w=box.size.width,h=box.size.height,cw=contourMaxX-contourMinX,ch=contourMaxY-contourMinY;
|
||||||
} else {
|
left/=7;mid/=3;right/=7;
|
||||||
// Reacquired detections and changes from/to incomplete topology do
|
return maxX-minX>=.25*w&&maxY-minY>=.25*h&&cw>=.2*w&&ch>=.04*h&&cw<=1.85*w&&ch<=1.85*h&&
|
||||||
// not borrow a held face's old projected pose or invent missing points.
|
fabs(right-left)>=.12*w&&mid>=MIN(left,right)-.35*w&&mid<=MAX(left,right)+.35*w;
|
||||||
stable[@"points76"] = current76;
|
}
|
||||||
|
static NSString *PGGeometryTransitionReason(NSDictionary *current,NSDictionary *previous) {
|
||||||
|
CGRect box=PGArrayRect(current[@"box"]),old=PGArrayRect(previous[@"box"]);
|
||||||
|
if(!PGCompatibleBoxes(box,old))return @"";
|
||||||
|
if(PGGeometryAxisFlip(current,previous))return @"axisFlip";
|
||||||
|
NSArray *a=current[@"points76"],*b=previous[@"points76"];
|
||||||
|
if(a.count!=76||b.count!=76)return @"";
|
||||||
|
NSMutableArray *distances=[NSMutableArray new];double scale=MAX(box.size.width,box.size.height);
|
||||||
|
for(NSUInteger i=0;i<76;i++) {
|
||||||
|
CGPoint p=PGRemapPoint(b[i],old,box),q=PGPoint(a[i]);
|
||||||
|
[distances addObject:@(hypot(p.x-q.x,p.y-q.y)/scale)];
|
||||||
}
|
}
|
||||||
}
|
[distances sortUsingSelector:@selector(compare:)];
|
||||||
return [stable copy];
|
return [distances[38] doubleValue]>.30||[distances[67] doubleValue]>.58?@"pointJump":@"";
|
||||||
|
}
|
||||||
|
static CIImage *PGPreparedVisionImage(CIImage *image,CGFloat limit) {
|
||||||
|
CGRect e=image.extent;
|
||||||
|
CIImage *prepared=[image imageByApplyingTransform:CGAffineTransformMakeTranslation(-e.origin.x,-e.origin.y)];
|
||||||
|
CGFloat scale=MIN(1,limit/MAX(e.size.width,e.size.height));
|
||||||
|
if(scale<1)prepared=[prepared imageByApplyingTransform:CGAffineTransformMakeScale(scale,scale)];
|
||||||
|
return [prepared imageByCroppingToRect:CGRectMake(0,0,MAX(1,floor(e.size.width*scale)),MAX(1,floor(e.size.height*scale)))];
|
||||||
}
|
}
|
||||||
|
|
||||||
// This live selector is deliberately independent of _targets. Its past-only
|
|
||||||
// .35s reference window is a documented project policy, not a recovered exact
|
|
||||||
// vendor formula. Continuous held outputs advance time, so this is not a hold
|
|
||||||
// timeout. Locks protect state, not global render-order equivalence: a cold seek
|
|
||||||
// cannot reconstruct frames the host has never supplied.
|
|
||||||
@interface PGRealtimeFaceTracker : NSObject {
|
@interface PGRealtimeFaceTracker : NSObject {
|
||||||
NSLock *_mutex;
|
NSLock *_mutex;
|
||||||
NSMutableArray<NSDictionary *> *_entries;
|
NSMutableArray<NSDictionary *> *_entries;
|
||||||
NSUInteger _estimatedBytes;
|
NSUInteger _estimatedBytes;
|
||||||
|
NSDictionary *_liveReference;
|
||||||
|
VNSequenceRequestHandler *_sequence;
|
||||||
|
VNTrackObjectRequest *_objectRequest;
|
||||||
|
CGRect _objectBox;
|
||||||
|
CMTime _objectTime;
|
||||||
|
BOOL _objectValid;
|
||||||
}
|
}
|
||||||
- (NSDictionary *)resultForGeometry:(NSArray<NSDictionary *> *)raw settings:(PGBeautySettings)settings time:(CMTime)time extent:(CGRect)extent;
|
- (NSDictionary *)resultForGeometry:(NSArray<NSDictionary *> *)raw settings:(PGBeautySettings)settings time:(CMTime)time extent:(CGRect)extent;
|
||||||
|
- (NSDictionary *)resultForGeometry:(NSArray<NSDictionary *> *)raw settings:(PGBeautySettings)settings time:(CMTime)time extent:(CGRect)extent image:(CIImage *)image;
|
||||||
|
- (CGRect)trackedBoxForImage:(CIImage *)image time:(CMTime)time seedBox:(CGRect)seed;
|
||||||
- (void)reset;
|
- (void)reset;
|
||||||
- (NSDictionary *)cacheStatistics;
|
- (NSDictionary *)cacheStatistics;
|
||||||
@end
|
@end
|
||||||
@implementation PGRealtimeFaceTracker
|
@implementation PGRealtimeFaceTracker
|
||||||
- (instancetype)init {if((self=[super init])){_mutex=[NSLock new];_entries=[NSMutableArray new];}return self;}
|
- (instancetype)init {if((self=[super init])){_mutex=[NSLock new];_entries=[NSMutableArray new];_objectTime=kCMTimeInvalid;}return self;}
|
||||||
- (void)reset {[_mutex lock];[_entries removeAllObjects];_estimatedBytes=0;[_mutex unlock];}
|
- (void)resetObjectTracker { _sequence=[VNSequenceRequestHandler new];_objectRequest=nil;_objectBox=CGRectNull;_objectTime=kCMTimeInvalid;_objectValid=NO; }
|
||||||
|
- (void)reset {[_mutex lock];[_entries removeAllObjects];_estimatedBytes=0;_liveReference=nil;[self resetObjectTracker];[_mutex unlock];}
|
||||||
- (NSDictionary *)cacheStatistics {[_mutex lock];NSDictionary *stats=@{@"frames":@(_entries.count),@"estimatedBytes":@(_estimatedBytes)};[_mutex unlock];return stats;}
|
- (NSDictionary *)cacheStatistics {[_mutex lock];NSDictionary *stats=@{@"frames":@(_entries.count),@"estimatedBytes":@(_estimatedBytes)};[_mutex unlock];return stats;}
|
||||||
- (NSDictionary *)resultForGeometry:(NSArray<NSDictionary *> *)input settings:(PGBeautySettings)s time:(CMTime)time extent:(CGRect)extent {
|
- (CGRect)trackedBoxForImage:(CIImage *)image time:(CMTime)time seedBox:(CGRect)seed {
|
||||||
|
if(!image||!PGTimeKey(time))return CGRectNull;
|
||||||
|
BOOL hasSeed=PGValidRect(seed),hasBox=PGValidRect(_objectBox);
|
||||||
|
double dt=PGTimeKey(_objectTime)?CMTimeGetSeconds(CMTimeSubtract(time,_objectTime)):NAN;
|
||||||
|
BOOL backward=isfinite(dt)&&dt<0;
|
||||||
|
if(hasBox&&_objectValid&&isfinite(dt)&&fabs(dt)<=.0005)return _objectBox;
|
||||||
|
if(!_sequence)_sequence=[VNSequenceRequestHandler new];
|
||||||
|
if(!_objectRequest||!_objectValid||backward||(isfinite(dt)&&dt>.2)) {
|
||||||
|
if(!backward&&hasBox&&!_objectValid&&isfinite(dt)&&dt<=.45&&(!hasSeed||!PGCompatibleBoxes(seed,_objectBox)))return _objectBox;
|
||||||
|
if(!hasSeed)return CGRectNull;
|
||||||
|
_sequence=[VNSequenceRequestHandler new];
|
||||||
|
_objectRequest=[[VNTrackObjectRequest alloc] initWithDetectedObjectObservation:[VNDetectedObjectObservation observationWithBoundingBox:seed]];
|
||||||
|
NSUInteger revision=VNTrackObjectRequest.defaultRevision;
|
||||||
|
if(![VNTrackObjectRequest.supportedRevisions containsIndex:revision])revision=VNTrackObjectRequest.currentRevision;
|
||||||
|
if([VNTrackObjectRequest.supportedRevisions containsIndex:revision])_objectRequest.revision=revision;
|
||||||
|
_objectRequest.trackingLevel=VNRequestTrackingLevelAccurate;
|
||||||
|
_objectBox=seed;_objectTime=time;_objectValid=YES;return seed;
|
||||||
|
}
|
||||||
|
NSError *error=nil;
|
||||||
|
BOOL ok=[_sequence performRequests:@[_objectRequest] onCIImage:PGPreparedVisionImage(image,960) orientation:kCGImagePropertyOrientationUp error:&error];
|
||||||
|
VNDetectedObjectObservation *result=ok?_objectRequest.results.firstObject:nil;
|
||||||
|
CGRect box=result.boundingBox;
|
||||||
|
if(result&&result.confidence>=.22&&PGValidRect(box)) {
|
||||||
|
_objectRequest.inputObservation=result;_objectRequest.trackingLevel=VNRequestTrackingLevelAccurate;
|
||||||
|
_objectBox=box;_objectTime=time;_objectValid=YES;return box;
|
||||||
|
}
|
||||||
|
_objectRequest=nil;_sequence=[VNSequenceRequestHandler new];_objectValid=NO;
|
||||||
|
return hasBox?_objectBox:(hasSeed?seed:CGRectNull);
|
||||||
|
}
|
||||||
|
- (NSDictionary *)resultForGeometry:(NSArray<NSDictionary *> *)raw settings:(PGBeautySettings)s time:(CMTime)time extent:(CGRect)extent {
|
||||||
|
return [self resultForGeometry:raw settings:s time:time extent:extent image:nil];
|
||||||
|
}
|
||||||
|
- (NSDictionary *)resultForGeometry:(NSArray<NSDictionary *> *)input settings:(PGBeautySettings)s time:(CMTime)time extent:(CGRect)extent image:(CIImage *)image {
|
||||||
NSMutableArray *valid=[NSMutableArray new];
|
NSMutableArray *valid=[NSMutableArray new];
|
||||||
for(id item in input) {NSDictionary *g=PGSanitizedGeometry(item);if(g && valid.count<16)[valid addObject:g];}
|
for(id item in input) {NSDictionary *g=PGSanitizedGeometry(item);if(g&&valid.count<16)[valid addObject:g];}
|
||||||
NSArray *raw=[valid copy];
|
NSArray *raw=[valid copy];NSUInteger mode=s.fastMultiFace?2:(s.singleFace?1:0);
|
||||||
NSUInteger mode=s.fastMultiFace?2:(s.singleFace?1:0);
|
|
||||||
NSString *key=PGTimeKey(time);
|
NSString *key=PGTimeKey(time);
|
||||||
NSString *context=[NSString stringWithFormat:@"%lu/%u/%lld/%.17g/%.17g/%.17g/%.17g/%.9g",(unsigned long)mode,s.targetIndex,
|
NSString *context=[NSString stringWithFormat:@"%lu/%u/%lld/%.17g/%.17g/%.17g/%.17g/%.9g",(unsigned long)mode,s.targetIndex,
|
||||||
(long long)time.epoch,extent.origin.x,extent.origin.y,extent.size.width,extent.size.height,s.faceJumpGuardRange];
|
(long long)time.epoch,extent.origin.x,extent.origin.y,extent.size.width,extent.size.height,s.faceJumpGuardRange];
|
||||||
[_mutex lock];
|
[_mutex lock];
|
||||||
NSDictionary *head=nil;
|
BOOL contextChanged=![_liveReference[@"context"] isEqual:context];
|
||||||
for(NSDictionary *record in _entries) if([record[@"context"] isEqual:context] &&
|
NSDictionary *reference=contextChanged?nil:_liveReference;
|
||||||
(!head || CMTimeCompare(PGRecordTime(record),PGRecordTime(head))>=0)) head=record;
|
if(contextChanged) {
|
||||||
NSString *scope=@"automatic";
|
// Fast-multi bypasses pin selection. Returning to a prior mode can use
|
||||||
|
// its accepted shape, but must never continue another mode's VN sequence.
|
||||||
|
for(NSDictionary *record in _entries)if([record[@"context"] isEqual:context]&&CMTimeCompare(PGRecordTime(record),time)<=0&&
|
||||||
|
(!reference||CMTimeCompare(PGRecordTime(record),PGRecordTime(reference))>0))reference=record;
|
||||||
|
}
|
||||||
double seedX=PGClamp(s.selectionX,0,1),seedY=PGClamp(s.selectionY,0,1);
|
double seedX=PGClamp(s.selectionX,0,1),seedY=PGClamp(s.selectionY,0,1);
|
||||||
if(mode==1) {
|
NSString *scope=mode==1?[NSString stringWithFormat:@"%.9g/%.9g",seedX,seedY]:@"automatic";
|
||||||
// A real point change (> .01) starts a new identity scope. Older render
|
if(mode==1&&reference&&PGSeedDistance(reference,s)<=.01){scope=reference[@"scope"];seedX=[reference[@"seedX"] doubleValue];seedY=[reference[@"seedY"] doubleValue];}
|
||||||
// snapshots may use their own past scope, but never replace a newer head.
|
if(reference&&![reference[@"scope"] isEqual:scope])reference=nil;
|
||||||
NSDictionary *seedRecord=head;
|
// Exact frame reuse is independent of a second Vision result or render count.
|
||||||
if(seedRecord && CMTimeCompare(time,PGRecordTime(seedRecord))<0) {
|
// A new uncached rewind clears live geometry and the stateful sequence handler.
|
||||||
seedRecord=nil;
|
NSDictionary *cached=nil;
|
||||||
for(NSDictionary *record in _entries) if([record[@"context"] isEqual:context] && PGSeedDistance(record,s)<=.01 &&
|
if(key)for(NSDictionary *record in _entries)if([record[@"context"] isEqual:context]&&[record[@"scope"] isEqual:scope]&&[record[@"key"] isEqual:key]){cached=record;break;}
|
||||||
CMTimeCompare(PGRecordTime(record),time)<=0 && (!seedRecord || CMTimeCompare(PGRecordTime(record),PGRecordTime(seedRecord))>=0))seedRecord=record;
|
BOOL backward=reference&&CMTimeCompare(time,PGRecordTime(reference))<0;
|
||||||
}
|
if(contextChanged||!reference||backward)[self resetObjectTracker];
|
||||||
if(seedRecord && PGSeedDistance(seedRecord,s)<=.01) {
|
if(cached){_liveReference=cached;[_mutex unlock];return cached;}
|
||||||
scope=seedRecord[@"scope"];seedX=[seedRecord[@"seedX"] doubleValue];seedY=[seedRecord[@"seedY"] doubleValue];
|
if(backward||!key)reference=nil;
|
||||||
} else scope=[NSString stringWithFormat:@"%@/%.9g/%.9g",key?:@"invalid",seedX,seedY];
|
NSDictionary *previous=reference[@"accepted"];
|
||||||
}
|
|
||||||
NSDictionary *reference=nil;
|
|
||||||
if(key) for(NSDictionary *record in _entries) {
|
|
||||||
if(![record[@"context"] isEqual:context] || ![record[@"scope"] isEqual:scope])continue;
|
|
||||||
if([record[@"key"] isEqual:key] && [record[@"raw"] isEqual:raw]) {[_mutex unlock];return record;}
|
|
||||||
CMTime stored=PGRecordTime(record);
|
|
||||||
// Same-time changed detections recompute from an earlier frame, never
|
|
||||||
// feed an already smoothed result into another pass at that same time.
|
|
||||||
if(CMTimeCompare(stored,time)>=0)continue;
|
|
||||||
double dt=CMTimeGetSeconds(CMTimeSubtract(time,stored));
|
|
||||||
if(dt<=.35 && (!reference || CMTimeCompare(stored,PGRecordTime(reference))>0))reference=record;
|
|
||||||
}
|
|
||||||
NSArray *prior=reference[@"output"];
|
|
||||||
NSDictionary *previous=prior.count?prior.firstObject:nil;
|
|
||||||
CGRect previousBox=PGArrayRect(previous[@"box"]);
|
CGRect previousBox=PGArrayRect(previous[@"box"]);
|
||||||
|
CGRect tracked=mode==2?CGRectNull:[self trackedBoxForImage:image time:time seedBox:CGRectNull];
|
||||||
|
CGRect selectionBox=PGValidRect(tracked)?tracked:previousBox;
|
||||||
NSInteger index=-1;double best=-DBL_MAX;
|
NSInteger index=-1;double best=-DBL_MAX;
|
||||||
if(mode!=2) for(NSUInteger i=0;i<raw.count;i++) {
|
if(mode!=2)for(NSUInteger i=0;i<raw.count;i++) {
|
||||||
CGRect box=PGArrayRect(raw[i][@"box"]);CGPoint center=PGCenter(box);double score;
|
CGRect box=PGArrayRect(raw[i][@"box"]);CGPoint center=PGCenter(box);double score;
|
||||||
if(PGValidRect(previousBox)) {
|
if(PGValidRect(previousBox)) {
|
||||||
CGFloat factor=PGClamp(s.faceJumpGuardRange,1,4);
|
CGFloat factor=PGClamp(s.faceJumpGuardRange,1,4);
|
||||||
CGRect guard=CGRectIntersection(CGRectMake(0,0,1,1),CGRectInset(previousBox,-previousBox.size.width*(factor-1)*.5,-previousBox.size.height*(factor-1)*.5));
|
CGRect guard=CGRectIntersection(CGRectMake(0,0,1,1),CGRectInset(previousBox,-previousBox.size.width*(factor-1)*.5,-previousBox.size.height*(factor-1)*.5));
|
||||||
if(center.x<CGRectGetMinX(guard)||center.x>CGRectGetMaxX(guard)||center.y<CGRectGetMinY(guard)||center.y>CGRectGetMaxY(guard))continue;
|
if(!CGRectContainsPoint(guard,center))continue;
|
||||||
CGPoint p=PGCenter(previousBox);score=-((center.x-p.x)*(center.x-p.x)+(center.y-p.y)*(center.y-p.y));
|
}
|
||||||
} else if(mode==1) score=-((center.x-seedX)*(center.x-seedX)+(center.y-seedY)*(center.y-seedY));
|
if(PGValidRect(selectionBox)) {
|
||||||
|
CGPoint p=PGCenter(selectionBox);double distance=hypot(center.x-p.x,center.y-p.y);
|
||||||
|
score=8*PGBoxIoU(box,selectionBox)+3.2*(1-MIN(1,distance/.32))-.35*distance;
|
||||||
|
if(CGRectContainsPoint(box,p))score+=1.2;
|
||||||
|
}else if(mode==1)score=-hypot(center.x-seedX,center.y-seedY);
|
||||||
else {CGRect clipped=CGRectIntersection(box,CGRectMake(0,0,1,1));CGPoint p=PGCenter(clipped);
|
else {CGRect clipped=CGRectIntersection(box,CGRectMake(0,0,1,1));CGPoint p=PGCenter(clipped);
|
||||||
score=1.8*fmin(1,clipped.size.width*clipped.size.height)+1.1*(1-fmin(1,hypot(p.x-.5,p.y-.5)/.72));}
|
score=1.8*fmin(1,clipped.size.width*clipped.size.height)+1.1*(1-fmin(1,hypot(p.x-.5,p.y-.5)/.72));}
|
||||||
if(index<0 || score>best) {index=(NSInteger)i;best=score;}
|
if(index<0||score>best){index=(NSInteger)i;best=score;}
|
||||||
|
}
|
||||||
|
NSDictionary *current=index>=0?raw[(NSUInteger)index]:nil;
|
||||||
|
if(mode!=2&&!PGValidRect(tracked)&¤t)tracked=[self trackedBoxForImage:image time:time seedBox:PGArrayRect(current[@"box"])];
|
||||||
|
NSString *reason=current?PGGeometryQualityReason(current):@"missingFace";
|
||||||
|
if(!reason.length&&previous)reason=PGGeometryTransitionReason(current,previous);
|
||||||
|
BOOL held=NO;NSArray *output=@[];NSDictionary *accepted=previous;
|
||||||
|
if(mode==2){output=raw;accepted=nil;reason=@"fastMulti";}
|
||||||
|
else if(!reason.length){output=@[current];accepted=current;}
|
||||||
|
else if(previous) {
|
||||||
|
CGRect box=PGValidRect(tracked)?tracked:(current?PGArrayRect(current[@"box"]):previousBox);
|
||||||
|
NSDictionary *mapped=PGRemappedGeometry(previous,box);
|
||||||
|
output=@[PGValidHeldGeometry(mapped)?mapped:previous];held=YES;
|
||||||
}
|
}
|
||||||
NSArray *output;BOOL held=NO;
|
|
||||||
if(mode==2) output=raw;
|
|
||||||
else if(index>=0) {
|
|
||||||
NSDictionary *current=raw[(NSUInteger)index];
|
|
||||||
double dt=reference?CMTimeGetSeconds(CMTimeSubtract(time,PGRecordTime(reference))):0;
|
|
||||||
output=@[previous?PGStableGeometry(current,previous,dt,![reference[@"held"] boolValue]):current];
|
|
||||||
} else {output=prior?:@[];held=output.count>0;}
|
|
||||||
NSMutableDictionary *record=[@{@"context":context,@"scope":scope,@"key":key?:@"invalid",@"value":@(time.value),@"scale":@(time.timescale),@"epoch":@(time.epoch),
|
NSMutableDictionary *record=[@{@"context":context,@"scope":scope,@"key":key?:@"invalid",@"value":@(time.value),@"scale":@(time.timescale),@"epoch":@(time.epoch),
|
||||||
@"seedX":@(seedX),@"seedY":@(seedY),@"raw":raw,@"output":output,@"index":@(index),@"held":@(held),@"referenceKey":reference[@"key"]?:@""} mutableCopy];
|
@"seedX":@(seedX),@"seedY":@(seedY),@"raw":raw,@"output":output,@"index":@(index),@"held":@(held),@"reason":reason,
|
||||||
|
@"trackedBox":PGValidRect(tracked)?PGRectArray(tracked):@[],@"referenceKey":reference[@"key"]?:@""} mutableCopy];
|
||||||
|
if(accepted)record[@"accepted"]=accepted;
|
||||||
|
// Keep the accepted time distinct from the output/held time for diagnostics.
|
||||||
|
record[@"acceptedKey"]=(!reason.length?key:reference[@"acceptedKey"])?:@"";
|
||||||
if(key) {
|
if(key) {
|
||||||
NSData *serialized=[NSPropertyListSerialization dataWithPropertyList:record format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil];
|
NSData *serialized=[NSPropertyListSerialization dataWithPropertyList:record format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil];
|
||||||
NSUInteger bytes=serialized.length+512;
|
NSUInteger bytes=serialized.length+512;record[@"bytes"]=@(bytes);
|
||||||
record[@"bytes"]=@(bytes);
|
if(bytes<=16*1024*1024){[_entries addObject:[record copy]];_estimatedBytes+=bytes;}
|
||||||
// Replace changed raw geometry at the same timestamp rather than letting
|
while(_entries.count>1200||_estimatedBytes>16*1024*1024){_estimatedBytes-=[_entries.firstObject[@"bytes"] unsignedIntegerValue];[_entries removeObjectAtIndex:0];}
|
||||||
// callbacks grow history or make smoothing depend on redraw count.
|
_liveReference=[record copy];
|
||||||
for(NSInteger i=(NSInteger)_entries.count-1;i>=0;i--) {
|
}else{_liveReference=nil;[self resetObjectTracker];}
|
||||||
NSDictionary *old=_entries[(NSUInteger)i];
|
|
||||||
if([old[@"context"] isEqual:context] && [old[@"scope"] isEqual:scope] && [old[@"key"] isEqual:key]) {
|
|
||||||
_estimatedBytes-=[old[@"bytes"] unsignedIntegerValue];[_entries removeObjectAtIndex:(NSUInteger)i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(bytes<=16*1024*1024) {[_entries addObject:[record copy]];_estimatedBytes+=bytes;}
|
|
||||||
while(_entries.count>1200 || _estimatedBytes>16*1024*1024) {
|
|
||||||
_estimatedBytes-=[_entries.firstObject[@"bytes"] unsignedIntegerValue];[_entries removeObjectAtIndex:0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
[_mutex unlock];return [record copy];
|
[_mutex unlock];return [record copy];
|
||||||
}
|
}
|
||||||
@end
|
@end
|
||||||
@@ -338,6 +445,8 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
|
|||||||
CIColorKernel *_subtract;
|
CIColorKernel *_subtract;
|
||||||
CIColorKernel *_box;
|
CIColorKernel *_box;
|
||||||
PGRealtimeFaceTracker *_realtime;
|
PGRealtimeFaceTracker *_realtime;
|
||||||
|
NSMutableArray<NSDictionary *> *_observationCache;
|
||||||
|
CIContext *_visionContext;
|
||||||
}
|
}
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@@ -348,6 +457,8 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
|
|||||||
if (self) {
|
if (self) {
|
||||||
_lock = [[NSLock alloc] init];
|
_lock = [[NSLock alloc] init];
|
||||||
_realtime = [PGRealtimeFaceTracker new];
|
_realtime = [PGRealtimeFaceTracker new];
|
||||||
|
_observationCache = [NSMutableArray new];
|
||||||
|
_visionContext = [CIContext contextWithOptions:@{kCIContextCacheIntermediates:@NO}];
|
||||||
_targets = [NSMutableArray arrayWithObjects:[NSMutableDictionary dictionary], [NSMutableDictionary dictionary], [NSMutableDictionary dictionary], nil];
|
_targets = [NSMutableArray arrayWithObjects:[NSMutableDictionary dictionary], [NSMutableDictionary dictionary], [NSMutableDictionary dictionary], nil];
|
||||||
// Thin/V/chin/jaw/short reconstruct recovered local coordinate math.
|
// Thin/V/chin/jaw/short reconstruct recovered local coordinate math.
|
||||||
// Four other controls retain this project's analytic approximation.
|
// Four other controls retain this project's analytic approximation.
|
||||||
@@ -423,26 +534,53 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (NSArray<VNFaceObservation *> *)observationsInImage:(CIImage *)image settings:(PGBeautySettings)settings error:(NSError **)error {
|
- (NSArray<VNFaceObservation *> *)observationsInImage:(CIImage *)image settings:(PGBeautySettings)settings error:(NSError **)error {
|
||||||
CGRect extent = image.extent;
|
// 2.4.0 defaults to a 960-pixel long edge (its configurable downsample
|
||||||
CGFloat longest = MAX(extent.size.width, extent.size.height);
|
// limit), not the former 1280-pixel path. The explicit fast mode keeps its
|
||||||
CGFloat scale = MIN(1.0, (settings.fastMultiFace ? 720.0 : 1280.0) / longest);
|
// lower-cost detector. Both handlers receive an upright, zero-origin image.
|
||||||
CIImage *prepared = [image imageByApplyingTransform:CGAffineTransformMakeTranslation(-extent.origin.x, -extent.origin.y)];
|
CIImage *prepared=PGPreparedVisionImage(image,settings.fastMultiFace?720:960);
|
||||||
prepared = [prepared imageByApplyingTransform:CGAffineTransformMakeScale(scale, scale)];
|
VNDetectFaceLandmarksRequest *(^newRequest)(void)=^{
|
||||||
VNDetectFaceLandmarksRequest *request = [[VNDetectFaceLandmarksRequest alloc] init];
|
VNDetectFaceLandmarksRequest *r=[VNDetectFaceLandmarksRequest new];
|
||||||
request.revision = VNDetectFaceLandmarksRequestRevision3;
|
r.revision=VNDetectFaceLandmarksRequestRevision3;r.constellation=VNRequestFaceLandmarksConstellation76Points;return r;
|
||||||
request.constellation = VNRequestFaceLandmarksConstellation76Points;
|
};
|
||||||
// An image request handler is intentionally local to one invocation. A shared
|
VNDetectFaceLandmarksRequest *request=newRequest();NSError *firstError=nil;
|
||||||
// sequence handler would make parallel / reverse render order observable.
|
VNImageRequestHandler *handler=[[VNImageRequestHandler alloc] initWithCIImage:prepared orientation:kCGImagePropertyOrientationUp options:@{}];
|
||||||
VNImageRequestHandler *handler = [[VNImageRequestHandler alloc] initWithCIImage:prepared options:@{}];
|
BOOL ok=[handler performRequests:@[request] error:&firstError];
|
||||||
if (![handler performRequests:@[request] error:error]) return nil;
|
NSArray<VNFaceObservation *> *faces=ok?(request.results?:@[]):@[];
|
||||||
NSArray<VNFaceObservation *> *faces = request.results ?: @[];
|
NSUInteger count=0;for(VNFaceObservation *face in faces)count=MAX(count,face.landmarks.allPoints.pointCount);
|
||||||
return [faces sortedArrayUsingComparator:^NSComparisonResult(VNFaceObservation *a, VNFaceObservation *b) {
|
if(!ok||!faces.count||count<76) {
|
||||||
CGRect x = a.boundingBox, y = b.boundingBox;
|
// The reference retries a materialized RGBA image, not the same lazy
|
||||||
if (CGRectGetMidX(x) != CGRectGetMidX(y)) return CGRectGetMidX(x) < CGRectGetMidX(y) ? NSOrderedAscending : NSOrderedDescending;
|
// CIImage request. Retain a successful first result if retry finds none.
|
||||||
if (CGRectGetMidY(x) != CGRectGetMidY(y)) return CGRectGetMidY(x) < CGRectGetMidY(y) ? NSOrderedAscending : NSOrderedDescending;
|
CGColorSpaceRef space=CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
||||||
return x.size.width > y.size.width ? NSOrderedAscending : NSOrderedDescending;
|
CGImageRef cg=[_visionContext createCGImage:prepared fromRect:prepared.extent format:kCIFormatRGBA8 colorSpace:space];
|
||||||
|
CGColorSpaceRelease(space);
|
||||||
|
if(cg) {
|
||||||
|
VNDetectFaceLandmarksRequest *retry=newRequest();NSError *retryError=nil;
|
||||||
|
VNImageRequestHandler *fallback=[[VNImageRequestHandler alloc] initWithCGImage:cg orientation:kCGImagePropertyOrientationUp options:@{}];
|
||||||
|
BOOL retried=[fallback performRequests:@[retry] error:&retryError];CGImageRelease(cg);
|
||||||
|
if(retried){ok=YES;firstError=nil;if(retry.results.count)faces=retry.results;}
|
||||||
|
else if(!ok)firstError=retryError?:firstError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!ok){if(error)*error=firstError;return nil;}
|
||||||
|
return [faces sortedArrayUsingComparator:^NSComparisonResult(VNFaceObservation *a,VNFaceObservation *b) {
|
||||||
|
CGRect x=a.boundingBox,y=b.boundingBox;
|
||||||
|
if(CGRectGetMidX(x)!=CGRectGetMidX(y))return CGRectGetMidX(x)<CGRectGetMidX(y)?NSOrderedAscending:NSOrderedDescending;
|
||||||
|
if(CGRectGetMidY(x)!=CGRectGetMidY(y))return CGRectGetMidY(x)<CGRectGetMidY(y)?NSOrderedAscending:NSOrderedDescending;
|
||||||
|
return x.size.width>y.size.width?NSOrderedAscending:NSOrderedDescending;
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
- (NSArray<VNFaceObservation *> *)cachedObservationsInImage:(CIImage *)image settings:(PGBeautySettings)settings time:(CMTime)time error:(NSError **)error {
|
||||||
|
NSString *key=PGTimeKey(time);CGRect e=image.extent;
|
||||||
|
NSString *context=[NSString stringWithFormat:@"%u/%.17g/%.17g/%.17g/%.17g",settings.fastMultiFace,e.origin.x,e.origin.y,e.size.width,e.size.height];
|
||||||
|
@synchronized(_observationCache) {
|
||||||
|
if(key)for(NSDictionary *entry in _observationCache)if([entry[@"key"] isEqual:key]&&[entry[@"context"] isEqual:context])return entry[@"faces"];
|
||||||
|
NSArray *faces=[self observationsInImage:image settings:settings error:error];
|
||||||
|
// A transient API error is retriable; a successful empty result is a
|
||||||
|
// real frame result and is cached, so redraws cannot toggle the effect.
|
||||||
|
if(key&&faces){[_observationCache addObject:@{@"key":key,@"context":context,@"faces":faces}];while(_observationCache.count>120)[_observationCache removeObjectAtIndex:0];}
|
||||||
|
return faces;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
- (NSDictionary *)recordForTime:(CMTime)time target:(NSUInteger)target nearest:(BOOL)nearest {
|
- (NSDictionary *)recordForTime:(CMTime)time target:(NSUInteger)target nearest:(BOOL)nearest {
|
||||||
NSString *key = PGTimeKey(time);
|
NSString *key = PGTimeKey(time);
|
||||||
@@ -508,7 +646,7 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
|
|||||||
if (error) *error = [NSError errorWithDomain:PGFaceErrorDomain code:1 userInfo:@{NSLocalizedDescriptionKey:@"Face analysis requires a finite image and valid frame time."}];
|
if (error) *error = [NSError errorWithDomain:PGFaceErrorDomain code:1 userInfo:@{NSLocalizedDescriptionKey:@"Face analysis requires a finite image and valid frame time."}];
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
NSArray *observations = [self observationsInImage:image settings:settings error:error];
|
NSArray *observations = [self cachedObservationsInImage:image settings:settings time:time error:error];
|
||||||
if (!observations) return NO;
|
if (!observations) return NO;
|
||||||
NSUInteger target = MIN((NSUInteger)settings.targetIndex, 2);
|
NSUInteger target = MIN((NSUInteger)settings.targetIndex, 2);
|
||||||
NSDictionary *prior = [self recordForTime:time target:target nearest:YES];
|
NSDictionary *prior = [self recordForTime:time target:target nearest:YES];
|
||||||
@@ -628,7 +766,7 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
|
|||||||
for (NSDictionary *face in geometry) [restored addObject:[VNFaceObservation faceObservationWithRequestRevision:VNDetectFaceLandmarksRequestRevision3 boundingBox:PGArrayRect(face[@"box"]) roll:nil yaw:nil pitch:nil]];
|
for (NSDictionary *face in geometry) [restored addObject:[VNFaceObservation faceObservationWithRequestRevision:VNDetectFaceLandmarksRequestRevision3 boundingBox:PGArrayRect(face[@"box"]) roll:nil yaw:nil pitch:nil]];
|
||||||
observations = restored;
|
observations = restored;
|
||||||
} else {
|
} else {
|
||||||
NSArray *detected = [self observationsInImage:image settings:settings error:error];
|
NSArray *detected = [self cachedObservationsInImage:image settings:settings time:time error:error];
|
||||||
if (!detected) return nil;
|
if (!detected) return nil;
|
||||||
if(settings.trackingEnabled) {
|
if(settings.trackingEnabled) {
|
||||||
// Preserve the explicit offline-analysis contract. Its stored
|
// Preserve the explicit offline-analysis contract. Its stored
|
||||||
@@ -645,7 +783,7 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
|
|||||||
for(VNFaceObservation *observation in detected) if(PGValidRect(observation.boundingBox) && raw.count<16) {
|
for(VNFaceObservation *observation in detected) if(PGValidRect(observation.boundingBox) && raw.count<16) {
|
||||||
[raw addObject:PGGeometry(observation)];[validObservations addObject:observation];
|
[raw addObject:PGGeometry(observation)];[validObservations addObject:observation];
|
||||||
}
|
}
|
||||||
NSDictionary *live=[_realtime resultForGeometry:raw settings:settings time:time extent:extent];
|
NSDictionary *live=[_realtime resultForGeometry:raw settings:settings time:time extent:extent image:image];
|
||||||
geometry=live[@"output"];
|
geometry=live[@"output"];
|
||||||
if(settings.singleFace && !settings.fastMultiFace) {
|
if(settings.singleFace && !settings.fastMultiFace) {
|
||||||
NSInteger index=[live[@"index"] integerValue];
|
NSInteger index=[live[@"index"] integerValue];
|
||||||
@@ -665,6 +803,16 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
|
|||||||
// skin/teeth/eyes support, which still covers the detected group.
|
// skin/teeth/eyes support, which still covers the detected group.
|
||||||
appearance=raw;observations=validObservations;
|
appearance=raw;observations=validObservations;
|
||||||
}
|
}
|
||||||
|
if([live[@"held"] boolValue] && geometry.count) {
|
||||||
|
// Keep beauty masks on the same held primary shape as the warp,
|
||||||
|
// while retaining support for independently detected secondary faces.
|
||||||
|
NSMutableArray *supported=[appearance mutableCopy];
|
||||||
|
NSInteger index=[live[@"index"] integerValue];
|
||||||
|
if(!settings.singleFace && index>=0 && (NSUInteger)index<supported.count) supported[(NSUInteger)index]=geometry.firstObject;
|
||||||
|
else if(settings.singleFace || !supported.count) supported=[geometry mutableCopy];
|
||||||
|
else [supported addObjectsFromArray:geometry];
|
||||||
|
appearance=supported;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CIImage *skin = PGBlack(extent), *teeth = PGBlack(extent), *eyes = PGBlack(extent), *protection = PGBlack(extent), *overlay = PGTransparent(extent);
|
CIImage *skin = PGBlack(extent), *teeth = PGBlack(extent), *eyes = PGBlack(extent), *protection = PGBlack(extent), *overlay = PGTransparent(extent);
|
||||||
@@ -724,6 +872,7 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ
|
|||||||
for (NSMutableDictionary *target in _targets) [target removeAllObjects];
|
for (NSMutableDictionary *target in _targets) [target removeAllObjects];
|
||||||
[_lock unlock];
|
[_lock unlock];
|
||||||
[_realtime reset];
|
[_realtime reset];
|
||||||
|
@synchronized(_observationCache) { [_observationCache removeAllObjects]; }
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSData *)trackingDataForTarget:(NSUInteger)target {
|
- (NSData *)trackingDataForTarget:(NSUInteger)target {
|
||||||
|
|||||||
@@ -0,0 +1,321 @@
|
|||||||
|
{
|
||||||
|
"decodedSize": [
|
||||||
|
1920,
|
||||||
|
1080
|
||||||
|
],
|
||||||
|
"decodeOrVisionErrors": 0,
|
||||||
|
"duplicateSourceFramePairs": 0,
|
||||||
|
"durationSeconds": 18.752066666666668,
|
||||||
|
"emptyDetectionFrames": 0,
|
||||||
|
"frames": 469,
|
||||||
|
"heldFrames": 0,
|
||||||
|
"landmarkConfidenceBelow055Frames": 0,
|
||||||
|
"largestContourChanges": [
|
||||||
|
{
|
||||||
|
"actualSeconds": 18.651966666666667,
|
||||||
|
"bboxCenterStep": 0.010619987358574903,
|
||||||
|
"bboxRelativeSizeStep": 0.05204244367729771,
|
||||||
|
"faceConfidence": 1,
|
||||||
|
"faceCount": 1,
|
||||||
|
"frame": 467,
|
||||||
|
"held": false,
|
||||||
|
"landmarkConfidence": 0.817047119140625,
|
||||||
|
"outputAllShapeStep": 0.061719217959260164,
|
||||||
|
"outputContourShapeStep": 0.06457580929340678,
|
||||||
|
"points76Count": 76,
|
||||||
|
"qualityReason": "",
|
||||||
|
"rawAllShapeStep": 0.061719217959260164,
|
||||||
|
"rawContourShapeStep": 0.06457580929340678,
|
||||||
|
"rawEyeShapeStep": 0.057909273674708016,
|
||||||
|
"requestedSeconds": 18.68,
|
||||||
|
"resultReason": "",
|
||||||
|
"trackedBoxAvailable": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"actualSeconds": 6.3063,
|
||||||
|
"bboxCenterStep": 0.009772060063233024,
|
||||||
|
"bboxRelativeSizeStep": 0.013879898508151411,
|
||||||
|
"faceConfidence": 1,
|
||||||
|
"faceCount": 1,
|
||||||
|
"frame": 158,
|
||||||
|
"held": false,
|
||||||
|
"landmarkConfidence": 0.842681884765625,
|
||||||
|
"outputAllShapeStep": 0.0594987598669281,
|
||||||
|
"outputContourShapeStep": 0.059070265481990666,
|
||||||
|
"points76Count": 76,
|
||||||
|
"qualityReason": "",
|
||||||
|
"rawAllShapeStep": 0.0594987598669281,
|
||||||
|
"rawContourShapeStep": 0.059070265481990666,
|
||||||
|
"rawEyeShapeStep": 0.059058695174623185,
|
||||||
|
"requestedSeconds": 6.32,
|
||||||
|
"resultReason": "",
|
||||||
|
"trackedBoxAvailable": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"actualSeconds": 0.9342666666666667,
|
||||||
|
"bboxCenterStep": 0.005945873831186665,
|
||||||
|
"bboxRelativeSizeStep": 0.04857391780763898,
|
||||||
|
"faceConfidence": 1,
|
||||||
|
"faceCount": 1,
|
||||||
|
"frame": 24,
|
||||||
|
"held": false,
|
||||||
|
"landmarkConfidence": 0.860382080078125,
|
||||||
|
"outputAllShapeStep": 0.03990833410408158,
|
||||||
|
"outputContourShapeStep": 0.05205992923951523,
|
||||||
|
"points76Count": 76,
|
||||||
|
"qualityReason": "",
|
||||||
|
"rawAllShapeStep": 0.03990833410408158,
|
||||||
|
"rawContourShapeStep": 0.05205992923951523,
|
||||||
|
"rawEyeShapeStep": 0.03331563844059785,
|
||||||
|
"requestedSeconds": 0.96,
|
||||||
|
"resultReason": "",
|
||||||
|
"trackedBoxAvailable": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"actualSeconds": 1.0343666666666667,
|
||||||
|
"bboxCenterStep": 0.0035810993172984357,
|
||||||
|
"bboxRelativeSizeStep": 0.07745302528191012,
|
||||||
|
"faceConfidence": 1,
|
||||||
|
"faceCount": 1,
|
||||||
|
"frame": 26,
|
||||||
|
"held": false,
|
||||||
|
"landmarkConfidence": 0.86907958984375,
|
||||||
|
"outputAllShapeStep": 0.04015940118859808,
|
||||||
|
"outputContourShapeStep": 0.05153229113886389,
|
||||||
|
"points76Count": 76,
|
||||||
|
"qualityReason": "",
|
||||||
|
"rawAllShapeStep": 0.04015940118859808,
|
||||||
|
"rawContourShapeStep": 0.05153229113886389,
|
||||||
|
"rawEyeShapeStep": 0.04106869710572574,
|
||||||
|
"requestedSeconds": 1.04,
|
||||||
|
"resultReason": "",
|
||||||
|
"trackedBoxAvailable": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"actualSeconds": 6.8735333333333335,
|
||||||
|
"bboxCenterStep": 0.011081990813070965,
|
||||||
|
"bboxRelativeSizeStep": 0.041020958444265565,
|
||||||
|
"faceConfidence": 1,
|
||||||
|
"faceCount": 1,
|
||||||
|
"frame": 172,
|
||||||
|
"held": false,
|
||||||
|
"landmarkConfidence": 0.861907958984375,
|
||||||
|
"outputAllShapeStep": 0.04919706037542044,
|
||||||
|
"outputContourShapeStep": 0.047788293575915936,
|
||||||
|
"points76Count": 76,
|
||||||
|
"qualityReason": "",
|
||||||
|
"rawAllShapeStep": 0.04919706037542044,
|
||||||
|
"rawContourShapeStep": 0.047788293575915936,
|
||||||
|
"rawEyeShapeStep": 0.04811240328922642,
|
||||||
|
"requestedSeconds": 6.88,
|
||||||
|
"resultReason": "",
|
||||||
|
"trackedBoxAvailable": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"actualSeconds": 5.538866666666666,
|
||||||
|
"bboxCenterStep": 0.009174106285954426,
|
||||||
|
"bboxRelativeSizeStep": 0.0696669648408832,
|
||||||
|
"faceConfidence": 1,
|
||||||
|
"faceCount": 1,
|
||||||
|
"frame": 139,
|
||||||
|
"held": false,
|
||||||
|
"landmarkConfidence": 0.86724853515625,
|
||||||
|
"outputAllShapeStep": 0.04712320285757311,
|
||||||
|
"outputContourShapeStep": 0.047267593615817144,
|
||||||
|
"points76Count": 76,
|
||||||
|
"qualityReason": "",
|
||||||
|
"rawAllShapeStep": 0.04712320285757311,
|
||||||
|
"rawContourShapeStep": 0.047267593615817144,
|
||||||
|
"rawEyeShapeStep": 0.04635983371628053,
|
||||||
|
"requestedSeconds": 5.56,
|
||||||
|
"resultReason": "",
|
||||||
|
"trackedBoxAvailable": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"actualSeconds": 5.5055,
|
||||||
|
"bboxCenterStep": 0.009205666419765468,
|
||||||
|
"bboxRelativeSizeStep": 0.04702815798474558,
|
||||||
|
"faceConfidence": 1,
|
||||||
|
"faceCount": 1,
|
||||||
|
"frame": 138,
|
||||||
|
"held": false,
|
||||||
|
"landmarkConfidence": 0.8653411865234375,
|
||||||
|
"outputAllShapeStep": 0.04678117299069693,
|
||||||
|
"outputContourShapeStep": 0.0464975192908153,
|
||||||
|
"points76Count": 76,
|
||||||
|
"qualityReason": "",
|
||||||
|
"rawAllShapeStep": 0.04678117299069693,
|
||||||
|
"rawContourShapeStep": 0.0464975192908153,
|
||||||
|
"rawEyeShapeStep": 0.044731979492293225,
|
||||||
|
"requestedSeconds": 5.52,
|
||||||
|
"resultReason": "",
|
||||||
|
"trackedBoxAvailable": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"actualSeconds": 6.2729333333333335,
|
||||||
|
"bboxCenterStep": 0.010523844047228498,
|
||||||
|
"bboxRelativeSizeStep": 0.04067544887614405,
|
||||||
|
"faceConfidence": 1,
|
||||||
|
"faceCount": 1,
|
||||||
|
"frame": 157,
|
||||||
|
"held": false,
|
||||||
|
"landmarkConfidence": 0.835052490234375,
|
||||||
|
"outputAllShapeStep": 0.05580485366082702,
|
||||||
|
"outputContourShapeStep": 0.045154734035835926,
|
||||||
|
"points76Count": 76,
|
||||||
|
"qualityReason": "",
|
||||||
|
"rawAllShapeStep": 0.05580485366082702,
|
||||||
|
"rawContourShapeStep": 0.045154734035835926,
|
||||||
|
"rawEyeShapeStep": 0.06010488116983429,
|
||||||
|
"requestedSeconds": 6.28,
|
||||||
|
"resultReason": "",
|
||||||
|
"trackedBoxAvailable": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"actualSeconds": 18.7187,
|
||||||
|
"bboxCenterStep": 0.00847507465061345,
|
||||||
|
"bboxRelativeSizeStep": 0.001839415731091032,
|
||||||
|
"faceConfidence": 1,
|
||||||
|
"faceCount": 1,
|
||||||
|
"frame": 468,
|
||||||
|
"held": false,
|
||||||
|
"landmarkConfidence": 0.81689453125,
|
||||||
|
"outputAllShapeStep": 0.039795819667405995,
|
||||||
|
"outputContourShapeStep": 0.043322334703151985,
|
||||||
|
"points76Count": 76,
|
||||||
|
"qualityReason": "",
|
||||||
|
"rawAllShapeStep": 0.039795819667405995,
|
||||||
|
"rawContourShapeStep": 0.043322334703151985,
|
||||||
|
"rawEyeShapeStep": 0.033949487128638954,
|
||||||
|
"requestedSeconds": 18.72,
|
||||||
|
"resultReason": "",
|
||||||
|
"trackedBoxAvailable": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"actualSeconds": 6.973633333333333,
|
||||||
|
"bboxCenterStep": 0.004802853824391058,
|
||||||
|
"bboxRelativeSizeStep": 0.0033162879732389072,
|
||||||
|
"faceConfidence": 1,
|
||||||
|
"faceCount": 1,
|
||||||
|
"frame": 175,
|
||||||
|
"held": false,
|
||||||
|
"landmarkConfidence": 0.850616455078125,
|
||||||
|
"outputAllShapeStep": 0.029986598311175987,
|
||||||
|
"outputContourShapeStep": 0.042164697332718326,
|
||||||
|
"points76Count": 76,
|
||||||
|
"qualityReason": "",
|
||||||
|
"rawAllShapeStep": 0.029986598311175987,
|
||||||
|
"rawContourShapeStep": 0.042164697332718326,
|
||||||
|
"rawEyeShapeStep": 0.026900138652230798,
|
||||||
|
"requestedSeconds": 7,
|
||||||
|
"resultReason": "",
|
||||||
|
"trackedBoxAvailable": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"actualSeconds": 6.740066666666666,
|
||||||
|
"bboxCenterStep": 0.008964342160362288,
|
||||||
|
"bboxRelativeSizeStep": 0.018475143321662002,
|
||||||
|
"faceConfidence": 1,
|
||||||
|
"faceCount": 1,
|
||||||
|
"frame": 169,
|
||||||
|
"held": false,
|
||||||
|
"landmarkConfidence": 0.857940673828125,
|
||||||
|
"outputAllShapeStep": 0.04248999607570733,
|
||||||
|
"outputContourShapeStep": 0.04199222726351612,
|
||||||
|
"points76Count": 76,
|
||||||
|
"qualityReason": "",
|
||||||
|
"rawAllShapeStep": 0.04248999607570733,
|
||||||
|
"rawContourShapeStep": 0.04199222726351612,
|
||||||
|
"rawEyeShapeStep": 0.0441200591307651,
|
||||||
|
"requestedSeconds": 6.76,
|
||||||
|
"resultReason": "",
|
||||||
|
"trackedBoxAvailable": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"actualSeconds": 6.172833333333333,
|
||||||
|
"bboxCenterStep": 0.006930516638962855,
|
||||||
|
"bboxRelativeSizeStep": 0.031192722054552036,
|
||||||
|
"faceConfidence": 1,
|
||||||
|
"faceCount": 1,
|
||||||
|
"frame": 155,
|
||||||
|
"held": false,
|
||||||
|
"landmarkConfidence": 0.8632049560546875,
|
||||||
|
"outputAllShapeStep": 0.03366104158267517,
|
||||||
|
"outputContourShapeStep": 0.04180684654646812,
|
||||||
|
"points76Count": 76,
|
||||||
|
"qualityReason": "",
|
||||||
|
"rawAllShapeStep": 0.03366104158267517,
|
||||||
|
"rawContourShapeStep": 0.04180684654646812,
|
||||||
|
"rawEyeShapeStep": 0.029757180729629833,
|
||||||
|
"requestedSeconds": 6.2,
|
||||||
|
"resultReason": "",
|
||||||
|
"trackedBoxAvailable": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"multipleFaceFrames": 0,
|
||||||
|
"nominalSourceFPS": 29.970029830932617,
|
||||||
|
"outputWithout76Frames": 0,
|
||||||
|
"partial76Frames": 0,
|
||||||
|
"samplingFPS": 25,
|
||||||
|
"scope": "Independent offline decoding and the current production detector/tracker; not FCP render logs or target execution. Shape steps remove previous bbox translation/scale, but still include actual pose/expression changes.",
|
||||||
|
"sourceBasename": "girl-in-sun-hat-smiling-in-rural-field-2026-10-03-13-54-14-utc.mov",
|
||||||
|
"statistics": {
|
||||||
|
"bboxCenterStep": {
|
||||||
|
"max": 0.011081990813070965,
|
||||||
|
"median": 0.002747360384438056,
|
||||||
|
"min": 0.00014653804115170532,
|
||||||
|
"n": 468,
|
||||||
|
"p95": 0.006589111497080662
|
||||||
|
},
|
||||||
|
"bboxRelativeSizeStep": {
|
||||||
|
"max": 0.07745302528191012,
|
||||||
|
"median": 0.009236047113881152,
|
||||||
|
"min": 9.126808167081446e-05,
|
||||||
|
"n": 468,
|
||||||
|
"p95": 0.03867668227601517
|
||||||
|
},
|
||||||
|
"landmarkConfidence": {
|
||||||
|
"max": 0.9249267578125,
|
||||||
|
"median": 0.867706298828125,
|
||||||
|
"min": 0.81689453125,
|
||||||
|
"n": 469,
|
||||||
|
"p95": 0.9015045166015625
|
||||||
|
},
|
||||||
|
"outputAllShapeStep": {
|
||||||
|
"max": 0.061719217959260164,
|
||||||
|
"median": 0.011493205285858545,
|
||||||
|
"min": 0.003266396399148707,
|
||||||
|
"n": 468,
|
||||||
|
"p95": 0.03265326908712946
|
||||||
|
},
|
||||||
|
"outputContourShapeStep": {
|
||||||
|
"max": 0.06457580929340678,
|
||||||
|
"median": 0.013767751423829355,
|
||||||
|
"min": 0.0022585721609373914,
|
||||||
|
"n": 468,
|
||||||
|
"p95": 0.03343570504134135
|
||||||
|
},
|
||||||
|
"rawAllShapeStep": {
|
||||||
|
"max": 0.061719217959260164,
|
||||||
|
"median": 0.011493205285858545,
|
||||||
|
"min": 0.003266396399148707,
|
||||||
|
"n": 468,
|
||||||
|
"p95": 0.03265326908712946
|
||||||
|
},
|
||||||
|
"rawContourShapeStep": {
|
||||||
|
"max": 0.06457580929340678,
|
||||||
|
"median": 0.013767751423829355,
|
||||||
|
"min": 0.0022585721609373914,
|
||||||
|
"n": 468,
|
||||||
|
"p95": 0.03343570504134135
|
||||||
|
},
|
||||||
|
"rawEyeShapeStep": {
|
||||||
|
"max": 0.06010488116983429,
|
||||||
|
"median": 0.010554830995294154,
|
||||||
|
"min": 0.0018890160668284224,
|
||||||
|
"n": 468,
|
||||||
|
"p95": 0.03347895433098691
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timelineOffsetSeconds": 18.72
|
||||||
|
}
|
||||||
文件差异过大,未显示。
加载差异
@@ -94,3 +94,10 @@ build/tracking-diagnostics/inspect-sequence '/绝对路径/源素材.mov' \
|
|||||||
|
|
||||||
|
|
||||||
分析与连续帧诊断的可恢复节点为 `a0c5357efd87a63dbee6269d9335e6aa3ba78799`,已推送并读回双远端。从 GitHub 匿名独立克隆核对284个文件的字节/blob/执行位、strict/full fsck,并重新构建合成诊断、逐项复现保存的JSON;产品源码与v1.3.2相同,无LFS/子模块。见[恢复回执](tracking-flicker-20261022/recovery.json)。后续main仅补充恢复说明,不改变已安装版或历史标签。
|
分析与连续帧诊断的可恢复节点为 `a0c5357efd87a63dbee6269d9335e6aa3ba78799`,已推送并读回双远端。从 GitHub 匿名独立克隆核对284个文件的字节/blob/执行位、strict/full fsck,并重新构建合成诊断、逐项复现保存的JSON;产品源码与v1.3.2相同,无LFS/子模块。见[恢复回执](tracking-flicker-20261022/recovery.json)。后续main仅补充恢复说明,不改变已安装版或历史标签。
|
||||||
|
|
||||||
|
|
||||||
|
## 2.4.0 对齐实施中(2026-10-22)
|
||||||
|
|
||||||
|
用户已要求实施。新增静态证据见 `tracking-flicker-20261022/implementation-evidence.json`。补核对检测长边默认960(0x1000bd1f8),失败/空结果/不足76点时使用RGBA CGImage重试;下巴轴为点66、67、68按1:1.65:1加权减脸框中心,并非眼轴。接纳分支仍为原样复制合格点。新代码已加入对应检测、连续框、质量门槛、下巴轴/中位数/P90保护和保持重映射,反向/模式变化重置对象跟踪,同帧检测和几何复用。
|
||||||
|
|
||||||
|
同一草帽素材1920解码、25fps离线重跑469帧:连续框469帧可用,检测/完整76/结构/置信均合格,held为0。输出轮廓步幅P95为0.03344、峰值0.06458;旧输出P95为0.02872、峰值0.05240。两者包含真实动作,不能把更大的输出步幅称为闪烁改善,也不能用合成坏点测试代替此片段的最终视觉验收。实施继续追查关键点后的变形路径;尚未发布或替换已安装版。完整实拍点阵/帧不入仓,仅保留标量摘要。
|
||||||
@@ -68,7 +68,7 @@ int main(int argc,const char *argv[]) {@autoreleasepool {
|
|||||||
NSMutableArray *raw=[NSMutableArray new],*faces=[NSMutableArray new];
|
NSMutableArray *raw=[NSMutableArray new],*faces=[NSMutableArray new];
|
||||||
for(VNFaceObservation *face in observations)if(PGValidRect(face.boundingBox)&&raw.count<16){[raw addObject:PGGeometry(face)];[faces addObject:face];}
|
for(VNFaceObservation *face in observations)if(PGValidRect(face.boundingBox)&&raw.count<16){[raw addObject:PGGeometry(face)];[faces addObject:face];}
|
||||||
CMTime time=CMTimeAdd(CMTimeMake(frame,fps),CMTimeMakeWithSeconds(offset,60000));
|
CMTime time=CMTimeAdd(CMTimeMake(frame,fps),CMTimeMakeWithSeconds(offset,60000));
|
||||||
NSDictionary *r=[tracker resultForGeometry:raw settings:settings time:time extent:image.extent];
|
NSDictionary *r=[tracker resultForGeometry:raw settings:settings time:time extent:image.extent image:image];
|
||||||
NSInteger index=[r[@"index"] integerValue];NSDictionary *current=(index>=0&&(NSUInteger)index<raw.count)?raw[index]:nil;
|
NSInteger index=[r[@"index"] integerValue];NSDictionary *current=(index>=0&&(NSUInteger)index<raw.count)?raw[index]:nil;
|
||||||
VNFaceObservation *face=(index>=0&&(NSUInteger)index<faces.count)?faces[index]:nil;
|
VNFaceObservation *face=(index>=0&&(NSUInteger)index<faces.count)?faces[index]:nil;
|
||||||
NSDictionary *stable=[r[@"output"] firstObject];
|
NSDictionary *stable=[r[@"output"] firstObject];
|
||||||
@@ -86,7 +86,7 @@ int main(int argc,const char *argv[]) {@autoreleasepool {
|
|||||||
}
|
}
|
||||||
NSMutableDictionary *row=[@{@"frame":@(frame),@"requestedSeconds":@(requested),@"actualSeconds":@(CMTimeGetSeconds(actual)),
|
NSMutableDictionary *row=[@{@"frame":@(frame),@"requestedSeconds":@(requested),@"actualSeconds":@(CMTimeGetSeconds(actual)),
|
||||||
@"faceCount":@(raw.count),@"points76Count":@([current[@"points76"] count]),@"held":r[@"held"],
|
@"faceCount":@(raw.count),@"points76Count":@([current[@"points76"] count]),@"held":r[@"held"],
|
||||||
@"landmarkConfidence":NumberOrNull(confidence),@"faceConfidence":NumberOrNull(face?face.confidence:NAN),
|
@"qualityReason":current?PGGeometryQualityReason(current):@"missingFace",@"resultReason":r[@"reason"]?:@"",@"trackedBoxAvailable":@([r[@"trackedBox"] count]==4),@"landmarkConfidence":NumberOrNull(confidence),@"faceConfidence":NumberOrNull(face?face.confidence:NAN),
|
||||||
@"rawContourShapeStep":NumberOrNull(ShapeRMS(priorRaw,current,59,75)),
|
@"rawContourShapeStep":NumberOrNull(ShapeRMS(priorRaw,current,59,75)),
|
||||||
@"outputContourShapeStep":NumberOrNull(ShapeRMS(priorOutput,stable,59,75)),
|
@"outputContourShapeStep":NumberOrNull(ShapeRMS(priorOutput,stable,59,75)),
|
||||||
@"rawAllShapeStep":NumberOrNull(ShapeRMS(priorRaw,current,0,75)),
|
@"rawAllShapeStep":NumberOrNull(ShapeRMS(priorRaw,current,0,75)),
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户