Ho 65 file video di media denominati tape_01.mov, tape_02.mov, …, tape_65.mov. Ognuno è di circa 2,5 ore e occupa molti gigabyte. Sono copie digitali di quello che sono stati i nastri VHS ("home movies" della mia famiglia).
Ogni file video .mov di 2,5 ore contiene molti "capitoli" (nel senso che a volte la scena termina in una dissolvenza a uno schermo nero e poi scompare una nuova scena).
Il mio objective principale è avere i 65 file di grandi size affiancati in file più piccoli per capitolo. E voglio che questo sia fatto automaticamente attraverso la rilevazione delle cornici nere tra "scene".
Potrei usare ffmpeg (o qualsiasi altra cosa) per passare nei 65 nomi di file originali e farlo automaticamente iterare su each video e tagliarla, nominando i pezzi risultanti tape_01-ch_01.mov, tape_01-ch_02.mov, tape_01-ch_03.mov, eccetera? E voglio che farlo senza ricodifica (voglio che sia una semplice fetta senza perdita).
Come posso fare questo?
Ecco i passaggi che voglio:
Mi piacerebbe che questi script siano script di shell che posso eseguire in Mac, Ubuntu e Windows Git Bash.
Ecco due script PowerShell per dividere video lunghi in capitoli più piccoli da scene nere.
Salvalo come Detect_black.ps1 e Cut_black.ps1. Scaricare ffmpeg per Windows e indicare lo script il path del tuo ffmpeg.exe e della tua cartella video sotto la sezione delle opzioni.
Entrambi gli script non toccheranno i file video esistenti, restano intatti.
Tuttavia, otterrai un paio di nuovi file nello stesso luogo in cui sono i tuoi video di input
### Options __________________________________________________________________________________________________________ $ffmpeg = ".\ffmpeg.exe" # Set path to your ffmpeg.exe; Build Version: git-45581ed (2014-02-16) $folder = ".\Videos\*" # Set path to your video folder; '\*' must be appended $filter = @("*.mov","*.mp4") # Set which file extensions should be processed $dur = 4 # Set the minimum detected black duration (in seconds) $pic = 0.98 # Set the threshold for considering a picture as "black" (in percent) $pix = 0.15 # Set the threshold for considering a pixel "black" (in luminance) ### Main Program ______________________________________________________________________________________________________ foreach ($video in dir $folder -include $filter -exclude "*_???.*" -r){ ### Set path to logfile $logfile = "$($video.FullName)_ffmpeg.log" ### analyse each video with ffmpeg and search for black scenes & $ffmpeg -i $video -vf blackdetect=d=`"$dur`":pic_th=`"$pic`":pix_th=`"$pix`" -an -f null - 2> $logfile ### Use regex to extract timings from logfile $report = @() Select-String 'black_start:.*black_end:' $logfile | % { $black = "" | Select start, end, cut # extract start time of black scene $start_s = $_.line -match '(?<=black_start:)\S*(?= black_end:)' | % {$matches[0]} $start_ts = [timespan]::fromseconds($start_s) $black.start = "{0:HH:mm:ss.fff}" -f ([datetime]$start_ts.Ticks) # extract duration of black scene $end_s = $_.line -match '(?<=black_end:)\S*(?= black_duration:)' | % {$matches[0]} $end_ts = [timespan]::fromseconds($end_s) $black.end = "{0:HH:mm:ss.fff}" -f ([datetime]$end_ts.Ticks) # calculate cut point: black start time + black duration / 2 $cut_s = ([double]$start_s + [double]$end_s) / 2 $cut_ts = [timespan]::fromseconds($cut_s) $black.cut = "{0:HH:mm:ss.fff}" -f ([datetime]$cut_ts.Ticks) $report += $black } ### Write start time, duration and the cut point for each black scene to a seperate CSV $report | Export-Csv -path "$($video.FullName)_cutpoints.csv" –NoTypeInformation }
Il primo script si estende attraverso tutti i file video che corrispondono a un'estensione specificata e non corrispondono al pattern *_???.*
Poiché i nuovi capitoli video sono stati denominati <filename>_###.<ext>
e vogliamo escludere loro.
<video_name>_cutpoints.txt
ricerca di tutte le scene nere e scrive il timestamp di inizio e la durata della scena nera in un nuovo file CSV denominato <video_name>_cutpoints.txt
Inoltre calcola i punti di taglio come mostrato: cutpoint = black_start + black_duration / 2
. Più tardi, il video viene segmentato in questi timestamps.
Il file cutpoints.txt per il video di esempio mostrerebbe:
start end cut 00:03:56.908 00:04:02.247 00:03:59.578 00:08:02.525 00:08:10.233 00:08:06.379
Dopo una corsa, è ansible manipolare manualmente i punti di taglio se desiderato. Se si esegue nuovamente lo script, tutti i vecchi contenuti vengono sovrascritti. Fare attenzione quando si modifica manualmente e salva il lavoro altrove.
Per il video di esempio il command ffmpeg per rilevare scene nere è
$ffmpeg -i "Tape_10_3b.mp4" -vf blackdetect=d=4:pic_th=0.98:pix_th=0.15 -an -f null
Ci sono 3 numbers importnti che sono modificabili nella sezione di opzioni del script
d=4
significa solo scene nere più lunghe di 4 secondi pic_th=0.98
è la soglia per considerare un'image come "nera" (in percentuale) pix=0.15
imposta la soglia per considerare un pixel come "nero" (in luminanza). Poiché hai vecchi video VHS, non hai scene completamente nere nei tuoi video. Il valore predefinito 10 non funziona e ho dovuto aumentare leggermente la soglia Se qualcosa va storto, controllare il logfile corrispondente chiamato <video_name>__ffmpeg.log
. Se mancano le seguenti righe, aumenta i numbers sopra indicati fino a individuare tutte le scene nere:
[blackdetect @ 0286ec80] black_start:236.908 black_end:242.247 black_duration:5.33877
### Options __________________________________________________________________________________________________________ $ffmpeg = ".\ffmpeg.exe" # Set path to your ffmpeg.exe; Build Version: git-45581ed (2014-02-16) $folder = ".\Videos\*" # Set path to your video folder; '\*' must be appended $filter = @("*.mov","*.mp4") # Set which file extensions should be processed ### Main Program ______________________________________________________________________________________________________ foreach ($video in dir $folder -include $filter -exclude "*_???.*" -r){ ### Set path to logfile $logfile = "$($video.FullName)_ffmpeg.log" ### Read in all cutpoints from *_cutpoints.csv; concat to string eg "00:03:23.014,00:06:32.289,..." $cuts = ( Import-Csv "$($video.FullName)_cutpoints.csv" | % {$_.cut} ) -join "," ### put together the correct new name, "%03d" is a generic number placeholder for ffmpeg $output = $video.directory.Fullname + "\" + $video.basename + "_%03d" + $video.extension ### use ffmpeg to split current video in parts according to their cut points & $ffmpeg -i $video -f segment -segment_times $cuts -c copy -map 0 $output 2> $logfile }
Il secondo script si estende su tutti i file video nello stesso modo in cui il primo script ha fatto. Legge solo i timestamps tagliati dai corrispondenti cutpoints.txt
di un video.
Successivamente, mette insieme un nome di file appropriato per i file capitolo e dice a ffmpeg di segmentare il video. Attualmente i video vengono tagliati senza ricodifica (superfast e lossless). A causa di questo, potrebbe esserci un impreciso di 1-2s con timestamps del punto di taglio perché ffmpeg può solo tagliare a key_frames. Poiché abbiamo solo copiato e non ricodifica, non possiamo inserire i key_frames da soli.
Il command per il video di esempio sarebbe
$ffmpeg -i "Tape_10_3b.mp4" -f segment -segment_times "00:03:59.578,00:08:06.379" -c copy -map 0 "Tape_10_3b_(%03d).mp4"
Se qualcosa va storto, osserva il corrispondente ffmpeg.log
-c copy
è sufficiente per lo scenario di OP o di che abbiamo bisogno di ri-codificare completamente.Riceverai un'image per each capitolo video che sembra l'esempio riportto di seguito.
### Options __________________________________________________________________________________________________________ $ffmpeg = ".\ffmpeg.exe" # Set path to your ffmpeg.exe; Build Version: git-45581ed (2014-02-16) $folder = ".\Videos\*" # Set path to your video folder; '\*' must be appended $x = 5 # Set how many images per x-axis $y = 4 # Set how many images per y-axis $w = 384 # Set thumbnail width in px (full image width: 384px x 5 = 1920px,) ### Main Program ______________________________________________________________________________________________________ foreach ($video in dir $folder -include "*_???.mp4" -r){ ### get video length in frames, an ingenious method $log = & $ffmpeg -i $video -vcodec copy -an -f null $video 2>&1 $frames = $log | Select-String '(?<=frame=.*)\S+(?=.*fps=)' | % { $_.Matches } | % { $_.Value } $frame = [Math]::floor($frames / ($x * $y)) ### put together the correct new picture name $output = $video.directory.Fullname + "\" + $video.basename + ".jpg" ### use ffmpeg to create one mosaic png per video file ### Basic explanation for -vf options: http://trac.ffmpeg.org/wiki/FilteringGuide #1 & $ffmpeg -y -i $video -vf "select=not(mod(n\,`"$frame`")),scale=`"$w`":-1,tile=`"$x`"x`"$y`"" -frames:v 1 $output #2 & $ffmpeg -y -i $video -vf "yadif,select=not(mod(n\,`"$frame`")),scale=`"$w`":-1,tile=`"$x`"x`"$y`"" -frames:v 1 $output & $ffmpeg -y -i $video -vf "mpdecimate,yadif,select=not(mod(n\,`"$frame`")),scale=`"$w`":-1,tile=`"$x`"x`"$y`"" -frames:v 1 $output #4 & $ffmpeg -y -i $video -vf "select='gt(scene\,0.06)',scale=`"$w`":-1,tile=`"$x`"x`"$y`"" -frames:v 1 -vsync vfr $output #5 & $ffmpeg -y -i $video -vf "yadif,select='gt(scene\,0.06)',scale=`"$w`":-1,tile=`"$x`"x`"$y`"" -frames:v 1 -vsync vfr $output #6 & $ffmpeg -y -i $video -vf "mpdecimate,yadif,select='gt(scene\,0.06)',scale=`"$w`":-1,tile=`"$x`"x`"$y`"" -frames:v 1 -vsync vfr $output #7 & $ffmpeg -y -i $video -vf "thumbnail,scale=`"$w`":-1,tile=`"$x`"x`"$y`"" -frames:v 1 $output #8 & $ffmpeg -y -i $video -vf "yadif,thumbnail,scale=`"$w`":-1,tile=`"$x`"x`"$y`"" -frames:v 1 $output #9 & $ffmpeg -y -i $video -vf "mpdecimate,yadif,thumbnail,scale=`"$w`":-1,tile=`"$x`"x`"$y`"" -frames:v 1 $output }
L'idea principale è quella di get un stream continuo di immagini sul video completo. Lo facciamo con l'opzione di selezione di ffmpeg.
In primo luogo, recupebranch il count totale del fotogramma con un metodo ingegnoso (es. 2000) e dividerlo attraverso il nostro numero di anteprima predefinito (es. 5 x 4 = 20). Quindi, vogliamo generare un'image each 100 fotogrammi dal 2000 / 20 = 100
Il command ffmpeg generato per generare la miniatura potrebbe essere simile
ffmpeg -y -i input.mp4 -vf "mpdecimate,yadif,select=not(mod(n\,100)),scale=384:-1,tile=5x4" -frames:v 1 output.png
Nel codice di cui sopra, si vedono 9 diverse combinazioni -vf
consistenti di
select=not(mod(n\,XXX))
where XXX è un framerato calcolato thumbnail
che seleziona automaticamente i fotogrammi più rappresentativi select='gt(scene\,XXX)
+ -vsync vfr
where XXX è una soglia con cui devi giocare mpdecimate
– rimuove i fotogrammi quasi duplicati. Buono contro scene nere yadif
– Deinterlace l'image di ingresso. Non so perché, ma funziona La versione 3 è la scelta migliore a mio parere. Tutti gli altri sono commentati, ma puoi ancora provarli. Ho potuto rimuovere la maggior parte delle miniature sfocate usando mpdecimate
, yadif
e select=not(mod(n\,XXX))
. Si!
Per il tuo video di esempio ottengo queste anteprime
clicca per ingrandire
clicca per ingrandire
Ho caricato tutte le miniature create da quelle versioni. Date un'occhiata a loro per un confronto completo.
Apprezzate entrambi gli script, ottimo modo per dividere i video.
Tuttavia, ho avuto alcuni problemi con i video che non hanno mostrato tempo corretto in seguito e non sono stato in grado di saltare a un tempo specifico. Una soluzione è stata quella di demux e poi mux i flussi utilizzando Mp4Box.
Un altro modo più semplice per me è stato quello di utilizzare mkvmerge per la divisione. Pertanto, entrambi gli scripts wherevano essere alterati. Per detect_black.ps1, solo l'opzione "* .mkv" whereva essere aggiunta all'opzione del filter, ma non è necessariamente se si inizia solo con i file mp4.
### Options $mkvmerge = ".\mkvmerge.exe" # Set path to mkvmerge.exe $folder = ".\*" # Set path to your video folder; '\*' must be appended $filter = @("*.mov", "*.mp4", "*.mkv") # Set which file extensions should be processed ### Main Program foreach ($video in dir $folder -include $filter -exclude "*_???.*" -r){ ### Set path to logfile $logfile = "$($video.FullName)_ffmpeg.log" ### Read in all cutpoints from *_cutpoints.csv; concat to string eg "00:03:23.014,00:06:32.289,..." $cuts = ( Import-Csv "$($video.FullName)_cutpoints.csv" | % {$_.cut} ) -join "," ### put together the correct new name, "%03d" is a generic number placeholder for mkvmerge $output = $video.directory.Fullname + "\" + $video.basename + "-%03d" + ".mkv" ### use mkvmerge to split current video in parts according to their cut points and convert to mkv & $mkvmerge -o $output --split timecodes:$cuts $video }
La function è la stessa, ma nessun problema con i tempi video finora. Grazie per l'ispirazione.
Spero che mi sbaglio, ma non credo che la tua richiesta sia ansible. Potrebbe essere ansible durante la nuova codifica del video, ma come una "fetta semplice senza perdita" non so di alcun modo.
Molti programmi di editing video avranno una function di analisi automatica o qualcosa di equivalente che potrebbe essere in grado di dividere i tuoi video nei frame neri, ma questo richiederà la nuova codifica del video.
In bocca al lupo!