2011/03/29

録画したテレビ番組のL字を除去する

大災害が発生したりすると、テレビ番組の画面の周囲にL字状の枠が追加されて被害情報などが表示されますが、録画した番組を後で視聴するときには邪魔でしかありません。
エンコード時にクロップして除去してしまえばよいのですが、CMなどでL時の有無が切り替わる部分ではフェードイン・アウトが行われるため、綺麗に除去するエンコード定義を手動で作成するのは非常に面倒です。
そこで、動画からL字の出現範囲を自動検出して、フェードイン・アウトの部分まで正確に除去するAviSynthスクリプトを生成するというNILScript用スクリプトを作成しました。

AviSynthは、動画の読み込みや加工処理を定義するスクリプト言語の処理系です。
AviSynthをインストールすると、VideoForWindows APIで拡張子「.avs」のスクリプトを動画として読み込めるようになります。
VirtualDubなどのソフトで読み込んで編集・エンコードする一般的な使い方ですが、NILScriptのVideoユニットもVFWを利用しているためAviSynthスクリプトを読み込み可能で、今回のスクリプトでも映像処理の大部分はAviSynthスクリプトで行っています。

NILScriptのVideoユニットは、このL字除去スクリプトを作成するために暫定的に実装されたものなので、限られた機能しか用意されていませんが、このスクリプトのようにAviSynthと組み合わせれば、様々な動画処理ツールを手軽に作成できるでしょう。


スクリプトと定義データ一式は以下のURLからダウンロードできます。
使用するには、別途NILScript本体とAviSynth、MPEG-2 VIDEO VFAPI Plug-Inが必要です。
スクリプトの内容や詳しい使用方法は、上記アーカイブ内のファイルを参照して下さい。

http://lukewarm.s151.xrea.com/rem.zip


L字の検出には、L字の左上辺りの映像が不変である部分のサンプル画像を用意しておき、動画の各フレーム画像との差を調べるという手法を用いています。
ほとんど全てのピクセルが一致した場合に完全なL字とみなし、それ以外はL字無しかフェードイン・アウト中のフレームとみなすことにしました。

スクリプトが実行されると、各フレームのL字の有無を示す小さな画像からなる動画を出力するAviSynthスクリプトを生成して、Videoユニットで読み込み、フレームを一定間隔で飛ばしながらL字の有無を調べます。
L字の有無が切り替わっている箇所が見つかったら、間のフレームを調べて正確な境目を探します。
ここでも、最初に範囲の中央を調べ、既に切り替わっていたら範囲の前半、切り替わっていなければ範囲の後半を再帰的に調べるという二分探索的な手法を採ることで高速化しています。
フェードイン・アウトのフレーム数は放送局ごとに一定なので、完全にL字である範囲がわかれば、フェードイン・アウトの範囲と、L字無しの範囲も確定します。
動画の末尾まで調べたら、元の動画を読み込んで各部分に対応するフィルタを適用して連結するというAviSynthスクリプトを構築して保存すれば処理完了です。

2011/03/24

動画ファイルから画像を取得してサムネイル生成などを行う

動画ファイルから寸法などの情報や任意のフレームの画像を取得できる「Video」ユニットを暫定的に実装しました。

動画のフレームは、Imageユニットで定義されているImageオブジェクトとして取得され、リサイズなど様々な処理が可能です。

下記のスクリプトは、コマンドライン引数で指定した動画ファイルから等間隔にフレームを抜き出し、縮小して並べた画像をファイルに保存するという例です。
var file=cwd().file(Main.params[0]);
var scale=4;//縮小率
var cx=4, cy=4;//横、縦に並べる数
try{
    var vf=require('Video').VideoFile.open(file);
    var st=vf.openVideo();
    var dist=Math.floor(st.length/(cx*cy));
    var w=Math.floor(st.width/scale), h=Math.floor(st.height/scale);
    var img=require('Image').Image.create(w*cx,h*cy);
    for(var i=0;i<cy;i++){
        for(var j=0;j<cx;j++){
            try{
                var frame=st.getFrazme((i*cx+j)*dist);
                img.drawImage(frame,w*j,h*i,w,h);
            }finally{
                free(frame);
            }
        }
    }
    img.save(cwd().file(file.name+'.jpg'),{quality:80});
}finally{
    free(st,vf,img);
}
実行すると、左記のような画像ファイルが生成されます。


