貧者のcomskip (1) - パラメータ多すぎてわかんねえ
erikkaashoek:Comskipを使ってはみたものの、パラメータが多すぎて調整しきれないし、吊るしだと誤爆しまくりだし、具合が良くない。コマーシャルとはナンゾやと考えこんでしまった。
私の場合、録画するのは自称公共放送のネコとか紀行物とか、民法BSの紀行物、たまにアニメとか。このうち、自称公共放送のやーつは最適解が見つかり、自動でカット・エンコしている。で、他をどうするかなんだが、いろいろ試したが誤爆が多い、カットする場所が不正確だとかで使いたくない。じゃ、ツモルしか無いねと。
- 先ずは編集点の検出が必要。本編とCMの境目、CMとCMの境目の編集点を検出する
- 編集点を区切りとして、録画素材の特徴量を抽出する
- 特徴量を判断材料にして、本編なのかCMなのか判別する
なんだ、簡単ではないか。
編集点の検出にはffmpegのフィルターを使う。ビデオフィルターと音声フィルターの両方を使う。シーンチェンジの出力からpts_timeを引っ張ってそのまま時刻として使っていたが、桁が足りないことに気づいて、ptsからTB決め打ちで計算させている。フレームアキュレートなはずだ。
$FFMPEG -i "$TS" -filter:v "select='gt(scene,0.375)',showinfo" -filter:a "silencedetect=noise=0.0001:duration=0.05" -f null - 2>
シーンチェンジと無音部分を検出している。無音で且つシーンチェンジしたら、そこが編集点じゃねえかっつうことだ。無音は-80dB、0.05秒を基準としたが今のところうまく行っている。行儀よく編集するとそのくらいの無音が入るんだと思う*1。ffmpegの出力はシーンチェンジも無音検出も一緒くたに吐いてくる。これを一時ファイルに置いて、そこからシーンチェンジを検証していく。
- 無音部分の開始時間と終了時間を取得する
- シーンチェンジの時間を取得する
- シーンチェンジが無音期間中だったらこれを編集点とする
- 加えて、編集点と編集点の間のシーンチェンジの回数を数え、編集転換の時間間隔で割り算しこれをシーンチェンジ率とする
これだけだ。編集点の開始時刻、シーンチェンジ率(毎秒)、シーケンスの長さ(秒)を出力するスクリプトを書いた。恥を覚悟で晒しておく。こんな長いのん初めてで破綻寸前だわ。で、こんなんが出力される。どあたまはファイル名のハッシュ。他にもいろいろ出力されてるけど後述。
md5sum start position duration rate max_amp min_amp mid_amp mean_norm mean_amp rms_amp max_delta min_delta mean_delta rms_delta freq vol_adj content ...snip 734adedd96ed3dc88bcf77fcdf2fd699 1510.046956 .835475 150.016533 .286635 0.383292 -0.336926 0.023183 0.029047 -0.000001 0.042969 0.314083 0.000000 0.010624 0.019106 3396 2.609 "p" 734adedd96ed3dc88bcf77fcdf2fd699 1660.063489 .918476 14.981633 .066748 0.372015 -0.336926 0.017544 0.029173 0.000013 0.043015 0.312446 0.000000 0.015749 0.024716 4389 2.688 "c" 734adedd96ed3dc88bcf77fcdf2fd699 1675.045122 .926765 15.015000 .466200 0.372015 -0.336926 0.017544 0.028238 0.000026 0.041929 0.312446 0.000000 0.013598 0.020932 3813 2.688 "c" 734adedd96ed3dc88bcf77fcdf2fd699 1690.060122 .935072 14.981634 1.334967 0.372015 -0.336926 0.017544 0.028167 0.000030 0.041846 0.312446 0.000000 0.013357 0.020510 3744 2.688 "c" 734adedd96ed3dc88bcf77fcdf2fd699 1705.041756 .943361 15.015000 1.065601 0.322565 -0.336926 -0.007181 0.028635 0.000035 0.042165 0.312446 0.000000 0.014033 0.021466 3889 2.968 "c" 734adedd96ed3dc88bcf77fcdf2fd699 1720.056756 .951669 29.996633 .100011 0.322565 -0.336926 -0.007181 0.029899 0.000044 0.043420 0.312446 0.000000 0.015578 0.022999 4046 2.968 "c" 734adedd96ed3dc88bcf77fcdf2fd699 1750.053389 .968265 29.996633 1.033449 0.311811 -0.336926 -0.012558 0.029218 -0.000014 0.042867 0.303896 0.000000 0.016110 0.024188 4310 2.968 "c" ...snip
4コラム目が切り出したシーケンスの長さ。ほぼ15秒おきにCMが切り出されているのが分かる。いつも綺麗に決まるわけではなく、ややオーバーキル気味、つまり、本編内の編集点やCM内の編集点も拾ってしまう。これら断片化した、本来ひとつづきのシーケンスを元に戻す方法も勘案したが、結局ロゴ検出などに頼る方法しか思いつかなかった。ま、それ以上考えずに諦めた。
ひと捻り入れて10倍位早くなったが、スクリプトは遅いっす。シーケンス長とシーンチェンジ率だけでCMカットが出来ないか、というアイデアの検証なので、手戻りが早いほうが良いでしょう。
#!/bin/bash FFMPEG="/home/poco/bin/ffmpeg"; TS=$1; DIR=$(dirname "${TS}"); BASE=$(basename "${TS}" .ts); EDLFILE="${DIR}/${BASE}.edl"; METAFILE="${DIR}/${BASE}.ffmeta"; OUTFILE="${DIR}/${BASE}_CUT.ts"; TEMP="${DIR}/${BASE}.ffout"; AUDIOFILE="${DIR}/${BASE}.sox"; CLEAN=false; DEBUG=false; function validate(){ # 無音のときにシーンチェンジしたらそこが境目 silence_start=() silence_end=() silence_start=($(grep silence_start "$TEMP" | sed -r 's/^.*silence_start:\s?([0-9.]+).*$/\1/')); silence_end=($(grep silence_end "$TEMP" | sed -r 's/^.*silence_end:\s?([0-9.]+) .*$/\1/')); total_length=$(grep Duration "$TEMP" | awk '{print $2}' | sed 's/,//' | awk -F':' '{printf "%f", $1 * 3600 + $2 * 60 + $3}') $DEBUG && echo 'silence_start:' ${silence_start[*]} $DEBUG && echo 'silence_end:' ${silence_end[*]} # startとendの数が同じかチェックする if [ ! "${#silence_start[@]}" = "${#silence_end[@]}" ]; then echo 'Not mached number of start/stop silence.' exit fi scene_change=($(grep Parsed_showinfo "$TEMP" | grep checksum | sed 's/^.*pts: *//' | awk '{printf "%f ", $1/90000}')); $DEBUG && echo 'scene_change:' ${scene_change[*]} # 無音期間中にあるシーンチェンジを拾う valid=() n_scene_change=() r_scene_change=() scene_change_copy=("${scene_change[@]}") # for speeding up for index in ${!silence_start[*]}; do $DEBUG && echo $index: silence_start at ${silence_start[$index]} n_scene_change[$index]=0 for i in `seq 0 ${#scene_change_copy[*]}`; do candy=${scene_change_copy[$i]} if [ -z $candy ]; then break; fi if [ `echo "$candy < ${silence_start[$index]}" | bc` == 1 ]; then unset scene_change_copy[$i] continue; fi if [ `echo "$candy > ${silence_end[$index]}" | bc` == 1 ]; then break; fi unset scene_change_copy[$i] $DEBUG && echo " $candy" valid=( "${valid[@]}" $candy ) done scene_change_copy=("${scene_change_copy[@]}") $DEBUG && echo "$index: silence_end at ${silence_end[$index]}" done # 前もってシーンの長さをチェックする valid=(0 "${valid[@]}" "$total_length") # 先頭と末尾に仕掛け scene_length=() # シーンの長さ $DEBUG && echo "number of validated scene change: ${#valid[*]}" novsc=${#valid[*]} merge=() let novsc-- for index in `seq 1 $novsc`; do j=$index; i=$*2; $DEBUG && echo "$i: scene_start at ${valid[$i]}" sl=$(echo "${valid[$j]} - ${valid[$i]}" | bc) scene_length=("${scene_length[@]}" "$sl") $DEBUG && echo " scene length: ${sl}" # 断片化したシーンを都合の良さそうな方へ統合する試み if [ $i -ge 2 ]; then k=$i; let k--; sl=$(echo "${scene_length[$k]} + ${scene_length[$i]}" | bc) $DEBUG && echo " scene length: $sl" # echo "${scene_length[$i]} < 15" # echo "scale=6; ($sl / 15) < 0.1" if [ `echo "${scene_length[$i]} < 15" | bc` == 1 ]; then if [ `echo "($sl % 15) < 0.1" | bc` == 1 ]; then $DEBUG && echo " scene marged: ${sl}" scene_length[$i]=$sl merge=("${merge[@]}" $k) fi fi fi $DEBUG && echo $i: scene_end at ${valid[$j]} done for index in ${merge[@]}; do unset scene_length[$index] unset valid[$index] done scene_length=("${scene_length[@]}") valid=("${valid[@]}") # 拾ったシーンでのシーンチェンジの回数を数える scene_change_copy=("${scene_change[@]}") n_scene_change=() # シーンチェンジの回数 r_scene_change=() # シーンチェンジの頻度 scene_length=() # シーンの長さ $DEBUG && echo "number of validated scene change: ${#valid[*]}" novsc=${#valid[*]} let novsc-- for index in `seq 1 $novsc`; do j=$index; i=$*3; $DEBUG && echo "$i: scene_start at ${valid[$i]}" nosc=0 for k in `seq 0 ${#scene_change_copy[*]}`; do candy=${scene_change_copy[$k]} if [ -z $candy ]; then break; fi if [ `echo "$candy < ${valid[$i]}" | bc` == 1 ]; then unset scene_change_copy[$k] continue; fi if [ `echo "$candy > ${valid[$j]}" | bc` == 1 ]; then break; fi unset scene_change_copy[$k] let nosc++ $DEBUG && echo " ${nosc}: $candy" done scene_change_copy=("${scene_change_copy[@]}") sl=$(echo "${valid[$j]} - ${valid[$i]}" | bc) rsc=$(echo "scale=6; ${nosc} / ${sl}" | bc) n_scene_change=( "${n_scene_change[@]}" $nosc ) r_scene_change=( "${r_scene_change[@]}" $rsc ) scene_length=( "${scene_length[@]}" $sl ) $DEBUG && echo " number of scene change: ${nosc}" $DEBUG && echo " rate of scene change: ${rsc}" $DEBUG && echo " scene length: ${sl}" $DEBUG && echo $i: scene_end at ${valid[$j]} done unset valid[${novsc}] # 仕掛けの回収 valid=("${valid[@]}") } ###function analyze(){ ### ###} # main to=10 seek=0 # シーンチェンジと無音検出を同時に if [ ! -e "$TEMP" ]; then $FFMPEG -i "$TS" -filter:v "select='gt(scene,0.375)',showinfo" -filter:a "silencedetect=noise=0.0001:duration=0.05" -f null - 2> "$TEMP" fi # 音声を別ファイルへ if [ ! -e "$AUDIOFILE" ]; then $FFMPEG -i "$TS" -f sox "$AUDIOFILE" fi # シーンを検証 validate valid=("${valid[@]}" "$total_length") # 再び末尾に仕掛け $DEBUG && echo "number of validated scene change: ${#valid[*]}" samples=() length=() scale=() max_amp=() min_amp=() mid_amp=() mean_norm=() mean_amp=() rms_amp=() max_delta=() min_delta=() mean_delta=() rms_delta=() freq=() vol_adj=() novsc=${#valid[*]} let novsc-- for index in `seq 1 $novsc`; do j=$index; i=$*4; if [ $index==$novsc ]; then stats=($(sox -t sox "$AUDIOFILE" -n trim ${valid[$i]} -0 stat 2>&1 | sed -r 's/^.+:\s+([0-9.-]+)/\1/')) else stats=($(sox -t sox "$AUDIOFILE" -n trim ${valid[$i]} =${valid[$j]} stat 2>&1 | sed -r 's/^.+:\s+([0-9.-]+)/\1/')) fi max_amp=("${max_amp[@]}" ${stats[3]}) min_amp=("${min_amp[@]}" ${stats[4]}) mid_amp=("${mid_amp[@]}" ${stats[5]}) mean_norm=("${mean_norm[@]}" ${stats[6]}) mean_amp=("${mean_amp[@]}" ${stats[7]}) rms_amp=("${rms_amp[@]}" ${stats[8]}) max_delta=("${max_delta[@]}" ${stats[9]}) min_delta=("${min_delta[@]}" ${stats[10]}) mean_delta=("${mean_delta[@]}" ${stats[11]}) rms_delta=("${rms_delta[@]}" ${stats[12]}) freq=("${freq[@]}" ${stats[13]}) vol_adj=("${vol_adj[@]}" ${stats[14]}) done echo "# $TS" echo "# md5sum start position duration rate max_amp min_amp mid_amp mean_norm mean_amp rms_amp max_delta min_delta mean_delta rms_delta freq vol_adj content" md5=$(echo "$TS" | md5sum | awk '{print $1}') let novsc-- for i in `seq 0 $novsc`; do position=$(echo "scale=6; ${valid[$i]} / $total_length" | bc) echo "$md5 ${valid[$i]} $position ${scene_length[$i]} ${r_scene_change[$i]} ${max_amp[$i]} ${min_amp[$i]} ${mid_amp[$i]} ${mean_norm[$i]} ${mean_amp[$i]} ${rms_amp[$i]} ${max_delta[$i]} ${min_delta[$i]} ${mean_delta[$i]} ${rms_delta[$i]} ${freq[$i]} ${vol_adj[$i]} \"c\"" done exit