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.")
|
.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")
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
BIN
Binary file not shown.
Reference in New Issue
Block a user