Pillow(PIL) の Image resize NEAREST の動きがバージョンで異なる

Pillow(PIL) の resize はデフォルトで NEAREST 補間方式を使います。
これ、実は Pillow-3.3.3 (2016年10月5日)以前と、3.4.0(2016年10月4日)以降で動きが異なります。

- https://github.com/python-pillow/Pillow/releases?after=3.4.1


```python
from PIL import Image
im = Image.open(sys.argv[1])
im = im.resize([2, 2], Image.NEAREST)
im.save("nearest.png")
```

4x4 画像を 2x2 にリサイズした結果の比較です。

|     | 画像 | (ドット拡大) | 対応するピクセル |
|---|---|---|---|
|リサイズ前画像|![4x4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/f4a7a75b-cd7c-bd34-bc58-be8ec3f70b0a.png) |![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/d99a56ff-f63b-de16-1dc8-55738244e474.png) | |
|3.3.3以前|![nearest.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/d1a0c4a9-1560-78f9-0749-5cb90b8c1d5b.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/615b6e10-0578-9a8b-5ab9-c7ab630f73bb.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/fcc93b13-bfe1-5e4a-a491-343b39a3d00e.png)|
|3.4.0以降| ![nearest.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/3131f5a2-ee9e-1b63-5158-efc722725b6e.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/04ab0319-98d9-4d14-ac85-01da82983913.png)| ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/158ee36d-8cc4-cfe6-a4b6-9af6e2852725.png)|

# why (何故？)


Pillow のリサイズはアフィン変換の関数を呼んで拡大縮小処理を任せます。
アフィン変換は拡大縮小、回転、移動などを一度に行える便利な手法です。

- [https://ja.wikipedia.org/wiki/アフィン写像](https://ja.wikipedia.org/wiki/%E3%82%A2%E3%83%95%E3%82%A3%E3%83%B3%E5%86%99%E5%83%8F)

|Scale | Rotate | Translate|
|---|---|---|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/abf45652-0c97-a412-b863-4d7bb42e749c.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/7034c4a8-3684-beb8-e0d9-0f962c19805d.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/c3e3b0b0-6e94-4b01-4dc2-53e72ea1fb4b.png) |

このアフィン変換の改善としてピクセル座標の計算がピクセル左上隅だったのをピクセル中心ベースに計算するよう変更した際、resize 処理はその巻き添えになったようです。(正直、雑すぎると思います ^^;)

# how (どのように？)

元画像のどのピクセルを使うかの座標計算で、Pillow-3.3.3 までは 0 (ピクセルの左or上隅)スタートだったのが、3.4.0 以降では 0.5 開始にしています。

| 3.3.3 以前 | 3.4.0 以降|
|---|---|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/a407727d-6903-992d-85c1-8cf8bf257005.png) |![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1798/49f64512-f141-ff6e-11a0-e32fd6fc8ff3.png)|

0.5 をピクセル中心とする場合、一つのグリッド 0〜0.999... を2分割すると 0〜4.999... と 5.0〜9.999... に分かれ、どちらかというと 0.5 は右下寄りになります。その違いが 3.4.0 での差異になります。
せめて 0.5 - ε にしてくれれば、ここまで劇的に変わらなかったものを。(ε 値をいくつにするか問題が出ててくるけど)

# Pillow 実装

素直な構造で、処理を追いやすいです。

- _image から Affin 指定で ImagingTransform
- Affin 指定なので ImagingTransformAffine
- 回転が無いので ImagingScaleAffine

```
% tar tvfz Pillow-3.3.3.tar.gz
% cd Pillow-3.3.3
```

 _imaging.c が起点で、具体的な処理は libImaging の内部にあります。

-  _imaging.c: _resize

```c

% grep ^_resize _imaging.c -A 32
_resize(ImagingObject* self, PyObject* args)
{
    Imaging imIn;
    Imaging imOut;

    int xsize, ysize;
    int filter = IMAGING_TRANSFORM_NEAREST;
    if (!PyArg_ParseTuple(args, "(ii)|i", &xsize, &ysize, &filter))
        return NULL;

    imIn = self->image;

    if (xsize < 1 || ysize < 1) {
        return ImagingError_ValueError("height and width must be > 0");
    }

    if (imIn->xsize == xsize && imIn->ysize == ysize) {
        imOut = ImagingCopy(imIn);
    }
    else if (filter == IMAGING_TRANSFORM_NEAREST) {
        double a[6];

        memset(a, 0, sizeof a);
        a[0] = (double) imIn->xsize / xsize;
        a[4] = (double) imIn->ysize / ysize;

        imOut = ImagingNew(imIn->mode, xsize, ysize);

        imOut = ImagingTransform(
            imOut, imIn, IMAGING_TRANSFORM_AFFINE,
            0, 0, xsize, ysize,
            a, filter, 1);
    }
```

