Filter (more comfortable)

次の手順に従って、BMPにフィルタを適用するプログラムを実装します。

$ ./filter -r IMAGE.bmp REFLECTED.bmp

ここで、IMAGE.bmpは画像ファイルの名前、REFLECTED.bmpは出力画像ファイルに付けられた名前であり、反映されています。

背景

ビットマップ

おそらく、画像を表現する最も簡単な方法は、それぞれが異なる色を持てるピクセルのグリッド (ドット) を使用することです。白黒画像の場合、以下のように、0が黒を表し、1が白を表すことができるので、ピクセルあたり1ビットが必要です。

a simple bitmap

この意味では、イメージは単なるビットマップ (ビットのマップ) です。よりカラフルな画像を作成するには、ピクセルあたりのビット数を増やすだけです。「24ビットカラー」 をサポートするファイルフォーマット (BMPJPEGPNGなど) は、ピクセルあたり24ビットを使用します。(BMPは、実際には1、4、8、16、24、および32ビットカラーをサポートします。)

24ビットBMPは、8ビットを使用してピクセルの色の赤の量を表し、8ビットを使用してピクセルの色の緑の量を表し、8ビットを使用してピクセルの色の青の量を表します。RGBカラーという言葉を耳にしたことがあれば、すでにこれらの赤、緑、青がどのように表されるか知っているでしょう。

BMP内のあるピクセルのR、G、およびB値が、例えば16進数で0xff0x00および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進数) に設定されている場合、ピクセルは白になります。赤、緑、青の値がすべて同じである限り、結果は黒と白のスペクトルに沿って変化するグレーのシェードになります。値が大きいほど明るいシェード (白に近い) 、値が小さいほど暗いシェード (黒に近い) になります。

ピクセルをグレースケールに変換するには、赤、緑、青の値がすべて同じであることを確認する必要があります。でもどうやってその値を知るのでしょう?元の赤、緑、青の値がすべてかなり高い場合、新しい値もかなり高いはずです。また、元の値がすべて低い場合は、新しい値も低くする必要があります。

実際、新しいイメージの各ピクセルが古いイメージと同じ明るさまたは暗さを持つようにするには、赤、緑、青の値の平均を取り、新しいピクセルを作成するグレーの濃淡を決定します。

これをイメージの各ピクセルに適用すると、結果はグレースケールに変換されたイメージになります。

Reflection反転

フィルタによっては、ピクセルが移動することもあります。たとえば、イメージの反転とは、ミラーの前に元のイメージを配置することによって得られるイメージをフィルタとして使用することです。したがって、イメージの左側のピクセルはすべて右側になり、その逆も同じです。

元のイメージの元のピクセルはすべて反転イメージに残りますが、これらのピクセルはイメージ内の別の場所に再配置されている場合があります。

Blurぼかし

イメージをぼかしたり柔らかくしたりするエフェクトを作成するには、いくつかの方法があります。この問題には、 「ボックスブラー」 を使用します。これは、各ピクセルを取得し、各カラー値に対して、隣接するピクセルのカラー値を平均化することで新しい値を与えるものです。

各ピクセルに番号を付けた次のピクセルグリッドを考えてみます。

a grid of pixels

各ピクセルの新しい値は、元のピクセルの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) を平均します。

Edgesエッジ

画像処理のための人工知能アルゴリズムでは、画像内のエッジを検出することがしばしば有用です。この効果を得る1つの方法は、イメージに Sobelオペレータを適用することです。

画像のぼかしと同様に、エッジ検出も各ピクセルを取得し、そのピクセルを囲む3 x 3グリッドのピクセルに基づいて修正することで機能します。しかし、9つのピクセルの平均を取る代わりに、Sobelオペレータは周囲のピクセルの値の加重合計を取ることによって各ピクセルの新しい値を計算します。また、オブジェクト間のエッジは垂直方向と水平方向の両方で発生する可能性があるため、実際には2つの加重和が計算されます。1つはx方向のエッジを検出するためのもので、もう1つはy方向のエッジを検出するためのものです。特に、次の2つの 「カーネル」 を使用します。

