画像の指定範囲を正方形に切り取るプログラムをpythonのOpenCVで作った

# はじめに

機械学習の中でも特に画像についてデータセットを自分で作ろうとすると**正方形**に画像を切り取ったものを大量に欲しくなりますよね。

例えばこのような画像から、目だったり、顔だったりを正方形に切り出そうとします。


![Lenna.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/9beda712-1a68-c074-123e-bbd3ebbd72f3.png)


今回のプログラムではマウスで選択することでこのように範囲が選択され、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/90b1d90b-ed9b-ea02-7e00-7181f0bd9193.png)


自動で保存されるというプログラムを作りました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/78b59d27-5229-08dd-786d-8861be3a268e.png)

大量に作業するように、
マウスだけで連続して一枚の画像から切り出し、
ボタン一つで次の画像に移れ、
ボタン一つでできるセーブ機能をつけました。


なお、以下のサイトのプログラムをもとに作成しました。


1から画像処理を学ぶ-指定範囲選択（python）

https://mask0-create.com/1%E3%81%8B%E3%82%89%E7%94%BB%E5%83%8F%E5%87%A6%E7%90%86%E3%82%92%E5%AD%A6%E3%81%B6-%E6%8C%87%E5%AE%9A%E7%AF%84%E5%9B%B2%E9%81%B8%E6%8A%9E%EF%BC%88python%EF%BC%89/




# コード
Githubにも置いておきます。

https://github.com/yuki-2000/face_cutter


```python
import sys
import cv2
import os.path
import glob
import os

 
# グローーバル変数
drawing = False
complete_region = False
ix,iy,width,height = -1,-1,0,0

 

 
 
# マウスコールバック関数(正方形)
def my_mouse_callback_square(event,x,y,flags,param):
    global ix,iy,width,height,box,drawing,complete_region
 
    if event == cv2.EVENT_MOUSEMOVE:      # マウスが動いた時
        if(drawing == True):
            width = x - ix
            height = y - iy



            #オリジナルから追加した分            
            length = max(abs(width), abs(height))
            
            if width>=0:
                width = length
            else:
                width = -length
                
            if height>=0:
                height = length
            else:
                height = -length
            
        
    elif event == cv2.EVENT_LBUTTONDOWN:  # マウス左押された時
        drawing = True
 
        ix = x
        iy = y
        width = 0
        height = 0
 
    elif event == cv2.EVENT_LBUTTONUP:    # マウス左離された時
        drawing = False
        complete_region = True
 
        if(width < 0):
            ix += width
            width *= -1
            
        if(height < 0):
           iy += height
           height *= -1
 

 






def main(filename):
    num = 0
    global ix,iy,width,height,drawing,complete_region
 
    source_window = "draw_rectangle"
 
    img = cv2.imread(filename)  # 画像の読み込み
    img_original = img.copy()   #切り取り用の画像
    temp = img.copy()                # 画像コピー
 
    cv2.namedWindow(source_window)
    cv2.setMouseCallback(source_window, my_mouse_callback_square)
 
    while(1):
        cv2.imshow(source_window,temp)
 
        if(drawing):             # 左クリック押されてたら
            temp = img.copy()    # 画像コピー
            cv2.rectangle(temp,(ix,iy),(ix + width, iy+ height),(0,255,0),2)  # 矩形を描画
        
        
        if(complete_region): # 矩形の選択が終了したら
            complete_region = False

            cv2.rectangle(img,(ix,iy),(ix + width, iy+ height),(0,0,255),2)
            cv2.rectangle(temp,(ix,iy),(ix + width, iy+ height),(0,0,255),2)

    
            if width==0:
                continue
 
            roi = img_original[iy:iy+height, ix:ix+width] # 元画像から選択範囲を切り取り
  
            #画像を保存
            output_path = os.path.join(output_dir, os.path.splitext(os.path.basename(filename))[0]+'_hand{0}.jpg'.format(num))
            cv2.imwrite(output_path,roi)
            num +=1
 
        # キー操作
        k = cv2.waitKey(1) & 0xFF
        if k == 27:          # esc押されたら終了
            cv2.destroyAllWindows()
            sys.exit()
            
        if k == 13:          # enter押されたら次へ
            break
        
        elif k ==ord('s'):   # 's'押されたら進行状況を保存
            print(filename)
            with open("save.txt", "w") as f:
                f.write(filename)
            

    cv2.destroyAllWindows()




#過去のセーブを読み込む
def open_save():
    if not os.path.exists("save.txt"):
        return False
    
    with open("save.txt", "r") as f:
        last_img = f.read()
    return last_img


#セーブ内容から始めるようにする
def list_posision(last_img, files):
    if last_img == False:
        return 0
    for i, file in enumerate(files):
        if file == last_img:
            return i




output_dir = "./hand_cut_images/"
imgs_dir = "./original_images/*"
files = glob.glob(imgs_dir)

del files[0:list_posision(open_save(), files)]
for filename in files:
    main(filename)
```






