計算写真におけるPython画像処理の概要
公開: 2022-03-11計算写真とは、計算によって写真プロセスを強化することです。 通常、これは最終結果の後処理(写真編集と同様)にのみ当てはまると考える傾向がありますが、シーンの照明から始まり、次のように、写真プロセスのすべてのステップで計算を有効にできるため、可能性ははるかに高くなります。レンズ、そして最終的にはキャプチャされた画像の表示でも。
これは、通常のカメラで達成できることよりもはるかに多くのさまざまな方法で実行できるため、重要です。 また、現在最も普及しているタイプのカメラであるモバイルカメラは、より大きな兄弟(DSLR)に比べてそれほど強力ではありませんが、デバイスで利用できるコンピューティング能力を活用することで、うまく機能することもできます。 。
計算によって写真を向上させることができる2つの例を見ていきます。より正確には、より多くのショットを撮り、Pythonを少し使用してそれらを組み合わせるだけで、モバイルカメラハードウェアがそうでない2つの状況で素晴らしい結果を生み出すことができることを確認します。本当に輝いています—低照度と高ダイナミックレンジ。
低照度写真
シーンの暗い場所で写真を撮りたいが、カメラの絞り(レンズ)が小さく、露光時間が限られているとします。 これは携帯電話のカメラの典型的な状況であり、暗いシーンでは次のような画像が生成される可能性があります(iPhone 6カメラで撮影)。
コントラストを改善しようとすると、結果は次のようになりますが、これもかなり悪いです。
何が起こるのですか? このすべてのノイズはどこから来るのですか?
答えは、ノイズはセンサーから発生するということです。センサーは、光がセンサーに当たるタイミングと、その光の強さを判断しようとするデバイスです。 ただし、暗い場所では、何かを登録するために感度を大幅に上げる必要があります。感度が高いということは、誤検知、つまり単にそこにない光子も検出し始めることを意味します。 (補足として、この問題はデバイスだけでなく、私たち人間にも影響します。次に暗い部屋にいるときは、視野に存在するノイズに注意してください。)
イメージングデバイスには、常にある程度のノイズが存在します。 ただし、信号(有用な情報)の強度が高い場合、ノイズは無視できます(信号対ノイズ比が高い)。 信号が低い場合(暗い場所など)、ノイズが目立ちます(信号対ノイズ比が低い)。
それでも、上記のものよりも良いショットを取得するために、すべてのカメラの制限があっても、ノイズの問題を克服することができます。
そのためには、時間の経過とともに何が起こるかを考慮する必要があります。ノイズは完全にランダムになりますが、信号は同じままです(同じシーンで、静的であると想定します)。 つまり、シーンを何度も撮影すると、ノイズのバージョンは異なりますが、有用な情報は同じになります。
したがって、時間の経過とともに撮影された多くの画像を平均すると、信号に影響を与えずにノイズがキャンセルされます。
次の図は、簡略化された例を示しています。ノイズの影響を受けた信号(三角形)があり、異なるノイズの影響を受けた同じ信号の複数のインスタンスを平均することによって信号を回復しようとしています。
ノイズは単一のインスタンスで信号を完全に歪ませるのに十分な強さですが、平均化するとノイズが徐々に減少し、元の信号が復元されることがわかります。
この原理が画像にどのように適用されるかを見てみましょう。まず、カメラが許容する最大の露出で被写体を複数回撮影する必要があります。 最良の結果を得るには、手動撮影が可能なアプリを使用してください。 同じ場所から撮影することが重要です。そのため、(即席の)三脚が役立ちます。
ショット数が多いほど品質は一般的に高くなりますが、正確な数は状況によって異なります。光の量、カメラの感度などです。適切な範囲は10〜100です。
これらの画像を(可能であれば生の形式で)取得したら、Pythonで読み取って処理できます。
Pythonでの画像処理に慣れていない場合は、画像がバイト値(0〜255)の2D配列として表されること、つまり、モノクロまたはグレースケールの画像であることに注意してください。 カラー画像は、各カラーチャネル(R、G、B)に1つずつ、または実質的に垂直位置、水平位置、およびカラーチャネル(0、1、2)によってインデックス付けされた3Dアレイの3つのそのような画像のセットと考えることができます。 。
NumPy(http://www.numpy.org/)とOpenCV(https://opencv.org/)の2つのライブラリを利用します。 1つ目は、配列の計算を非常に効果的に(驚くほど短いコードで)実行できるようにします。この場合、OpenCVは画像ファイルの読み取り/書き込みを処理しますが、はるかに優れており、多くの高度なグラフィックス手順を提供します。記事の後半で使用します。
import os import numpy as np import cv2 folder = 'source_folder' # We get all the image files from the source folder files = list([os.path.join(folder, f) for f in os.listdir(folder)]) # We compute the average by adding up the images # Start from an explicitly set as floating point, in order to force the # conversion of the 8-bit values from the images, which would otherwise overflow average = cv2.imread(files[0]).astype(np.float) for file in files[1:]: image = cv2.imread(file) # NumPy adds two images element wise, so pixel by pixel / channel by channel average += image # Divide by count (again each pixel/channel is divided) average /= len(files) # Normalize the image, to spread the pixel intensities across 0..255 # This will brighten the image without losing information output = cv2.normalize(average, None, 0, 255, cv2.NORM_MINMAX) # Save the output cv2.imwrite('output.png', output)
結果(自動コントラストを適用)は、ノイズがなくなったことを示しています。これは、元の画像から大幅に改善されています。
ただし、緑がかったフレームやグリッドのようなパターンなど、いくつかの奇妙なアーティファクトがまだあります。 今回はランダムノイズではなく、固定パターンノイズです。 どうしたの?
繰り返しますが、センサーのせいにすることができます。 この場合、センサーのさまざまな部分が光に対してさまざまに反応し、目に見えるパターンが生じることがわかります。 これらのパターンのいくつかの要素は規則的であり、おそらくセンサー基板(金属/シリコン)と、それが入射光子をどのように反射/吸収するかに関連しています。 白いピクセルなどの他の要素は、単に欠陥のあるセンサーピクセルであり、光に対して過度に敏感または過度に鈍感になる可能性があります。
幸いなことに、このタイプのノイズを取り除く方法もあります。 これは、ダークフレーム減算と呼ばれます。
そのためには、パターンノイズ自体の画像が必要です。これは、暗闇を撮影すると取得できます。 はい、そうです。カメラの穴を覆い、最大露光時間とISO値で多くの写真(たとえば100枚)を撮り、上記のように処理します。
多くの黒いフレーム(ランダムノイズのために実際には黒ではない)で平均化すると、固定パターンノイズになります。 この固定ノイズは一定であると想定できるため、この手順は1回だけ必要です。結果の画像は、将来のすべての低照度ショットで再利用できます。
パターンノイズ(コントラスト調整済み)の右上部分は、iPhone6では次のようになります。
繰り返しになりますが、グリッドのようなテクスチャと、スタックした白いピクセルのように見えるものにさえ気づきます。
このダークフレームノイズの値( average_noise
変数内)を取得したら、正規化する前に、これまでのショットから単純に減算できます。

average -= average_noise output = cv2.normalize(average, None, 0, 255, cv2.NORM_MINMAX) cv2.imwrite('output.png', output)
これが私たちの最後の写真です:
ハイダイナミックレンジ
小型(モバイル)カメラのもう1つの制限は、ダイナミックレンジが小さいことです。つまり、細部をキャプチャできる光の強度の範囲はかなり狭いということです。
言い換えると、カメラはシーンからの光強度の狭帯域のみをキャプチャできます。 そのバンドの下の強度は純粋な黒として表示され、その上の強度は純粋な白として表示され、これらの領域から詳細が失われます。
ただし、カメラ(または写真家)が使用できるトリックがあります。それは、センサーに到達する光の総量を効果的に制御するために、露出時間(センサーが光にさらされる時間)を調整することです。特定のシーンに最適な範囲をキャプチャするために、範囲を上下にシフトします。
しかし、これは妥協案です。 多くの詳細が最終的な写真になりません。 下の2つの画像では、同じシーンが異なる露出時間でキャプチャされています。非常に短い露出(1/1000秒)、中程度の露出(1/50秒)、長い露出(1/4秒)です。
ご覧のとおり、3つの画像のいずれも、利用可能なすべての詳細をキャプチャすることはできません。ランプのフィラメントは最初のショットでのみ表示され、花の詳細の一部は中央または最後のショットで表示されますが、表示されません。両方。
良いニュースは、それについて私たちにできることがあるということです。また、Pythonコードを少し使って複数のショットを構築する必要があります。
私たちが採用するアプローチは、ここで彼の論文でその方法を説明しているPaul Debevecetal。の研究に基づいています。 メソッドは次のように機能します。
まず、同じシーン(静止)の複数のショットが必要ですが、露光時間は異なります。 繰り返しになりますが、前の場合と同様に、カメラがまったく動かないようにするために三脚またはサポートが必要です。 また、露出時間を制御し、カメラの自動調整を防ぐために、手動撮影アプリ(電話を使用している場合)も必要です。 必要なショット数は、画像に存在する強度の範囲(3以上)によって異なります。保存したい詳細が少なくとも1つのショットにはっきりと表示されるように、露光時間はその範囲全体で間隔を空ける必要があります。
次に、アルゴリズムを使用して、異なる露光時間にわたる同じピクセルの色に基づいてカメラの応答曲線を再構築します。 これにより、基本的に、ポイントの実際のシーンの明るさ、露出時間、および対応するピクセルがキャプチャされた画像に持つ値の間のマップを確立できます。 OpenCVライブラリからのDebevecのメソッドの実装を使用します。
# Read all the files with OpenCV files = ['1.jpg', '2.jpg', '3.jpg', '4.jpg', '5.jpg'] images = list([cv2.imread(f) for f in files]) # Compute the exposure times in seconds exposures = np.float32([1. / t for t in [1000, 500, 100, 50, 10]]) # Compute the response curve calibration = cv2.createCalibrateDebevec() response = calibration.process(images, exposures)
応答曲線は次のようになります。
縦軸には、ポイントのシーンの明るさと露出時間の累積効果があり、横軸には、対応するピクセルの値(チャネルあたり0〜255)があります。
この曲線は、逆の操作(プロセスの次のステップ)を実行できるようにします。ピクセル値と露光時間を指定すると、シーン内の各ポイントの実際の明るさを計算できます。 この明るさの値は放射照度と呼ばれ、センサー領域の単位に当たる光エネルギーの量を測定します。 画像データとは異なり、はるかに広い範囲の値(したがって、高いダイナミックレンジ)を反映するため、浮動小数点数を使用して表されます。 放射照度画像(HDR画像)を取得したら、それを簡単に保存できます。
# Compute the HDR image merge = cv2.createMergeDebevec() hdr = merge.process(images, exposures, response) # Save it to disk cv2.imwrite('hdr_image.hdr', hdr)
HDRディスプレイ(ますます一般的になっている)を持っている幸運な私たちにとって、この画像をその栄光の中で直接視覚化することが可能かもしれません。 残念ながら、HDR標準はまだ初期段階にあるため、それを行うプロセスはディスプレイごとに多少異なる場合があります。
通常の表示では画像にバイト値(0〜255)チャネルが必要ですが、残りのユーザーにとっては、このデータを引き続き利用できることは朗報です。 放射照度マップの豊富さの一部を放棄する必要がありますが、少なくともそれを行う方法を制御できます。
このプロセスはトーンマッピングと呼ばれ、浮動小数点放射照度マップ(値の範囲が広い)を標準のバイト値イメージに変換する必要があります。 余分な詳細の多くが保持されるようにそれを行うためのテクニックがあります。 これがどのように機能するかの例を示すために、浮動小数点範囲をバイト値にスクイーズする前に、HDRイメージに存在するエッジを強調(シャープ)することを想像してください。 これらのエッジを強化すると、低ダイナミックレンジの画像でもエッジ(および暗黙的に提供される詳細)を維持するのに役立ちます。
OpenCVは、Drago、Durand、Mantiuk、Reinhardtなどのこれらのトーンマッピング演算子のセットを提供します。 これらの演算子の1つ(Durand)を使用する方法と、それが生成する結果の例を次に示します。
durand = cv2.createTonemapDurand(gamma=2.5) ldr = durand.process(hdr) # Tonemap operators create floating point images with values in the 0..1 range # This is why we multiply the image with 255 before saving cv2.imwrite('durand_image.png', ldr * 255)
Pythonを使用すると、プロセスをさらに制御する必要がある場合に、独自の演算子を作成することもできます。 たとえば、これは、値の範囲を8ビットに縮小する前に非常に少数のピクセルで表される強度を削除するカスタム演算子で得られた結果です(自動コントラストステップが続きます)。
上記の演算子のコードは次のとおりです。
def countTonemap(hdr, min_fraction=0.0005): counts, ranges = np.histogram(hdr, 256) min_count = min_fraction * hdr.size delta_range = ranges[1] - ranges[0] image = hdr.copy() for i in range(len(counts)): if counts[i] < min_count: image[image >= ranges[i + 1]] -= delta_range ranges -= delta_range return cv2.normalize(image, None, 0, 1, cv2.NORM_MINMAX)
結論
少しのPythonといくつかのサポートライブラリを使用して、最終結果を改善するために物理カメラの限界を押し上げる方法を見てきました。 これまでに説明した両方の例では、複数の低品質のショットを使用してより良いものを作成していますが、さまざまな問題や制限に対して他の多くのアプローチがあります。
多くのカメラ付き携帯電話には、これらの特定の例に対応するストアまたは組み込みのアプリがありますが、これらを手動でプログラムし、得られるより高いレベルの制御と理解を楽しむことは明らかに難しいことではありません。
モバイルデバイスでの画像計算に興味がある場合は、OpenCVチュートリアル:仲間のToptalerおよびエリートOpenCV開発者AltaibayarTseveenbayarによるiOSでのMSERを使用したリアルタイムオブジェクト検出を確認してください。