次の手順に従って、BMPにフィルタを適用するプログラムを実装します。
$ ./filter -r IMAGE.bmp REFLECTED.bmp
ここで、IMAGE.bmpは画像ファイルの名前、REFLECTED.bmpは出力画像ファイルに付けられた名前であり、反映されています。
背景
ビットマップ
おそらく、画像を表現する最も簡単な方法は、それぞれが異なる色を持てるピクセルのグリッド (ドット) を使用することです。白黒画像の場合、以下のように、0が黒を表し、1が白を表すことができるので、ピクセルあたり1ビットが必要です。
この意味では、イメージは単なるビットマップ (ビットのマップ) です。よりカラフルな画像を作成するには、ピクセルあたりのビット数を増やすだけです。「24ビットカラー」 をサポートするファイルフォーマット (BMP、JPEG、PNGなど) は、ピクセルあたり24ビットを使用します。(BMPは、実際には1、4、8、16、24、および32ビットカラーをサポートします。)
24ビットBMPは、8ビットを使用してピクセルの色の赤の量を表し、8ビットを使用してピクセルの色の緑の量を表し、8ビットを使用してピクセルの色の青の量を表します。RGBカラーという言葉を耳にしたことがあれば、すでにこれらの赤、緑、青がどのように表されるか知っているでしょう。
BMP内のあるピクセルのR、G、およびB値が、例えば16進数で0xff
、0x00
および0x00
である場合、そのピクセルは純粋な赤となります。なぜなら、0xff
(10進数で255) は 「赤が多い」 ことを意味し、0x00
および0x00
はそれぞれ 「緑がない」 および 「青がない」 ことを意味するからです。
A Bit(map) More Technicalビットマップについてもう少し
ファイルは単にビットの並びであり、何らかの方法で配置されていることを思い出してください。24ビットBMPファイルは、基本的にビットのシーケンスであり、 (ほぼ) 24ビットごとに何らかのピクセルのカラーを表します。ただし、BMPファイルには、イメージの高さや幅などの 「メタデータ」 情報も含まれます。このメタデータは、一般に 「ヘッダ」 と呼ばれる2つのデータ構造の形式でファイルの先頭に格納されます。Cのヘッダファイルと混同しないようにしてください (ちなみに、これらのヘッダは歴史とともに進化してきました。この問題では、Windows 95でデビューしたMicrosoftのBMPフォーマットの最新バージョン4.0を使用します) 。
最初のヘッダーはBITMAPFILEHEADER
と呼ばれ、長さは14バイトです (1バイトが8ビットであることを思い出してください) 。 BITMAPINFOHEADER
という2番目のヘッダーは、40バイトの長さです。これらのヘッダーの直後に実際のビットマップがあります。バイトの配列であり、3つ組でピクセルのカラーを表します。ただし、BMPはこれらの3つ組を逆順 (すなわちBGRとして) で格納します。つまり、青8ビット、緑8ビット、赤8ビットの順で格納します (BMPの中には、ビットマップ全体を逆方向に格納し、イメージの一番上の行をBMPファイルの最後に配置するものもあります。しかし、ここで説明するように、各ビットマップの一番上の行を先頭に、一番下の行を最後に、この問題セットのBMPを保存しました)。つまり、上記の1ビットのスマイルを黒の代わりに赤の24ビットのスマイルに変換する場合、24ビットのBMPはこのビットマップを次のように保存します。0000ff
は赤、ffffff
は白を表します。0000ff
のすべてのインスタンスが赤でハイライト表示されています。
左から右へ、上から下へ、8列に分けて表示しています。一歩下がると赤いスマイルが見えるはずです。
16進数字が4ビットを表すことを思い出してください。したがって、16進数のffffff
は実際にはバイナリの111111111111111111111111
を意味します。
ビットマップをピクセルの2次元配列として表すことができます。イメージは行の配列で、各行はピクセルの配列です。実際、この問題でビットマップ画像を表現するには、この方法を選択しました。
イメージフィルタリング
画像をフィルタリングするとはどういうことでしょうか?イメージをフィルタリングすることは、元のイメージのピクセルを取り込み、結果のイメージに特定の効果が現れるように各ピクセルを修正することと考えることができます。
Grayscaleグレースケール
一般的なフィルタの1つに 「グレースケール」 フィルタがあります。このフィルタでは、イメージを白黒に変換します。どういう仕組みでしょうか?
赤、緑、青の値がすべて0x00
(0
は16進数) に設定されている場合、ピクセルは黒になります。すべての値が0xff
(255
を表す16進数) に設定されている場合、ピクセルは白になります。赤、緑、青の値がすべて同じである限り、結果は黒と白のスペクトルに沿って変化するグレーのシェードになります。値が大きいほど明るいシェード (白に近い) 、値が小さいほど暗いシェード (黒に近い) になります。
ピクセルをグレースケールに変換するには、赤、緑、青の値がすべて同じであることを確認する必要があります。でもどうやってその値を知るのでしょう?元の赤、緑、青の値がすべてかなり高い場合、新しい値もかなり高いはずです。また、元の値がすべて低い場合は、新しい値も低くする必要があります。
実際、新しいイメージの各ピクセルが古いイメージと同じ明るさまたは暗さを持つようにするには、赤、緑、青の値の平均を取り、新しいピクセルを作成するグレーの濃淡を決定します。
これをイメージの各ピクセルに適用すると、結果はグレースケールに変換されたイメージになります。
Sepiaセピア
ほとんどの画像編集プログラムは 「セピア」 フィルタをサポートしています。これは画像全体を少し赤褐色に見せることで、画像に古風な感じを与えます。
イメージをセピアに変換するには、各ピクセルを取得し、3つの元の値に基づいて新しい赤、緑、青の値を計算します。
イメージをセピアに変換するためのアルゴリズムはいくつかありますが、この問題には次のアルゴリズムを使用してください。各ピクセルのセピア色の値は、下記の元の色の値に基づいて計算されます。
sepiaRed = .393 * originalRed + .769 * originalGreen + .189 * originalBlue
sepiaGreen = .349 * originalRed + .686 * originalGreen + .168 * originalBlue
sepiaBlue = .272 * originalRed + .534 * originalGreen + .131 * originalBlue
もちろん、これらの式のそれぞれの結果は整数ではないかもしれませんが、それぞれの値は最も近い整数に丸められます。また、数式の結果が、8ビットカラー値の最大値である255よりも大きい数値になる場合もあります。この場合、赤、緑、青の値の上限は255です。その結果、赤、緑、青の値は0から255までの整数になります。
Reflection反転
フィルタによっては、ピクセルが移動することもあります。たとえば、イメージの反転とは、ミラーの前に元のイメージを配置することによって得られるイメージをフィルタとして使用することです。したがって、イメージの左側のピクセルはすべて右側になり、その逆も同じです。
元のイメージの元のピクセルはすべて反転イメージに残りますが、これらのピクセルはイメージ内の別の場所に再配置されている場合があります。
Blurぼかし
イメージをぼかしたり柔らかくしたりするエフェクトを作成するには、いくつかの方法があります。この問題には、 「ボックスブラー」 を使用します。これは、各ピクセルを取得し、各カラー値に対して、隣接するピクセルのカラー値を平均化することで新しい値を与えるものです。
各ピクセルに番号を付けた次のピクセルグリッドを考えてみます。
各ピクセルの新しい値は、元のピクセルの1行および1列内 (3 x 3ボックスを形成) にあるすべてのピクセルの値の平均です。例えば、ピクセル6の各色値は、ピクセル1、2、3、5、6、7、9、10、および11の元の色値を平均することによって得られます (ピクセル6自体も平均に含まれることに留意しましょう) 。同様に、ピクセル11の色の値は、ピクセル6、7、8、10、11、12、14、15、および16の色値を平均することによって得られます。
エッジまたはコーナーに沿ったピクセル (ピクセル15など) については、1行および1列内のすべてのピクセル (この場合は、ピクセル10、11、12、14、15、および16) を平均します。
始め方
VS Codeを開きます。
ターミナルウィンドウ内をクリックすることから始めて、それからcd
を実行します。
その後プロンプトは次のようになっていることがわかります。
$
ターミナルウィンドウの内側をクリックし、次のように入力します。
wget https://cdn.cs50.net/2021/fall/psets/4/filter-less.zip
その後にEnterを押すと、filter-less.zipというZIPがあなたのCodespaceにダウンロードされます。
次に
unzip filter-less.zip
を実行して、filter-lessというフォルダを作成します。
ZIPファイルは不要になったため、
rm filter-less.zip
を実行し、プロンプトで “y “に続いてEnterで応答すると、ダウンロードしたZIPファイルが削除されます。
次に
cd filter-less
の後にEnterを押して、そのディレクトリに移動する(つまり、開く)。これでプロンプトは以下のようになります。
filter-less/ $
ls
を実行すると、
, bmp.h
filter.c
, helpers.h
, helpers.c
, Makefile
といういくつかのファイルが見えるはずです。また、images
というフォルダに4つのBMPファイルがあるのが見えるはずです。もし何か問題が発生したら、もう一度同じ手順を踏んで、どこで間違ったのか判断してください。
理解を深める
次に、配布コードとして提供されるファイルの一部を見て、ファイルの内容を理解します。
bmp.h
bmp.h
を開いて (ファイルブラウザでダブルクリックして) 見てください。
前述のヘッダ (BITMAPINFOHEADER
およびBITMAPFILEHEADER
) の定義が表示されます。さらに、このファイルは、Windowsプログラミングの世界で通常見られるデータ型であるBYTE
、DWORD
、LONG
、WORD
を定義します。これらが、(願わくば) すでに使い慣れているプリミティブのエイリアスであることに注目してください。BITMAPFILEHEADER
およびBITMAPINFOHEADER
は、これらのタイプを使用するようです。
おそらく皆さんにとって最も重要なことは、このファイルでRGBTRIPLE
という構造体 (struct
) も定義していることです。この構造体は、非常に単純に、青、緑、赤の3つのバイト (RGBの組が実際にディスク上で見つかる順序を思い出してください) を 「カプセル化」 しています。
なぜこれらの構造体が有用なのでしょうか?ファイルはディスク上のバイト列 (究極的にはビット) にすぎないことを思い出してください。これらのバイトは通常、最初の数バイトが何かを表し、次の数バイトが何かを表すというように順序付けられます。「ファイルフォーマット」 が存在するのは、バイトが何を意味するかを世界が標準化したからです。これで、ファイルをディスクからRAMに1つの大きなバイト配列として読み込むことができます。array[i]
のバイトが1つのものを表し、array[j]
のバイトが別のものを表すなどです。しかし、より簡単にメモリから取り出すことができるように、これらのバイト名の一部を与えてはどうでしょうか。これがまさに、bmp.h
の構造体でできることです。ファイルを1つの長いバイト列と考えるのではなく、構造体の列と考えることができます。
filter.c
filter.c
を開きます。このファイルはすでに作成されていますが、ここで注意すべき重要な点がいくつかあります。
まず、11行目のfilters
の定義に注目してください。この文字列は、プログラムに許されているコマンドライン引数b
、g
、r
、s
をプログラムに伝えます。それぞれに、ブラー、グレースケール、反転、セピアなど、イメージに適用できる異なるフィルタを指定するものです。
次の数行はイメージファイルを開き、それが実際にBMPファイルであることを確認し、すべてのピクセル情報をimage
という2次元配列に読み込みます。
行102から始まるswitch
ステートメントまでスクロールします。選択したフィルタに応じて、別の関数が呼び出されることに注意してください。ユーザがフィルタb
を選択すると、プログラムはblur関数を呼び出します。g
の場合、grayscale
が呼び出されます。r
の場合、reflect
が呼び出されます。s
の場合、sepia
が呼び出されます。また、これらの関数はそれぞれ、イメージの高さ、イメージの幅、ピクセルの2D配列を引数として取ります。
これらの関数は (すぐに) 実装します。ご想像のとおり、これらの各関数の目的は、目的のフィルタがイメージに適用されるように、ピクセルの2 D配列を編集することです。
プログラムの残りの行で、結果のimage
を取得し、新しいイメージファイルに書き出します。
helpers.h
次に、helpers.h
を見てください。このファイルは非常に短く、前に見た関数の関数プロトタイプを提供するだけです。
ここでは、各関数が引数としてimage
と呼ばれる2D配列を取ることに注意してください。image
は高さが多くのheight
の配列で、各行はそれ自体が幅が多数のRGBTRIPLE
の配列です。つまり、image
が画像全体を表す場合、image[0]
は最初の行を表し、image[0][0]
は画像の左上隅のピクセルを表します。
helpers.c
helpers.cを開きます。ここでは、helpers.hで宣言された関数の実装が記されています。しかし、現時点では、実装されていないことに注意してください。この部分はあなたが完成させます。
Makefile
最後に、Makefile
を見てみましょう。このファイルは、make filter
のようなターミナルコマンドを実行したときの動作を指定しています。以前に書いたプログラムは1つのファイルに制限されていましたが、filter
は複数のファイル、filter.c
、bmp.h
、helpers.h
、helpers.c
を使っているようです。そのため、このファイルのコンパイル方法をmake
に指示する必要があります。
自分のターミナルでfilter
をコンパイルしてみてください。
$ make filter
その後、次のコマンドを実行してプログラムを実行できます。
$ ./filter -g images/yard.bmp out.bmp
これは、images/yard.bmp
でイメージを取得し、grayscale
関数でピクセルを実行した後にout.bmp
という新しいイメージを生成します。grayscale
はまだ何もしないので、出力イメージは元のyard
と同じように見えるはずです。
仕様
ユーザがグレースケール、セピア、反転、またブラーフィルタをイメージに適用できるように、helpers.c
の関数を実装します。
grayscale
関数は、イメージを取り込み、同じイメージの白黒バージョンに変換します。sepia
関数は画像を取得し、同じ画像のセピアバージョンに変換します。reflect
関数は、イメージを取得して水平方向に反転する必要があります。- 最後に、
blur
関数はイメージを取得し、同じイメージのボックスブラーバージョンに変換します。
関数シグネチャは変更しないでください。また、helpers.c
以外のファイルも変更しないでください。
ウォークスルー
使い方
プログラムは、次の例に従って動作する必要があります。INFILE.bmp は入力画像の名前、OUTFILE.bmp はフィルタ適用後の結果の画像の名前です。
$ ./filter -g INFILE.bmp OUTFILE.bmp
$ ./filter -s INFILE.bmp OUTFILE.bmp
$ ./filter -r INFILE.bmp OUTFILE.bmp
$ ./filter -b INFILE.bmp OUTFILE.bmp
ヒント
- ピクセルの
rgbtRed
、rgbtGreen
、およびrgbtBlue
コンポーネントの値はすべて整数であるため、浮動小数点数をピクセル値に割り当てるときは、必ず最も近い整数に丸めてください grayscale
関数を実装するとき、3つの整数の値を平均化する必要があります。なぜこれらの整数の和を 3 ではなく 3.0 で割りたいのでしょうか?- reflect関数では、行の反対側にあるピクセルの値を交換する必要があります。講義の中で、一時的な変数を使って2つの値を入れ替える実装をしたことを思い出してください。スワッピングのために別の関数を使用する必要はありません。
- セピアの実装では、特に色の値が255以上でないことを確認する必要がある場合、2つの整数のうち小さい方を返す関数がどのように役に立つでしょうか。
blur
関数を実装するとき、あるピクセルをぼかすと、別のピクセルのぼかしにも影響が及ぶことがあります。RGBTRIPLE copy[height][width];
のようなコードで新しい(2次元)配列を宣言してimage
のコピー(関数の第3引数)を作成し、for
ループを入れ子にしimage
をピクセルごとcopy
にコピーしてはどうでしょうか。そして、copy
からピクセルの色を読み出し、image
のピクセルの色を書き込む(つまり、変更する)のはどうですか?
テスト
用意されているサンプルビットマップファイルですべてのフィルタをテストしてください。
check50
を使用して以下を実行し、コードの正確さを評価してください。ただし、コンパイルとテストは必ず自分で行ってください。
check50 cs50/problems/2022/x/filter/less
以下を実行し、style50
を使用してコードのスタイルを評価します。
style50 helpers.c
提出方法
ターミナルで、以下を実行して提出してください。
submit50 cs50/problems/2022/x/filter/less