10bit・12bitグレースケール画像の扱い方 ～保存・表示・処理でつまずかないために～

## 1. はじめに

最近のマシンビジョンカメラでは、10bitや12bitのグレースケール画像出力に対応する製品が増えてきました。  
これは微細な濃淡の変化を捉えたり、高ダイナミックレンジ（HDR）画像処理に対応するために有効です。

しかし、10bit/12bit画像には**3つの大きな落とし穴**があります：

1. 画像が正しく**表示できない**
2. 適切な**保存や読込ができない**
3. 画像処理ライブラリが**対応していない**

この記事では、これらの問題に対する**実践的な解決策**を、Python + OpenCV環境を中心に紹介します。

---

## 2. なぜ10bit/12bit画像は扱いが難しいのか？

通常の8bitグレースケール画像（0〜255）に比べ、  
10bit画像は **0〜1023**、12bit画像は **0〜4095** というより広い階調を持ちます。

このため、**従来の8bit前提のソフトウェアでは、正しく表示・保存・処理ができない**という問題が発生します。

---

## 3. 表示：OpenCVで10bit/12bit画像を明るく表示するには？

### ❌ 問題点：そのまま表示すると「真っ暗」に見える

OpenCVの`cv2.imshow()`で16bit画像を表示すると、ダイナミックレンジが**0〜65535**として扱われます。  
そのため、10bit（最大1023）や12bit（最大4095）画像は、非常に暗く表示されてしまいます。

(10bitグレースケール画像の表示例)
![2025-04-20_22h15_24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3996436/96e067ea-0a5c-4db3-8645-3c3762a5764d.png)


### ✅ 対処法：ビットシフトで8bitに変換

```python
import cv2
import numpy as np

img_16bit = cv2.imread("image_10bit.tiff", cv2.IMREAD_UNCHANGED)  # dtype=uint16
img_8bit = (img_16bit >> 2).astype(np.uint8)  # 10bit → 8bit（右に2bitシフト）

cv2.imshow("10bit image (converted)", img_8bit)
cv2.waitKey(0)
cv2.destroyAllWindows()
```

> 📌 10bit画像 → 2bitシフト  
> 📌 12bit画像 → 4bitシフト

(10bitグレースケール画像を2bitシフトした画像表示)
![2025-04-20_22h15_53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3996436/489afbb8-7a16-4f27-9310-dbe717120438.png)


これは表示専用の変換です。元のbit深度のまま処理したい場合は、`uint16`のまま扱います。

---

## 4. 保存・読込：10bit/12bit画像は保存できない？ → TIFFを使う

### ❌ 問題点：そもそも対応していないフォーマットが多い。

- Bitmap(*.bmp)ファイルでBitfieldを使って10bit/12bitとして保存することもできますが、画像を開くアプリが対応していないことが多い。
- 保存時に**自動で他bitに変換される**ことがあります。（ライブラリ、アプリ依存です。）

### ✅ 対処法：TIFF形式で保存（MinSampleValue/MaxSampleValueタグ付き）

TIFF形式は柔軟性が高く、以下のようなタグで階調情報を保持できます：

- `BitsPerSample = 16`
- `MinSampleValue = 0`
- `MaxSampleValue = 1023 or 4095`

### 🧪 保存例：tifffileで10bit TIFF保存

```python
import tifffile
import numpy as np

width = 1024
height = 256
gradient_line = np.linspace(0, 1023, width, dtype=np.uint16)
img_10bit = np.tile(gradient_line, (height, 1))

tifffile.imwrite(
    "output_10bit.tiff",
    img_10bit,
    metadata={"MinSampleValue": 0, "MaxSampleValue": 1023},
)
```

> ⚠ **ほとんどの画像表示ソフトではMinSampleValue/MaxSampleValueタグを無視**します。
>画像解析ソフトをして有名なImageJでは、初期設定で、最小輝度値を**黒**、最大輝度値を**白**となるように明るさが調整され表示されます。

---

### 📥 読込例：TIFF画像の読込と確認

```python
import tifffile
import cv2
import numpy as np

# 読込
img = tifffile.imread("output_10bit.tiff")  # dtype=np.uint16

# 情報確認
print("dtype:", img.dtype)
print("shape:", img.shape)

# 表示用に8bit変換
img_8bit = (img >> 2).astype(np.uint8)
cv2.imshow("TIFF 10bit image (converted)", img_8bit)
cv2.waitKey(0)
cv2.destroyAllWindows()
```

---

## 5. 処理：OpenCVなどのライブラリで対応できる？

### ❌ 問題点：多くの関数は8bit専用

以下のOpenCV関数は、`uint16`には非対応です：

| 関数名             | 対応bit深度       |
|--------------------|------------------|
| `cv2.Canny()`      | `uint8` のみ     |
| `cv2.equalizeHist()`| `uint8` のみ     |
| `cv2.findContours()`| `uint8` のみ     |

### ✅ 対処法：処理前に一時的に8bitへ変換する

```python
# 10bit画像（uint16）→ 8bit に変換して処理
img_8bit = (img_16bit >> 2).astype(np.uint8)
edges = cv2.Canny(img_8bit, 50, 150)
```


---

## 6. おわりに

10bitや12bitグレースケール画像は、高精細な画像処理を実現する強力な手段です。  
しかし、次のような**落とし穴**に注意が必要です：

| 課題                  | 対処方法                                      |
|-----------------------|-----------------------------------------------|
| 表示が真っ暗になる     | → ビットシフトして8bitに変換                  |
| 保存ができない         | → TIFF形式＋タグ付きで保存                    |
| 処理できない関数が多い | → 処理前に一時的に8bit変換 or ライブラリ選定 or 自作 |