pythonでundo機能を実装してみた。

##はじめに

undo・・・直前の操作を取り消すこと （パワポのctr＋Z）
redo・・・undoで一旦取り消した操作をやり直すこと（パワポのctr＋Shift＋Z）

画像処理とする際undoとredoがあると便利だなと思い、
自分なりにpythonで実装してみました。

##undo redo 実装の説明
undo, redo用のデータを保存するリストをundo_list, redo_listとします。

画像処理をする際に、処理前のデータを都度undo_listに追加することで,後から復元できるようにしていきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997363/835ae5f3-7d87-ba26-c20d-50c008d4e5b6.png)

undo操作の際は、以下の操作を行います
・現在の画像をredo_listに追加する。
・undo_listの末尾のデータを現在の画像と置き換える。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997363/cdd93f56-9c99-fcb8-cb21-296b06139a71.png)


redo操作の際はundo操作の逆を行います。
・現在の画像をundo_listに追加する。
・redo_listの画像を、現在の画像と置き換える。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997363/3ee478e4-0646-ce7d-6533-8c2b1bf0a26b.png)

##実際の実装
以上のことを踏まえて、以下のように実装してみました。

```py
class Image(object):
    def __init__(self,data):
        self.data=data
        self.shape=data.shape
        self.undos=[] #undo_list
        self.redos=[] #redo_list

    def __set_undo(self):
        self.undos.append(self.data.copy()) #undo_listの末尾に今のデータを追加    
        if len(self.undos)>5: 
            self.undos.pop(0) #undo listの先頭の値を削除する。
            #undo_listにデータが蓄積されることを防ぐために、
            #5つ以上の数のデータが追加された場合は、リストの先頭のデータを削除。
            
    def __set_redo(self):
        self.redos.append(self.data.copy()) #redo_listの末尾に今のデータを追加 
        if len(self.undos)>5:
            self.redos.pop(0) #redo listの先頭の値を削除する。

    def undo(self):
        if not self.undos: #undo listが空の場合         
            print('ERROR: No undo data')
            sys.exit() 
        self.__set_redo()
        self.data=self.undos.pop(-1) #undo listから一番最後の値を取得し、削除する。

    def redo(self):
        if not self.redos: #redo listが空の場合  
            print('ERROR: No redo data')
            sys.exit() 
        self.__set_undo()
        self.data=self.redos.pop(-1) #redo listから一番最後の値を取得し、削除する。

    #適当な処理
    def add_noise(self,var=30):
        self.__set_undo()
        noise=np.random.normal(scale=var, size=self.shape)
        self.data+=noise
        
    def rot(self, k):
        self.__set_undo()
        self.data=np.rot90(self.data, k=k)
        
    def plot(self):
        plt.figure()
        plt.imshow(self.data,  cmap='gray')


if __name__=='__main__':
    data=misc.ascent().astype(np.float32) #　階段の画像の取得
    img=Image(data)        
```

適当な処理として、ガウシアンノイズを加える操作と、回転操作の関数を加えました。
各処理の前にself.__set_undo()を行うことで、処理前の画像をundo_listに加えています。

画像処理をしていきます。

```py
    img.plot()     #最初の画像の表示
    img.add_noise()
    img.plot()
    img.rot(k=1)
    img.plot()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997363/2482cf92-d607-6432-1b1c-a196c7f66908.png)

ここからundoしていきます。

```py
    img.undo()
    img.plot()
    img.undo()
    img.plot()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997363/16949dd4-f5b2-4967-0fc0-164bd8d8ec2e.png)

ここでもう一回`img.undo()`をするとundo listが空のためエラーが返されます。

```
ERROR: No undo data
An exception has occurred, use %tb to see the full traceback.

SystemExit
```

今度はredoしていきます

```
    img.redo()
    img.plot()
    img.redo()
    img.plot()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997363/1dbf9dd2-ea91-664d-4f3c-db0261d04c71.png)

##まとめ
自分の勉強がてら、undo機能を作ってみました。
なにかありましたらコメントいただけると幸いです。