現在の所、VideoForWindowsのAPIを利用しているため、AVIなど限られた形式にしか対応していませんが、AviSynthをインストールして、DirectShowSource()などで動画を読み込むAVSスクリプトを作成してそれを読み込むようにすれば、MPEGなど他の形式にも対応させられるはずです。

2011/03/06

非アクティブのウィンドウにキー操作を送信

GUIプログラムの自動操作を行うとき、対象のウィンドウをいちいちアクティブにしていたのでは、他の作業の邪魔になってしまいます。
ウィンドウを非アクティブのままで操作したければ、キー操作時に送られるウィンドウメッセージを送信する「sendKeys()」を使うといいでしょう。(最近追加された機能なので、使用するには最新版にアップデートして下さい)

下記の例は、ある音楽プレイヤーソフトにPauseのショートカットキーを送信する例です。
require('Window').Window.find('{DA7CD0DE-1602-45e6-89A1-C2CA151E008E}').sendKeys('[x]');
「require('Window').Window.find('...')」で、指定のクラス名を持つウィンドウの内、一番手前のものを表すWindowオブジェクトが返されます。
そのsendKeys()メソッドを呼び出すことで、ウィンドウメッセージによる擬似的なキーストロークの送信を行います。
大抵のソフトでは、タスクトレイに格納されたりして非表示になっていても、ウィンドウ自体は非表示の状態で存在しているため、上記の要領で操作可能です。

ショートカットキーは、親ウィンドウではなく配下のコントロールに送信しなければ動作しない場合があります。
配下のコントロールを表すオブジェクトを取得するには、「control()」メソッドを使用します。
次の例は、「TVXEForm」というクラス名の一番手前のウィンドウの配下の「TEditorX」というクラス名のコントロールに「Ctrlキーを押しながらsキーを押して放しCtrlキーを放す」というキーストロークを送信するというものです。
require('Window').Window.find('TVXEForm').control('TEditorX').sendKeys('Ctrl+[s]');
controlの第2引数に数値を指定すれば、複数の同クラス名コントロールの内、任意のオフセットのものを取得できます。未指定時は0を指定したものとみなされ、最初の物が取得されます。

sendKeysで指定するキーストロークの定義には、NILScriptの「Hotstrokes」ユニットで使用されているストローク定義文字列の内、通常のキーボードのキーのみで構成された物が指定できます。
Hotstrokesでは様々なキーストロークを定義するためのルールがありますが、標準的なショートカットキーを送信するだけなら、修飾キー名(Shift/Ctrl/Alt)に「+」を付けたものに続いてキー名を「[]」で囲んで記述するという方式を覚えておけば十分でしょう。

なお、送信されるキーストロークの内、修飾キーの部分だけはウィンドウメッセージではなくシステム全体に影響する方式で擬似的なキーストローク生成が行われます。
これは、多くのソフトのショートカットキー認識では、修飾キーの状態をシステムに問い合わせて取得しており、ウィンドウメッセージでCtrlの押し下げを送信しても無視されてしまうためです。
このため、修飾キーを伴うショートカットキーを送信すると、他のソフトに意図しない操作が認識されてしまう可能性があります。
できれば実行したい動作を「F1」のような修飾キーを伴わないショートカットキーに割り当てておき、それを送信すると良いでしょう。

2011/02/17

ContentExtractorのプラグインの仕様

NILScriptに同梱の「Notebook.ng」でWebページから本文を抽出するために用意されている「ContentExtractor」では、本文の抽出方法をプラグインとして追加できるようになっています。
現在のところ、はてなダイアリーやFC2ブログなどの代表的なブログサービスや、Vector、窓の杜、ITMediaなどのIT系情報サイトに対応するプラグインなどが用意されていますが、各自でプラグインを作成すればどんなサイトにでも対応させられます。
今回は、ContentExtractorプラグインの仕様について説明します。

基本的な定義

