Fix FxPlug CI orientation and preserve incompatible legacy tracks; prepare tested 1.0.6 Release

这个提交包含在:
dayu 于 2026-10-21 20:56:54 +08:00 提交
1 父节点 16525d62e5
当前提交 ad54726b17
18 个文件已更改
+12145 -25

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

+211 -2
查看文件
@@ -7,6 +7,9 @@
#import <ImageIO/ImageIO.h>
#import "beatygrilPlugIn.h"
#import "PGParameterIDs.h"
#import "PGFaceProcessor.h"
#import "PGBeautyRenderer.h"
#include <math.h>
// This adapter simulates host callbacks, but uses real IOSurface buffers and the
// production FxPlug entry points. It does not simulate FCP plugin discovery/UI.
@@ -173,6 +176,12 @@ static NSUInteger MaximumDifference(NSData *a, NSData *b) {
for (NSUInteger i = 0; i < a.length; i++) maximum = MAX(maximum, (NSUInteger)abs(x[i] - y[i]));
return maximum;
}
static double MeanDifference(NSData *a, NSData *b) {
if (!a.length || a.length != b.length) return INFINITY;
const uint8_t *x = a.bytes, *y = b.bytes; double total = 0;
for (NSUInteger i = 0; i < a.length; i++) total += abs(x[i]-y[i]);
return total/a.length;
}
static NSData *ExpectedCrop(PGHostTile *source, NSUInteger x, NSUInteger y, NSUInteger width, NSUInteger height, BOOL flip) {
NSData *sourceBytes = SurfaceBytes(source); size_t sourceWidth = IOSurfaceGetWidth((__bridge IOSurfaceRef)source.storage);
NSMutableData *result = [NSMutableData dataWithLength:width * height * 4];
@@ -200,6 +209,204 @@ static void RejectState(beatygrilPlugIn *plugin, PGHostTile *destination, PGHost
Check(!rendered && !escaped && error != nil, description);
}
static NSData *PlistState(id value) { return [NSPropertyListSerialization dataWithPropertyList:value format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil]; }
// Decode independently of CI/IOSurface, so a pair of adapter flips cannot
// silently cancel in our test input and expected output. ImageIO + Quartz gives
// the NASA fixture top-first BGRA bytes, also cross-checked against Pillow.
static NSData *DecodedPortrait(NSString *path, size_t *width, size_t *height) {
CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)[NSURL fileURLWithPath:path], NULL);
CGImageRef image = source ? CGImageSourceCreateImageAtIndex(source, 0, NULL) : NULL;
if (source) CFRelease(source);
if (!image) return nil;
*width = CGImageGetWidth(image); *height = CGImageGetHeight(image);
NSMutableData *bytes = [NSMutableData dataWithLength:*width * *height * 4];
CGColorSpaceRef space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
CGContextRef context = CGBitmapContextCreate(bytes.mutableBytes, *width, *height, 8, *width * 4, space,
kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
if (context) {
CGContextSetBlendMode(context, kCGBlendModeCopy);
CGContextSetInterpolationQuality(context, kCGInterpolationNone);
CGContextDrawImage(context, CGRectMake(0, 0, *width, *height), image);
CGContextRelease(context);
}
CGColorSpaceRelease(space); CGImageRelease(image);
return context ? bytes : nil;
}
static BOOL StoreSurfaceBytes(PGHostTile *tile, NSData *bytes) {
IOSurfaceRef surface = (__bridge IOSurfaceRef)tile.storage;
size_t width = IOSurfaceGetWidth(surface), height = IOSurfaceGetHeight(surface);
if (bytes.length != width * height * 4) return NO;
IOSurfaceLock(surface, 0, NULL);
for (size_t row = 0; row < height; row++)
memcpy((uint8_t *)IOSurfaceGetBaseAddress(surface) + row * IOSurfaceGetBytesPerRow(surface),
(const uint8_t *)bytes.bytes + row * width * 4, width * 4);
IOSurfaceUnlock(surface, 0, NULL);
return YES;
}
static CIContext *NumericContext(void) {
return [CIContext contextWithMTLDevice:MTLCreateSystemDefaultDevice() options:@{
kCIContextWorkingColorSpace:NSNull.null, kCIContextOutputColorSpace:NSNull.null}];
}
static NSData *ImageBytes(CIContext *context, CIImage *image) {
if (!image) return nil;
size_t width = (size_t)image.extent.size.width, height = (size_t)image.extent.size.height;
NSMutableData *bytes = [NSMutableData dataWithLength:width * height * 4];
[context render:image toBitmap:bytes.mutableBytes rowBytes:width * 4 bounds:image.extent format:kCIFormatBGRA8 colorSpace:nil];
return bytes;
}
static CIImage *ProcessedReference(CIImage *image, PGBeautySettings settings, CMTime time, NSData *track) {
PGFaceProcessor *faces = [PGFaceProcessor new];
if (track) [faces setTrackingData:track forTarget:settings.targetIndex];
NSError *error = nil;
PGFaceFrame *frame = [faces processImage:image settings:settings time:time error:&error];
if (!frame) return nil;
return [[PGBeautyRenderer new] processImage:frame.image settings:settings skinMask:frame.skinMask
colorMask:frame.colorMask teethMask:frame.teethMask eyeMask:frame.eyeMask error:&error];
}
static CGPoint LandmarkCenter(VNFaceLandmarkRegion2D *region, CGRect box, CGSize size) {
CGPoint center = CGPointZero;
if (!region.pointCount || !region.normalizedPoints) return center;
for (NSUInteger i = 0; i < region.pointCount; i++) {
center.x += region.normalizedPoints[i].x; center.y += region.normalizedPoints[i].y;
}
center.x /= region.pointCount; center.y /= region.pointCount;
return CGPointMake((box.origin.x + center.x * box.size.width) * size.width,
(1 - box.origin.y - center.y * box.size.height) * size.height);
}
static double ChangeMass(NSData *baseline, NSData *changed, size_t width, CGRect region) {
if (baseline.length != changed.length || !width) return INFINITY;
const uint8_t *a = baseline.bytes, *b = changed.bytes;
size_t height = baseline.length / (width * 4); double total = 0;
for (size_t row = 0; row < height; row++) for (size_t x = 0; x < width; x++)
if (CGRectContainsPoint(region, CGPointMake(x + .5, row + .5))) {
size_t i = (row * width + x) * 4;
for (size_t c = 0; c < 3; c++) total += abs(a[i+c] - b[i+c]);
}
return total;
}
static void PortraitAnatomyTests(NSString *fixture, CIImage *portrait) {
size_t width = 0, height = 0;
NSData *canonical = DecodedPortrait(fixture, &width, &height);
Check(canonical != nil && width == 512 && height == 512, @"independent ImageIO/Quartz decodes the 512px NASA fixture");
if (!canonical || width != 512 || height != 512 || !portrait) return;
// Asymmetric samples pin the decoder's physical row/column order. These
// values match Pillow's RGB decode exactly, independently of Core Image.
const size_t positions[][2] = {{16,16},{100,250},{205,100},{222,178},{480,450}};
const uint8_t samples[][4] = {{35,6,14,255},{93,132,234,255},{11,35,58,255},{42,73,102,255},{2,1,3,255}};
BOOL samplesMatch = YES;
for (size_t i = 0; i < 5; i++)
samplesMatch &= memcmp((const uint8_t *)canonical.bytes + (positions[i][1]*width + positions[i][0])*4, samples[i], 4) == 0;
Check(samplesMatch, @"independent asymmetric pixels establish top-first BGRA and the horizontal direction");
CIContext *context = NumericContext();
Check(MaximumDifference(canonical, ImageBytes(context, portrait)) <= 1, @"upright CI file decode agrees with independent physical source bytes");
PGFaceFrame *detected = [[PGFaceProcessor new] processImage:portrait settings:PGBeautyDefaults() time:kCMTimeZero error:nil];
VNFaceObservation *face = detected.faces.firstObject;
Check(face.landmarks.allPoints.pointCount == 76, @"real portrait provides the 76-point anatomy used by chin pins");
if (face.landmarks.allPoints.pointCount != 76) return;
CGSize size = CGSizeMake(width, height); CGRect box = face.boundingBox;
CGPoint left = LandmarkCenter(face.landmarks.leftEye, box, size), right = LandmarkCenter(face.landmarks.rightEye, box, size);
CGPoint chin = face.landmarks.allPoints.normalizedPoints[67];
chin = CGPointMake((box.origin.x + chin.x * box.size.width)*width, (1-box.origin.y-chin.y*box.size.height)*height);
CGFloat fw = box.size.width*width, fh = box.size.height*height;
CGRect leftEye = CGRectMake(left.x-fw*.24, left.y-fh*.17, fw*.48, fh*.34);
CGRect rightEye = CGRectMake(right.x-fw*.24, right.y-fh*.17, fw*.48, fh*.34);
CGRect chinArea = CGRectMake(chin.x-fw*.30, chin.y-fh*.22, fw*.60, fh*.44);
CGRect upsideDownFace = CGRectMake(box.origin.x*width, box.origin.y*height, fw, fh);
const UInt32 controls[] = {0, PGBigEye, PGChin, PGChin};
const double values[] = {0, 1, 1, -1};
NSArray<NSString *> *names = @[@"default", @"big-eye", @"chin-positive", @"chin-negative"];
for (NSUInteger effect = 0; effect < 4; effect++) {
PGBeautySettings settings = PGBeautyDefaults();
if (effect == 1) settings.faceBigEye = 1;
else if (effect > 1) settings.faceChin = values[effect];
NSData *expected = ImageBytes(context, ProcessedReference(portrait, settings, kCMTimeZero, nil));
Check(expected.length == canonical.length, [names[effect] stringByAppendingString:@" independent upright face/beauty chain renders"]);
if (expected.length != canonical.length) continue;
NSData *originBaseline = nil;
for (NSUInteger sourceTop = 0; sourceTop < 2; sourceTop++) for (NSUInteger destinationTop = 0; destinationTop < 2; destinationTop++) {
@autoreleasepool {
PGHostAPI *api = [PGHostAPI new]; beatygrilPlugIn *plugin = [[beatygrilPlugIn alloc] initWithAPIManager:api];
Check([plugin addParametersWithError:nil], @"anatomy case creates the real plugin parameters");
if (effect) api.values[@(controls[effect])] = @(values[effect]);
PGHostTile *input = Tile(width, height, 37, -21, sourceTop ? kFxImageOrigin_TOP_LEFT : kFxImageOrigin_BOTTOM_LEFT);
PGHostTile *output = Tile(width, height, 37, -21, destinationTop ? kFxImageOrigin_TOP_LEFT : kFxImageOrigin_BOTTOM_LEFT);
// imageOrigin changes coordinate semantics, never these bytes.
Check(StoreSurfaceBytes(input, canonical), @"all origin combinations receive identical independently decoded physical rows");
Check(Render(plugin, output, @[input], Snapshot(plugin, kCMTimeZero), kCMTimeZero, nil, NULL), @"active anatomy case renders with nonzero tile origin");
NSData *actual = SurfaceBytes(output); NSUInteger difference = MaximumDifference(actual, expected);
NSString *label = [NSString stringWithFormat:@"%@ source%@/destination%@", names[effect], sourceTop?@"TOP":@"BOTTOM", destinationTop?@"TOP":@"BOTTOM"];
// Vision's file-image vs IOSurface path and translated float
// coordinates can round by a few byte levels at high contrast.
// Bound that error tightly; changing only origin tags must be
// exactly invariant, independently of this reference tolerance.
Check(difference <= 4 && MeanDifference(actual,expected) < .005, [label stringByAppendingString:@" matches independent upright CI output"]);
if (!originBaseline) originBaseline = actual;
Check(MaximumDifference(actual,originBaseline) == 0, [label stringByAppendingString:@" is byte-identical across coordinate tags"]);
double lm = ChangeMass(canonical, actual, width, leftEye), rm = ChangeMass(canonical, actual, width, rightEye);
double cm = ChangeMass(canonical, actual, width, chinArea), um = ChangeMass(canonical, actual, width, upsideDownFace);
BOOL located = effect == 0 ? MaximumDifference(canonical, actual) <= 1 :
(effect == 1 ? lm > 100 && rm > 100 && cm < (lm+rm)*.01 && um == 0 :
cm > 100 && cm > 4*(lm+rm) && um == 0);
Check(located, [label stringByAppendingString:effect == 1 ? @" changes the two real eye regions" : (effect ? @" changes the real chin rather than eyes or mirrored anatomy" : @" preserves the independently decoded photo")]);
printf("Anatomy %-13s %lu/%lu max=%lu mean=%.7f eye=%.0f/%.0f chin=%.0f inverted=%.0f\n", names[effect].UTF8String,
(unsigned long)sourceTop, (unsigned long)destinationTop, (unsigned long)difference, MeanDifference(actual,expected), lm, rm, cm, um);
}
}
}
}
static void TrackingCoordinateTests(NSDictionary *archive, NSDictionary *hostValues, PGHostTile *source, CIImage *portrait, CMTime time) {
Check([archive[@"hostCoordinateSpace"] isEqual:@"iosurface-ci-y-up-v1"], @"new host analysis stamps its explicit IOSurface/CI coordinate contract");
NSMutableDictionary *forged = [NSPropertyListSerialization propertyListWithData:PlistState(archive)
options:NSPropertyListMutableContainers format:nil error:nil];
if (![forged[@"frames"] isKindOfClass:NSDictionary.class]) { Check(NO, @"analysis archive supplies frames for compatibility regression"); return; }
// Retain fully valid topology but move the saved face away from the actual
// portrait. A falsely replayed legacy archive must produce a visible error.
for (NSMutableDictionary *frame in [forged[@"frames"] allValues]) for (NSMutableDictionary *face in frame[@"faces"]) {
NSMutableArray *box = [face[@"box"] mutableCopy]; box[0] = @([box[0] doubleValue]+.2); box[1] = @([box[1] doubleValue]-.25); face[@"box"] = box;
for (NSString *name in @[@"leftEye",@"rightEye",@"leftBrow",@"rightBrow",@"nose",@"lips",@"mouth",@"contour",@"points76"]) {
if (!face[name]) continue;
NSMutableArray *points = [NSMutableArray new];
for (NSArray *point in face[name]) [points addObject:@[@([point[0] doubleValue]+.2),@([point[1] doubleValue]-.25)]];
face[name] = points;
}
}
[forged removeObjectForKey:@"hostCoordinateSpace"];
NSString *legacyString = [PlistState(forged) base64EncodedStringWithOptions:0];
NSMutableDictionary *values = [hostValues mutableCopy];
values[@(PGTarget)] = @2; values[@(PGTrackEnabled)] = @YES; values[@(PGBigEye)] = @1; values[@(PGChin)] = @1;
values[@(PGTrackData3)] = legacyString;
PGHostAPI *legacyAPI = [PGHostAPI new]; legacyAPI.hostTime = time;
beatygrilPlugIn *legacy = [[beatygrilPlugIn alloc] initWithAPIManager:legacyAPI]; [legacy addParametersWithError:nil]; legacyAPI.values = [values mutableCopy];
PGHostTile *output = Tile(512,512,0,0,kFxImageOrigin_TOP_LEFT);
Check(Render(legacy, output, @[source], Snapshot(legacy,time), time, nil, NULL), @"unmarked old coordinate archive renders through the real adapter");
NSData *legacyBytes = SurfaceBytes(output);
PGBeautySettings realtime = [legacy settingsAtTime:time]; realtime.trackingEnabled = 0;
CIContext *context = NumericContext();
NSData *expectedLive = ImageBytes(context, ProcessedReference(portrait,realtime,time,nil));
printf("Legacy fallback upright comparison: max=%lu mean=%.7f\n",(unsigned long)MaximumDifference(legacyBytes,expectedLive),MeanDifference(legacyBytes,expectedLive));
Check(expectedLive.length && MaximumDifference(legacyBytes,expectedLive) <= 4 && MeanDifference(legacyBytes,expectedLive) < .005, @"unmarked forged face geometry falls back to realtime upright detection");
Check([legacyAPI.values[@(PGTrackData3)] isEqualToString:legacyString] && [legacyAPI.values[@(PGTrackEnabled)] boolValue], @"legacy fallback preserves the user's original archive string and enabled parameter");
forged[@"hostCoordinateSpace"] = @"iosurface-ci-y-up-v1";
NSData *trustedTrack = PlistState(forged);
PGHostAPI *trustedAPI = [PGHostAPI new]; trustedAPI.hostTime = time;
beatygrilPlugIn *trusted = [[beatygrilPlugIn alloc] initWithAPIManager:trustedAPI]; [trusted addParametersWithError:nil];
trustedAPI.values = [values mutableCopy]; trustedAPI.values[@(PGTrackData3)] = [trustedTrack base64EncodedStringWithOptions:0];
Check(Render(trusted, output, @[source], Snapshot(trusted,time), time, nil, NULL), @"fresh plugin restores a marked coordinate archive");
NSData *trustedBytes = SurfaceBytes(output);
PGBeautySettings tracked = [trusted settingsAtTime:time];
NSData *expectedSaved = ImageBytes(context, ProcessedReference(portrait,tracked,time,trustedTrack));
Check(expectedSaved.length && MaximumDifference(trustedBytes,expectedSaved) <= 1, @"marked saved geometry matches independent exact-time archive replay");
Check(MaximumDifference(trustedBytes,expectedLive) > 5, @"forged marked geometry is observably different, making the legacy non-replay assertion meaningful");
// Reanalysis of an unmarked target starts a fresh compatible archive.
[legacy analyzeForward];
CMTime next = CMTimeAdd(time,CMTimeMake(10,24));
[legacy setupAnalysisForTimeRange:CMTimeRangeMake(time,CMTimeMake(240,24)) frameDuration:CMTimeMake(1,24) error:nil];
BOOL analyzed = [legacy analyzeFrame:source atTime:next error:nil] && [legacy cleanupAnalysis:nil];
NSData *resaved = [[NSData alloc] initWithBase64EncodedString:legacyAPI.values[@(PGTrackData3)] options:0];
NSDictionary *fresh = resaved ? [NSPropertyListSerialization propertyListWithData:resaved options:0 format:nil error:nil] : nil;
Check(analyzed && [fresh[@"frames"] count] == 1 && [fresh[@"hostCoordinateSpace"] isEqual:@"iosurface-ci-y-up-v1"], @"reanalysis replaces incompatible geometry with one newly marked frame");
}
static void WritePortrait(PGHostTile *tile, CIImage *image) {
CIContext *context = [CIContext contextWithOptions:@{kCIContextWorkingColorSpace:NSNull.null, kCIContextOutputColorSpace:NSNull.null}];
[context render:image toIOSurface:(__bridge IOSurfaceRef)tile.storage bounds:image.extent colorSpace:nil];
@@ -283,7 +490,7 @@ int main(int argc, const char *argv[]) {
Check(MaximumDifference(SurfaceBytes(input), SurfaceBytes(output)) <= 1, top ? @"top-left tile preserves sample rows at nonzero origin" : @"bottom-left tile preserves sample rows at nonzero origin");
output.origin = top ? kFxImageOrigin_BOTTOM_LEFT : kFxImageOrigin_TOP_LEFT;
Check(Render(plugin, output, @[input], state, kCMTimeZero, &error, NULL), @"opposite-origin tile renders");
Check(MaximumDifference(ExpectedCrop(input, 0, 0, 48, 32, YES), SurfaceBytes(output)) <= 1, @"opposite image origins flip rows exactly once");
Check(MaximumDifference(SurfaceBytes(input), SurfaceBytes(output)) <= 1, @"opposite coordinate tags preserve the same physical IOSurface rows");
}
PGHostTile *full = Tile(48, 32, 37, -21, kFxImageOrigin_BOTTOM_LEFT), *crop = Tile(16, 12, 43, -17, kFxImageOrigin_BOTTOM_LEFT); Fill(full);
Check(Render(plugin, crop, @[full], state, kCMTimeZero, &error, NULL), @"destination sub-tile renders from full input buffer");
@@ -298,11 +505,12 @@ int main(int argc, const char *argv[]) {
Check(cropDifference <= 1, @"destination sub-tile translates global pixel bounds correctly");
full.origin = kFxImageOrigin_TOP_LEFT; crop.origin = kFxImageOrigin_TOP_LEFT;
Check(Render(plugin, crop, @[full], state, kCMTimeZero, &error, NULL), @"top-left destination sub-tile renders");
Check(MaximumDifference(ExpectedCrop(full, 6, 4, 16, 12, NO), SurfaceBytes(crop)) <= 1, @"top-left sub-tile selects correct physical source rows");
Check(MaximumDifference(ExpectedCrop(full, 6, 16, 16, 12, NO), SurfaceBytes(crop)) <= 1, @"top-left coordinate tag does not reverse the sub-tile's physical source rows");
NSString *fixture = argc > 1 ? @(argv[1]) : @"tests/fixtures/astronaut.png";
CIImage *portrait = [CIImage imageWithContentsOfURL:[NSURL fileURLWithPath:fixture] options:@{kCIImageColorSpace:NSNull.null}];
Check(portrait != nil, @"NASA portrait opens for full adapter test");
PortraitAnatomyTests(fixture, portrait);
PGHostTile *portraitSource = Tile(512, 512, 0, 0, kFxImageOrigin_BOTTOM_LEFT), *portraitOutput = Tile(512, 512, 0, 0, kFxImageOrigin_BOTTOM_LEFT);
WritePortrait(portraitSource, portrait);
api.values[@(PGThin)] = @0.8; api.values[@(PGBigEye)] = @0.6; api.values[@(PGMidtone)] = @0.8; api.values[@(PGBrightness)] = @0.25; api.values[@(PGMix)] = @0;
@@ -339,6 +547,7 @@ int main(int argc, const char *argv[]) {
NSData *tracking = [[NSData alloc] initWithBase64EncodedString:target options:0];
NSDictionary *trackingArchive = tracking ? [NSPropertyListSerialization propertyListWithData:tracking options:0 format:nil error:nil] : nil;
Check([trackingArchive[@"frames"] count] == 2, @"old render snapshot does not erase either newly analyzed frame");
TrackingCoordinateTests(trackingArchive,api.values,portraitSource,portrait,api.hostTime);
Check([api.values[@(PGTrackEnabled)] boolValue] && CMTimeCompare(api.lastSetBoolTime, api.hostTime) == 0, @"analysis enables saved target at host seed time");
NSData *trackedState = Snapshot(plugin, api.hostTime);
beatygrilPlugIn *restoredPlugin = [[beatygrilPlugIn alloc] initWithAPIManager:api];