94788
# 環境

  * Python 3.11.2
  * numpy 1.24.2

# やりたいこと

画像をPillowなどで読み込み、bool配列を使って、特定のピクセルを白色（`[255,255,255]`）にしたいです。

# 起きたこと

マスク用の配列の`dtype`を`bool`ではなく、間違えて`numpy.uint8`にしてしまいました。  
そのマスク用の配列を利用して、特定のピクセルを白色に設定しようとしたら、処理時間が1分以上かかりました。

```

    In [131]: height=1080; width=1920; channel=3
    
    # Pillowなどで画像を読み込むことを想定
    In [132]: data=numpy.random.randint(256, size=(height,width,channel))
    
    In [133]: correct_mask = numpy.zeros((height, width), dtype=bool)
    
    In [134]: %time data[correct_mask] = [255,255,255]
    CPU times: user 773 µs, sys: 0 ns, total: 773 µs
    Wall time: 526 µs
    
    
    # マスク用の配列の型を間違えて`numpy.uint8`にしてしまった
    In [135]: wrong_mask = numpy.zeros((height, width), dtype=numpy.uint8)
    
    # 処理に時間がかかる。なぜ？
    In [136]: %time data[wrong_mask] = [255,255,255]
    CPU times: user 2min 17s, sys: 0 ns, total: 2min 17s
    Wall time: 2min 17s
    
    
    
```

# 質問

なぜ`dtype`が`numpy.uint8`の配列をインデックスに利用すると、`dtype`が`bool`の配列を利用するときと比較して、処理時間が長くなるのでしょうか  
また、`dtype`が`numpy.uint8`の配列をインデックスに利用すると、どのような処理になるのでしょうか？
CC BY-SA 4.0
2023-05-08T02:00:57.640
94779
2023-08-28T06:13:53.740
19524
question
python numpy
`dtype`が`numpy.uint8`の配列をnumpy.arrayのインデックスに利用すると処理時間が長くなってしまいます。なぜでしょうか？
> また、dtypeがnumpy.uint8の配列をインデックスに利用すると、どのような処理になるのでしょうか？

`Y[i]`とすれば`i`番目の要素が取れます。  
`Y[[i]]`とインデックスをリストにすると、  
最初の要素が`Y[i]`であるndarrayが取得できます。すなわち`[Y[i]]`  
このような感じで、`Y[X]`とすると、(Xは整数配列)  
Xの各要素x_iをY[x_i]に置き換えたndarrayが取得できます。  
例えば`Y[ [[0,1],[2,3]] ]`は`[[Y[0],Y[1]],[Y[2],Y[3]]]`

これを踏まえて質問文の`data[wrong_mask]`を考えると、  
`wrong_mask`の各要素は全部0ですので、  
これは`wrong_mask`の各要素を`data[0]`で置き換えたものとなります。

`shape`は  
`wrong_mask`が`(1080, 1920)`  
`data[0]`が`(1920, 3)`ですから、

結果`data[wrong_mask]`は`(1080, 1920, 1920, 3)`になってしまいます。

[Indexing on ndarrays — NumPy v1.24
Manual](https://numpy.org/doc/stable/user/basics.indexing.html#integer-array-
indexing)
CC BY-SA 4.0
2023-05-09T06:12:11.583
94788
2023-05-09T06:12:11.583
13127
94779
answer
#### ブール配列のインデックス付け

ブール配列の使用例

```

    data[data[:, :, 1] == 0] = {255, 255, 255}   # RGBの緑の成分が無ければ白へ
    data[data.sum(-1) < 32] = {255, 255, 255}   # RGB合計値が 32 未満(暗め) なら白へ
    
```

形状(`.shape`)の詳細を見てみると

```

    >>> import numpy as np
    >>> height, width, channel = 1080, 1920, 3
    >>> data = np.random.randint(256, size=(height, width, channel), dtype=np.uint8)
    >>> data.shape
    (1080, 1920, 3)
    >>> filt = data[:, :, 1] == 0
    >>> filt.shape, filt.dtype
    ((1080, 1920), dtype('bool'))
    
```

`data[filt]` の記述では, `filt` の次元の数は `data` より少ないので, `data[filt, ...]` と解釈される  
上記コードの場合, `data[filt, :]` ということになる(次元が一つ少ないため)

結果の形状については, `ヒットした件数 * 残りの次元` の形状(`.shape`) になり

```

    >>> filt.sum()
    8100
    >>> data[filt, :].shape
    (8100, 3)
    
```

#### 整数配列のインデックス付け

演算では, ブール配列は整数配列と同様に演算可能 (上記の `filt.sum()` のように) だが,  
indexing(インデックス付け) においては ブール配列と整数配列はまったくの別物

整数, 整数配列, スライスなどの使用例

```

    >>> data[100, 160]   # 座標指定
    array([167, 167, 181], dtype=uint8)
    >>> data[100, 160, 1]   # 座標と 緑のチャンネル
    167
    >>> data[0].shape   # 最初の一つの次元なら (このような指定の形状に意味があるとは思えないが)
    (1920, 3)
    
```

仮に `(2,3,4)` の多次元配列での indexingなら, 残りの次元と合わせて 以下のような結果になる

```

    >>> nums = np.zeros((2, 3, 4), dtype=int)
    >>> data[nums, :, :].shape   # data[nums] は残りの次元から data[nums, :, :] と同じ意味
    (2, 3, 4, 1920, 3)
    
```

それぞれの `.shape` について, 少しまとめ

  1. ブール配列の形状(`.shape`) は, もとの配列の (先頭に指定した場合)先頭から (途中または最後まで)合致している必要があり, `.ndim` の分の次元を指定したことになる
  2. 整数配列の形状(`.shape`) は, **結果の形状** にのみ影響を与える

* * *

### 処理が遅い理由

処理が遅い理由は, 上記とはまた(少し)別のところにある

`data[0]`, `data[3:7]` などの指定はビューが返され  
`data[[0]]` などの高度なインデックス付け [advanced
indexing](https://numpy.org/doc/stable/user/basics.indexing.html#advanced-
indexing) はコピーが返される

```

    >>> data[[0]].base is None   # 「コピー」が返される
    True
    
```

`data[[0, 0, 0, 0] + [1]*100]` のような整数配列では, `[0]` が `4`つ, `[1]` が `100` の分
内容が繰り返された ndarray が返される (ビューでは返しようがない)  
代入であっても似たようなもので, `[0]` へ `4`回, `[1]` へ `100`回, 代入することになる

質問のコードで, 仮に `data[[0]]` の代入に 50マイクロ秒の時間がかかると仮定すると,  
単純計算で, トータル `50μsec * 1000 * 2000` でほぼ 100秒の処理時間になる

それほど単純でないにしても, それに近い倍率で行われると予測できる  
以下は Colabでの実行時間

```

    nums = np.zeros((height, width), dtype=np.uint8)
    %timeit data[[0]] = (255,255,255)
    # 20 µs ± 550 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
    
    %timeit data[nums] = (255,255,255)
    # 38.6 s ± 807 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    
```

参考

  * [NumPy quickstart](https://numpy.org/doc/stable/user/quickstart.html)
  * [Indexing on ndarrays](https://numpy.org/doc/stable/user/basics.indexing.html)
CC BY-SA 4.0
2023-08-26T07:18:56.943
96048
2023-08-28T06:13:53.740
2023-08-28T06:13:53.740
43025
43025
94779
answer