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.")
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 {
Text("Ziel-Gesichtshöhe")
+77 -30
View File
@@ -1579,7 +1579,7 @@ final class VideoProcessor: ObservableObject {
private func renderFingerprint(settings: RenderSettings, targetDimensions: VideoDimensions) -> String {
[
"portrait-alignment-v3",
"portrait-alignment-v4-full-frame",
"\(targetDimensions.width)x\(targetDimensions.height)",
decimal(settings.targetClipLength),
"\(settings.fps)",
@@ -1786,8 +1786,10 @@ final class VideoProcessor: ObservableObject {
if let alignment {
let eyeMode = alignment.usesEyeLandmarks ? "Augenlinie" : "Gesichtsmitte"
let zoomOut = alignment.contentScale < 0.999 ? ", Bild auf \(Int(alignment.contentScale * 100))% verkleinert" : ""
appendLog("Gesichtsausrichtung Clip \(clipNumber): \(alignment.width)x\(alignment.height), \(eyeMode), Gesicht \(Int(alignment.faceHeightRatio * 100))%\(zoomOut)")
let limitedByFrame = alignment.faceHeightRatio > settings.targetFaceHeightRatio + 0.01
? ", 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 {
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 desiredCropHeight = faceHeightPixels / targetFaceHeightRatio
let desiredCropWidth = desiredCropHeight * outputAspect
let contentScale = min(
1,
Double(imageWidth) / desiredCropWidth,
Double(imageHeight) / desiredCropHeight
)
let cropWidth = max(2, even(Int((desiredCropWidth * contentScale).rounded(.down))))
let cropHeight = max(2, even(Int((desiredCropHeight * contentScale).rounded(.down))))
var cropHeight = faceHeightPixels / targetFaceHeightRatio
var cropWidth = cropHeight * outputAspect
if cropWidth > Double(imageWidth) {
cropWidth = Double(imageWidth)
cropHeight = cropWidth / outputAspect
}
if cropHeight > Double(imageHeight) {
cropHeight = Double(imageHeight)
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 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 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 fallback = CGPoint(
@@ -2780,32 +2801,58 @@ private enum FaceAlignmentAnalyzer {
)
var anchor = sample.eyeCenter ?? fallback
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
return FaceAlignmentSample(
time: sample.time,
x: clamp(anchor.x - Double(cropWidth) / 2, min: 0, max: Double(imageWidth - cropWidth)),
y: clamp(anchor.y - Double(cropHeight) * desiredY, min: 0, max: Double(imageHeight - cropHeight))
x: clamp(anchor.x - Double(evenCropWidth) / 2, min: minX, max: maxX),
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(
sourceWidth: imageWidth,
sourceHeight: imageHeight,
width: cropWidth,
height: cropHeight,
width: evenCropWidth,
height: evenCropHeight,
rotationRadians: rotation,
contentScale: contentScale,
contentScale: 1,
samples: positions,
faceHeightRatio: targetFaceHeightRatio,
faceHeightRatio: faceHeightPixels / Double(evenCropHeight),
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 {
let dx = point.x - center.x
let dy = point.y - center.y
@@ -2819,10 +2866,10 @@ private enum FaceAlignmentAnalyzer {
private static func smoothed(
_ samples: [FaceAlignmentSample],
imageWidth: Int,
imageHeight: Int,
cropWidth: Int,
cropHeight: Int
minX: Double,
maxX: Double,
minY: Double,
maxY: Double
) -> [FaceAlignmentSample] {
samples.indices.map { index in
let lower = max(0, index - 2)
@@ -2830,8 +2877,8 @@ private enum FaceAlignmentAnalyzer {
let window = samples[lower...upper]
return FaceAlignmentSample(
time: samples[index].time,
x: clamp(median(window.map(\.x)), min: 0, max: Double(imageWidth - cropWidth)),
y: clamp(median(window.map(\.y)), min: 0, max: Double(imageHeight - cropHeight))
x: clamp(median(window.map(\.x)), min: minX, max: maxX),
y: clamp(median(window.map(\.y)), min: minY, max: maxY)
)
}
}
+2 -2
View File
@@ -19,9 +19,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.2.0</string>
<string>0.2.1</string>
<key>CFBundleVersion</key>
<string>2</string>
<string>3</string>
<key>LSMinimumSystemVersion</key>
<string>13.0</string>
<key>NSHighResolutionCapable</key>
Binary file not shown.