これらのカーネルをどのように解釈しますか?つまり、各ピクセルの3つのカラー値それぞれについて、2つの値Gx とGyを計算します。たとえば、ピクセルの赤チャンネル値のGx を計算するには、ピクセルの周囲に3 x 3ボックスを形成する9つのピクセルの元の赤の値を取得し、それぞれにGx カーネルの対応する値を掛けて、結果の値の合計を取得します。

なぜカーネルにこれらの特定の値を使うのでしょうか?たとえば、Gx 方向では、ターゲットピクセルの右側のピクセルに正の数を乗算し、ターゲットピクセルの左側のピクセルに負の数を乗算します。合計を求めたときに、右側のピクセルが左側のピクセルと同じ色であれば、結果は0に近くなります (数値はキャンセルされます) 。しかし、右側のピクセルが左側のピクセルと大きく異なる場合、結果の値は非常に正または負になり、オブジェクト間の境界の結果である可能性が高い色の変化を示します。y方向のエッジを計算する場合にも、同様の議論が成り立ちます。

これらのカーネルを使用して、ピクセルの赤、緑、青の各チャネルのGx値とGy値を生成できます。しかし、各チャネルは2つではなく1つの値しか取ることができないので、GxGyを1つの値に結合する何らかの方法が必要です。Sobelフィルタアルゴリズムは、Gx^2 + Gy^2の平方根を計算することによってGxGyを組み合わせて最終値にします。また、チャネル値は0から255までの整数値のみを取ることができるため、結果の値は最も近い整数に丸められ、255で制限されるようにしてください。

では、画像の端や隅のピクセルを処理するのはどうでしょうか。エッジのピクセルを処理するにはさまざまな方法がありますが、この問題を解決するために、イメージのエッジの周囲に1ピクセルの黒の実線の境界線があるかのようにイメージを処理してください。したがって、イメージのエッジを超えるピクセルにアクセスしようとすると、黒の実線のピクセルとして処理されます (赤、緑、青のそれぞれの値が0)。これでGxGyの計算においてこれらのピクセルを無視することができます。

始め方

VS Codeを開きます。

ターミナルウィンドウ内をクリックすることから始めて、それからcdを実行します。
その後プロンプトは次のようになっていることがわかります。

$

ターミナルウィンドウの内側をクリックし、次のように入力します。

wget https://cdn.cs50.net/2021/fall/psets/4/filter-more.zip

その後にEnterを押すと、filter-more.zipというZIPがあなたのCodespaceにダウンロードされます。

次に

unzip filter-more.zip

を実行して、filter-moreというフォルダを作成します。
ZIPファイルは不要になったため、

rm filter-more.zip

を実行し、プロンプトで “y “に続いてEnterで応答すると、ダウンロードしたZIPファイルが削除されます。

次に

cd filter-more

の後にEnterを押して、そのディレクトリに移動する(つまり、開く)。これでプロンプトは以下のようになります。

filter-more/ $

lsを実行すると、bmp.h, filter.c, helpers.h, helpers.c, Makefileといういくつかのファイルが見えるはずです。また、imagesというフォルダに4つのBMPファイルがあるのが見えるはずです。もし何か問題が発生したら、もう一度同じ手順を踏んで、どこで間違ったのか判断してください。

理解を深める

次に、配布コードとして提供されるファイルの一部を見て、ファイルの内容を理解します。

bmp.h

bmp.hを開いて (ファイルブラウザでダブルクリックして) 見てください。

前述のヘッダ (BITMAPINFOHEADERおよびBITMAPFILEHEADER) の定義が表示されます。さらに、このファイルは、Windowsプログラミングの世界で通常見られるデータ型であるBYTEDWORDLONGWORDを定義します。これらが、(願わくば) すでに使い慣れているプリミティブのエイリアスであることに注目してください。BITMAPFILEHEADER およびBITMAPINFOHEADER は、これらのタイプを使用するようです。