# 使い方

最後のほうの

```python
output_dir = "./hand_cut_images/"
imgs_dir = "./original_images/*"
```

を自分の環境に合わせてください。
この時、imgs_dirは`フォルダ名` + `/*` としてください。
`/*`とつけることで、フォルダ内のすべての画像を指定したことになります。


今回の入力画像があるフォルダ内はこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/f418dda5-bea8-b9a1-207b-31b4934ba0ce.png)




次にプログラムを実行すると、
このようにウィンドウが出てきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/50381e2c-56ac-c62d-4e52-27b0f0693194.png)


このウィンドウ内で切り取りたい部分をマウスで長押ししながら動かすと、このように緑の正方形の枠がマウスに合わせて変化します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/ad5c897a-39d4-4172-80f9-c113d06952ce.png)

ちょうどいいところでマウスを離すと、赤い枠となって指定範囲が保存されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/1b879f4a-7b12-367c-190b-89017bef1da0.png)

その後も同じように、赤い枠を無視して範囲を指定すると保存されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/4619cbff-10f5-8e6d-3741-61ad60440ded.png)


最終的にはこんな感じに（最も何を切り取っているかわかりませんが）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/47d3e34b-8cba-eeb6-74c8-8716f64bc55b.png)

出力フォルダにはこのようになっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/1c4f663b-565e-6791-2a6d-0635c71e84fa.png)


Enterを押すと次の画像に代わります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/0569eb42-d7bc-7b31-179d-76929550a77b.png)


進行状況を保存したいときはsを押してください。
あくまでどの画像から再開するかを保存できるだけで、どこを切り取ったかは保存していません。
save.txtという名前で保存されます。

ここで、保存後、その画像を削除してしまうとプログラムが動かなくなるので、どのときはsave.txtを削除して下さい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/f9db4883-83f9-09fb-1fa6-238cabbd3baa.png)


画像が最後まで行くか、escを押すと、プログラムが中断されます。













# 元のプログラムからの変更点


## 正方形
元のプログラムでは、画像を矩形で範囲選択して切り取るというプログラムでした。
内容的には、マウスを押した際の座標を記録し、マウスを動かした際の座標との変分を`width`,`height`として範囲を指定していました。
今回は正方形にするために、`width`と`height`の絶対値の大きいほうを`length`として`width`,`height`の代わりに使用するようにしました。

```python
            #オリジナルから追加した分            
            length = max(abs(width), abs(height))
            
            if width>=0:
                width = length
            else:
                width = -length
                
            if height>=0:
                height = length
            else:
                height = -length
```







## クリック問題
指定範囲を保存する際にマウスを押した点と離した点が同じだと、範囲ではなく点となってしまい、エラーとなっていしまう問題がありました。
そこで、lengthが0の時は保存しないようにしました。
プログラム内は`width`で判断していますが、`width`に`length`を代入しているので同じものと思ってください。

