貧者のcomskip (1) - パラメータ多すぎてわかんねえ

erikkaashoek:Comskipを使ってはみたものの、パラメータが多すぎて調整しきれないし、吊るしだと誤爆しまくりだし、具合が良くない。コマーシャルとはナンゾやと考えこんでしまった。

私の場合、録画するのは自称公共放送のネコとか紀行物とか、民法BSの紀行物、たまにアニメとか。このうち、自称公共放送のやーつは最適解が見つかり、自動でカット・エンコしている。で、他をどうするかなんだが、いろいろ試したが誤爆が多い、カットする場所が不正確だとかで使いたくない。じゃ、ツモルしか無いねと。

  1. 先ずは編集点の検出が必要。本編とCMの境目、CMとCMの境目の編集点を検出する
  2. 編集点を区切りとして、録画素材の特徴量を抽出する
  3. 特徴量を判断材料にして、本編なのか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秒を基準としたが今のところうまく行っている。行儀よく編集するとそのくらいの無音が入るんだと思う*1ffmpegの出力はシーンチェンジも無音検出も一緒くたに吐いてくる。これを一時ファイルに置いて、そこからシーンチェンジを検証していく。

  1. 無音部分の開始時間と終了時間を取得する
  2. シーンチェンジの時間を取得する
  3. シーンチェンジが無音期間中だったらこれを編集点とする
  4. 加えて、編集点と編集点の間のシーンチェンジの回数を数え、編集転換の時間間隔で割り算しこれをシーンチェンジ率とする


これだけだ。編集点の開始時刻、シーンチェンジ率(毎秒)、シーケンスの長さ(秒)を出力するスクリプトを書いた。恥を覚悟で晒しておく。こんな長いのん初めてで破綻寸前だわ。で、こんなんが出力される。どあたまはファイル名のハッシュ。他にもいろいろ出力されてるけど後述。

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

*1:例外も有る、後日触れる。

*2: ${index} - 1

*3: ${index} - 1

*4: ${index} - 1