ContentExtractorのプラグインは、NILScriptのディレクトリ配下の「plugins\ContentExtractor」ディレクトリ内にプラグイン名のディレクトリを作って格納します。
各プラグインのディレクトリには、プラグイン名に拡張子「.ng」を付けたファイル名でプラグインのスクリプトファイルを格納します。
スクリプトファイル内には、以下のようなオブジェクト定義を記述します。

({
    description:String(<![CDATA[
        http://wiredvision.jp/の巡回と記事本文抽出。
    ]]>),
    level:50,
    rules:{
        atom:{
            urlPattern:/^http:\/\/(wiredvision\.jp|rss\.rssad\.jp\/rss\/wiredvision)\/.*\/atom\.xml$/i,
            inherit:"std/atom",
            childContentBodyPattern:true,
            replace:['NoTracker'],
        },
        article:{
            urlPattern:/^http:\/\/wiredvision\.jp\/.*\.html$/i,
            contentPattern:/<!--\s*google_ad_section_start\(name=s2\)\s*-->([\s\S]*?)<!--\s*google_ad_section_end\(name=s2\)\s*-->/i,
            sectionLinkPattern:/(<p><a href="http:\/\/wiredvision\.jp\/[^">]+">[^<]*へ続く<\/a><\/p>)/i,
            replace:[
                
            ],
        },
    },
})

オブジェクトの「rules」メンバの下にページの解析ルールを列挙し、各ルールのurlPatternメンバで、そのルールが対応しているページのURLにマッチする正規表現を指定します。
上記の「atom」ルールの「inherit:"std/atom"」は、stdプラグインで定義されている「atom」ルールを継承し上書きすることを示しています。
std/atomルールは、Atomフィードから各entry要素のlinkやcontentを子アイテムとして抽出するためのルールです。他に「std/rss」や「std/rdf」もあります。
これらのルールでフィードから項目を抽出する際には、アクセス解析用の中継URLをリダイレクト先のURLに置換したり広告項目を除去する機能などが自動的に適用されるようになっています。
「childContentBodyPattern:true」は、content要素を無視してlinkが示すWebページから対応するルールで抽出した本文を本文として使用することを示しています。
これは、フィードに含まれている本文が途中で終って「続きを読む」のリンクになっている場合に本当の本文を保存させるためなどに使用します。
「replace:['NoTracker']」は、フィードのsummary要素に仕込まれているアクセス解析用の不可視の画像を取り除く処理を定義しています。replaceの詳細については後で説明します。
このようなフィード用定義は、urlPatternとinheritの値以外はほとんど使い回せるので、コピペ・改編して利用するといいでしょう。

「article」ルールでは、記事のページから本文を抽出するルールを定義しています。
ここでは、「contentPattern」に指定された正規表現に従って、ページのHTMLから本文の部分が抽出されます。この時、正規表現中に「()」が使われている場合は、最初の括弧にマッチした部分が抜き出し結果となり、括弧がない場合はマッチ範囲全体が使われます。これは、前後の部分を目印に使いつつ、抜き出し結果には含めたくない場合などのための仕様です。途中に「(p|div)」のような括弧を使ったパターンを含めたい場合は、「(?:p|div)」のように、キャプチャ無し括弧を使ってください。
上記の例のように、コンテキストマッチング型広告サービスを利用しているサイトでは、本文の範囲を認識させるために埋め込まれたコメントタグを利用するのが手っ取り早いでしょう。

正規表現で抜き出しパターンを指定する際に特に注意すべきなのは「.」は改行にマッチしないということです。改行を含むあらゆる文字列にマッチさせるためには「[\s\S]」などと指定しなければなりません。
また、量指定子「*」や「+」は、そのままでは「貪欲」にマッチングを行うため、[\s\S]と組み合わせて使用する場合は、「?」を付けて「無欲」にする必要があります。
これを忘れて「<h1>aaa</h1><p>AAAA</p><h1>bbb</h1><p>BBBB</p>」のようなHTMLに対して「/<h1>.*<\/h1>/i」のようなマッチングを行うと、「<h1>aaa</h1><p>AAAA</p><h1>bbb</h1>」までが一致してしまったりします。
今回のcontentPatternの場合、終了タグが1回しか出現しないので貪欲でも問題はありませんが、無欲の方が余計な検索が行われず高速に動作するはずです。