```python
        if(complete_region): # 矩形の選択が終了したら
            complete_region = False

            cv2.rectangle(img,(ix,iy),(ix + width, iy+ height),(0,0,255),2)
            cv2.rectangle(temp,(ix,iy),(ix + width, iy+ height),(0,0,255),2)


            if width==0:
                continue
```


## 連続切り取り
1枚のプログラムから何枚も切り取れるように、sを押したら保存ではなく選択範囲を指定したら自動で保存に切り替えました。

```python        
        if(complete_region): # 矩形の選択が終了したら
            complete_region = False

            cv2.rectangle(img,(ix,iy),(ix + width, iy+ height),(0,0,255),2)
            cv2.rectangle(temp,(ix,iy),(ix + width, iy+ height),(0,0,255),2)

    
            if width==0:
                continue
 
            roi = img_original[iy:iy+height, ix:ix+width] # 元画像から選択範囲を切り取り
  
            #画像を保存
            output_path = os.path.join(output_dir, os.path.splitext(os.path.basename(filename))[0]+'_hand{0}.jpg'.format(num))
            cv2.imwrite(output_path,roi)
            num +=1
```




## 切り取り箇所の表示
一枚の画像の中から何枚も切り抜いていると、どこを切り取ってどこを切り取っていないかわからなくなります。
なのですでに切り取ったところを赤の枠線で囲うようにしました。
もちろん切り取り時にこの枠線は入らないようにしています。
そのために、切り取り用、画像更新用、背景用の3つの画像を内部で用意しています。

```python 
    img = cv2.imread(filename)  # 画像の読み込み
    img_original = img.copy()   #切り取り用の画像
    temp = img.copy()                # 画像コピー
```


## 大量画像の処理
元のプログラムでは、プログラム一回で1枚の画像からしか切り抜きできませんでした。
このプログラムでは、まず画像パスをすべて取得し、その後forで渡していく方法に変更しました。

```python
output_dir = "./hand_cut_images/"
imgs_dir = "./painted_images/*"
files = glob.glob(imgs_dir)

del files[0:list_posision(open_save(), files)]
for filename in files:
    main(filename)
```



## セーブ機能
画像を複数扱おうとするときにforで順番に送っているため、途中でプログラムを中断してしまうとまた初めからになってしまいます。
そこでsボタンを押すことで現在の画像パスをtxtファイルに保存し、次回実行時にそこから読み取って途中から再開できるようにしました。
プログラムとしては画像のパスのリストのうち、保存されたパスの1つ前までを削除することで途中から再開できるようにしています。

```python
#過去のセーブを読み込む
def open_save():
    if not os.path.exists("save.txt"):
        return False
    
    with open("save.txt", "r") as f:
        last_img = f.read()
    return last_img


#セーブ内容から始めるようにする
def list_posision(last_img, files):
    if last_img == False:
        return 0
    for i, file in enumerate(files):
        if file == last_img:
            return i




output_dir = "./hand_cut_images/"
imgs_dir = "./original_images/*"
files = glob.glob(imgs_dir)

del files[0:list_posision(open_save(), files)]
for filename in files:
    main(filename)
```

## 保存ファイル名
元のファイル名 + _hand + 連番
となっています。
変えたい人は自分で変えてください。

```python
            #画像を保存
            output_path = os.path.join(output_dir, os.path.splitext(os.path.basename(filename))[0]+'_hand{0}.jpg'.format(num))
            cv2.imwrite(output_path,roi)
            num +=1
```









# まとめ
機械学習の画像のデータセットを自分で作りたいときに、正方形に簡単に切り取れるソフトがぱっとなかったので、既存のプログラムを改造して作りました。
正方形にさえ切り取ってしまえば、あとはリサイズして64*64にしたりできるので、データセットが自作できるようになります。

データセットの作り方や、リサイズの仕方は以下の記事をご覧ください。

https://qiita.com/yuki_2020/items/551e4c0cef20b01e7600