Keep portrait renders full frame
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
BIN
Binary file not shown.
Reference in New Issue
Block a user