Keep portrait renders full frame

This commit is contained in:
Mikei386
2026-07-24 08:24:12 +02:00
parent 51d86dc3bd
commit 0b85f7d73a
4 changed files with 80 additions and 33 deletions
+1 -1
View File
@@ -186,7 +186,7 @@ struct ContentView: View {
.help("Analysiert das Originalvideo vor dem Schneiden und sucht das Fenster, in dem Gesicht und beide Augen am besten sichtbar sind. Wenn nichts Brauchbares gefunden wird, nutzt GrowthLapse die normale Mitte/Start-Offset-Logik.") .help("Analysiert das Originalvideo vor dem Schneiden und sucht das Fenster, in dem Gesicht und beide Augen am besten sichtbar sind. Wenn nichts Brauchbares gefunden wird, nutzt GrowthLapse die normale Mitte/Start-Offset-Logik.")
Toggle("Gesichtsgröße normalisieren", isOn: $settings.faceNormalizationEnabled) Toggle("Gesichtsgröße normalisieren", isOn: $settings.faceNormalizationEnabled)
.help("Verfolgt Gesicht und Augen mit Apple Vision, richtet die Augenlinie aus und bewegt den Bildausschnitt geglättet mit. Ist das Gesicht bereits zu groß, wird zur Not herausgezoomt.") .help("Verfolgt Gesicht und Augen mit Apple Vision und bewegt den Bildausschnitt geglättet mit. Das Bild bleibt immer vollflächig; fehlt Rand für die Zielgröße oder Drehung, hat ein randloses Bild Vorrang.")
HStack { HStack {
Text("Ziel-Gesichtshöhe") Text("Ziel-Gesichtshöhe")
+77 -30
View File
@@ -1579,7 +1579,7 @@ final class VideoProcessor: ObservableObject {
private func renderFingerprint(settings: RenderSettings, targetDimensions: VideoDimensions) -> String { private func renderFingerprint(settings: RenderSettings, targetDimensions: VideoDimensions) -> String {
[ [
"portrait-alignment-v3", "portrait-alignment-v4-full-frame",
"\(targetDimensions.width)x\(targetDimensions.height)", "\(targetDimensions.width)x\(targetDimensions.height)",
decimal(settings.targetClipLength), decimal(settings.targetClipLength),
"\(settings.fps)", "\(settings.fps)",
@@ -1786,8 +1786,10 @@ final class VideoProcessor: ObservableObject {
if let alignment { if let alignment {
let eyeMode = alignment.usesEyeLandmarks ? "Augenlinie" : "Gesichtsmitte" let eyeMode = alignment.usesEyeLandmarks ? "Augenlinie" : "Gesichtsmitte"
let zoomOut = alignment.contentScale < 0.999 ? ", Bild auf \(Int(alignment.contentScale * 100))% verkleinert" : "" let limitedByFrame = alignment.faceHeightRatio > settings.targetFaceHeightRatio + 0.01
appendLog("Gesichtsausrichtung Clip \(clipNumber): \(alignment.width)x\(alignment.height), \(eyeMode), Gesicht \(Int(alignment.faceHeightRatio * 100))%\(zoomOut)") ? ", Zielgröße wegen Bildrand nicht vollständig erreichbar"
: ""
appendLog("Gesichtsausrichtung Clip \(clipNumber): \(alignment.width)x\(alignment.height), \(eyeMode), Gesicht \(Int(alignment.faceHeightRatio * 100))%\(limitedByFrame)")
} else { } else {
appendLog("Kein Gesicht in Clip \(clipNumber) erkannt. Render ohne Face-Normalisierung.") appendLog("Kein Gesicht in Clip \(clipNumber) erkannt. Render ohne Face-Normalisierung.")
} }
@@ -2759,19 +2761,38 @@ private enum FaceAlignmentAnalyzer {
} }
let outputAspect = Double(targetDimensions.width) / Double(targetDimensions.height) let outputAspect = Double(targetDimensions.width) / Double(targetDimensions.height)
let desiredCropHeight = faceHeightPixels / targetFaceHeightRatio var cropHeight = faceHeightPixels / targetFaceHeightRatio
let desiredCropWidth = desiredCropHeight * outputAspect var cropWidth = cropHeight * outputAspect
let contentScale = min( if cropWidth > Double(imageWidth) {
1, cropWidth = Double(imageWidth)
Double(imageWidth) / desiredCropWidth, cropHeight = cropWidth / outputAspect
Double(imageHeight) / desiredCropHeight }
) if cropHeight > Double(imageHeight) {
let cropWidth = max(2, even(Int((desiredCropWidth * contentScale).rounded(.down)))) cropHeight = Double(imageHeight)
let cropHeight = max(2, even(Int((desiredCropHeight * contentScale).rounded(.down)))) cropWidth = cropHeight * outputAspect
}
let evenCropWidth = max(2, even(Int(cropWidth.rounded(.down))))
let evenCropHeight = max(2, even(Int(cropHeight.rounded(.down))))
let eyeAngles = samples.compactMap(\.eyeAngle) let eyeAngles = samples.compactMap(\.eyeAngle)
let rotation = clamp(-median(eyeAngles), min: -0.14, max: 0.14) let proposedRotation = clamp(-median(eyeAngles), min: -0.14, max: 0.14)
let safeFrame = safeFrameAfterRotation(
imageWidth: imageWidth,
imageHeight: imageHeight,
aspect: outputAspect,
rotation: proposedRotation
)
let canRotateWithoutBlack = Double(evenCropWidth) <= safeFrame.width
&& Double(evenCropHeight) <= safeFrame.height
let rotation = canRotateWithoutBlack ? proposedRotation : 0
let usesEyes = !eyeAngles.isEmpty let usesEyes = !eyeAngles.isEmpty
let center = CGPoint(x: Double(imageWidth) / 2, y: Double(imageHeight) / 2) let center = CGPoint(x: Double(imageWidth) / 2, y: Double(imageHeight) / 2)
let allowedFrame = rotation == 0
? CGRect(x: 0, y: 0, width: imageWidth, height: imageHeight)
: safeFrame
let minX = allowedFrame.minX
let minY = allowedFrame.minY
let maxX = allowedFrame.maxX - Double(evenCropWidth)
let maxY = allowedFrame.maxY - Double(evenCropHeight)
let rawPositions = samples.map { sample -> FaceAlignmentSample in let rawPositions = samples.map { sample -> FaceAlignmentSample in
let fallback = CGPoint( let fallback = CGPoint(
@@ -2780,32 +2801,58 @@ private enum FaceAlignmentAnalyzer {
) )
var anchor = sample.eyeCenter ?? fallback var anchor = sample.eyeCenter ?? fallback
anchor = rotated(anchor, around: center, by: rotation) anchor = rotated(anchor, around: center, by: rotation)
anchor = CGPoint(
x: center.x + (anchor.x - center.x) * contentScale,
y: center.y + (anchor.y - center.y) * contentScale
)
let desiredY = sample.eyeCenter == nil ? 0.43 : 0.40 let desiredY = sample.eyeCenter == nil ? 0.43 : 0.40
return FaceAlignmentSample( return FaceAlignmentSample(
time: sample.time, time: sample.time,
x: clamp(anchor.x - Double(cropWidth) / 2, min: 0, max: Double(imageWidth - cropWidth)), x: clamp(anchor.x - Double(evenCropWidth) / 2, min: minX, max: maxX),
y: clamp(anchor.y - Double(cropHeight) * desiredY, min: 0, max: Double(imageHeight - cropHeight)) y: clamp(anchor.y - Double(evenCropHeight) * desiredY, min: minY, max: maxY)
) )
} }
let positions = smoothed(rawPositions, imageWidth: imageWidth, imageHeight: imageHeight, cropWidth: cropWidth, cropHeight: cropHeight) let positions = smoothed(
rawPositions,
minX: minX,
maxX: maxX,
minY: minY,
maxY: maxY
)
return FaceAlignment( return FaceAlignment(
sourceWidth: imageWidth, sourceWidth: imageWidth,
sourceHeight: imageHeight, sourceHeight: imageHeight,
width: cropWidth, width: evenCropWidth,
height: cropHeight, height: evenCropHeight,
rotationRadians: rotation, rotationRadians: rotation,
contentScale: contentScale, contentScale: 1,
samples: positions, samples: positions,
faceHeightRatio: targetFaceHeightRatio, faceHeightRatio: faceHeightPixels / Double(evenCropHeight),
usesEyeLandmarks: usesEyes usesEyeLandmarks: usesEyes
) )
} }
private static func safeFrameAfterRotation(
imageWidth: Int,
imageHeight: Int,
aspect: Double,
rotation: Double
) -> CGRect {
let angle = abs(rotation)
let cosine = cos(angle)
let sine = sin(angle)
let width = Double(imageWidth)
let height = Double(imageHeight)
let safeHeight = min(
width / (aspect * cosine + sine),
height / (aspect * sine + cosine)
)
let safeWidth = safeHeight * aspect
return CGRect(
x: (width - safeWidth) / 2,
y: (height - safeHeight) / 2,
width: safeWidth,
height: safeHeight
)
}
private static func rotated(_ point: CGPoint, around center: CGPoint, by angle: Double) -> CGPoint { private static func rotated(_ point: CGPoint, around center: CGPoint, by angle: Double) -> CGPoint {
let dx = point.x - center.x let dx = point.x - center.x
let dy = point.y - center.y let dy = point.y - center.y
@@ -2819,10 +2866,10 @@ private enum FaceAlignmentAnalyzer {
private static func smoothed( private static func smoothed(
_ samples: [FaceAlignmentSample], _ samples: [FaceAlignmentSample],
imageWidth: Int, minX: Double,
imageHeight: Int, maxX: Double,
cropWidth: Int, minY: Double,
cropHeight: Int maxY: Double
) -> [FaceAlignmentSample] { ) -> [FaceAlignmentSample] {
samples.indices.map { index in samples.indices.map { index in
let lower = max(0, index - 2) let lower = max(0, index - 2)
@@ -2830,8 +2877,8 @@ private enum FaceAlignmentAnalyzer {
let window = samples[lower...upper] let window = samples[lower...upper]
return FaceAlignmentSample( return FaceAlignmentSample(
time: samples[index].time, time: samples[index].time,
x: clamp(median(window.map(\.x)), min: 0, max: Double(imageWidth - cropWidth)), x: clamp(median(window.map(\.x)), min: minX, max: maxX),
y: clamp(median(window.map(\.y)), min: 0, max: Double(imageHeight - cropHeight)) y: clamp(median(window.map(\.y)), min: minY, max: maxY)
) )
} }
} }
+2 -2
View File
@@ -19,9 +19,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.2.0</string> <string>0.2.1</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>2</string> <string>3</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>13.0</string> <string>13.0</string>
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>
Binary file not shown.