「sectionLinkPattern」では、本文が複数のページに分かれているときに、以降のページにリンクしている部分を抽出・結合するためのパターンを指定します。
また、この例では登場しませんが、ページのタイトルをtitleタグ以外から抽出したい場合に指定する「titlePattern」や、ページの概要を抽出する「summaryPattern」などもあります。

これらの抜き出しパターンでは、正規表現以外にもいくつかの抽出方法が指定できます。
HTMLの要素に付けられたid属性で抽出するには、「contentPattern:['#contentBody']」のように指定します。
「contentPattern:function(html){/*何らかの処理*/;return(content);}」のように、HTML文字列を引数に受取り抜き出した文字列を返す関数を指定することも可能です。
他にもいくつかの方法がありますが、この3つを覚えておけばほとんどの状況には対応できるでしょう。


HTMLからの記事一覧の抽出

RSSフィードなどを配信していないサイトで、記事一覧のページから記事を抽出したい場合は、「childContentPattern」を使用します。
以下は、「vector」プラグインで定義されているカテゴリ別ソフト一覧のページの定義例です。

        soft_category:{
            urlPattern:/^http:\/\/www\.vector\.co\.jp\/vpack\/filearea\/(?:win|mac)\/.*$/i,
            childContentPattern:/(<LI>\s*<A HREF="[^">]*\/se\d+\.html">[^<]*<\/A>\s*<IMG[^>]*>[\s\S]*?<BR>[\s\S]*?<\/LI>)/ig,
            childContentTitlePattern:/<a href="[^">]*\/se\d+\.html">(.*?)<\/a>/i,
            childContentInternalLinkPattern:/(<a href="[^">]*\/se\d+\.html">.*?<\/a>)/i,
            childContentSummaryPattern:/<\/A>([\s\S]*?)<\/LI>/i,
            childContentBodyPattern:true,
            
            sectionLinkPattern:/<div class="pagenav">([\s\S]*?)<\/div>/i,
            replace:[
                'NoImageLink',
                [/<img\b[^>]*alt="([^">]*)"[^>]*>([\s\S]*?)<br[^>]*>/ig,'[$1] $2'],
            ],
        },

まず、「childContentPattern」で各子アイテムのHTMLを抜き出します。
「childContentTitlePattern」、「childContentInternalLinkPattern」、「childContentSummaryPattern」は、抜き出した子アイテムのHTMLからタイトル、ページ本体へのリンク、概要を抜き出す定義です。
一覧の項目に本文が丸ごと含まれている場合は、「childContentBodyPattern」で本文の抜き出し定義を指定しますが、この例では本文はリンク先のページから抜き出す必要があるため、「childContentBodyPattern:true」を指定しています。
また、複数ページに分かれた一覧を全て取り出すため、「sectionLinkPattern」も指定しています。
std/atomなどのルールも、このようにして定義されています。


replaceによる置換ルール

contentPatternなどで抜き出した範囲に広告やブログパーツなどの不要部分が含まれてしまう場合は、「replace」に置換ルールの配列を記述して削除などを行います。
配列の要素として単なる正規表現を指定すると、その正規表現に一致する箇所が空文字列に置換されて削除されます。この時、全ての出現箇所を削除するには、正規表現に「g」オプションを付ける必要があることに注意してください。
削除ではなく別の内容に置換したい場合には、「[/<b>([\s\S]*?)<\/b>/ig,'$1']」のように、正規表現と置換文字列の2要素からなる配列を指定します。第2要素には、文字列のreplace()メソッドの第2引数と同じように、関数を指定することも可能です。
よく使われる置換ルールは、「HTMLRewriter」という機能のプラグインで定義されており、ルール名を単に文字列として指定するだけで使用できます。
標準で用意されている置換ルールの一覧と説明は、「plugins\HTMLRewriter\Cleanup\Cleanup.ng」内に書かれています。



画像の保存

