よっしゃぁ! 完成じゃ! 基本 DCT,スペクトラル・セレクション,サクセッシブ・アプロクシメーション,24 bit フルカラー,8 bit 単色なんでも展開やぁ! てめえら目ぇかっぽじってよく見y あ,以上でツカミはヨロシですか。
->昨日〜今日の成果物;
展開速度がまだアレなんですが,とりあえず私がテスト用に準備していた jpeg は全て展開する事が確認できています。これで展開できない形式と言えば,,,何だろう? 算術圧縮 jpeg なんか見たことないですよねえ?
コードの中にはデバグ用のモノがたくさん紛れているので大変ですが,アルゴリズムを学習するにはなかなか優れていると自負しています。ijg のライブラリよりもコンパクト。なお idct.c の著作権は MPEG Software Simulation Group にあります。勝手にこんな所で使っちゃってますが,どうか堪忍してや・・。
添付の jdec.exe はコンソールアプリケーションで,起動するとメインのウィンドウの他に DOS 窓も表示されます。メインのウィンドウには jpeg ファイルをドラッグ・ドロップして下さい。DOS 窓には jpeg ファイルの中に存在するマーカのうち,興味深いものを列挙しています。jpeg の展開は数秒 〜 遅い時には 1 分ほど掛かるかもしれません。プログレッシブ jpeg では画像が徐々に綺麗になっていく様子が観察できます。エラーが起こるとウィンドウのキャプションに「エラー」と表示されます。
さて次は,アプリケーション自身の改良を続けて行こうと思います。
ついにプログレッシブ JPEG の展開にも成功しました。これで web 上に寝っころがっている JPEG ファイルの 99% 以上は正常に展開できる自信があります。少なくとも私の手持ちの JPEG たちは,実験した限り全て正常にデコードできました。
数々の難問が解決したのは ( 昼に限りなく近い ) 午前中だったので,アプリケーション自身をもう少しエレガントなものに書き直そうと思ったわけですよ。とりあえず食料の買出しに出て,若干の汗をかいたのでシャワーを浴びて,おもむろにパソコンの前に座って class Application {... とかタイプし始めたですよ。ああ,よせばいいのに・・・
なかなか小奇麗な「アプリケーションクラス」「フォームクラス」などなどが出来上がったのでコンパイル,実行,JPEG を食べさせてみます。
. . . ん,WinAPI の SetDIBitsToDevice() 関数が失敗してしまう模様 . .
これはつまりあれですよあの有名な
「蛇足」キターーーーーーー(゜∀゜)ーーーーーーーー!!11234えおゃ
既に jdecoder.cpp の実装は終了してるのに! 「くれ」と言われればあげられるのに! もうベッタベタの「蛇足」ってやつです。とほほ。
こういうコードを書いてみます。
#include <iostream>
class A {};
int main(int, const char *[]) {
void (A::*f)() = 0;
if (!f)
std::cout << "f is null" << std::endl;
return 0;
}
VC++5.0 でコンパイルしてみると:
E:\C\test\test.cpp(8) : error C2171: '!' : オペランドが不正です。 cl.exe の実行エラー
g++ でコンパイルしてみると (on cygwin):
tkuri@mycom ~ $ g++ test.cpp tkuri@mycom ~ $
g++ では普通にコンパイルが通りましたが,まあこんなもんっすよね。ちなみに VC++5.0 では上記の通りただのエラーでしたが,会社の VC++6.0 では内部コンパイルエラーとか何とかが出てきてびびりました。この流れでいけば,7 ではもう対応済みだったりしますか?
ここまででもイイ感じまでプログレッシブ JPEG のデコードは完成していますが,もう少しのところでデコードに失敗してしまいます。ライブラリのコードを見てみると,EOB 検出時に微妙に 1 ビット読んでいる気がします。
昨日のコードにちょっと修正を加えるなら,こんな感じ?
// end_of_band_run がある間は 8x8 ブロックのデコードはスキップ
if (end_of_band_run > 0) {
--end_of_band_run;
read_bits_for_eob(); // ここになんとなく追加?
return;
}
...(snip)
else {
// EOB (End Of Band)
s = bits >> 4;
if (s > 0) {
r = read( s );
end_of_band_run = r + (1 << s);
--end_of_band_run;
}
read_bits_for_eob(); // ここにもなんとなく追加?
return;
}
さ,この関数 read_bits_for_eob() の中身をどう実装すればよいのやら? なんですが・・・もうちょっとコードを読んで見ます。
「ウイルスを含んだ土ぼこりが風で運ばれた」ってアンタ・・・。
・・・ところで,そろそろ黄砂が観測される季節じゃないすか? 例えば 2001 年の 4 月 10 日,北海道の札幌市にて黄砂現象が確認されています。->参照:毎日新聞;
問題はこの砂がどこから運ばれて来るのかです。幸い,黄砂は中国の北西部からの偏西風により運ばれるものとの事。そして SARS の被害が多いと言われている広東省・香港は中国の南に位置する区域。あまり関連はなさげです。ですが・・・やっぱし怖いよう・・。
いかんせん正式な JPEG の仕様書なるものを持っていないので大変ですが,プログレッシブ JPEG における EOB の意味がやっと分かりました。これは End Of Block であり,且つ次にデコードされる予定のいくつかの 8x8 ブロックを 0 にするという意味でした。
プログレッシブ JPEG における,DC / AC 係数のデコードの概要はこんな仮想コードでよいかと。
Decoder::decode_8x8_block() {
// end_of_band_run がある間は 8x8 ブロックのデコードはスキップ
if (end_of_band_run > 0) {
--end_of_band_run;
return;
}
for (p = Ss; p <= Se; ++p) { // Ss と Se は SOS で採取される
if (p == 0) { // DC 係数
bits = decode_huffman_code( DC 係数用のテーブルでデコード );
dc_pred[ cindex ] = read_coeff( bits ); // 少し処理が必要
coeff[ zigzag[ p ] ] = dc_pred[ cindex ];
}
else { // AC 係数
bits = decode_huffman_code( AC 係数用のテーブルでデコード );
if (bits & 0x0f) {
// 下位 4 ビットが非ゼロなら,いくつかのゼロと係数
p += bits >> 4;
coeff[ zigzag[ p ] ] = read_coeff( bits & 0x0f );
}
else if (bits == 0xf0) {
// ZRL
p += 15;
}
else {
// EOB (End Of Band)
s = bits >> 4;
if (s > 0) {
r = read( s ); // s ビット読む
end_of_band_run = r + (1 << s); // これが重要
--end_of_band_run; // このブロックも終了という事で
}
return; // このブロックは終了
}
}
}
}
基本 JPEG では decode_huffman_code() 関数でハフマン符号から 0x00 が採取されると,それは End Of Block でした。しかしプログレッシブ JPEG では End Of Block ではなくなり,より多くの係数の 0 を表現できる End Of Band になったというわけですね。
以前「プログレッシブ JPEG は普通の JPEG よりもよく縮む」という情報を得ていましたが,なるほど,これならよく縮むわけですわん。
今日もいつものように JPEG デコーダを作っていました。デバッガでコードを追いかけながら,ある関数上にカーソルがある時に F11 キーを押してステップインを行ったのです。
IDE 硬直!
数十秒経った後にようやく処理が IDE に渡り,デバッグ作業は続行されました・・が,任意の変数を観察できる「ウォッチ ウィンドウ」に異変が。なんと,ウォッチしている変数がことごとく破壊されています。以降は当然の如くヘンな例外の嵐でまともにデバッグもできません。IDE を起動しなおしたりマシンを再起動してもだめ。「クリーン」を行っても無意味です。
結局「ウォッチ ウィンドウ」でウォッチしている変数を全部削除する事で,障害は回復しました。私はここに変数だけでなく関数もウォッチしていたわけですが,もしかしたらこれはヤっちゃいけない事だったかなあ? 関数をウォッチすると,その戻り値を逐一評価してくれるので驚いたものですが。。。
昨日のネタの続きになります。昨日は,小さいルーチンならばコンパイラの最適化はあまり期待せずにアセンブラで書いちゃえというのが結論。その実験の中でふと気づいた事がありました。
注意:残念ながら「なぜか?」は分かりません。とっほっほー。
#include <iostream>
#include <string.h>
#include <time.h>
int main(int, char *[]) {
int i;
clock_t now;
size_t step, size = 1024 * 1000;
char *mem = new char[ size ];
char *m, *end = mem + size;
// (A)
step = 1024 * 1;
now = clock();
for (i = 0; i < 1000; ++i)
for (m = mem; m < end; m += step )
memset( m, 0, step );
std::cout << "step " << step << " : " << clock() - now << std::endl;
// (B)
step = 1024 * 10;
now = clock();
for (i = 0; i < 1000; ++i)
for (m = mem; m < end; m += step )
memset( m, 0, step );
std::cout << "step " << step << " : " << clock() - now << std::endl;
delete[] mem;
return 0;
}
ちょっとごちゃごちゃしてますが,size バイト = 1000 キロバイトの空間をゼロクリアする時間を計測しています。上のコードでは (A) は step バイト = 1 キロバイトごとに区切りながら 1000 回,(B) は 10 キロバイトに区切りながら 100 回 memset() が呼ばれます。( それだけだと計測が一瞬で終わってしまうので,それぞれ 1000 回繰り返しています )
実行結果はこう:
step 1024 : 3350 step 10240 : 2800
1 キロバイトずつ 1000 回 memset() を呼び出した場合は 3.35 秒,10 キロバイトずつ 100 回 memset() を呼び出した場合は 2.8 秒かかった事を表します。これはなんとなく当たり前の結果に見えますね。1 キロバイトずつちまちまと初期化をするより,一気に多くの領域を処理した方が速いに決まってます。
では,コードをこんな風に変えてみます。
... (前略)
// (A)
step = 1024 * 10;
... (中略)
// (B)
step = 1024 * 1;
(A) と (B) で step の値を交換してみました。この場合,結果はさっきとは逆になっているはずです。あたりまえですね。ほら・・・
step 10240 : 3240 step 1024 : 2920
いや,「ほら」じゃねぇよ自分。
えーっと,何のジョークかは分かりませんが ( そう言えば今日はエイプリルフールだ・・関係ないけど ),期待とはまったく逆の値が出てきました。いろいろ考えてはみたものの結局うまい説明は思いつきません。memset() の初回呼び出し時に,関数のアドレスがキャッシュにてヒットしないから (A) の方は遅いのかな?とも思ったけど,どうもそうでもない模様。
とりあえず今日の教訓:関数のコストを計算するならば,その環境を注意深く構築してやる必要があるという事。ちょこちょこっとコードを書いて得られた結果に満足していると,思わぬ計算違いをしてしまうかも知れません。
じゃあこの場合はどうすればいいんだろう・・・結論が出ないー・・・
JPEG のコードでは bezero() なんて関数を自慢げに作っているわけですが,これを標準関数 memset() と比較してみるとどうよ?
テストコードはこんなの。100 キロバイトのゼロクリアを 1 万回行います。関数の効率だけでなく,関数呼び出しのコストも計測しようと思いました。
#include <iostream>
#include <string.h>
#include <time.h>
void * __fastcall bezero(void *mem, size_t size) {
register size_t lsize = size >> 2;
register size_t csize = size & 3;
long *lmem = (long *)mem;
while (lsize--) *lmem++ = 0;
char *cmem = (char *)lmem;
while (csize--) *cmem++ = 0;
return mem;
}
int main(int, char *[]) {
clock_t now;
int i, times = 10000;
size_t size = 1024 * 100;
char *mem = new char[ size ];
now = clock();
for (i = 0; i < times; ++i ) {
bezero( mem, size );
}
std::cout << "bezero: " << clock() - now << std::endl;
now = clock();
for (i = 0; i < times; ++i ) {
memset( mem, 0, size );
}
std::cout << "memset: " << clock() - now << std::endl;
delete[] mem;
return 0;
}
結果はこう:
bezero: 2130 memset: 399
bezero() を __fastcall 規約にしたり register 宣言をしたりと悪あがきを施しましたが,実に 6 倍ほどの差が出ています。一体 memset() の中身はどうなっているのかと VC++ に付属のアセンブラコードを覗いてみると,アルゴリズムは基本的に私の bezero() と同じでした。いやむしろ事前に半端なアラインメントの処理を丁寧に行っているあたり,memset() はかなり上品なコードに仕上がっています。それなのに,C で書いたかアセンブラで書いたかだけでこれほどの差が出るとは。。。
ちょっと私,「最適化」不信です。
なお今回のネタについて,もうちょっと興味深い事実も明らかにっ ( 続く・・らしいです・・ )。
__fastcall 規約 ]__cdecl や __stdcall と並ぶ関数呼び出し規約。可能な限り引数をレジスタで渡すなど,少しだけ速度が上がる悪あがき。Microsoft 独自規格なんだけど,Borland の VCL でよく見かけるのは七不思議の一つという事で。register 宣言 ]・・と言い放ったのはノーベル経済学賞受賞者としても名高いスティグリッツ先生。例によってソース記事へのリンクはありません。
私が大学に入学した当初 = 6 年前から既に大学の教授たちは口を揃えて「紙幣でもなんでも発行してインフレに持っていくべきだ」とのたまわってらっしゃいましたが,やはりスティグリッツ先生も同様。これはかなり単純でほぼ何も考えていないかのような策ですが,本当にこんなんでいいの?
要するに,将来の不安を取り除く事が重要なポインツのようです。買い物をしたとして,近い将来にそれ以上のお金が手に入る事が期待できるならば私たちはわりと安泰。日銀のインフレ目標の宣言によりわりと正確な期待も可能で,インフレのまま暴走を起こす ( ハイパーインフレーション ) こともないとか。
消費税の導入やその増率が行われた時,拙い脳みそながら「できるだけ安いモノを選らんで慎重に消費活動をしなきゃ」などと思ったわけですが,それって見事にデフレスパイラルの始まりだったんですよね。消費税 ( とその増率 ) や累進課税緩和は,消費低迷経済における大多数の人間にとって迷惑極まりない物だったわけですが,・・えーと,このあたりの愚痴は別にページを作りたいなあ・・
あ,初めての経済学ネタです (笑い
まだハッキリと分かったわけではありませんが:プログレッシブ JPEG,もしかしたらスキャンの途中でセグメントが登場しませんか?
まだまだバグだらけなので大変ですが,ちょうど半分あたりで雰囲気が違っています。
あそこまではなんとかうまくハフマンコードをデコードできていますが,その次,0xffc4 という値を検出してデコードできなくなります。これって DHT セグメントっすか。面倒くさい事が起こりそうな予感。
ちょっと前に出会ったネタなんだけど,JPEG の方も特に書く事がないので出しちゃえぃ。
Mozilla は HTTP/1.1 のパイプライン機能をサポートしています。サーバからデータが転送されている間にも,同じ接続をもってサーバにリクエストを行うんですね。・・書いてしまえば簡単なんだけど,実はわりと大変。
そのパイプライン処理に些細なバグがあると,こんな感じに。
-> 破綻した Google 検索ページ:パイプライン処理に失敗?;
Google さんのサイトが微妙に破綻しています。多分パイプライン処理が失敗しちゃったんだと想像。パイプライン処理は一つのソケットディスクリプタでリクエストの送信とレスポンスの受信を平行するわけですが,その接続が途中でコケてしまうとなんかもう話がこんがらがっちゃって現実逃避をキメてしまいたくなるわけで。
ちなみに今現在私が使っている Mozilla のビルド ID は 2003022808。パイプライン機能を有効にしても,なかなか快調です。
ちょっと昔に焼いた CD-R の中から面白いモノを発掘してきました。その昔はインターネットにも「アングラ」なるテリトリーがありまして,私もドキドキしながらよく覗いたもんです。ああ懐かしき厨房時代 ( ていうか私はまだ厨房なんですが )。
そんな中で投稿されたネタ。どなたのアイデアかはもうすっかり忘れてしまいましたが,CGI のデバッガとしてこんなシェルスクリプトを書き残して行きました。
#!/bin/sh
echo "Content-type: text/plain\n\n"
echo ""
./$* <&0 2>&1
これを wrap.cgi として保存し,例えば http://.../bbs.cgi をデバッグしたい場合は
http://.../wrap.cgi?bbs.cgi
とします。一度 Content-type: text/plain が出力されているので CGI の出力はまるでページのソースが表示されたような感じになります。通常,スクリプトにエラーがある場合は 500 ステータス以外の情報が帰ってくる事は殆どありませんでしたが,これで perl の詳細なエラーなどを知る事が出来ます。