8-bit grayscale画像へのダウンスケーリングと画像コントラスト

# 前提

ここでいうダウンスケーリングは、画像の標本化でいうところのダウンスケールを意味します。

# 画像のダウンスケーリング

ここでは、グレースケール画像を対象に話をします。
一般的に、グレースケール画像は8-bit, 16-bit, 32-bit等、「1チャンネル」の画素の集まりで構成されています。
（RGBなどのカラー画像は、R、G、Bそれぞれのチャンネルがあり、これらを統合して画像にしています。この点は重要です。言い換えれば、RGB画像は、8-bit grayscale画像が3枚分あると言えます。厳密にはα成分（透明度を指定する成分）がある場合もありますが、α成分も8-bit grayscale画像が一枚分増えたようなものです。）

よく医用画像で使われるのが、16-bit画像です。
16-bit画像は、0-65535の間の数値をピクセル値として保持できます。
一方で、8-bit画像は、0-255までの数値しか保持できません。
（符号付き、符号なしについてはここでは敢えて触れません。）

画像処理や機械学習などでは、計算を簡便にするため、あるいは処理を高速にするために、高いビットの画像を8-bit画像にダウンスケールして変換する処理を行います。
このダウンスケールの際にお作法を間違えると、画像がおかしなことになるので注意が必要です。

# サンプル画像

JIRAの胸部レントゲンをお借りします。
LEEとついているものは非圧縮画像という意味です。これを使います。
[サンプル画像](http://www.jira-net.or.jp/dicom/file/dcm/CR_LEE_IR87a.dcm)

# ダウンスケール例（16-bitから8-bit）

まずは、画像をロードしてnumpyのndarrayとして扱うために、pydicomを使って行きます。

```
!pip install pydicom
```

データをロードします。

```
import pydicom
import numpy as np
ds = pydicom.dcmread('CR_LEE_IR87a.dcm')#パスを指定する

#ピクセルを取り出す（ndarrayとして扱っています）
pixels = ds.pixel_array

# 左右反転しているので戻す
pixels = np.fliplr(pixels)

#画像として表示
import matplotlib.pyplot as plt
plt.imshow(pixels,cmap="gray")#２つとも必要
plt.show() 
```
![ダウンロード (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/379410/3b7f2942-8ae9-54a3-e154-bd9f7e0b65d1.png)

念のため、この画像が8-bitでないことを確認します。
DICOMタグからもわかりますが、直接ピクセルをみてもわかります。

```
# この画像の画素を見てみる
print (pixels[512,512]) # 3186
```

画素(x,y)(512,512)にあるピクセル値は3186であることが確認できます。
つまり、0-255の間にない、かつ、実数ではないので16-bitであることがわかります。

これで準備が出来ました。


# numpy

単純に8bitに変換してみます。

```
# numpyで8 bit変換してみる
np_img = pixels.astype('uint8')
plt.imshow(np_img,cmap="gray")
plt.show() 
```
![ダウンロード (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/379410/bfbff884-1320-266c-9997-07122c4c4766.png)

違うなあ。

# Pillow

```
# pillowを使う
from PIL import Image, ImageOps
pil_img = Image.fromarray(pixels)
pil_img = pil_img.convert('L')#L:grayscale
plt.imshow(pil_img,cmap="gray")#２つとも必要
plt.show() 
print(np.asarray(pil_img)[512,512]) #255, 完全にアウトオブレンジな感じです。
```
![ダウンロード (4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/379410/302a8ef5-c3a6-9165-8603-33f9f72ef21c.png)

これも違うなあ。

# OpenCV

```
# opencvを使う
# 変換可能
# pixels は16 bit配列の変数とする。
import cv2
cv_img8 = np.zeros(pixels.shape)  # dummy変数
cv_img8 = cv2.convertScaleAbs(pixels-np.min(pixels), cv_img8, 255/(np.max(pixels)-np.min(pixels)), beta=0) #  2021/6/22修正
cv_img8 = cv_img8.astype(np.uint8)
plt.imshow(cv_img8, cmap="gray")
plt.show() 
print(cv_img8[512,512])
```

# Scikit-image

```
# skimageを使う
import skimage
sk_img = skimage.img_as_ubyte(pixels)
plt.imshow(sk_img,cmap="gray")#２つとも必要
plt.show() 
```
お！と思うが、これはまやかしで、よくみると画素が潰れている。

![ダウンロード (5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/379410/b8777fbc-4934-c2b3-32ee-0ec475350b87.png)

# ImageJのアルゴリズムを用いたダウンスケール

ImageJのダウンスケールを実装してみました。
最大値、最小値と、スケーリングファクタで正規化します。

```
# ImageJから拝借したノーマリゼーション
pixels_ = np.copy(pixels)
# 符号付16 bitの場合、オフセットを加えて符号なしと同じ条件にする。
# pixels_ = pixels_ + 32768  # 符号付き16ビットを符号なし16ビットに変換
amin=np.amin(pixels_)
amax=np.amax(pixels_)
pixelsByte = ((pixels_-amin)/(amax-amin))*255
# pixelsByte = ((pixels_-amin)/(amax-amin+1))*256
pixelsByte = np.clip(pixelsByte,0,255)
pixelsByte = np.uint8(pixelsByte) # 符号なしバイトに変換

print(np.asarray(pixelsByte)[512,512])
plt.imshow(pixelsByte,cmap="gray")
plt.show() 
```
![ダウンロード (6).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/379410/52d2d9f6-fb22-14d8-1542-f30e61a7b8aa.png)

これこれ。求めてた画像です。

Visionary Imaging Services, Inc.