openCVのimshowをちょっとだけ使いやすくする

# imshow使いにくい
　OpenCVのimshowは、おまけ機能みたいなものなので、大変使いにくいです。
でも、imshowを使う理由はせいぜい確認程度だから、わざわざ別の物を使うのは面倒です

## そんなわけで
有る程度簡単に使えて、確認程度ならそれなりに機能するものを作りました

# ソースコード
```python:catalog.py
import numpy as np, cv2, math

class Catalog:
    def __init__(self, width, height, title = 'image',reduce = True):
        self._width = width
        self._height = height
        self._v = np.zeros((height,width,3),dtype = np.uint8)
        self._title = title
        self._th = []
        self._func = []
        self._images = 0
        self._reduce = reduce
    
    def show(self,time):
        cv2.imshow(self._title, self._v)
        key =cv2.waitKey(int(time*1000))

    def close(self):
        cv2.destroyAllWindows()

    def add(self,img,func=None,args=None):
        if img.dtype == np.float32:
            img = np.clip(img * 255, a_min = 0, a_max = 255).astype(np.uint8)
        self._th += [img.copy()]
        self._func += [(func,args)]
        self._images += 1
 
        y_split = math.ceil(math.sqrt(self._images))
        x_split = round(math.sqrt(self._images))
 
        if self._width > self._height:
            x_split, y_split = y_split,x_split
        pos_x, pos_y = 0, 0
        grd_x, grd_y = int(self._width / x_split), int(self._height/ y_split)
        self._v.fill(0)

        for i in range(self._images):
            h, w = self._th[i].shape[:2]
            if self._reduce:
                aspect = w / h
                if grd_x / grd_y >= aspect:
                    nh = grd_y
                    rpc_x, rpc_y = round(nh * aspect), int(nh)
                else:
                    nw = grd_x
                    rpc_x, rpc_y = int(nw), round(nw / aspect)
                ofs_x, ofs_y = int((grd_x-rpc_x)/2), int((grd_y-rpc_y)/2)
                rimg = cv2.resize(self._th[i], dsize= (rpc_x, rpc_y))
                simg = np.zeros((grd_y,grd_x,3),dtype = np.uint8)
                simg[ofs_y:ofs_y+rpc_y, ofs_x:ofs_x+rpc_x,:] = rimg
            else:
                cpt_x, cpt_y = int(w / 2 - grd_x / 2), int(h / 2 - grd_y / 2)
                ofs_x = ofs_y = 0
                if grd_x > w:
                    ofs_x = -cpt_x
                    cpt_x = 0
                if grd_y > h:
                    ofs_y = -cpt_y
                    cpt_y = 0
                simg = np.zeros((grd_y,grd_x,3),dtype = np.uint8)
                simg[ofs_y:grd_y-ofs_y,
                     ofs_x:grd_x-ofs_x,:] = self._th[i][cpt_y:cpt_y+grd_y,
                                                        cpt_x:cpt_x+grd_x,:]
            if self._func[i][0] != None:
                simg = self._func[i][0](simg,*self._func[i][1])

            h, w = simg.shape[:2]
            self._v[pos_y:pos_y + h, pos_x:pos_x + w,:]= simg
            if i % x_split == x_split - 1:
                pos_x = 0
                pos_y += h
            else:
                pos_x += w
    
    def pop(self):
        if self._images > 0:
            self._th.pop(0)
            self._func.pop(0)
            self._images -= 1

# --以下サンプルコード
def posterization(img: np.ndarray,color: int) -> np.ndarray:
    tbf = np.array([int(
        int(float(color+1)/256.0*i)*255.0/float(color))
        for i in range(256)],dtype=np.uint8)
    return cv2.LUT(img, tbf)

def write_text(img,text,color):
    StartPoint = (10,50)
    Font = cv2.FONT_HERSHEY_COMPLEX
    FontScale = 1
    thickness = 2
    return cv2.putText(img,text,StartPoint,Font,FontScale,color,thickness)

def main():
    img = cv2.imread('./lenna.bmp')
    catalog = Catalog(1024,768,title ='Posterization',reduce = True)
    for i in range(1,65,1):
        catalog.add(posterization(img,i),write_text,(str(i),(255,255,255),))
        catalog.show(0)
    catalog.close()

if __name__ == '__main__':
    main()
```

## 使い方
`cat = Catalog(横サイズ,縦サイズ,title=タイトル,reduce=bool値)`
でクラス定義します。
サイズはウィンドウのサイズで、titleはウィンドウタイトル,reduceをTrueにすると縮小表示します。Falseの場合等倍で中心部分を切り抜きます。titleを省略したときは'image'、reduceを省略したときはTrueです

`cat.add(img,func=追加の表示関数,args=追加関数の引数)`
で画像を追加していきます。画像は整列されて、縮小されます
追加の関数がある場合、配置される画像のを引数とした
`func(img, *args)`
で呼び出されるので、追加の描画処理をして、imgを返してください。サンプルでは文字を書き込んでいます
画像に追加が必要ない場合は
`cat.add(img)`
だけで構いません

`cat.show(秒数)`で表示処理を行います
`cat.pop()`で、先頭の画像を削除します
`cat.close()`でウィンドウを閉じます

## サンプル実行結果
![2023-09-02 03_40_33-Posterization.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570715/826f580a-39b9-762d-d85f-c80ffa388483.png)
サンプルはポスタリゼーションを実行して、その結果を表示しています
キーを押すたびに追加され、1～64までの結果を縮小表示します

余談ですが、Pythonでのポスタリゼーションでは、ビンを使用した物が多いですが、ビンを使用せずにルックアップテーブルを一行で作っています。がんばった(笑)


# 注意
`cat.add()`する度に再計算しているため、追加した画像がそのままのサイズで保存されています。あまり数が多いとメモリも圧迫するので、表示数が多い場合は気をつけてください

画面の分割は縦と横が交互に行うため、横長のウインドウとか縦長のウインドウだと、あまりよろしくありません
```python
        y_split = math.ceil(math.sqrt(self._images))
        x_split = round(math.sqrt(self._images))
         if self._width > self._height:
            x_split, y_split = y_split,x_split
```
この部分で分割数と縦と横どちらを先に分割するかを決めているので、ここを書き直せばなんとかなるはずですが、どういう式を使えばいいかちょっとわかりませんでした。
ここを適切な分割数にする式にすれば、他は書き換えなくても動作するはずです