Align default realtime face-follow with automatic primary association and stabilization
这个提交包含在:
1 父节点
764994a1c4
当前提交
e03e460a9b
4 个文件已更改
+392
-14
没有与搜索条件相匹配的文件
@@ -10,6 +10,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
/// Vision observations in normalized, bottom-left image coordinates. Restored
|
/// Vision observations in normalized, bottom-left image coordinates. Restored
|
||||||
/// observations retain their bounding box; render geometry is stored separately.
|
/// observations retain their bounding box; render geometry is stored separately.
|
||||||
@property (nonatomic, copy, readonly) NSArray<VNFaceObservation *> *faces;
|
@property (nonatomic, copy, readonly) NSArray<VNFaceObservation *> *faces;
|
||||||
|
/// Stable, normalized control geometry used for image and mask deformation.
|
||||||
|
/// Default realtime processing deforms one primary face while faces/masks may
|
||||||
|
/// cover all current detections. Explicit fast-multi mode deforms every face.
|
||||||
|
@property (nonatomic, copy, readonly) NSArray<NSDictionary *> *deformationGeometry;
|
||||||
@property (nonatomic, strong, readonly) CIImage *skinMask;
|
@property (nonatomic, strong, readonly) CIImage *skinMask;
|
||||||
/// Continuous face support before eye/nose/lip detail-protection cutouts.
|
/// Continuous face support before eye/nose/lip detail-protection cutouts.
|
||||||
@property (nonatomic, strong, readonly) CIImage *colorMask;
|
@property (nonatomic, strong, readonly) CIImage *colorMask;
|
||||||
@@ -31,7 +35,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
error:(NSError * _Nullable * _Nullable)error;
|
error:(NSError * _Nullable * _Nullable)error;
|
||||||
- (void)resetTracking;
|
- (void)resetTracking;
|
||||||
/// Targets are zero-based (0, 1, 2). Data is a versioned property-list archive,
|
/// Targets are zero-based (0, 1, 2). Data is a versioned property-list archive,
|
||||||
/// independent of Vision implementation classes. Only analysis writes frames.
|
/// independent of Vision implementation classes. Only analysis writes these
|
||||||
|
/// persisted frames; realtime geometry has a separate bounded, in-memory cache.
|
||||||
- (nullable NSData *)trackingDataForTarget:(NSUInteger)target;
|
- (nullable NSData *)trackingDataForTarget:(NSUInteger)target;
|
||||||
- (void)setTrackingData:(nullable NSData *)data forTarget:(NSUInteger)target;
|
- (void)setTrackingData:(nullable NSData *)data forTarget:(NSUInteger)target;
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ static NSString * const PGFaceErrorDomain = @"media.dayu.PaintFCP.Face";
|
|||||||
|
|
||||||
@interface PGFaceFrame ()
|
@interface PGFaceFrame ()
|
||||||
@property (nonatomic, copy, readwrite) NSArray<VNFaceObservation *> *faces;
|
@property (nonatomic, copy, readwrite) NSArray<VNFaceObservation *> *faces;
|
||||||
|
@property (nonatomic, copy, readwrite) NSArray<NSDictionary *> *deformationGeometry;
|
||||||
@property (nonatomic, strong, readwrite) CIImage *skinMask;
|
@property (nonatomic, strong, readwrite) CIImage *skinMask;
|
||||||
@property (nonatomic, strong, readwrite) CIImage *colorMask;
|
@property (nonatomic, strong, readwrite) CIImage *colorMask;
|
||||||
@property (nonatomic, strong, readwrite) CIImage *teethMask;
|
@property (nonatomic, strong, readwrite) CIImage *teethMask;
|
||||||
@@ -119,6 +120,151 @@ static CIImage *PGMaximum(CIImage *a, CIImage *b) {
|
|||||||
return [a imageByApplyingFilter:@"CIMaximumCompositing" withInputParameters:@{kCIInputBackgroundImageKey:b}];
|
return [a imageByApplyingFilter:@"CIMaximumCompositing" withInputParameters:@{kCIInputBackgroundImageKey:b}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static CMTime PGRecordTime(NSDictionary *record) {
|
||||||
|
return CMTimeMakeWithEpoch([record[@"value"] longLongValue], [record[@"scale"] intValue], [record[@"epoch"] longLongValue]);
|
||||||
|
}
|
||||||
|
static double PGSeedDistance(NSDictionary *record, PGBeautySettings settings) {
|
||||||
|
return hypot([record[@"seedX"] doubleValue]-settings.selectionX, [record[@"seedY"] doubleValue]-settings.selectionY);
|
||||||
|
}
|
||||||
|
static NSArray<NSString *> *PGGeometryRegions(void) {
|
||||||
|
return @[@"leftEye",@"rightEye",@"leftBrow",@"rightBrow",@"nose",@"lips",@"mouth",@"contour"];
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
current.origin.y+([point[1] doubleValue]-old.origin.y)*current.size.height/old.size.height);
|
||||||
|
}
|
||||||
|
static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previous, double dt) {
|
||||||
|
CGRect old=PGArrayRect(previous[@"box"]),box=PGArrayRect(current[@"box"]);
|
||||||
|
if(!PGValidRect(old) || !PGValidRect(box))return current;
|
||||||
|
// Independent stabilization: translate/scale old landmarks into the current
|
||||||
|
// detection box first, so normal head motion does not accumulate box lag.
|
||||||
|
double weight=.9*fmin(1,fmax(0,dt/.03));
|
||||||
|
CGPoint oldLE=PGCenter(PGRegionBounds(previous,@"leftEye",PGInFace(old,.16,.53,.25,.15)));
|
||||||
|
CGPoint oldRE=PGCenter(PGRegionBounds(previous,@"rightEye",PGInFace(old,.59,.53,.25,.15)));
|
||||||
|
CGPoint newLE=PGCenter(PGRegionBounds(current,@"leftEye",PGInFace(box,.16,.53,.25,.15)));
|
||||||
|
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;
|
||||||
|
double oldLength=hypot(ox,oy),newLength=hypot(nx,ny);
|
||||||
|
BOOL axisHold=oldLength>MAX(.003,.055*MIN(old.size.height,box.size.height)) &&
|
||||||
|
newLength>MAX(.003,.055*MIN(old.size.height,box.size.height)) &&
|
||||||
|
(ox*nx+oy*ny)/(oldLength*newLength)<-.18;
|
||||||
|
NSMutableDictionary *stable=[NSMutableDictionary dictionaryWithObject:current[@"box"] forKey:@"box"];
|
||||||
|
for(NSString *name in PGGeometryRegions()) {
|
||||||
|
NSArray *a=previous[name],*b=current[name];
|
||||||
|
if(!a.count || (!axisHold && a.count!=b.count)) {stable[name]=b;continue;}
|
||||||
|
NSMutableArray *points=[NSMutableArray arrayWithCapacity:a.count];
|
||||||
|
for(NSUInteger i=0;i<a.count;i++) {
|
||||||
|
CGPoint p=PGRemapPoint(a[i],old,box);
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
return [stable copy];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
NSLock *_mutex;
|
||||||
|
NSMutableArray<NSDictionary *> *_entries;
|
||||||
|
NSUInteger _estimatedBytes;
|
||||||
|
}
|
||||||
|
- (NSDictionary *)resultForGeometry:(NSArray<NSDictionary *> *)raw settings:(PGBeautySettings)settings time:(CMTime)time extent:(CGRect)extent;
|
||||||
|
- (void)reset;
|
||||||
|
- (NSDictionary *)cacheStatistics;
|
||||||
|
@end
|
||||||
|
@implementation PGRealtimeFaceTracker
|
||||||
|
- (instancetype)init {if((self=[super init])){_mutex=[NSLock new];_entries=[NSMutableArray new];}return self;}
|
||||||
|
- (void)reset {[_mutex lock];[_entries removeAllObjects];_estimatedBytes=0;[_mutex unlock];}
|
||||||
|
- (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 {
|
||||||
|
NSMutableArray *valid=[NSMutableArray new];
|
||||||
|
for(id item in input) {NSDictionary *g=PGSanitizedGeometry(item);if(g && valid.count<16)[valid addObject:g];}
|
||||||
|
NSArray *raw=[valid copy];
|
||||||
|
NSUInteger mode=s.fastMultiFace?2:(s.singleFace?1:0);
|
||||||
|
NSString *key=PGTimeKey(time);
|
||||||
|
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];
|
||||||
|
[_mutex lock];
|
||||||
|
NSDictionary *head=nil;
|
||||||
|
for(NSDictionary *record in _entries) if([record[@"context"] isEqual:context] &&
|
||||||
|
(!head || CMTimeCompare(PGRecordTime(record),PGRecordTime(head))>=0)) head=record;
|
||||||
|
NSString *scope=@"automatic";
|
||||||
|
double seedX=PGClamp(s.selectionX,0,1),seedY=PGClamp(s.selectionY,0,1);
|
||||||
|
if(mode==1) {
|
||||||
|
// A real point change (> .01) starts a new identity scope. Older render
|
||||||
|
// snapshots may use their own past scope, but never replace a newer head.
|
||||||
|
NSDictionary *seedRecord=head;
|
||||||
|
if(seedRecord && CMTimeCompare(time,PGRecordTime(seedRecord))<0) {
|
||||||
|
seedRecord=nil;
|
||||||
|
for(NSDictionary *record in _entries) if([record[@"context"] isEqual:context] && PGSeedDistance(record,s)<=.01 &&
|
||||||
|
CMTimeCompare(PGRecordTime(record),time)<=0 && (!seedRecord || CMTimeCompare(PGRecordTime(record),PGRecordTime(seedRecord))>=0))seedRecord=record;
|
||||||
|
}
|
||||||
|
if(seedRecord && PGSeedDistance(seedRecord,s)<=.01) {
|
||||||
|
scope=seedRecord[@"scope"];seedX=[seedRecord[@"seedX"] doubleValue];seedY=[seedRecord[@"seedY"] doubleValue];
|
||||||
|
} else scope=[NSString stringWithFormat:@"%@/%.9g/%.9g",key?:@"invalid",seedX,seedY];
|
||||||
|
}
|
||||||
|
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"]);
|
||||||
|
NSInteger index=-1;double best=-DBL_MAX;
|
||||||
|
if(mode!=2) for(NSUInteger i=0;i<raw.count;i++) {
|
||||||
|
CGRect box=PGArrayRect(raw[i][@"box"]);CGPoint center=PGCenter(box);double score;
|
||||||
|
if(PGValidRect(previousBox)) {
|
||||||
|
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));
|
||||||
|
if(center.x<CGRectGetMinX(guard)||center.x>CGRectGetMaxX(guard)||center.y<CGRectGetMinY(guard)||center.y>CGRectGetMaxY(guard))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));
|
||||||
|
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));}
|
||||||
|
if(index<0 || score>best) {index=(NSInteger)i;best=score;}
|
||||||
|
}
|
||||||
|
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):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),
|
||||||
|
@"seedX":@(seedX),@"seedY":@(seedY),@"raw":raw,@"output":output,@"index":@(index),@"held":@(held),@"referenceKey":reference[@"key"]?:@""} mutableCopy];
|
||||||
|
if(key) {
|
||||||
|
NSData *serialized=[NSPropertyListSerialization dataWithPropertyList:record format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil];
|
||||||
|
NSUInteger bytes=serialized.length+512;
|
||||||
|
record[@"bytes"]=@(bytes);
|
||||||
|
// Replace changed raw geometry at the same timestamp rather than letting
|
||||||
|
// callbacks grow history or make smoothing depend on redraw count.
|
||||||
|
for(NSInteger i=(NSInteger)_entries.count-1;i>=0;i--) {
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
@interface PGFaceProcessor () {
|
@interface PGFaceProcessor () {
|
||||||
NSLock *_lock;
|
NSLock *_lock;
|
||||||
NSMutableArray<NSMutableDictionary<NSString *, NSDictionary *> *> *_targets;
|
NSMutableArray<NSMutableDictionary<NSString *, NSDictionary *> *> *_targets;
|
||||||
@@ -126,6 +272,7 @@ static CIImage *PGMaximum(CIImage *a, CIImage *b) {
|
|||||||
CIColorKernel *_ellipse;
|
CIColorKernel *_ellipse;
|
||||||
CIColorKernel *_subtract;
|
CIColorKernel *_subtract;
|
||||||
CIColorKernel *_box;
|
CIColorKernel *_box;
|
||||||
|
PGRealtimeFaceTracker *_realtime;
|
||||||
}
|
}
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@@ -135,6 +282,7 @@ static CIImage *PGMaximum(CIImage *a, CIImage *b) {
|
|||||||
self = [super init];
|
self = [super init];
|
||||||
if (self) {
|
if (self) {
|
||||||
_lock = [[NSLock alloc] init];
|
_lock = [[NSLock alloc] init];
|
||||||
|
_realtime = [PGRealtimeFaceTracker new];
|
||||||
_targets = [NSMutableArray arrayWithObjects:[NSMutableDictionary dictionary], [NSMutableDictionary dictionary], [NSMutableDictionary dictionary], nil];
|
_targets = [NSMutableArray arrayWithObjects:[NSMutableDictionary dictionary], [NSMutableDictionary dictionary], [NSMutableDictionary dictionary], nil];
|
||||||
// Analytic, feathered sampling deformation authored for this project.
|
// Analytic, feathered sampling deformation authored for this project.
|
||||||
// Positive short-face shortens the lower face; positive chin lengthens it.
|
// Positive short-face shortens the lower face; positive chin lengthens it.
|
||||||
@@ -292,29 +440,56 @@ static CIImage *PGMaximum(CIImage *a, CIImage *b) {
|
|||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
NSDictionary *record = settings.trackingEnabled ? [self recordForTime:time target:MIN((NSUInteger)settings.targetIndex, 2) nearest:NO] : nil;
|
NSDictionary *record = settings.trackingEnabled ? [self recordForTime:time target:MIN((NSUInteger)settings.targetIndex, 2) nearest:NO] : nil;
|
||||||
NSArray<NSDictionary *> *geometry;
|
NSArray<NSDictionary *> *geometry,*appearance;
|
||||||
NSArray<VNFaceObservation *> *observations;
|
NSArray<VNFaceObservation *> *observations;
|
||||||
if (record) {
|
if (record) {
|
||||||
geometry = record[@"faces"];
|
geometry = record[@"faces"];
|
||||||
|
appearance = geometry;
|
||||||
NSMutableArray *restored = [NSMutableArray array];
|
NSMutableArray *restored = [NSMutableArray array];
|
||||||
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 observationsInImage:image settings:settings error:error];
|
||||||
if (!detected) return nil;
|
if (!detected) return nil;
|
||||||
// A missing time may use a nearby analyzed identity as a guard, while
|
if(settings.trackingEnabled) {
|
||||||
// still detecting the current image. Rendering never appends to history.
|
// Preserve the explicit offline-analysis contract. Its stored
|
||||||
NSDictionary *anchor = settings.trackingEnabled ? [self recordForTime:time target:MIN((NSUInteger)settings.targetIndex, 2) nearest:YES] : nil;
|
// identity fallback never reads or writes realtime temporal state.
|
||||||
geometry = [self geometryForObservations:detected settings:settings previous:anchor];
|
NSDictionary *anchor=[self recordForTime:time target:MIN((NSUInteger)settings.targetIndex,2) nearest:YES];
|
||||||
NSMutableArray *selected = [NSMutableArray array];
|
geometry=[self geometryForObservations:detected settings:settings previous:anchor];
|
||||||
for (NSDictionary *face in geometry) {
|
appearance=geometry;
|
||||||
CGRect box = PGArrayRect(face[@"box"]);
|
NSMutableArray *selected=[NSMutableArray new];
|
||||||
for (VNFaceObservation *observation in detected) if (CGRectEqualToRect(observation.boundingBox, box)) { [selected addObject:observation]; break; }
|
for(NSDictionary *face in geometry) for(VNFaceObservation *observation in detected)
|
||||||
|
if(CGRectEqualToRect(observation.boundingBox,PGArrayRect(face[@"box"]))) {[selected addObject:observation];break;}
|
||||||
|
observations=selected;
|
||||||
|
} else {
|
||||||
|
NSMutableArray *raw=[NSMutableArray new],*validObservations=[NSMutableArray new];
|
||||||
|
for(VNFaceObservation *observation in detected) if(PGValidRect(observation.boundingBox) && raw.count<16) {
|
||||||
|
[raw addObject:PGGeometry(observation)];[validObservations addObject:observation];
|
||||||
|
}
|
||||||
|
NSDictionary *live=[_realtime resultForGeometry:raw settings:settings time:time extent:extent];
|
||||||
|
geometry=live[@"output"];
|
||||||
|
if(settings.singleFace && !settings.fastMultiFace) {
|
||||||
|
NSInteger index=[live[@"index"] integerValue];
|
||||||
|
if(index>=0 && (NSUInteger)index<validObservations.count) {
|
||||||
|
// Retain the selected original Vision observation. A stable
|
||||||
|
// geometry box need not equal its unsmoothed detection box.
|
||||||
|
observations=@[validObservations[(NSUInteger)index]];
|
||||||
|
appearance=@[raw[(NSUInteger)index]];
|
||||||
|
} else {
|
||||||
|
appearance=geometry;
|
||||||
|
NSMutableArray *held=[NSMutableArray new];
|
||||||
|
for(NSDictionary *face in geometry) [held addObject:[VNFaceObservation faceObservationWithRequestRevision:VNDetectFaceLandmarksRequestRevision3 boundingBox:PGArrayRect(face[@"box"]) roll:nil yaw:nil pitch:nil]];
|
||||||
|
observations=held;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Default single-primary geometry does not narrow the separate
|
||||||
|
// skin/teeth/eyes support, which still covers the detected group.
|
||||||
|
appearance=raw;observations=validObservations;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
observations = selected;
|
|
||||||
}
|
}
|
||||||
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);
|
||||||
for (NSDictionary *face in geometry) {
|
for (NSDictionary *face in appearance) {
|
||||||
CGRect box = PGArrayRect(face[@"box"]);
|
CGRect box = PGArrayRect(face[@"box"]);
|
||||||
CGRect skinRect = PGInFace(box, -0.10, -0.04, 1.20, 1.16);
|
CGRect skinRect = PGInFace(box, -0.10, -0.04, 1.20, 1.16);
|
||||||
skin = PGMaximum(skin, [self ellipseInRect:PGToPixels(skinRect, extent) extent:extent inner:0.78]);
|
skin = PGMaximum(skin, [self ellipseInRect:PGToPixels(skinRect, extent) extent:extent inner:0.78]);
|
||||||
@@ -334,17 +509,24 @@ static CIImage *PGMaximum(CIImage *a, CIImage *b) {
|
|||||||
}
|
}
|
||||||
CGRect mouth = PGRegionBounds(face, @"mouth", PGInFace(box, 0.30, 0.20, 0.40, 0.08));
|
CGRect mouth = PGRegionBounds(face, @"mouth", PGInFace(box, 0.30, 0.20, 0.40, 0.08));
|
||||||
teeth = PGMaximum(teeth, [self ellipseInRect:PGToPixels(mouth, extent) extent:extent inner:0.35]);
|
teeth = PGMaximum(teeth, [self ellipseInRect:PGToPixels(mouth, extent) extent:extent inner:0.35]);
|
||||||
if (settings.showFaceBoxes || settings.showTrackBox) {
|
if (settings.showFaceBoxes) {
|
||||||
CGRect pixels = PGToPixels(box, extent);
|
CGRect pixels = PGToPixels(box, extent);
|
||||||
CGFloat inset = MAX(1, MIN(pixels.size.width, pixels.size.height) * 0.005);
|
CGFloat inset = MAX(1, MIN(pixels.size.width, pixels.size.height) * 0.005);
|
||||||
CIImage *outline = [_box applyWithExtent:extent arguments:@[[CIVector vectorWithX:CGRectGetMinX(pixels) Y:CGRectGetMinY(pixels) Z:CGRectGetMaxX(pixels) W:CGRectGetMaxY(pixels)], @(inset), [CIVector vectorWithX:0.12 Y:1.0 Z:0.35 W:0.95]]];
|
CIImage *outline = [_box applyWithExtent:extent arguments:@[[CIVector vectorWithX:CGRectGetMinX(pixels) Y:CGRectGetMinY(pixels) Z:CGRectGetMaxX(pixels) W:CGRectGetMaxY(pixels)], @(inset), [CIVector vectorWithX:0.12 Y:1.0 Z:0.35 W:0.95]]];
|
||||||
overlay = [outline imageByCompositingOverImage:overlay];
|
overlay = [outline imageByCompositingOverImage:overlay];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(settings.showTrackBox) for(NSDictionary *face in geometry) {
|
||||||
|
CGRect pixels=PGToPixels(PGArrayRect(face[@"box"]),extent);
|
||||||
|
CGFloat inset=MAX(1,MIN(pixels.size.width,pixels.size.height)*.005);
|
||||||
|
CIImage *outline=[_box applyWithExtent:extent arguments:@[[CIVector vectorWithX:CGRectGetMinX(pixels) Y:CGRectGetMinY(pixels) Z:CGRectGetMaxX(pixels) W:CGRectGetMaxY(pixels)],@(inset),[CIVector vectorWithX:1 Y:.8 Z:.2 W:.95]]];
|
||||||
|
overlay=[outline imageByCompositingOverImage:overlay];
|
||||||
|
}
|
||||||
CIImage *colorCoverage=skin;
|
CIImage *colorCoverage=skin;
|
||||||
if (settings.featureProtection) skin = [_subtract applyWithExtent:extent arguments:@[skin, protection]];
|
if (settings.featureProtection) skin = [_subtract applyWithExtent:extent arguments:@[skin, protection]];
|
||||||
PGFaceFrame *frame = [[PGFaceFrame alloc] init];
|
PGFaceFrame *frame = [[PGFaceFrame alloc] init];
|
||||||
frame.faces = observations ?: @[];
|
frame.faces = observations ?: @[];
|
||||||
|
frame.deformationGeometry=geometry?:@[];
|
||||||
frame.image = [self warpImage:image geometry:geometry settings:settings extent:extent];
|
frame.image = [self warpImage:image geometry:geometry settings:settings extent:extent];
|
||||||
frame.skinMask = [self warpImage:skin geometry:geometry settings:settings extent:extent];
|
frame.skinMask = [self warpImage:skin geometry:geometry settings:settings extent:extent];
|
||||||
frame.colorMask = [self warpImage:colorCoverage geometry:geometry settings:settings extent:extent];
|
frame.colorMask = [self warpImage:colorCoverage geometry:geometry settings:settings extent:extent];
|
||||||
@@ -362,6 +544,7 @@ static CIImage *PGMaximum(CIImage *a, CIImage *b) {
|
|||||||
[_lock lock];
|
[_lock lock];
|
||||||
for (NSMutableDictionary *target in _targets) [target removeAllObjects];
|
for (NSMutableDictionary *target in _targets) [target removeAllObjects];
|
||||||
[_lock unlock];
|
[_lock unlock];
|
||||||
|
[_realtime reset];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSData *)trackingDataForTarget:(NSUInteger)target {
|
- (NSData *)trackingDataForTarget:(NSUInteger)target {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
title.font=[NSFont systemFontOfSize:28 weight:NSFontWeightSemibold];[stack addArrangedSubview:title];
|
title.font=[NSFont systemFontOfSize:28 weight:NSFontWeightSemibold];[stack addArrangedSubview:title];
|
||||||
NSTextField *subtitle=[NSTextField labelWithString:@"beatygril · FxPlug 4"];
|
NSTextField *subtitle=[NSTextField labelWithString:@"beatygril · FxPlug 4"];
|
||||||
subtitle.textColor=NSColor.secondaryLabelColor;[stack addArrangedSubview:subtitle];
|
subtitle.textColor=NSColor.secondaryLabelColor;[stack addArrangedSubview:subtitle];
|
||||||
NSTextField *description=[NSTextField wrappingLabelWithString:@"在 Motion 的滤镜资源库中选择「大鱼 → 大鱼人像美化」。Final Cut Pro 的效果浏览器中也可使用配套效果。\n\n磨皮、肤色、细节和脸型可分别开关。启用「只处理选中人脸」后,在检视器调整选脸位置,或直接点击画面选脸。选定目标后可向前、向后分析并将跟踪数据保存在工程中。"];
|
NSTextField *description=[NSTextField wrappingLabelWithString:@"在 Motion 的滤镜资源库中选择「大鱼 → 大鱼人像美化」。Final Cut Pro 的效果浏览器中也可使用配套效果。\n\n美化会随画面人脸自动更新,无需先点分析。脸型默认跟随主要人脸,多人模式可处理多张脸。启用「只处理选中人脸」后,可点击画面更换目标。向前/向后分析为可选功能,结果可随工程保存。"];
|
||||||
description.font=[NSFont systemFontOfSize:13];[stack addArrangedSubview:description];
|
description.font=[NSFont systemFontOfSize:13];[stack addArrangedSubview:description];
|
||||||
NSStackView *buttons=[NSStackView new];buttons.orientation=NSUserInterfaceLayoutOrientationHorizontal;buttons.spacing=12;
|
NSStackView *buttons=[NSStackView new];buttons.orientation=NSUserInterfaceLayoutOrientationHorizontal;buttons.spacing=12;
|
||||||
[buttons addArrangedSubview:[NSButton buttonWithTitle:@"打开 Motion" target:self action:@selector(openMotion:)]];
|
[buttons addArrangedSubview:[NSButton buttonWithTitle:@"打开 Motion" target:self action:@selector(openMotion:)]];
|
||||||
|
|||||||
+190
@@ -3,6 +3,16 @@
|
|||||||
#import <Vision/Vision.h>
|
#import <Vision/Vision.h>
|
||||||
#import <ImageIO/ImageIO.h>
|
#import <ImageIO/ImageIO.h>
|
||||||
#import "PGFaceProcessor.h"
|
#import "PGFaceProcessor.h"
|
||||||
|
#import <dispatch/dispatch.h>
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
// Exercise the independent selector with authored landmark geometry, without
|
||||||
|
// requiring Vision to manufacture a particular jitter or eye-axis failure.
|
||||||
|
@interface PGRealtimeFaceTracker : NSObject
|
||||||
|
- (NSDictionary *)resultForGeometry:(NSArray<NSDictionary *> *)raw settings:(PGBeautySettings)settings time:(CMTime)time extent:(CGRect)extent;
|
||||||
|
- (NSDictionary *)cacheStatistics;
|
||||||
|
- (void)reset;
|
||||||
|
@end
|
||||||
|
|
||||||
// Injectable detections let geometry, selection and saved-frame behavior be
|
// Injectable detections let geometry, selection and saved-frame behavior be
|
||||||
// tested independently of the separate public-domain NASA portrait fixture.
|
// tested independently of the separate public-domain NASA portrait fixture.
|
||||||
@@ -58,6 +68,185 @@ static CIImage *Gradient(void) {
|
|||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
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 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)];
|
||||||
|
}
|
||||||
|
static void RealtimeChecks(CIContext *context,CIImage *image) {
|
||||||
|
NSError *error=nil;PGBeautySettings s=PGBeautyDefaults();
|
||||||
|
VNFaceObservation *left=Face(CGRectMake(.10,.25,.30,.50));
|
||||||
|
VNFaceObservation *right=Face(CGRectMake(.66,.30,.20,.40));
|
||||||
|
PGFaceFixture *p=[PGFaceFixture new];p.detections=@[left,right];
|
||||||
|
s.faceThin=.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.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);
|
||||||
|
Check([Pixels(context,[automatic.image imageByCroppingToRect:rightCrop]) isEqualToData:Pixels(context,[image imageByCroppingToRect:rightCrop])],@"default deformation leaves the distant secondary face unchanged");
|
||||||
|
PGBeautySettings multi=s;multi.fastMultiFace=1;multi.singleFace=1;
|
||||||
|
PGFaceFrame *many=[p processImage:image settings:multi time:kCMTimeZero error:&error];
|
||||||
|
Check(many.deformationGeometry.count==2 && many.faces.count==2,@"fast-multi takes precedence over a stale single-face flag");
|
||||||
|
Check(![Pixels(context,[many.image imageByCroppingToRect:rightCrop]) isEqualToData:Pixels(context,[image imageByCroppingToRect:rightCrop])],@"fast-multi visibly deforms the secondary face");
|
||||||
|
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");
|
||||||
|
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");
|
||||||
|
|
||||||
|
PGRealtimeFaceTracker *t=[PGRealtimeFaceTracker new];
|
||||||
|
NSDictionary *a=Geometry(CGRectMake(.1,.25,.3,.5),0),*b=Geometry(CGRectMake(.66,.3,.2,.4),0);
|
||||||
|
NSDictionary *r=Live(t,@[a,b],s,0);
|
||||||
|
Check([r[@"output"] count]==1 && [r[@"index"] integerValue]==0,@"standalone default selector chooses the scored primary");
|
||||||
|
PGBeautySettings ignoredSeed=s;ignoredSeed.selectionX=.9;ignoredSeed.selectionY=.1;
|
||||||
|
Check([r isEqual:Live(t,@[a,b],ignoredSeed,0)],@"seed changes are irrelevant while explicit single-face selection is disabled");
|
||||||
|
NSDictionary *aMoved=Geometry(CGRectMake(.15,.25,.3,.5),0),*large=Geometry(CGRectMake(.43,.005,.56,.99),0);
|
||||||
|
r=Live(t,@[aMoved,large],s,40);
|
||||||
|
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");
|
||||||
|
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");
|
||||||
|
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");
|
||||||
|
|
||||||
|
// Rectangular guard includes a diagonal candidate that a small circular
|
||||||
|
// center threshold would reject, and excludes a point just over an edge.
|
||||||
|
PGRealtimeFaceTracker *guard=[PGRealtimeFaceTracker new];
|
||||||
|
PGBeautySettings guardSettings=s;guardSettings.faceJumpGuardRange=1.8;
|
||||||
|
NSDictionary *square=Geometry(CGRectMake(.4,.4,.2,.2),0);
|
||||||
|
Live(guard,@[square],guardSettings,0);
|
||||||
|
NSDictionary *diagonal=Geometry(CGRectMake(.625,.625,.1,.1),0);
|
||||||
|
r=Live(guard,@[diagonal],guardSettings,40);
|
||||||
|
Check(![r[@"held"] boolValue] && [r[@"index"] integerValue]==0,@"expanded rectangular guard accepts a candidate near both positive corners");
|
||||||
|
[guard reset];Live(guard,@[square],guardSettings,0);
|
||||||
|
r=Live(guard,@[Geometry(CGRectMake(.631,.45,.1,.1),0)],guardSettings,40);
|
||||||
|
Check([r[@"held"] boolValue],@"candidate just beyond a rectangular guard edge cannot replace the identity");
|
||||||
|
|
||||||
|
PGRealtimeFaceTracker *smooth=[PGRealtimeFaceTracker new];
|
||||||
|
CGRect box=CGRectMake(.2,.2,.4,.6);
|
||||||
|
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");
|
||||||
|
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");
|
||||||
|
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([smoothed[@"output"] isEqual:Live(smooth,@[jitter],s,10)[@"output"]],@"repeated rewind to a cached time reproduces its original stable geometry");
|
||||||
|
|
||||||
|
PGRealtimeFaceTracker *seeds=[PGRealtimeFaceTracker new];
|
||||||
|
PGBeautySettings selected=s;selected.singleFace=1;selected.selectionX=.25;selected.selectionY=.5;
|
||||||
|
Live(seeds,@[a,b],selected,0);
|
||||||
|
selected.selectionX=.255;r=Live(seeds,@[],selected,40);
|
||||||
|
Check([r[@"held"] boolValue],@"sub-.01 seed movement retains the existing identity");
|
||||||
|
selected.selectionX=.27;r=Live(seeds,@[],selected,80);
|
||||||
|
Check([r[@"output"] count]==0 && ![r[@"held"] boolValue],@"seed change over .01 starts a new identity and does not inherit an old held target");
|
||||||
|
selected.selectionX=.78;r=Live(seeds,@[a,b],selected,80);
|
||||||
|
Check([r[@"index"] integerValue]==1,@"large manual reselection immediately chooses the new seed target");
|
||||||
|
NSDictionary *bMoved=Geometry(CGRectMake(.50,.30,.2,.4),0);
|
||||||
|
r=Live(seeds,@[a,bMoved],selected,120);
|
||||||
|
Check([r[@"index"] integerValue]==1,@"manually seeded target follows a near moving face");
|
||||||
|
NSDictionary *bContinued=Geometry(CGRectMake(.45,.3,.2,.4),0),*seedStranger=Geometry(CGRectMake(.8,.3,.15,.4),0);
|
||||||
|
r=Live(seeds,@[a,bContinued,seedStranger],selected,160);
|
||||||
|
Check([r[@"index"] integerValue]==1,@"following does not repeatedly select the person nearest the fixed original click");
|
||||||
|
|
||||||
|
PGRealtimeFaceTracker *reseedRewind=[PGRealtimeFaceTracker new];
|
||||||
|
PGBeautySettings seedA=s;seedA.singleFace=1;seedA.selectionX=.2;seedA.selectionY=.5;
|
||||||
|
for(int64_t i=0;i<=20;i++) {
|
||||||
|
NSDictionary *movingFace=Geometry(CGRectMake(.15+.02*i,.3,.1,.4),0);
|
||||||
|
Live(reseedRewind,@[movingFace],seedA,i*40);
|
||||||
|
}
|
||||||
|
NSArray *rewindFaces=@[Geometry(CGRectMake(.15,.3,.1,.4),0),Geometry(CGRectMake(.55,.3,.1,.4),0),Geometry(CGRectMake(.75,.3,.1,.4),0)];
|
||||||
|
NSDictionary *beforeReselect=Live(reseedRewind,rewindFaces,seedA,840);
|
||||||
|
PGBeautySettings seedB=seedA;seedB.selectionX=.8;Live(reseedRewind,rewindFaces,seedB,900);
|
||||||
|
Live(reseedRewind,rewindFaces,seedA,1000);
|
||||||
|
Check([beforeReselect isEqual:Live(reseedRewind,rewindFaces,seedA,840)],@"A-to-B-to-A reselection does not let the future A scope replace an earlier A exact result");
|
||||||
|
|
||||||
|
PGRealtimeFaceTracker *order=[PGRealtimeFaceTracker new];
|
||||||
|
NSDictionary *future=Live(order,@[b],s,1000);
|
||||||
|
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");
|
||||||
|
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)];
|
||||||
|
Check([epoch[@"output"] count]==0,@"CMTime epochs isolate realtime identity caches");
|
||||||
|
NSDictionary *extent=[order resultForGeometry:@[] settings:s time:CMTimeMake(1080,1000) extent:CGRectMake(20,30,256,128)];
|
||||||
|
Check([extent[@"output"] count]==0,@"source extent size and origin changes isolate realtime geometry");
|
||||||
|
PGBeautySettings target=s;target.targetIndex=1;r=Live(order,@[],target,1080);
|
||||||
|
Check([r[@"output"] count]==0,@"target changes cannot reuse another target's realtime geometry");
|
||||||
|
PGBeautySettings fast=s;fast.fastMultiFace=1;r=Live(order,@[a,b],fast,1080);
|
||||||
|
Check([r[@"output"] count]==2 && [r[@"referenceKey"] length]==0,@"fast-multi mode has a separate geometry context");
|
||||||
|
r=Live(order,@[],fast,1120);Check([r[@"output"] count]==0,@"fast-multi does not inherit primary hold behavior");
|
||||||
|
Check([future isEqual:Live(order,@[b],s,1000)],@"same-time historical exact result remains stable after other contexts and seeks");
|
||||||
|
|
||||||
|
// Parallel calls may have cold/warm identity differences, but each must be
|
||||||
|
// data safe, reject future references, and freeze exact repeated requests.
|
||||||
|
PGRealtimeFaceTracker *parallel=[PGRealtimeFaceTracker new];
|
||||||
|
dispatch_apply(40,dispatch_get_global_queue(QOS_CLASS_USER_INITIATED,0),^(size_t i) {
|
||||||
|
@autoreleasepool {Live(parallel,@[a],s,(int64_t)i*40);}
|
||||||
|
});
|
||||||
|
BOOL pastOnly=YES,exact=YES;
|
||||||
|
for(int64_t i=0;i<40;i++) {
|
||||||
|
NSDictionary *first=Live(parallel,@[a],s,i*40),*again=Live(parallel,@[a],s,i*40);
|
||||||
|
exact=exact&&[first isEqual:again];
|
||||||
|
NSString *reference=first[@"referenceKey"];
|
||||||
|
if(reference.length) {NSArray *parts=[reference componentsSeparatedByString:@"/"];pastOnly=pastOnly&&[parts[0] doubleValue]/[parts[1] doubleValue] <= i*.04;}
|
||||||
|
}
|
||||||
|
Check(pastOnly && exact,@"parallel result cache is safe and repeated requests retain past-only references");
|
||||||
|
PGRealtimeFaceTracker *bounded=[PGRealtimeFaceTracker new];
|
||||||
|
for(int64_t i=0;i<1210;i++) {@autoreleasepool {Live(bounded,@[],s,i*40);}}
|
||||||
|
NSDictionary *stats=[bounded cacheStatistics];
|
||||||
|
Check([stats[@"frames"] unsignedIntegerValue]==1200 && [stats[@"estimatedBytes"] unsignedIntegerValue]<=16*1024*1024,@"realtime geometry cache enforces frame and serialized-size bounds");
|
||||||
|
[bounded reset];Check([[bounded cacheStatistics][@"frames"] unsignedIntegerValue]==0,@"realtime reset releases cached geometry");
|
||||||
|
NSData *before=[p trackingDataForTarget:0];
|
||||||
|
PGBeautySettings offline=s;offline.singleFace=1;offline.trackingEnabled=1;offline.selectionX=.25;offline.selectionY=.5;
|
||||||
|
p.detections=@[left];Check([p analyzeImage:image settings:offline time:kCMTimeZero error:&error],@"offline analysis remains available beside live history");
|
||||||
|
NSData *archive=[p trackingDataForTarget:0];
|
||||||
|
p.detections=@[];[p processImage:image settings:s time:CMTimeMake(3,25) error:&error];
|
||||||
|
Check(before==nil && [archive isEqual:[p trackingDataForTarget:0]],@"subsequent realtime hold does not mutate persisted offline geometry");
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, const char *argv[]) {
|
int main(int argc, const char *argv[]) {
|
||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
CIContext *context = [CIContext contextWithOptions:@{kCIContextCacheIntermediates:@NO}];
|
CIContext *context = [CIContext contextWithOptions:@{kCIContextCacheIntermediates:@NO}];
|
||||||
@@ -205,6 +394,7 @@ int main(int argc, const char *argv[]) {
|
|||||||
CFRelease(destination); CGImageRelease(preview);
|
CFRelease(destination); CGImageRelease(preview);
|
||||||
CGContextRelease(canvas); CGColorSpaceRelease(space);
|
CGContextRelease(canvas); CGColorSpaceRelease(space);
|
||||||
}
|
}
|
||||||
|
RealtimeChecks(context,image);
|
||||||
printf("Face processing: %lu checks passed\n", (unsigned long)PGChecks);
|
printf("Face processing: %lu checks passed\n", (unsigned long)PGChecks);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户