おそらく皆さんにとって最も重要なことは、このファイルでRGBTRIPLE という構造体 (struct) も定義していることです。この構造体は、非常に単純に、青、緑、赤の3つのバイト (RGBの組が実際にディスク上で見つかる順序を思い出してください) を 「カプセル化」 しています。

なぜこれらの構造体が有用なのでしょうか?ファイルはディスク上のバイト列 (究極的にはビット) にすぎないことを思い出してください。これらのバイトは通常、最初の数バイトが何かを表し、次の数バイトが何かを表すというように順序付けられます。「ファイルフォーマット」 が存在するのは、バイトが何を意味するかを世界が標準化したからです。これで、ファイルをディスクからRAMに1つの大きなバイト配列として読み込むことができます。array[i]のバイトが1つのものを表し、array[j]のバイトが別のものを表すなどです。しかし、より簡単にメモリから取り出すことができるように、これらのバイト名の一部を与えてはどうでしょうか。これがまさに、bmp.hの構造体でできることです。ファイルを1つの長いバイト列と考えるのではなく、構造体の列と考えることができます。

filter.c

filter.cを開きます。このファイルはすでに作成されていますが、ここで注意すべき重要な点がいくつかあります。

まず、11行目のfilters の定義に注目してください。この文字列は、プログラムに許されているコマンドライン引数begsをプログラムに伝えます。それぞれに、ブラー、エッジ検出、グレースケール、反転など、イメージに適用できる異なるフィルタを指定するものです。

次の数行はイメージファイルを開き、それが実際にBMPファイルであることを確認し、すべてのピクセル情報をimageという2次元配列に読み込みます。

行102から始まるswitchステートメントまでスクロールします。選択したフィルタに応じて、別の関数が呼び出されることに注意してください。ユーザがフィルタbを選択すると、プログラムはblur関数を呼び出します。gの場合、grayscaleが呼び出されます。eの場合、edgesが呼び出されます。rの場合、reflectが呼び出されます。また、これらの関数はそれぞれ、イメージの高さ、イメージの幅、ピクセルの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.cbmp.hhelpers.hhelpers.cを使っているようです。そのため、このファイルのコンパイル方法をmakeに指示する必要があります。

自分のターミナルでfilter をコンパイルしてみてください。

$ make filter

その後、次のコマンドを実行してプログラムを実行できます。

$ ./filter -g images/yard.bmp out.bmp

これは、images/yard.bmpでイメージを取得し、grayscale関数でピクセルを実行した後にout.bmpという新しいイメージを生成します。grayscaleはまだ何もしないので、出力イメージは元のyardと同じように見えるはずです。

仕様

ユーザがグレースケール、セピア、反転、またブラーフィルタをイメージに適用できるように、helpers.cの関数を実装します。

  • grayscale 関数は、イメージを取り込み、同じイメージの白黒バージョンに変換します。
  • reflect 関数は、イメージを取得して水平方向に反転する必要があります。
  • blur 関数はイメージを取得し、同じイメージのボックスブラーバージョンに変換します。
  • edges 関数はイメージを取り込み、Sobelオペレータによってオブジェクト間のエッジをハイライトします。

関数シグネチャは変更しないでください。また、helpers.c以外のファイルも変更しないでください。

ウォークスルー

このプレイリストには5つの動画があります。

使い方

プログラムは、次の例に従って動作する必要があります。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

ヒント

  • ピクセルのrgbtRedrgbtGreen、およびrgbtBlueコンポーネントの値はすべて整数であるため、浮動小数点数をピクセル値に割り当てるときは、必ず最も近い整数に丸めてください。

テスト

用意されているサンプルビットマップファイルですべてのフィルタをテストしてください。

check50を使用して以下を実行し、コードの正確さを評価してください。ただし、コンパイルとテストは必ず自分で行ってください。

check50 cs50/problems/2022/x/filter/more

以下を実行し、style50を使用してコードのスタイルを評価します。

style50 helpers.c

提出方法

ターミナルで、以下を実行して提出してください。

submit50 cs50/problems/2022/x/filter/more