# DRA（ダイナミックレンジ自動調整）設計メモ

画像（または地図の表示範囲）の画素値の分布から黒点・白点を自動で決め、その範囲を 0〜255 いっぱいに引き伸ばす機能。ArcGIS の DRA（Dynamic Range Adjustment）と同じ考え方で、地図を動かすたびに「いま見えている範囲」の統計で引き伸ばし直す使い方を主な用途とする。

設計メモ（[design.md](design.md)）の「第3段階：画像全体を見る処理」のうち、ヒストグラム取得と自動レベル（両端クリップ）を先に実装するもの。

## 方針

統計を取る処理と、引き伸ばしを掛ける処理を分ける。

1. **統計**: `histogram(image, { rect })` で 256 段階のヒストグラムを取る。ヒストグラムは足し合わせられる（`mergeHistograms`）ので、タイルごとに取った統計を表示範囲全体の統計にまとめられる
2. **範囲の決定**: ヒストグラムから黒点・白点を決める（パーセントクリップ / 最小最大 / 標準偏差）
3. **引き伸ばし**: 決まった黒点・白点で全画素を引き伸ばす。これは普通の点処理（`stretch`）なので、既存の補正と同じく 1 パスに融合され、どのタイルにも同じ結果が出る

タイル表示では「統計は表示範囲全体から 1 回だけ取り、その結果（具体的な数値になった `stretch`）をすべてのタイルに掛ける」。タイルごとに自動補正すると、タイルごとに範囲が変わって継ぎ目が出るため。

## API

```ts
import { pipeline, histogram, mergeHistograms } from 'browser-image-enhancement';

// 1 枚の画像: その画像の統計で自動的に引き伸ばす（run のときに統計を取る）
await pipeline().autoStretch().contrast(0.1).run(img);
await pipeline().autoStretch({ method: 'standardDeviation', stdDevs: 2 }).run(img);

// タイル・表示範囲: 統計を自分で集めてから確定させる
const stats = mergeHistograms(tiles.map((t) => histogram(t.image, { rect: t.visibleRect })));
const p = pipeline().autoStretch().contrast(0.1).resolve(stats); // autoStretch → stretch({ black, white })
for (const t of tiles) await p.run(t.image);

// 範囲を自分で指定する（sRGB の符号化値 0〜1。チャンネル別に [R, G, B] でも指定できる）
pipeline().stretch({ black: [0.05, 0.04, 0.08], white: [0.8, 0.85, 0.9] });
```

関数型 API にも `stretch(image, params)` と `autoStretch(image, options)` を足す。

### `autoStretch` のオプション

| オプション | 既定 | 意味 |
| --- | --- | --- |
| `method` | `'percentClip'` | `'percentClip'`: 暗い側・明るい側から指定割合の画素を切り捨てた範囲。`'minMax'`: 最小値〜最大値。`'standardDeviation'`: 平均 ± `stdDevs` × 標準偏差（最小値〜最大値の内側に収める） |
| `lowPercent` / `highPercent` | `0.5` / `0.5` | `percentClip` で暗い側・明るい側から切り捨てる画素の割合（%） |
| `stdDevs` | `2` | `standardDeviation` の幅 |
| `linked` | `false` | `false` は R・G・B を別々に引き伸ばす（ArcGIS と同じ。色かぶりも取れる）。`true` は 3 チャンネルをまとめた統計で同じ範囲を使う（色のバランスを保つ） |

チャンネルごとの既定を「別々」にしたのは、衛星画像の表示（ArcGIS の DRA やパーセントクリップ）に合わせたため。写真で色味を変えたくない場合は `linked: true` を使う。

### 統計の対象

- **透明な画素（α = 0）は数えない**。OpenLayers は nodata やマスクの外側を α = 0 にして渡すので、画像の外の黒い余白が統計に入らない
- `rect` で画像の一部だけを数えられる（表示範囲にかかるタイルの一部分など）
- 色モードは他の処理と同じ。モノクロ（または `colorMode: 'gray'`）では輝度 1 チャンネルの統計になる
- 数える画素が 1 つもない場合と、黒点 ≥ 白点になった場合（真っ平らな画像）は、そのチャンネルを変えない

### パイプライン内の位置

`autoStretch` の統計は「その位置に届いた画像」の統計とする。前に露出などがあれば、露出を掛けた後の分布で範囲を決める。

- 点処理はすべて単調（値の順序を入れ替えない）なので、元画像のヒストグラムの各段階の値を前段の補正に通すだけで、補正後の分布が正確に分かる。画像を 2 回処理する必要はない
- ただし彩度はチャンネルを混ぜるので、チャンネル別のヒストグラムからは計算できない。彩度より後ろに置いた `autoStretch` は彩度を無視した統計を使い、開発ビルドで警告する

### 確定（`resolve`）と保存

- `pipeline.resolve(histogram)` は `autoStretch` を具体的な `stretch` に置き換えた新しいパイプラインを返す
- `run` / `runSync` は内部で入力画像のヒストグラムを取って `resolve` してから処理する。Worker で横帯に分けて処理する場合は、各 Worker が自分の帯のヒストグラムを返し、メインスレッドが合算して確定させてから処理させる（帯ごとに範囲が割れない。モノクロ判定と同じ 2 段階の流れ）
- `toJSON()` は `autoStretch` のまま保存する（どの画像に掛けるかで値が変わるため）。確定した値を保存したければ `resolve` してから `toJSON()` する

## 表示範囲 DRA（OpenLayers の例）

`examples/openlayers-cog` の `EnhancedGeoTIFF` に DRA を入れる。

- `source.updateDra(map)` が、地図の表示範囲をソースの投影法に変換し、その範囲にかかるタイルの補正前データからヒストグラムを集めて、パイプラインを確定させる。地図の `moveend` で呼ぶ
- 統計用のタイルは、表示中のズームではなく「表示範囲が 1024 画素四方程度になる」粗いズームレベル（COG のオーバービュー）から読む。ArcGIS がオーバービューで統計を取るのと同じ考え方で、広い範囲を見ていても読み込み量が増えない
- タイルが表示範囲からはみ出す部分は `rect` で切り落とし、画像の外側（タイルの余白）も数えない
- 確定した範囲が前回と同じなら何もしない。変わったときだけ全タイルを補正し直す（補正前タイルのキャッシュを使うので COG は読み直さない）
- 確定するまでの間は `autoStretch` を無いものとして表示する

### 制約

- 統計は OpenLayers が 0〜255 に正規化した後の 8bit 値で取る。16bit の COG では、正規化の段階で範囲が狭い（例えば 12bit 相当の値しか使っていない）と階調が粗くなる。16bit のまま統計を取るには、正規化前のデータを扱う仕組みが別に要る（今回は対象外）
- 地図が回転しているときは、表示範囲を囲む矩形で統計を取る
- 投影法の変換は範囲の外接矩形で近似する
