diff --git a/beatygril/beatygril/Plugin/PGFaceProcessor.m b/beatygril/beatygril/Plugin/PGFaceProcessor.m index 08c1e85..639a599 100644 --- a/beatygril/beatygril/Plugin/PGFaceProcessor.m +++ b/beatygril/beatygril/Plugin/PGFaceProcessor.m @@ -13,6 +13,7 @@ static const uint8_t PGThinPins[17][2] = { {72,48},{62,48},{71,48},{63,48},{70,49},{64,49}, {69,49},{65,49},{68,52},{66,52},{67,47} }; +static const uint8_t PGChinPins[3][2] = {{68,38},{67,37},{66,36}}; @interface PGFaceFrame () @property (nonatomic, copy, readwrite) NSArray *faces; @@ -124,7 +125,7 @@ static NSDictionary *PGSanitizedGeometry(id value) { result[name] = points; } // Old archives and partially detected faces remain usable by the other - // controls. Missing or malformed full topology only disables local thinning. + // controls. Missing or malformed full topology disables local thin/chin pins. NSArray *points76 = PGSanitizedPoints76(value[@"points76"]); if (points76) result[@"points76"] = points76; return [result copy]; @@ -343,8 +344,10 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ _lock = [[NSLock alloc] init]; _realtime = [PGRealtimeFaceTracker new]; _targets = [NSMutableArray arrayWithObjects:[NSMutableDictionary dictionary], [NSMutableDictionary dictionary], [NSMutableDictionary dictionary], nil]; - // Local thinning reconstructs the recovered 17-pin coordinate math. - // The other eight controls retain this project's analytic approximation. + // Local thinning and chin use the recovered 17/3-pin coordinate math. + // The other seven controls retain this project's analytic approximation. + // Chin follows those analytic body controls and precedes the eye warp; + // this combined order is not a reconstruction of the full reference chain. // Each pin changes the sampling coordinate, with only one image resample. NSMutableString *source = [NSMutableString stringWithString: @"vec2 pgThinPin(vec2 q, vec4 control, float ratio) {" @@ -352,8 +355,9 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ " if(radius<=0.0001) return q;" " float weight=clamp(1.0-length(q-control.xy)/radius,0.0,1.0);" " return clamp(q-direction*ratio*weight,vec2(0.0),vec2(1.0)); }" - " kernel vec2 pgFaceWarp(vec2 c, vec2 size, vec2 le, vec2 re, vec2 er, float thinRatio, float bigEye, float shortFace, float narrow, float chin, float vFace, float jaw, float cheek, float smallHead, float headRange, vec2 imageOrigin, vec2 imageSize"]; + " kernel vec2 pgFaceWarp(vec2 c, vec2 size, vec2 le, vec2 re, vec2 er, float thinRatio, float bigEye, float shortFace, float narrow, float chinRatio, float vFace, float jaw, float cheek, float smallHead, float headRange, vec2 imageOrigin, vec2 imageSize"]; for (NSUInteger i=0; i<17; i++) [source appendFormat:@", vec4 pin%lu",(unsigned long)i]; + for (NSUInteger i=0; i<3; i++) [source appendFormat:@", vec4 chinPin%lu",(unsigned long)i]; [source appendString:@") { vec2 p=destCoord();" " if(thinRatio>0.000001) { vec2 q=(p-imageOrigin)/imageSize; q.y=1.0-q.y;"]; for (NSUInteger i=0; i<17; i++) [source appendFormat:@" q=pgThinPin(q,pin%lu,thinRatio);",(unsigned long)i]; @@ -367,7 +371,10 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ " float head = 1.0-smoothstep(0.38,0.75+0.32*headRange,length(n*vec2(0.9,0.65)));" " p += (p-c)*smallHead*0.18*head;" " p.x += size.x*n.x*(narrow*0.30*body + vFace*0.24*jawBand + jaw*0.20*jawBand + cheek*0.18*cheekBand)*sides;" - " float cn=n.x/0.30; p.y += size.y*(-shortFace*0.16*lower + chin*0.13*exp(-cn*cn)*jawBand - vFace*0.025*lower);" + " p.y += size.y*(-shortFace*0.16*lower - vFace*0.025*lower);" + " if(abs(chinRatio)>0.000001) { vec2 q=(p-imageOrigin)/imageSize; q.y=1.0-q.y;"]; + for (NSUInteger i=0; i<3; i++) [source appendFormat:@" q=pgThinPin(q,chinPin%lu,chinRatio);",(unsigned long)i]; + [source appendString:@" q.y=1.0-q.y; p=imageOrigin+q*imageSize; }" " vec2 dl = (p-le)/er; float wl = 1.0-smoothstep(0.0,1.0,length(dl));" " p -= (p-le)*bigEye*0.33*wl*wl;" " vec2 dr = (p-re)/er; float wr = 1.0-smoothstep(0.0,1.0,length(dr));" @@ -491,7 +498,7 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ for (NSDictionary *face in geometry) { CGRect box = PGArrayRect(face[@"box"]), pixels = PGToPixels(box, extent); NSArray *points76 = PGSanitizedPoints76(face[@"points76"]); - float thinRatio = 0; + float thinRatio = 0, chinRatio = 0; if (points76) { double width = hypot(([points76[75][0] doubleValue]-[points76[59][0] doubleValue])*extent.size.width, ([points76[75][1] doubleValue]-[points76[59][1] doubleValue])*extent.size.height); @@ -501,18 +508,20 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ ([points76[67][1] doubleValue]-midY)*extent.size.height); double gain = fmin(2.25,fmax(1,280/fmax(1,fmax(width,height)))); thinRatio = PGClamp(s.faceThin,0,1)*.025*gain; + float chin = PGClamp(s.faceChin,-1,1); + chinRatio = chin*(chin>0 ? .04 : .02)*gain; } - BOOL otherControls = s.faceBigEye || s.faceShort || s.faceNarrow || s.faceChin || s.faceV || s.faceJaw || s.faceCheekbone || s.faceSmallHead; - // No full topology means no guessed bbox thinning. In particular, legacy - // archives with thinning alone remain an exact image/mask pass-through. - if (thinRatio <= .000001 && !otherControls) continue; + BOOL otherControls = s.faceBigEye || s.faceShort || s.faceNarrow || s.faceV || s.faceJaw || s.faceCheekbone || s.faceSmallHead; + // No full topology means no guessed bbox thin/chin deformation. Legacy + // archives using only those controls remain exact image/mask pass-through. + if (thinRatio <= .000001 && fabsf(chinRatio) <= .000001 && !otherControls) continue; CGRect le = PGRegionBounds(face, @"leftEye", PGInFace(box, 0.16, 0.53, 0.25, 0.15)); CGRect re = PGRegionBounds(face, @"rightEye", PGInFace(box, 0.59, 0.53, 0.25, 0.15)); CGPoint left = PGCenter(PGToPixels(le, extent)), right = PGCenter(PGToPixels(re, extent)); NSMutableArray *arguments = [@[PGVector(PGCenter(pixels)), [CIVector vectorWithX:pixels.size.width Y:pixels.size.height], PGVector(left), PGVector(right), [CIVector vectorWithX:MAX(2, pixels.size.width * 0.24) Y:MAX(2, pixels.size.height * 0.17)], @(thinRatio), @(PGClamp(s.faceBigEye, 0, 1)), @(PGClamp(s.faceShort, -1, 1)), - @(PGClamp(s.faceNarrow, -1, 1)), @(PGClamp(s.faceChin, -1, 1)), @(PGClamp(s.faceV, 0, 1)), + @(PGClamp(s.faceNarrow, -1, 1)), @(chinRatio), @(PGClamp(s.faceV, 0, 1)), @(PGClamp(s.faceJaw, 0, 1)), @(PGClamp(s.faceCheekbone, 0, 1)), @(PGClamp(s.faceSmallHead, 0, 1)), @(PGClamp(s.faceSmallHeadRange, 0, 1)), [CIVector vectorWithX:extent.origin.x Y:extent.origin.y], [CIVector vectorWithX:extent.size.width Y:extent.size.height]] mutableCopy]; for (NSUInteger i=0; i<17; i++) { @@ -524,6 +533,15 @@ static NSDictionary *PGStableGeometry(NSDictionary *current, NSDictionary *previ } [arguments addObject:[CIVector vectorWithX:ox Y:oy Z:ax W:ay]]; } + for (NSUInteger i=0; i<3; i++) { + double ox=0,oy=0,ax=0,ay=0; + if (points76) { + NSArray *origin=points76[PGChinPins[i][0]],*anchor=points76[PGChinPins[i][1]]; + ox=[origin[0] doubleValue];oy=1-[origin[1] doubleValue]; + ax=[anchor[0] doubleValue];ay=1-[anchor[1] doubleValue]; + } + [arguments addObject:[CIVector vectorWithX:ox Y:oy Z:ax W:ay]]; + } CIImage *input = [result imageByClampingToExtent]; result = [_warp applyWithExtent:extent roiCallback:^CGRect(int index, CGRect rect) { (void)index; (void)rect; return extent; } inputImage:input arguments:arguments]; if (!result) return nil; diff --git a/docs/analysis/chin-report.json b/docs/analysis/chin-report.json new file mode 100644 index 0000000..254a262 --- /dev/null +++ b/docs/analysis/chin-report.json @@ -0,0 +1,50 @@ +{ + "date": "2026-10-21", + "target_sha256": "f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb", + "claim_type": "static CPU assembly/constants and Metal AIR reconstruction, independently tested; no native-host observation", + "chin_pins": [ + [ + 68, + 38 + ], + [ + 67, + 37 + ], + [ + 66, + 36 + ] + ], + "chin_parameter_index": 5, + "signed_slider_range": [ + -1, + 1 + ], + "positive_scale": 0.04, + "negative_scale": 0.02, + "cpu_evidence": [ + "ev_640ccb714c5ce00d3864844af319fc574d223810dde08443d2ef2e8f1e989c47", + "ev_8bc31392c07788d2482546121297c3f177d527282275ef21fd3e5edef9921544" + ], + "air_sha256": "f97705b88f5491057df2117aef457ffca84e11020574cd57742b0670db4484d8", + "air_file_local_ignored": "build/pose-analysis/face-thin.air.txt", + "coordinate_contract": "Vision normalized y-up; upload pins and sampling coordinate converted once to y-down UV, result returned to CI y-up", + "gain": "clamp(280/max(length((P75-P59)*imageSize),2*length((P67-(P74+P60)/2)*imageSize),1),1,2.25)", + "pin_formula": "q=clamp(q-(anchor-origin)*ratio*clamp(1-distance(q,origin)/distance(anchor,origin),0,1),0,1)", + "positive_visual_direction": "chin toward lower lip (shorter)", + "negative_visual_direction": "chin away from lower lip (longer)", + "verification": { + "beauty": 49, + "face": 116, + "thin_chin": 394, + "failures": 0, + "maximum_uv_oracle_error": 1.09828022e-05 + }, + "limitations": [ + "Seven other shape controls remain approximations.", + "Combined analytic controls and multi-face resampling order is not fully reference equivalent.", + "FxPlug adapter imageOrigin handling is under separate investigation.", + "Motion/FCP final visual validation is reserved for user." + ] +} diff --git a/docs/analysis/rea-chin.json b/docs/analysis/rea-chin.json new file mode 100644 index 0000000..ec98c68 --- /dev/null +++ b/docs/analysis/rea-chin.json @@ -0,0 +1 @@ +{"artifacts":[{"architecture":"arm64","digest":{"sha256":"f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb"},"format":"mach-o"}],"captures":[],"environments":[],"providers":[{"id":"ghidra","name":"Ghidra","version":"12.1.4"}],"records":[{"analysis_profile":{"digest":"20565088524a2d0359b9b96f46ac48e679d774c7b0092c56abc06de8fdbec11c","parameters":{"analyzer_preset":"ghidra-default","annotation_policy":"atomic-function-entry-metadata-v1","architecture":"arm64","available_architectures":["arm64"],"compiler_spec_id":"auto-default","decompiler_jump_loads":true,"function_body_evidence":"complete-inclusive-ranges-v1","function_boundary_observations":"ghidra-terminal-call-limitations-v1","function_references":"complete-body-and-entry-reference-manager-v2","import_mode":"ephemeral-source-immutable","instruction_flow_evidence":"decoded-return-pcode-v1","jump_table_evidence":"typed-case-default-blocks-v1","language_id":"auto-from-header","load_image_observations":"source-mappings-entry-context-v2","loader":"auto-from-header","location_resolution":"explicit-address-exact-entry-symbol-first-v3","no_return_repair":"returning-imports-decoded-return-v2","process_launch":"inspected-jvm-launch-support-v1","string_inventory_evidence":"defined-data-coverage-v1","target_format":"mach-o","target_kind":"executable"},"provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"}},"authority":"shipped-artifact","confidence":"observed","environment":null,"evidence_id":"ev_640ccb714c5ce00d3864844af319fc574d223810dde08443d2ef2e8f1e989c47","evidence_links":[],"limitations":["The session serves operations only after Ghidra reports default auto-analysis complete; incomplete analysis does not expose partial results.","The imported Program and temporary project are ephemeral and deleted on close. Annotation operations edit session database metadata; original executable bytes are never written.","Cancelling an active Ghidra request terminates its ephemeral database and discards session annotations. The selected target remains open; the next provider query imports the original artifact again.","Default-space addresses use lowercase 0x-prefixed hexadecimal; other address spaces use :0x.","Bytes reflect Ghidra's initialized memory, including loader fixups. Reads stop at an unmapped or uninitialized byte and report completeness explicitly.","macOS sessions require a matching executable Ghidra native decompiler; REA checks for it but does not build native components or change Gatekeeper quarantine state."],"locations":[],"normalized_result":{"address":"0x10005f9c0","bytes_hex":"7b14ae47e17a943f7b14ae47e17aa43f","complete":true,"requested_bytes":16,"returned_bytes":16},"operation":"read_bytes","parameters":{"address":"0x10005f9c0","length":16},"predicate_type":"rea.analysis","provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"},"raw_result":{"address":"0x10005f9c0","bytes_hex":"7b14ae47e17a943f7b14ae47e17aa43f","complete":true,"requested_bytes":16,"returned_bytes":16},"subject":{"architecture":"arm64","digest":{"sha256":"f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb"},"format":"mach-o","local_path":"/private/tmp/paintfcp-rea.b3j9Te/MSE18V2-arm64","name":"MSE18V2-arm64"}},{"analysis_profile":{"digest":"20565088524a2d0359b9b96f46ac48e679d774c7b0092c56abc06de8fdbec11c","parameters":{"analyzer_preset":"ghidra-default","annotation_policy":"atomic-function-entry-metadata-v1","architecture":"arm64","available_architectures":["arm64"],"compiler_spec_id":"auto-default","decompiler_jump_loads":true,"function_body_evidence":"complete-inclusive-ranges-v1","function_boundary_observations":"ghidra-terminal-call-limitations-v1","function_references":"complete-body-and-entry-reference-manager-v2","import_mode":"ephemeral-source-immutable","instruction_flow_evidence":"decoded-return-pcode-v1","jump_table_evidence":"typed-case-default-blocks-v1","language_id":"auto-from-header","load_image_observations":"source-mappings-entry-context-v2","loader":"auto-from-header","location_resolution":"explicit-address-exact-entry-symbol-first-v3","no_return_repair":"returning-imports-decoded-return-v2","process_launch":"inspected-jvm-launch-support-v1","string_inventory_evidence":"defined-data-coverage-v1","target_format":"mach-o","target_kind":"executable"},"provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"}},"authority":"shipped-artifact","confidence":"observed","environment":null,"evidence_id":"ev_8bc31392c07788d2482546121297c3f177d527282275ef21fd3e5edef9921544","evidence_links":[],"limitations":["The session serves operations only after Ghidra reports default auto-analysis complete; incomplete analysis does not expose partial results.","The imported Program and temporary project are ephemeral and deleted on close. Annotation operations edit session database metadata; original executable bytes are never written.","Cancelling an active Ghidra request terminates its ephemeral database and discards session annotations. The selected target remains open; the next provider query imports the original artifact again.","Default-space addresses use lowercase 0x-prefixed hexadecimal; other address spaces use :0x.","Assembly is Ghidra Listing text and does not claim textual equivalence with Hopper output.","macOS sessions require a matching executable Ghidra native decompiler; REA checks for it but does not build native components or change Gatekeeper quarantine state."],"locations":[],"normalized_result":"0x1000279f4: stp d9, d8, [sp, #-0x30]!\n0x1000279f8: stp x20, x19, [sp, #0x10]\n0x1000279fc: stp x29, x30, [sp, #0x20]\n0x100027a00: add x29, sp, #0x20\n0x100027a04: fmov d8, d0\n0x100027a08: str xzr, [x8, #0x20]\n0x100027a0c: movi v0.2D, #0x0\n0x100027a10: stp q0, q0, [x8]\n0x100027a14: cbz x0, 0x100027bac\n0x100027a18: mov x20, x0\n0x100027a1c: mov x19, x8\n0x100027a20: bl 0x10001a8c0\n0x100027a24: cbz w0, 0x100027bac\n0x100027a28: fmov d1, 0x3ff0000000000000\n0x100027a2c: fcmp d8, d1\n0x100027a30: fcsel d2, d1, d8, gt\n0x100027a34: fcmp d8, #0.0\n0x100027a38: movi d0, #0x0\n0x100027a3c: fcsel d2, d0, d2, mi\n0x100027a40: fcvt s2, d2\n0x100027a44: ldr d3, [x20, #0x80]\n0x100027a48: fcmp d3, d1\n0x100027a4c: fcsel d4, d1, d3, gt\n0x100027a50: fmov d5, -0x4010000000000000\n0x100027a54: fcmp d3, d5\n0x100027a58: fcsel d4, d5, d4, mi\n0x100027a5c: fcmp d4, #0.0\n0x100027a60: cset w8, gt\n0x100027a64: fcvt d3, s2\n0x100027a68: ldp d5, d2, [x20, #0x60]\n0x100027a6c: fcmp d2, d1\n0x100027a70: fcsel d6, d1, d2, gt\n0x100027a74: adrp x9, 0x10005f000\n0x100027a78: ldr d7, [x9, #0xb80]\n0x100027a7c: fmul d6, d6, d7\n0x100027a80: fcmp d2, #0.0\n0x100027a84: fcsel d2, d0, d6, mi\n0x100027a88: fmul d2, d2, d3\n0x100027a8c: fcvt s2, d2\n0x100027a90: add x9, x20, #0x78\n0x100027a94: ldur q6, [x20, #0x88]\n0x100027a98: ld1 v5, x9, \n0x100027a9c: adrp x9, 0x10005f000\n0x100027aa0: ldr q7, [x9, #0x980]\n0x100027aa4: fcmgt v7.2D, v7.2D, v5.2D\n0x100027aa8: fcmlt v16.2D, v6.2D\n0x100027aac: fmov v17.2D, #0x3ff0000000000000\n0x100027ab0: fcmgt v18.2D, v5.2D, v17.2D\n0x100027ab4: bit v5.16B, v17.16B, v18.16B\n0x100027ab8: fcmgt v18.2D, v6.2D, v17.2D\n0x100027abc: bit v6.16B, v17.16B, v18.16B\n0x100027ac0: adrp x9, 0x10005f000\n0x100027ac4: ldr q18, [x9, #0x990]\n0x100027ac8: fmul v5.2D, v5.2D, v18.2D\n0x100027acc: adrp x9, 0x10005f000\n0x100027ad0: ldr q18, [x9, #0x9a0]\n0x100027ad4: fmul v6.2D, v6.2D, v18.2D\n0x100027ad8: adrp x9, 0x10005f000\n0x100027adc: ldr q18, [x9, #0x9b0]\n0x100027ae0: bit v5.16B, v18.16B, v7.16B\n0x100027ae4: bic v6.16B, v6.16B, v16.16B\n0x100027ae8: fmul v6.2D, v6.2D, v3.D[0x0]\n0x100027aec: fmul v5.2D, v5.2D, v3.D[0x0]\n0x100027af0: fcvtn v5.2S, v5.2D\n0x100027af4: fcvtn2 v5.4S, v6.2D\n0x100027af8: ldr d6, [x20, #0x98]\n0x100027afc: fcmp d6, d1\n0x100027b00: fcsel d7, d1, d6, gt\n0x100027b04: adrp x9, 0x10005f000\n0x100027b08: ldr d16, [x9, #0xe68]\n0x100027b0c: fmul d7, d7, d16\n0x100027b10: fcmp d6, #0.0\n0x100027b14: mov x9, #-0x8000000000000000\n0x100027b18: fmov d6, x9\n0x100027b1c: adrp x9, 0x10005f000\n0x100027b20: add x9, x9, #0x9c0\n0x100027b24: ldr d16, x9, w8, , uxtw, #3\n0x100027b28: fcsel d6, d6, d7, mi\n0x100027b2c: fmul d4, d4, d16\n0x100027b30: ldr d7, [x20, #0x70]\n0x100027b34: add x8, x20, #0xa0\n0x100027b38: ld1 v7, x8, \n0x100027b3c: adrp x8, 0x10005f000\n0x100027b40: ldr q16, [x8, #0x9d0]\n0x100027b44: fcmgt v16.2D, v16.2D, v7.2D\n0x100027b48: fcmgt v18.2D, v7.2D, v17.2D\n0x100027b4c: bit v7.16B, v17.16B, v18.16B\n0x100027b50: adrp x8, 0x10005f000\n0x100027b54: ldr q17, [x8, #0x9e0]\n0x100027b58: fmul v7.2D, v7.2D, v17.2D\n0x100027b5c: adrp x8, 0x10005f000\n0x100027b60: ldr q17, [x8, #0x9f0]\n0x100027b64: mov v6.D[0x1], v4.D[0x0]\n0x100027b68: mov v4.16B, v16.16B\n0x100027b6c: bsl v4.16B, v17.16B, v7.16B\n0x100027b70: fmul v6.2D, v6.2D, v3.D[0x0]\n0x100027b74: fmul v3.2D, v4.2D, v3.D[0x0]\n0x100027b78: fcvtn v4.2S, v6.2D\n0x100027b7c: fcvtn2 v4.4S, v3.2D\n0x100027b80: stp q5, q4, [x19]\n0x100027b84: ldr d3, [x20, #0xa8]\n0x100027b88: fcmp d3, d1\n0x100027b8c: fcsel d1, d1, d3, gt\n0x100027b90: adrp x8, 0x10005f000\n0x100027b94: ldr d4, [x8, #0xc40]\n0x100027b98: fmul d1, d1, d4\n0x100027b9c: fcmp d3, #0.0\n0x100027ba0: fcsel d0, d0, d1, mi\n0x100027ba4: fcvt s0, d0\n0x100027ba8: stp s0, s2, [x19, #0x20]\n0x100027bac: ldp x29, x30, [sp, #0x20]\n0x100027bb0: ldp x20, x19, [sp, #0x10]\n0x100027bb4: ldp d9, d8, [sp], #0x30\n0x100027bb8: ret","operation":"procedure_assembly","parameters":{"procedure":"0x1000279f4"},"predicate_type":"rea.analysis","provider":{"id":"ghidra","name":"Ghidra","version":"12.1.4"},"raw_result":{"limitations":[],"value":"0x1000279f4: stp d9, d8, [sp, #-0x30]!\n0x1000279f8: stp x20, x19, [sp, #0x10]\n0x1000279fc: stp x29, x30, [sp, #0x20]\n0x100027a00: add x29, sp, #0x20\n0x100027a04: fmov d8, d0\n0x100027a08: str xzr, [x8, #0x20]\n0x100027a0c: movi v0.2D, #0x0\n0x100027a10: stp q0, q0, [x8]\n0x100027a14: cbz x0, 0x100027bac\n0x100027a18: mov x20, x0\n0x100027a1c: mov x19, x8\n0x100027a20: bl 0x10001a8c0\n0x100027a24: cbz w0, 0x100027bac\n0x100027a28: fmov d1, 0x3ff0000000000000\n0x100027a2c: fcmp d8, d1\n0x100027a30: fcsel d2, d1, d8, gt\n0x100027a34: fcmp d8, #0.0\n0x100027a38: movi d0, #0x0\n0x100027a3c: fcsel d2, d0, d2, mi\n0x100027a40: fcvt s2, d2\n0x100027a44: ldr d3, [x20, #0x80]\n0x100027a48: fcmp d3, d1\n0x100027a4c: fcsel d4, d1, d3, gt\n0x100027a50: fmov d5, -0x4010000000000000\n0x100027a54: fcmp d3, d5\n0x100027a58: fcsel d4, d5, d4, mi\n0x100027a5c: fcmp d4, #0.0\n0x100027a60: cset w8, gt\n0x100027a64: fcvt d3, s2\n0x100027a68: ldp d5, d2, [x20, #0x60]\n0x100027a6c: fcmp d2, d1\n0x100027a70: fcsel d6, d1, d2, gt\n0x100027a74: adrp x9, 0x10005f000\n0x100027a78: ldr d7, [x9, #0xb80]\n0x100027a7c: fmul d6, d6, d7\n0x100027a80: fcmp d2, #0.0\n0x100027a84: fcsel d2, d0, d6, mi\n0x100027a88: fmul d2, d2, d3\n0x100027a8c: fcvt s2, d2\n0x100027a90: add x9, x20, #0x78\n0x100027a94: ldur q6, [x20, #0x88]\n0x100027a98: ld1 v5, x9, \n0x100027a9c: adrp x9, 0x10005f000\n0x100027aa0: ldr q7, [x9, #0x980]\n0x100027aa4: fcmgt v7.2D, v7.2D, v5.2D\n0x100027aa8: fcmlt v16.2D, v6.2D\n0x100027aac: fmov v17.2D, #0x3ff0000000000000\n0x100027ab0: fcmgt v18.2D, v5.2D, v17.2D\n0x100027ab4: bit v5.16B, v17.16B, v18.16B\n0x100027ab8: fcmgt v18.2D, v6.2D, v17.2D\n0x100027abc: bit v6.16B, v17.16B, v18.16B\n0x100027ac0: adrp x9, 0x10005f000\n0x100027ac4: ldr q18, [x9, #0x990]\n0x100027ac8: fmul v5.2D, v5.2D, v18.2D\n0x100027acc: adrp x9, 0x10005f000\n0x100027ad0: ldr q18, [x9, #0x9a0]\n0x100027ad4: fmul v6.2D, v6.2D, v18.2D\n0x100027ad8: adrp x9, 0x10005f000\n0x100027adc: ldr q18, [x9, #0x9b0]\n0x100027ae0: bit v5.16B, v18.16B, v7.16B\n0x100027ae4: bic v6.16B, v6.16B, v16.16B\n0x100027ae8: fmul v6.2D, v6.2D, v3.D[0x0]\n0x100027aec: fmul v5.2D, v5.2D, v3.D[0x0]\n0x100027af0: fcvtn v5.2S, v5.2D\n0x100027af4: fcvtn2 v5.4S, v6.2D\n0x100027af8: ldr d6, [x20, #0x98]\n0x100027afc: fcmp d6, d1\n0x100027b00: fcsel d7, d1, d6, gt\n0x100027b04: adrp x9, 0x10005f000\n0x100027b08: ldr d16, [x9, #0xe68]\n0x100027b0c: fmul d7, d7, d16\n0x100027b10: fcmp d6, #0.0\n0x100027b14: mov x9, #-0x8000000000000000\n0x100027b18: fmov d6, x9\n0x100027b1c: adrp x9, 0x10005f000\n0x100027b20: add x9, x9, #0x9c0\n0x100027b24: ldr d16, x9, w8, , uxtw, #3\n0x100027b28: fcsel d6, d6, d7, mi\n0x100027b2c: fmul d4, d4, d16\n0x100027b30: ldr d7, [x20, #0x70]\n0x100027b34: add x8, x20, #0xa0\n0x100027b38: ld1 v7, x8, \n0x100027b3c: adrp x8, 0x10005f000\n0x100027b40: ldr q16, [x8, #0x9d0]\n0x100027b44: fcmgt v16.2D, v16.2D, v7.2D\n0x100027b48: fcmgt v18.2D, v7.2D, v17.2D\n0x100027b4c: bit v7.16B, v17.16B, v18.16B\n0x100027b50: adrp x8, 0x10005f000\n0x100027b54: ldr q17, [x8, #0x9e0]\n0x100027b58: fmul v7.2D, v7.2D, v17.2D\n0x100027b5c: adrp x8, 0x10005f000\n0x100027b60: ldr q17, [x8, #0x9f0]\n0x100027b64: mov v6.D[0x1], v4.D[0x0]\n0x100027b68: mov v4.16B, v16.16B\n0x100027b6c: bsl v4.16B, v17.16B, v7.16B\n0x100027b70: fmul v6.2D, v6.2D, v3.D[0x0]\n0x100027b74: fmul v3.2D, v4.2D, v3.D[0x0]\n0x100027b78: fcvtn v4.2S, v6.2D\n0x100027b7c: fcvtn2 v4.4S, v3.2D\n0x100027b80: stp q5, q4, [x19]\n0x100027b84: ldr d3, [x20, #0xa8]\n0x100027b88: fcmp d3, d1\n0x100027b8c: fcsel d1, d1, d3, gt\n0x100027b90: adrp x8, 0x10005f000\n0x100027b94: ldr d4, [x8, #0xc40]\n0x100027b98: fmul d1, d1, d4\n0x100027b9c: fcmp d3, #0.0\n0x100027ba0: fcsel d0, d0, d1, mi\n0x100027ba4: fcvt s0, d0\n0x100027ba8: stp s0, s2, [x19, #0x20]\n0x100027bac: ldp x29, x30, [sp, #0x20]\n0x100027bb0: ldp x20, x19, [sp, #0x10]\n0x100027bb4: ldp d9, d8, [sp], #0x30\n0x100027bb8: ret"},"subject":{"architecture":"arm64","digest":{"sha256":"f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb"},"format":"mach-o","local_path":"/private/tmp/paintfcp-rea.b3j9Te/MSE18V2-arm64","name":"MSE18V2-arm64"}}],"scenarios":[],"unknowns":[]} \ No newline at end of file diff --git a/docs/analysis/下巴与坐标分析.md b/docs/analysis/下巴与坐标分析.md new file mode 100644 index 0000000..ae30908 --- /dev/null +++ b/docs/analysis/下巴与坐标分析.md @@ -0,0 +1,23 @@ +# 下巴控制与图像坐标核对 + +2026-10-21,接用户反馈“调整下巴却调整眼睛”。本轮不操作、启动或退出Motion/FCP。以已保存的原版AIR和新REA CPU汇编/常量证据继续分析;没有执行原版或复制其二进制到产品。 + +## 已确认的下巴差异 + +参数发布、PGChin=24、settings.faceChin与GPU参数位置没有串到大眼。1.0.5的下巴仍用脸框高斯场,存在两项错误:作用范围延伸到嘴部甚至眼睑,下巴正负方向与原版相反。独立真实照片上,旧公式的最大位移出现在嘴附近;侧脸真实下巴位移接近零。这些结论来自离屏照片/坐标场探针,不能代替实际宿主当帧诊断。 + +原版`0x1000279f4`汇编将下巴滑块约束到[-1,1],正值乘0.04、负值乘0.02,写入脸型结构第5项(byte offset20);常量位于`0x10005f9c0`。REA记录:`ev_8bc31392c07788d2482546121297c3f177d527282275ef21fd3e5edef9921544`与`ev_640ccb714c5ce00d3864844af319fc574d223810dde08443d2ef2e8f1e989c47`,完整小型证据包见[rea-chin.json](rea-chin.json)。原件身份与上一轮一致,arm64切片SHA-256为`f01e8c0b7e2676b2961950b0e48efdd2589af218e4b36de7d39f5d1c76852bfb`。 + +`mssFaceShapePinCoordinate`的AIR分支读取第5项及共同尺寸增益,顺序执行68→38、67→37、66→36三组局部采样位移。独立Vision区域核对:66/67/68在下巴轮廓,36/37/38在下唇。其半径、线性衰减、逐步clamp与17组瘦脸相同;支持负值。视觉正值缩短下巴、负值拉长,方向随下巴→嘴唇向量变化,不固定沿画面Y轴。细节和边界见[机器报告](chin-report.json)。 + +## 修正与测试 + +已移除旧下巴高斯项,改用原版三组点、正负系数、尺寸增益。完整76点缺失或不合法时跳过下巴,避免旧档案使用猜测框位置;其他七类脸型控制仍是现有近似实现,组合顺序未宣称全部恢复。 + +核心回归559项通过:美化49、人脸116、瘦脸/下巴394。新增正负下巴、半强度、左右侧脸投影、歪头、三种宽高比、非零原点、瘦脸叠加、坏点与旧档案保护。独立double采样场最大UV误差1.09828022e-5。真实NASA人像额外检查下巴位移、正负向量符号和两侧瞳孔保持不动;以坐标位移判定,避免只检查“像素有改变”。 + +## 图像适配层待核对项 + +纯CI图像中,76点落在真实瞳孔和下巴,没有全局Y轴反置。但FxPlug适配层曾根据imageOrigin在输入、输出各翻转一次。以相同物理top-first照片字节离屏构造TOP_LEFT输入输出时,旧代码默认直通仍精确一致,却能复现下巴作用到眼部的现象;这说明直通测试无法排除中间处理倒置。 + +imageOrigin描述坐标轴方向,不能单凭枚举推断IOSurface的物理行序。原版render主入口包装CIImage后仅见正尺度和位移,仍需核对其输出helper,不能仅凭主函数的有界检索宣称原版整个链条无翻转。原生FCP本次实际缓冲行序未捕获,最后视觉交由用户自测。临时照片、完整人脸坐标和探针输出均留在被忽略的build目录。 diff --git a/tests/face_tests.m b/tests/face_tests.m index 9fe21dd..1f37a52 100644 --- a/tests/face_tests.m +++ b/tests/face_tests.m @@ -323,7 +323,8 @@ int main(int argc, const char *argv[]) { *controls[i] = 1; PGFaceFrame *warped = [processor processImage:image settings:changed time:kCMTimeZero error:&error]; BOOL changedPixels=![Pixels(context, warped.image) isEqualToData:base]; - Check(warped != nil && (i==0 ? !changedPixels : changedPixels), i==0 ? @"thin safely skips a face with no indexed landmarks" : [@"geometry control changes pixels: " stringByAppendingString:names[i]]); + BOOL requiresPoints=(i==0 || i==4); + Check(warped != nil && (requiresPoints ? !changedPixels : changedPixels), requiresPoints ? @"thin/chin safely skip a face with no indexed landmarks" : [@"geometry control changes pixels: " stringByAppendingString:names[i]]); Check(CGRectEqualToRect(warped.image.extent, image.extent), @"warp preserves image extent"); } settings.faceShort = 0.8; diff --git a/tests/slim_tests.m b/tests/slim_tests.m index 3f7654f..cdfe27f 100644 --- a/tests/slim_tests.m +++ b/tests/slim_tests.m @@ -25,6 +25,7 @@ static NSDictionary *Geometry(double yaw,double roll) { for(NSUInteger i=0;i<76;i++)[p addObject:@[@.5,@.5]]; p[6]=@[@.34,@.65];p[13]=@[@.66,@.65]; p[47]=@[@.5,@.60];p[48]=@[@.5,@.52];p[49]=@[@.5,@.45];p[52]=@[@.5,@.42]; + p[36]=@[@.54,@.36];p[37]=@[@.5,@.35];p[38]=@[@.46,@.36]; for(NSUInteger i=0;i<17;i++) { double t=M_PI+M_PI*i/16; p[59+i]=@[@(.5+.29*cos(t)),@(.65+.43*sin(t))]; @@ -60,6 +61,21 @@ static CGPoint Oracle(CGPoint pixel,NSDictionary *g,CGRect extent,double thin) { } return CGPointMake(q.x,1-q.y); } +static CGPoint ChinOracle(CGPoint pixel,NSDictionary *g,CGRect e,double thin,double chin) { + CGPoint q=Oracle(pixel,g,e,thin);q.y=1-q.y; + NSArray *raw=g[@"points76"];CGPoint points[76]; + for(NSUInteger i=0;i<76;i++){points[i]=ReadPoint(raw[i]);points[i].y=1-points[i].y;} + double width=hypot((points[75].x-points[59].x)*e.size.width,(points[75].y-points[59].y)*e.size.height); + double height=2*hypot((points[67].x-(points[74].x+points[60].x)/2)*e.size.width,(points[67].y-(points[74].y+points[60].y)/2)*e.size.height); + double ratio=Clamp(chin,-1,1)*(chin>0?.04:.02)*Clamp(280/fmax(1,fmax(width,height)),1,2.25); + for(NSUInteger i=0;i<3;i++) { + CGPoint a=points[68-i],b=points[38-i];double r=hypot(a.x-b.x,a.y-b.y); + if(r<=.0001)continue; + double w=Clamp(1-hypot(q.x-a.x,q.y-a.y)/r,0,1); + q.x=Clamp(q.x-(b.x-a.x)*ratio*w,0,1);q.y=Clamp(q.y-(b.y-a.y)*ratio*w,0,1); + } + return CGPointMake(q.x,1-q.y); +} static CIImage *CoordinateImage(CGRect extent) { NSUInteger w=extent.size.width,h=extent.size.height; NSMutableData *d=[NSMutableData dataWithLength:w*h*4*sizeof(float)];float *p=d.mutableBytes; @@ -77,8 +93,8 @@ static NSData *Pixels(CIContext *c,CIImage *image) { static NSData *Archive(NSDictionary *geometry) { return [NSPropertyListSerialization dataWithPropertyList:@{@"version":@1,@"frames":@{@"0/1/0":@{@"value":@0,@"scale":@1,@"epoch":@0,@"faces":@[geometry]}}} format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil]; } -static void NumericCase(CIContext *c,PGFaceProcessor *p,CGRect e,NSDictionary *g,double amount,NSString *name) { - PGBeautySettings s=PGBeautyDefaults();s.faceThin=amount; +static void NumericSettingsCase(CIContext *c,PGFaceProcessor *p,CGRect e,NSDictionary *g,double amount,double chin,NSString *name) { + PGBeautySettings s=PGBeautyDefaults();s.faceThin=amount;s.faceChin=chin; CIImage *input=CoordinateImage(e),*output=[p warpImage:input geometry:@[g] settings:s extent:e]; Check(output!=nil && CGRectEqualToRect(output.extent,e),[name stringByAppendingString:@" preserves extent"]); NSData *data=Pixels(c,output);const float *pixels=data.bytes;double worst=0,maxMove=0; @@ -86,7 +102,7 @@ static void NumericCase(CIContext *c,PGFaceProcessor *p,CGRect e,NSDictionary *g NSUInteger w=e.size.width,h=e.size.height; for(NSUInteger y=3;y0.00006)fprintf(stderr,"%s oracle max error=%g at %lu,%lu expected=(%g,%g) actual=(%g,%g)\n",name.UTF8String,worst,(unsigned long)worstX,(unsigned long)worstY,worstExpected.x,worstExpected.y,worstActual.x,worstActual.y); maxOracleError=fmax(maxOracleError,worst); - Check(worst<0.00006,[name stringByAppendingString:@" GPU inverse field matches independent 17-pin oracle"]); - Check(amount==0 || maxMove>.0001,[name stringByAppendingString:@" exercises a nonzero displacement"]); + Check(worst<0.00006,[name stringByAppendingString:@" GPU field matches the independent thin/chin oracle"]); + Check((amount==0 && chin==0) || maxMove>.0001,[name stringByAppendingString:@" exercises a nonzero displacement"]); NSUInteger corner=4*((h-3)*w+(w-3)); Check(fabs(pixels[corner]-(w-2.5)/w)<1e-6 && fabs(pixels[corner+1]-(h-2.5)/h)<1e-6,[name stringByAppendingString:@" preserves distant background"]); } +static void NumericCase(CIContext *c,PGFaceProcessor *p,CGRect e,NSDictionary *g,double amount,NSString *name) { + NumericSettingsCase(c,p,e,g,amount,0,name); +} +static CGPoint Displacement(CIContext *c,CIImage *ramp,CIImage *warped,CGPoint uv) { + CGRect e=ramp.extent;CGRect point=CGRectMake(floor(e.origin.x+uv.x*e.size.width),floor(e.origin.y+uv.y*e.size.height),1,1); + float a[4],b[4]; + [c render:ramp toBitmap:a rowBytes:sizeof(a) bounds:point format:kCIFormatRGBAf colorSpace:nil]; + [c render:warped toBitmap:b rowBytes:sizeof(b) bounds:point format:kCIFormatRGBAf colorSpace:nil]; + return CGPointMake((b[0]-a[0])*e.size.width,-(b[1]-a[1])*e.size.height); +} +static void AnatomicalChecks(CIContext *c,PGFaceProcessor *p,NSDictionary *g,CGRect e) { + CIImage *ramp=CoordinateImage(e);NSArray *points=g[@"points76"]; + CGPoint chin=ReadPoint(points[67]),lip=ReadPoint(points[37]); + double ax=(lip.x-chin.x)*e.size.width,ay=(lip.y-chin.y)*e.size.height; + for(int sign=-1;sign<=1;sign+=2) { + PGBeautySettings s=PGBeautyDefaults();s.faceChin=sign; + CIImage *out=[p warpImage:ramp geometry:@[g] settings:s extent:e]; + CGPoint d=Displacement(c,ramp,out,chin); + Check(hypot(d.x,d.y)>.05,@"chin control moves the detected physical chin landmark"); + Check(sign*(d.x*ax+d.y*ay)<0,@"signed chin direction follows chin-to-lip anchor instead of a fixed image Y axis"); + BOOL outside=YES,eyesUnchanged=YES; + for(NSNumber *index in @[@6,@13]) { + CGPoint eye=ReadPoint(points[index.unsignedIntegerValue]); + for(NSUInteger i=0;i<3;i++) { + CGPoint a=ReadPoint(points[68-i]),b=ReadPoint(points[38-i]); + if(hypot(eye.x-a.x,eye.y-a.y)<=hypot(a.x-b.x,a.y-b.y)+.003)outside=NO; + } + CGPoint shift=Displacement(c,ramp,out,eye);eyesUnchanged &= hypot(shift.x,shift.y)<.002; + } + Check(outside && eyesUnchanged,@"both physical pupils outside chin supports remain unchanged"); + } +} int main(int argc,const char *argv[]) {@autoreleasepool { CIContext *c=[CIContext contextWithMTLDevice:MTLCreateSystemDefaultDevice() options:@{kCIContextWorkingColorSpace:NSNull.null,kCIContextOutputColorSpace:NSNull.null}]; @@ -112,6 +160,15 @@ int main(int argc,const char *argv[]) {@autoreleasepool { NumericCase(c,p,extents[e],Geometry(yaw[y],roll[r]),1,name); } NumericCase(c,p,extents[1],Geometry(.6,.2),.5,@"half strength"); + for(NSUInteger e=0;e<3;e++)for(NSUInteger y=0;y<3;y++)for(NSUInteger r=0;r<2;r++)for(int sign=-1;sign<=1;sign+=2) { + double angle=y==0?0:(y==1?1.4:-1.4); + NumericSettingsCase(c,p,extents[e],Geometry(angle,r?.45:0),0,sign, + [NSString stringWithFormat:@"chin size%lu yaw%lu roll%lu sign%d",(unsigned long)e,(unsigned long)y,(unsigned long)r,sign]); + } + NumericSettingsCase(c,p,extents[1],Geometry(.4,.2),.8,1,@"thin plus positive chin"); + NumericSettingsCase(c,p,extents[2],Geometry(-.6,-.2),1,-1,@"thin plus negative chin"); + NumericSettingsCase(c,p,extents[0],Geometry(0,0),0,.5,@"half positive chin"); + NumericSettingsCase(c,p,extents[0],Geometry(0,0),0,-.5,@"half negative chin"); CIImage *image=CoordinateImage(extents[1]);NSData *source=Pixels(c,image); PGBeautySettings s=PGBeautyDefaults();s.faceThin=1;s.trackingEnabled=1;NSError *error=nil; NSDictionary *g=Geometry(.9,.15);[p setTrackingData:Archive(g) forTarget:0]; @@ -136,6 +193,8 @@ int main(int argc,const char *argv[]) {@autoreleasepool { [p setTrackingData:Archive(bad) forTarget:0];frame=[p processImage:image settings:s time:kCMTimeZero error:&error]; Check(frame!=nil && frame.deformationGeometry.count==1,@"legacy/malformed indexed points retain the valid face record"); Check([Pixels(c,frame.image) isEqualToData:source],@"legacy/malformed indexed points never trigger a guessed symmetric thin warp"); + PGBeautySettings chinOnly=s;chinOnly.faceThin=0;chinOnly.faceChin=1; + Check([Pixels(c,[p processImage:image settings:chinOnly time:kCMTimeZero error:&error].image) isEqualToData:source],@"legacy/malformed indexed points never trigger guessed bbox chin deformation"); PGBeautySettings other=s;other.faceNarrow=.5; Check(![Pixels(c,[p processImage:image settings:other time:kCMTimeZero error:&error].image) isEqualToData:source],@"other controls remain available in legacy archives"); } @@ -158,6 +217,7 @@ int main(int argc,const char *argv[]) {@autoreleasepool { Check([real analyzeImage:portrait settings:rs time:kCMTimeZero error:&error],@"real pose analysis saves the complete geometry"); PGFaceProcessor *copy=[PGFaceProcessor new];[copy setTrackingData:[real trackingDataForTarget:0] forTarget:0];rs.trackingEnabled=1; Check([Pixels(c,[copy processImage:portrait settings:rs time:kCMTimeZero error:&error].image) isEqualToData:Pixels(c,f.image)],@"real pose replays from persisted 76 points with identical pixels"); + AnatomicalChecks(c,real,f.deformationGeometry.firstObject,portrait.extent); }} printf("PASS: %lu slim alignment checks; maximum UV oracle error %.9g\n",(unsigned long)checks,maxOracleError);return 0; }}