VIDEO
動画の4秒あたりから10秒間をgifファイルに変換してみました。間隔を30msにしたものは動画と同じように見えますが、ファイルサイズも12MB超と大きいです(元の動画全体よりも大きい)。スマホのようにデータ通信量を気にしないといけない人もいるので別ページにしました。
GIFは、Graphics Interchange Format(グラフィックスインターチェンジ形式)の略語で、比較的単純な画像向けに設計されたファイル形式です。主にインターネットでの表示で使用されます。
自分でつくったゲームをプレイしているところを動画にするだけではなく、gifファイルにできないでしょうか? やっぱり同じ画像でも動きがあったほうがわかりやすいです。
FFmpegで動画から画像を抜き出す
まず動画から画像を抜き出すにはFFmpegを使います。FFmpegは無料で利用できます。以下のリンクからダウンロードできます。
Download FFmpeg
Windowsで使うならWindowsのロゴの上にマウスを置くと「Windows builds from gyan.dev」が表示されるので、これをクリックします。
リンク先のページにあるffmpeg-git-full.7zをクリックするとダウンロードできます。解凍したらbinフォルダのなかにあるffmpeg.exeとffprobe.exeを適当なところへコピーします。鳩でもわかるC# 管理人はCドライブにffmpegというフォルダを作成して、そこにコピーしました。
C# WindowsFormsで以下のようなものを作ります。
上のボタンでmp4ファイルを選択して下のボタンでgifファイルを生成します。label1には選択されているmp4ファイルのパス、label2には処理中か完了したことを示す文字列が表示されます。3つのNumericUpDownコントロールで画像を抜き出す開始時刻と終了時刻、抜き出す間隔を指定できるようにしています。
public partial class Form1 : Form
{
public Form1 ( )
{
InitializeComponent ( ) ;
label1 . Text = "未選択" ;
label2 . Text = "" ;
}
}
画像を抜き出す時間とファイル名の数字部分のペアをリストに格納するクラスを定義します。
public class SeekInfo
{
public SeekInfo ( double seekSec , int no )
{
SeekSec = seekSec ;
No = no ;
}
public double SeekSec { get ; }
public int No { get ; }
}
第一引数はmp4ファイルのパス、第二引数は画像ファイルが出力されるフォルダのパス、第三引数は開始秒(単位は秒)、第四引数は終了秒(同前)、第五引数は間隔(単位はミリ秒)です。
あとはProcessクラスを使ってffmpeg.exeに引数を渡して実行するだけです。これで第二引数で指定されたフォルダのなかにmp4ファイルから抜き出した画像ファイル(PNG形式)が保存されます。そしてこのメソッドは保存されたファイルのパスの配列を返します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public partial class Form1 : Form
{
static readonly string FfmpegPath = @ "C:\ffmpeg\ffmpeg.exe" ;
static readonly string FfprobePath = @ "C:\ffmpeg\ffprobe.exe" ;
public async Task < string [ ] > ExtractImagesAsync ( string inputMoviePath , string outputImageDir , double start , double end , double interval )
{
// 画像を抜き出す時間とファイル名のペアをリストに格納する
List < SeekInfo > seekInfos = new List < SeekInfo > ( ) ;
for ( int i = 0 ; ; i ++)
{
if ( 1.00 * interval * i + start > = end )
break ;
seekInfos . Add ( new SeekInfo ( 1d * interval * i + start , i ) ) ;
}
// seekInfos.Countが0なら空の配列を返す
if ( seekInfos . Count == 0 )
return new string [ ] { } ;
// 処理に時間がかかるのでどこまで進行しているかプログレスバーに表示させる
progressBar1 . Maximum = seekInfos . Count ;
progressBar1 . Value = 0 ;
List < string > imagePaths = new List < string > ( ) ;
List < Task > tasks = new List < Task > ( ) ;
foreach ( SeekInfo seekInfo in seekInfos )
{
Task task = Task . Run ( ( ) => {
string outputImagePath = Path . Combine ( outputImageDir , $ "{seekInfo.No:D8}.png" ) ;
string arguments = $ "-y -ss {seekInfo.SeekSec} -i \"{inputMoviePath}\" -vframes 1 -f image2 \"{outputImagePath}\"" ;
using ( var process = new Process ( ) )
{
process . StartInfo = new ProcessStartInfo
{
FileName = FfmpegPath ,
Arguments = arguments ,
CreateNoWindow = true ,
UseShellExecute = false ,
} ;
process . Start ( ) ;
process . WaitForExit ( ) ;
}
imagePaths . Add ( outputImagePath ) ;
progressBar1 . Value ++;
} ) ;
tasks . Add ( task ) ;
}
await Task . WhenAll ( tasks ) ;
return imagePaths . OrderBy ( _ => _ ) . ToArray ( ) ;
}
}
抜き出した画像でgifファイルを生成する
この部分は以前とほとんど同じです。第二引数で渡された画像ファイルがあるかどうかのチェックが新たに追加されています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public partial class Form1 : Form
{
void SaveGifFile ( string gifFilePath , string [ ] imageFilePaths , int millisecond )
{
if ( imageFilePaths . Length == 0 )
return ;
MemoryStream memoryStream = new MemoryStream ( ) ;
BinaryWriter binaryWriter = new BinaryWriter ( new FileStream ( gifFilePath , FileMode . Create ) ) ;
Bitmap bitmap1 = new Bitmap ( imageFilePaths [ 0 ] ) ;
bitmap1 . Save ( memoryStream , ImageFormat . Gif ) ;
bitmap1 . Dispose ( ) ;
byte [ ] bytes = memoryStream . ToArray ( ) ;
bytes [ 10 ] = ( byte ) ( bytes [ 10 ] & 0x78);
binaryWriter . Write ( bytes , 0 , 13 ) ;
byte [ ] applicationExtension = { 33 , 255 , 11 , 78 , 69 , 84 , 83 , 67 , 65 , 80 , 69 , 50 , 46 , 48 , 3 , 1 , 0 , 0 , 0 } ;
binaryWriter . Write ( applicationExtension ) ;
WriteGifImg ( binaryWriter , bytes , millisecond ) ;
for ( int i = 1 ; i < imageFilePaths . Length ; i ++)
{
if ( ! File . Exists ( imageFilePaths [ i ] ) )
continue ;
memoryStream . SetLength ( 0 ) ;
Bitmap bitmap2 = new Bitmap ( imageFilePaths [ i ] ) ;
bitmap2 . Save ( memoryStream , ImageFormat . Gif ) ;
bitmap2 . Dispose ( ) ;
bytes = memoryStream . ToArray ( ) ;
WriteGifImg ( binaryWriter , bytes , millisecond ) ;
}
binaryWriter . Write ( bytes [ bytes . Length - 1 ] ) ;
binaryWriter . Close ( ) ;
memoryStream . Dispose ( ) ;
}
public void WriteGifImg ( BinaryWriter binaryWriter , byte [ ] bytes , int millisecond )
{
byte [ ] delayTimeBytes = BitConverter . GetBytes ( millisecond / 10 ) ;
bytes [ 785 ] = delayTimeBytes [ 0 ] ;
bytes [ 786 ] = delayTimeBytes [ 1 ] ;
bytes [ 798 ] = ( byte ) ( bytes [ 798 ] | 0X87 ) ;
binaryWriter . Write ( bytes , 781 , 18 ) ;
binaryWriter . Write ( bytes , 13 , 768 ) ;
binaryWriter . Write ( bytes , 799 , bytes . Length - 800 ) ;
}
}
以下はgifファイルを生成する元になるmp4ファイルを選択するダイアログを表示させる処理です。
public partial class Form1 : Form
{
string _moviePath = "" ;
private void button1_Click ( object sender , EventArgs e )
{
OpenFileDialog dialog = new OpenFileDialog ( ) ;
dialog . Filter = "動画ファイル(*.mp4)|*.mp4" ;
if ( dialog . ShowDialog ( ) == DialogResult . OK )
{
_moviePath = dialog . FileName ;
label1 . Text = dialog . FileName ;
}
dialog . Dispose ( ) ;
}
}
gifファイルを保存するためのダイアログを表示させて、実際にmp4ファイルからgifファイルを生成する処理です。
mp4ファイルの開始秒から終了秒まで一定の間隔で画像が抜き出されて実行ファイルがあるフォルダに保存されます。そのあとSaveGifFileメソッドによってgifファイルが生成されます。開始秒や終了秒が適切に指定されていないと画像ファイルを抜き出すことができません。そのためSaveGifFileメソッド内でファイルが存在するかどうかのチェックがされています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public partial class Form1 : Form
{
private async void button2_Click ( object sender , EventArgs e )
{
if ( _moviePath == "" )
{
MessageBox . Show ( "動画を選択してください" ) ;
return ;
}
string gifPath = "" ;
SaveFileDialog dialog = new SaveFileDialog ( ) ;
dialog . Filter = "gifファイル|*.gif" ;
if ( dialog . ShowDialog ( ) == DialogResult . OK )
{
gifPath = dialog . FileName ;
}
dialog . Dispose ( ) ;
if ( gifPath == "" )
return ;
label2 . Text = "処理中" ;
string tempFolder = Application . StartupPath + "\\temp" ;
if ( ! Directory . Exists ( tempFolder ) )
Directory . CreateDirectory ( tempFolder ) ;
int startSec = ( int ) numericUpDown1 . Value ;
int endSec = ( int ) numericUpDown2 . Value ;
int intervalMiliSec = ( int ) numericUpDown3 . Value ;
double intervalSec = 1.00 * intervalMiliSec / 1000 ;
string [ ] imageFilePaths = await ExtractImagesAsync ( _moviePath , tempFolder , startSec , endSec , intervalSec ) ;
SaveGifFile ( gifPath , imageFilePaths , intervalMiliSec ) ;
// gifファイルが生成されたら画像ファイルは使わないので消去する
foreach ( string path in imageFilePaths )
File . Delete ( path ) ;
label2 . Text = "完了" ;
// 処理が完了したら生成されたgifファイルがあるフォルダを開く
FileInfo info = new FileInfo ( gifPath ) ;
System . Diagnostics . Process . Start ( info . DirectoryName ) ;
}
}
VIDEO
動画の4秒あたりから10秒間をgifファイルに変換してみました。間隔を30msにしたものは動画と同じように見えますが、ファイルサイズも12MB超と大きいです(元の動画全体よりも大きい)。スマホのようにデータ通信量を気にしないといけない人もいるので別ページにしました。