contentPatternなどで抽出された本文にimgタグで埋め込まれている画像は、抽出時に自動的に保存されますが、aタグでリンクされているだけの画像は、そのままでは保存されません。
画像が保存されるようにするには、以下のようにして明示的にダウンロードすべきコンテンツのURLであることを指定します。
        image:{
            urlPattern:/^http:\/\/blog-imgs-\d+-origin\.fc2\.com\/.*\.(?:jpe?g|png|gif|bmp)$/i,
            download:true,
        },

本文からリンクされているのが画像ファイル本体でなく画像を表示するためのHTMLである場合には、URLの変換定義を用意することで、画像本体のみを保存し直接リンクさせられます。
URLの変換定義の方式には、「urlRewrite」と「urlRedirect」の2つがあります。
単なるURLの正規表現置換で画像本体のURLが得られ、HTMLにアクセスしていなくても画像に直接アクセス出来る場合は、以下のように「urlRewrite」で置換文字列を指定します。置換後のURLを保存させるための定義も別途必要であることに注意してください。

        image_html:{
            urlPattern:/^(http:\/\/japan\.cnet\.com\/)image\/l\/(.*\.(?:jpg|jpeg|png|gif|bmp))$/i,
            urlRewrite:"$1$2",
        },
        image:{
            urlPattern:/^http:\/\/japan\.cnet\.com\/(?:story_image|image|storage)\/.*\.(jpg|jpeg|png|gif|bmp)$/i,
            download:true,
        },

HTMLのURLから画像本体のURLが定まらない場合や、HTMLにアクセスしてCookieの取得などを行わないと画像本体へのアクセスが出来ない場合などは、以下のように「urlRedirect」に、HTML内から画像本体のURLが含まれる部分を抽出する定義を記述します。この定義で抜き出された範囲に最初に出現するhref=やsrc=のリンク先URLが、変換後のURLとなります。

        softnews_ss:{
            urlPattern:/^http:\/\/www\.vector\.co\.jp\/magazine\/softnews\/\d+\/n\d+_pic\.html$/i,
            urlRedirect:/<td [^>]*>\s*<a href="[^">]*">(<img [^>]*>)<\/a>/i,
        },

また、フィードから抜き出されたURLに「?ref=rss」のようなゴミがくっついている場合なども、urlRewriteによるURLの置換を定義しておくと良いでしょう。


ContentExtractorのルール定義の主要部分の説明は以上です。
まだ説明されていない機能やテクニックもいくつかあるので、「doc\ContentExtractor.txt」の説明や、「plugins\ContentExtractor\」内の標準プラグイン内の実際の定義なども参照してみてください。

出来上がったプラグインを配布したい場合は、ZIPで圧縮してアップロードするなどしてください。そのうち、プラグインの配布や入手、アップデートなどを行う機能も実装される予定です。

HTTPオブジェクトのCookieを保存しておいて次回実行時に読み込む

HTTPオブジェクトでCookieを扱う際、受取ったCookieをファイルに保存しておいて、次にスクリプトを実行したときに読み込んで利用したい場合があるかも知れません。
このような場合は、HTTPオブジェクトのコンストラクタの引数に以下のようなオプションオブジェクトを指定してください。

try{
    var http=new (require('HTTP').HTTP)({
        cookies:Main.scriptDirectory.file('cookies.json'),
        saveCookies:true
    });
    //何らかの処理
}finally{
 free(http);
}

オプションオブジェクトの「cookies」メンバにファイルパスやFileオブジェクトを指定すると、そのファイルからCookieが読み込まれます。この時、ファイルが存在しなくてもエラーになったりはせず、単に何も読み込まずに初期化されます。
また、cookiesに加えて「saveCookies:true」を指定すれば、オブジェクトをfree()で解放する際に、cookiesで指定したファイルにCookieが保存されます。
ファイルから読み込まずに保存だけを行いたい場合は、cookiesを指定せずにsaveCookiesにファイルパスやFileオブジェクトを指定します。

このCookie保存機能では、ブラウザを閉じると破棄されてしまうセッションCookieも保存できます。これにより、ブラウザをずっと開いたままにしているように装うことも可能となります。
オプションオブジェクトに「discardSessionCookies:true」を指定することで、セッションCookieは保存せず破棄させることも可能です。