Extract stills for photo morphing
This commit is contained in:
@@ -18,14 +18,14 @@ struct ContentView: View {
|
||||
Text(mode.rawValue).tag(mode)
|
||||
}
|
||||
}
|
||||
.help("Video-Zeitraffer verarbeitet MOV/MP4-Clips. Foto-Morphing verarbeitet Bilder aus dem Input-Ordner und erzeugt Landmark-basierte Morph-Übergänge.")
|
||||
.help("Video-Zeitraffer verarbeitet MOV/MP4-Clips. Foto-Morphing nutzt Bilder aus dem Input-Ordner oder extrahiert passende Standbilder aus Videos und erzeugt Landmark-basierte Morph-Übergänge.")
|
||||
|
||||
pickerRow(
|
||||
title: "Input-Ordner",
|
||||
value: settings.inputFolder?.path(percentEncoded: false) ?? "Nicht gewählt",
|
||||
buttonTitle: "Auswählen",
|
||||
help: settings.renderMode == .photoMorphing
|
||||
? "Ordner mit chronologisch sortierten Bildern für Foto-Morphing."
|
||||
? "Ordner mit chronologisch sortierten Bildern oder Videos. Wenn keine Bilder gefunden werden, extrahiert GrowthLapse Standbilder aus den Videos."
|
||||
: "Ordner mit den chronologisch sortierten Quellvideos. Die gewählte Output-Datei wird automatisch aus der Input-Liste ausgeschlossen."
|
||||
) {
|
||||
selectInputFolder()
|
||||
|
||||
@@ -171,7 +171,7 @@ final class VideoProcessor: ObservableObject {
|
||||
appendLog("ffprobe: \(tools.ffprobe.path)")
|
||||
|
||||
if effectiveSettings.renderMode == .photoMorphing {
|
||||
return try await renderPhotoMorph(settings: effectiveSettings, ffmpeg: tools.ffmpeg)
|
||||
return try await renderPhotoMorph(settings: effectiveSettings, ffmpeg: tools.ffmpeg, ffprobe: tools.ffprobe)
|
||||
}
|
||||
|
||||
if effectiveSettings.stabilizationEnabled {
|
||||
@@ -269,7 +269,7 @@ final class VideoProcessor: ObservableObject {
|
||||
return RenderOutcome(outputURL: outputFile, project: renderProject)
|
||||
}
|
||||
|
||||
private func renderPhotoMorph(settings: RenderSettings, ffmpeg: URL) async throws -> RenderOutcome {
|
||||
private func renderPhotoMorph(settings: RenderSettings, ffmpeg: URL, ffprobe: URL) async throws -> RenderOutcome {
|
||||
let inputFolder = settings.inputFolder!
|
||||
let outputFile = settings.outputFile!
|
||||
let tempDirectory = try createWorkingDirectory(
|
||||
@@ -281,19 +281,28 @@ final class VideoProcessor: ObservableObject {
|
||||
temporaryDirectory = tempDirectory
|
||||
appendLog("Foto-Morphing: Temporärer Ordner \(tempDirectory.path)")
|
||||
|
||||
let images = try discoverImages(in: inputFolder, sortMode: settings.sortMode)
|
||||
guard images.count >= 2 else {
|
||||
throw VideoProcessingError.invalidSettings("Für Foto-Morphing braucht GrowthLapse mindestens zwei Bilder im Input-Ordner.")
|
||||
}
|
||||
appendLog("Foto-Morphing: \(images.count) Bild(er) gefunden.")
|
||||
|
||||
let targetDimensions = outputDimensionsForStillMode(settings)
|
||||
let framesDirectory = tempDirectory.appendingPathComponent("morph_frames", isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: framesDirectory, withIntermediateDirectories: true)
|
||||
let stillsDirectory = tempDirectory.appendingPathComponent("morph_stills", isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: stillsDirectory, withIntermediateDirectories: true)
|
||||
|
||||
let imageSources = try await photoMorphSources(
|
||||
inputFolder: inputFolder,
|
||||
outputFile: outputFile,
|
||||
settings: settings,
|
||||
ffmpeg: ffmpeg,
|
||||
ffprobe: ffprobe,
|
||||
stillsDirectory: stillsDirectory
|
||||
)
|
||||
guard imageSources.count >= 2 else {
|
||||
throw VideoProcessingError.invalidSettings("Für Foto-Morphing braucht GrowthLapse mindestens zwei Bilder oder Videos im Input-Ordner.")
|
||||
}
|
||||
appendLog("Foto-Morphing: \(imageSources.count) Standbild(er) bereit.")
|
||||
|
||||
progressText = "Analysiere Bilder"
|
||||
let morphImages = try await PhotoMorphRenderer.prepareImages(
|
||||
urls: images,
|
||||
urls: imageSources,
|
||||
targetDimensions: targetDimensions,
|
||||
targetFaceHeightRatio: settings.targetFaceHeightRatio
|
||||
) { [weak self] index, total, name, usedLandmarks in
|
||||
@@ -331,7 +340,7 @@ final class VideoProcessor: ObservableObject {
|
||||
settings: settings,
|
||||
outputFile: outputFile,
|
||||
targetDimensions: targetDimensions,
|
||||
images: images,
|
||||
images: imageSources,
|
||||
frameCount: frameCount
|
||||
)
|
||||
appendLog("Render-Protokoll: \(reportURL.path)")
|
||||
@@ -349,6 +358,89 @@ final class VideoProcessor: ObservableObject {
|
||||
))
|
||||
}
|
||||
|
||||
private func photoMorphSources(
|
||||
inputFolder: URL,
|
||||
outputFile: URL,
|
||||
settings: RenderSettings,
|
||||
ffmpeg: URL,
|
||||
ffprobe: URL,
|
||||
stillsDirectory: URL
|
||||
) async throws -> [URL] {
|
||||
let images = try discoverImages(in: inputFolder, sortMode: settings.sortMode)
|
||||
if !images.isEmpty {
|
||||
appendLog("Foto-Morphing: \(images.count) Bild(er) im Input-Ordner gefunden.")
|
||||
return images
|
||||
}
|
||||
|
||||
appendLog("Foto-Morphing: Keine Bilder gefunden, extrahiere Standbilder aus Videos.")
|
||||
let videos = try await discoverVideos(
|
||||
in: inputFolder,
|
||||
sortMode: settings.sortMode,
|
||||
duplicateClipSelection: settings.duplicateClipSelection,
|
||||
ffprobe: ffprobe,
|
||||
excluding: outputFile
|
||||
)
|
||||
var stills: [URL] = []
|
||||
for (index, video) in videos.enumerated() {
|
||||
try Task.checkCancellation()
|
||||
let stillURL = stillsDirectory.appendingPathComponent(String(format: "still_%04d.png", index + 1))
|
||||
let time = try await stillTime(for: video, settings: settings, clipNumber: index + 1, totalClips: videos.count)
|
||||
try await extractStillFrame(source: video.url, output: stillURL, time: time, ffmpeg: ffmpeg, index: index + 1, total: videos.count)
|
||||
stills.append(stillURL)
|
||||
}
|
||||
return stills
|
||||
}
|
||||
|
||||
private func stillTime(
|
||||
for video: VideoFile,
|
||||
settings: RenderSettings,
|
||||
clipNumber: Int,
|
||||
totalClips: Int
|
||||
) async throws -> Double {
|
||||
if settings.bestSegmentDetectionEnabled, video.duration > 0.5 {
|
||||
progressText = "Standbild suchen \(clipNumber) von \(totalClips)"
|
||||
appendLog("Foto-Morphing: suche Standbild aus \(video.displayName)")
|
||||
if let result = try await BestSegmentAnalyzer.detectBestSegment(
|
||||
source: video.url,
|
||||
duration: video.duration,
|
||||
segmentLength: min(settings.segmentLength, video.duration)
|
||||
) {
|
||||
let time = min(video.duration, result.start + min(settings.segmentLength, video.duration) / 2)
|
||||
appendLog("Foto-Morphing Standbild \(clipNumber): \(formatSeconds(time)), gute Frames \(result.goodFrameCount)/\(result.totalFrameCount)")
|
||||
return time
|
||||
}
|
||||
}
|
||||
|
||||
let fallback = settings.takeMiddleSegment ? video.duration / 2 : max(0, min(video.duration, settings.startOffset))
|
||||
appendLog("Foto-Morphing Standbild \(clipNumber): Fallback \(formatSeconds(fallback))")
|
||||
return fallback
|
||||
}
|
||||
|
||||
private func extractStillFrame(
|
||||
source: URL,
|
||||
output: URL,
|
||||
time: Double,
|
||||
ffmpeg: URL,
|
||||
index: Int,
|
||||
total: Int
|
||||
) async throws {
|
||||
progressText = "Standbild \(index) von \(total)"
|
||||
let arguments = [
|
||||
"-y",
|
||||
"-nostdin",
|
||||
"-ss", decimal(max(0, time)),
|
||||
"-i", source.path,
|
||||
"-frames:v", "1",
|
||||
"-vf", "format=rgb24",
|
||||
output.path
|
||||
]
|
||||
let result = try await runFFmpeg(ffmpeg, arguments: arguments)
|
||||
guard result.exitCode == 0 else {
|
||||
throw VideoProcessingError.ffmpegFailed("Standbild extrahieren \(source.lastPathComponent)", result.output)
|
||||
}
|
||||
appendLog("Foto-Morphing Standbild: \(source.lastPathComponent) -> \(output.lastPathComponent)")
|
||||
}
|
||||
|
||||
private func discoverImages(in folder: URL, sortMode: SortMode) throws -> [URL] {
|
||||
let allowed = Set(["jpg", "jpeg", "png", "heic", "heif", "tif", "tiff"])
|
||||
var urls = try FileManager.default.contentsOfDirectory(
|
||||
@@ -371,9 +463,6 @@ final class VideoProcessor: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
guard !urls.isEmpty else {
|
||||
throw VideoProcessingError.invalidSettings("Im Input-Ordner wurden keine Bilder gefunden. Unterstützt: JPG, PNG, HEIC, TIFF.")
|
||||
}
|
||||
return urls
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user