- libImaging/Geometry.c: ImagingTransform

```c

% grep "^ImagingTransform(" libImaging/Geometry.c  -A 10
ImagingTransform(Imaging imOut, Imaging imIn, int method,
                 int x0, int y0, int x1, int y1,
                 double a[8], int filterid, int fill)
{
    ImagingTransformMap transform;

    switch(method) {
    case IMAGING_TRANSFORM_AFFINE:
        return ImagingTransformAffine(
            imOut, imIn, x0, y0, x1, y1, a, filterid, fill);
        break;
```

- libImaging/Geometry.c: ImagingTransformAffine

```
% grep ^ImagingTransformAffine libImaging/Geometry.c  -A 25
ImagingTransformAffine(Imaging imOut, Imaging imIn,
                       int x0, int y0, int x1, int y1,
                       double a[6], int filterid, int fill)
{
    /* affine transform, nearest neighbour resampling, floating point
       arithmetics*/

    ImagingSectionCookie cookie;
    int x, y;
    int xin, yin;
    int xsize, ysize;
    double xx, yy;
    double xo, yo;

    if (filterid || imIn->type == IMAGING_TYPE_SPECIAL) {
        return ImagingGenericTransform(
            imOut, imIn,
            x0, y0, x1, y1,
            affine_transform, a,
            filterid, fill);
    }

    if (a[1] == 0 && a[3] == 0)
        /* Scaling */
        return ImagingScaleAffine(imOut, imIn, x0, y0, x1, y1, a, fill);
```

- libImaging/Geometry.c:ImagingScaleAffine

```c

% grep ^ImagingScaleAffine libImaging/Geometry.c  -A 50
ImagingScaleAffine(Imaging imOut, Imaging imIn,
                   int x0, int y0, int x1, int y1,
                   double a[6], int fill)
{
    /* scale, nearest neighbour resampling */

    ImagingSectionCookie cookie;
    int x, y;
    int xin;
    double xo, yo;
    int xmin, xmax;
    int *xintab;

    if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0)
        return (Imaging) ImagingError_ModeError();

    ImagingCopyInfo(imOut, imIn);

    if (x0 < 0)
        x0 = 0;
    if (y0 < 0)
        y0 = 0;
    if (x1 > imOut->xsize)
        x1 = imOut->xsize;
    if (y1 > imOut->ysize)
        y1 = imOut->ysize;

    /* malloc check ok, uses calloc for overflow */
    xintab = (int*) calloc(imOut->xsize, sizeof(int));
    if (!xintab) {
        ImagingDelete(imOut);
        return (Imaging) ImagingError_MemoryError();
    }

    xo = a[2];
    yo = a[5];

    xmin = x1;
    xmax = x0;

    /* Pretabulate horizontal pixel positions */
    for (x = x0; x < x1; x++) {
        xin = COORD(xo);
        if (xin >= 0 && xin < (int) imIn->xsize) {
            xmax = x+1;
            if (x < xmin)
                xmin = x;
            xintab[x] = xin;
        }
        xo += a[0];
    }
```

この xo と yo の初期値が Pillow-3.4.0 で変わります。

```
% tar tvfz Pillow-3.4.0.tar.gz
% cd Pillow-3.4.0
```

```c
% grep ^ImagingScaleAffine libImaging/Geometry.c  -A 50 | grep "xo =" -A 4
    xo = a[2] + a[0] * 0.5;
    yo = a[5] + a[4] * 0.5;

    xmin = x1;
    xmax = x0;
```

# 最後に

resize デフォルトの NEAREST について状況を説明しましたが、BILINEAR, BICUBIC, LANCZOS 等も同様の理由により違いが生じます。ご注意下さい。


# 参考

- https://github.com/python-pillow/Pillow/
- https://pypi.org/project/Pillow/