개발로 하는 개발

[졸업프로젝트] UI를 이용한 Image Completion 모델과의 협업 구현하기 본문

Projects/Capstone

[졸업프로젝트] UI를 이용한 Image Completion 모델과의 협업 구현하기

jiwon152 2024. 5. 9. 13:38

https://github.com/JiWon0502/StrokeCollaborativeDrawing.git

-  졸업 프로젝트를 위한 보고서의 일환으로 작성된 글입니다.

 연구 결과를 시험하기 위한 Guideline 위주로 본 " AI와 인간의 그림 그리기 "

목차
0. 연구 주제에 대해서
1. Lmser pix2seq 모델 실행
2. 졸업 프로젝트 데모 실행
3. UI 설계

 

 


 

0. 연구 주제에 대해서 

 해당 졸업 프로젝트의 연구 주제는

Stroke-based Collaborative Drawing between AI and Human

 

으로, 획 기반 그림을 사람과 인공지능이 번갈아가면서 그려서 완성하도록 하는 것입니다.

 

해당 연구의 주 목표는

- Image completion 모델을 이용하여 해당 모델이 중간 중간 사람의 입력을 받는 협업 과정을 통해 완성되는 그림의 수준 개선

- Stroke Ordering 알고리즘을 개발하여, 인공지능 모델이 그리는 그림을 human-like하게 개선

이렇게 두 가지 입니다.

 

 이번 학기에는 UI를 구현하고, 모델들의 연결을 총괄하면서, 인간과 인공지능의 협업에 초점을 맞추어 개발을 진행했습니다.

 

 연구의 pipeline은 다음과 같습니다. 우선, User가 Interface를 통해 획을 입력합니다. 자동으로 입력된 획 data에 대한 전처리를 거치게 되고, Image Completion 모델로 data가 전달됩니다. Image Completion과 Stroke Ordering이 합쳐진 모델은 기존 Lmser-pix2seq의 pretrained model 그대로 사용하되, 해당 모델의 결과 가장 가능성이 높은 획만을 선택하여 반환합니다. 반환된 획을 반영한 모습을 확인한 사용자는 그림에 획을 추가합니다. 이 과정을 계속 반복하여 그림을 완성합니다.

 

 

 

 이를 위해 저희가 사용한 baseline은 Lmser pix2seq 모델로, 별도의 finetuning 없이 기존의 모델을 그대로 사용합니다. 다만, Apple Silicon을 사용하는 맥북에서는 cuda가 지원 안되는 부분이 있고, Inference만 사용할 것이기 때문에 gpu를 사용하지 않도록 수정하여 사용했습니다. 이 글에서는 해당 모델을 어떤 식으로 수정하였는지, 그리고 완성된 데모에 대한 간단한 설명과 실행 방법, 그리고 UI 설계까지 설명하려고 합니다.


 

1. Lmser pix2seq 모델 실행

 우선 사용을 위해서는 python 환경을 만들어주어야 합니다. 서로 다른 Python 버전과 모듈로 인한 충돌을 막기 위해서 가상환경(virtual environment)를 사용하여 코드를 실행합니다. 해당 환경은 졸업프로젝트 데모 소스코드의 conda_requirements.yaml을 다운로드 받아서 설치할 것입니다. 파일을 다운로드 받은 뒤, 열어서 본인의 conda 환경이 있는 path로 prefix를 수정해 줍니다.

# 현재 코드
prefix: /Users/userID/opt/anaconda3/envs/vLmser
# 수정
prefix: /path/to/new/prefix

 

또한, 만약 가상환경의 이름을 변경하고 싶다면, name도 변경해 주면 됩니다. 저는 vSCD로 변경해서 진행하도록 하겠습니다. 

# 터미널을 열어 yaml 파일이 있는 경로로 이동
cd "/Path/to/your/directory/where/yaml/is"
# 가상 환경 생성
conda env create -f conda_requirements.yaml

수정한 yaml 파일 내용 / 생성 완료 후 터미널 내용

 터미널에서 설치를 완료하면 위와 같은 화면이 뜨는데, 여기서 이 가상환경을 활성화하기 위해서 다음 코드를 입력해줍니다. 그러면 현재 터미널 명령어 앞에 (vSCD)가 붙어 있는 것을 볼 수 있습니다. 

conda activate vSCD
# 비활성화를 위해서는 
# conda deactivate

 

 이제 Lmser-pix2seq-main으로 넘어가 해당 모델을 어떤 식으로 수정해서 사용했는지부터 설명을 드리려고 합니다.  https://github.com/CMACH508/Lmser-pix2seq 에서 코드를 clone 합니다. zip file로 다운로드 받아도 되고, 아니면 github desktop을 이용해 clone해도 됩니다. 저는 재현을 위해 다시 다운로드 받는 것이라 그냥 zip file로 다운로드를 진행했고, 다운로드를 받으면 "Lmser-pix2seq-main.zip"이라는 이름으로 저장됩니다. seed.npy 파일을 다운로드 받아, utils 폴더 내부에 넣어줍니다.

zip file과 내부 소스코드 및 디렉토리들

 

  이제 해당 코드를 돌려보려 하는데, 우선 그냥 돌려보겠습니다. 물론 지금 현재 터미널에는 (vSCD)가 보여야 합니다.

# lmser pix2seq 코드가 있는 곳으로 디렉토리 변경
cd "path/to/Lmserpix2seq/folder"
# inference.py 파일을 Python으로 실행
python inference.py

 

 다음과 같은 에러가 뜨게 되는데, Apple Silicon에서 nvidia에 최적화 된 cuda를 사용되지 않아 발생하는 오류입니다. 현재 cuda에서 "mps"라는 이름을 통해 지원을 하고는 있으나, 여전히 오류가 많이 발생하므로 사용을 하지 않기로 했습니다.

Traceback (most recent call last):
    model.load(f"./model_save/encoderRNN_epoch_150000.pth",
    saved_encoder = torch.load(encoder_name)
    return _load(opened_zipfile,
    result = unpickler.load()
    typed_storage = load_tensor(dtype, nbytes, key, _maybe_decode_ascii(location))
    wrap_storage=restore_location(storage, location),
    result = fn(storage, location)
    device = validate_cuda_device(location)
	raise RuntimeError('Attempting to deserialize object on a CUDA '
RuntimeError: Attempting to deserialize object on a CUDA device but torch.cuda.is_available() is False. If you are running on a CPU-only machine, please use torch.load with map_location=torch.device('cpu') to map your storages to the CPU.

 

 그렇다면 이 오류를 어떻게 해결할 수 있을까요? cpu에 모델을 올려서 사용하면 됩니다. 따라서 device를 cpu로 지정하고, 옛날에 사용하던 방식인 .cuda() 대신, 올리는 위치를 지정할 수 있는 .to(device)로 변경해 주었습니다. hyper_params.py의 self.use_cuda와 같은 경우엔, 해당 논문에서 cuda없이 돌리는 경우를 가정하지 않고 작성해서, 해당 논문의 encoder를 사용하는 코드가 없어서 True로 지정해주었습니다.

hyper_params.py
self.use_cuda = torch.cuda.is_available()
-> self.use_cuda = True


Lmser.py
# in line 15, add
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
# find and replace all
.cuda() -> .to(device)

inference.py
# in line 17, add
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
# find and replace all
.cuda() -> .to(device)
# find and replace
saved_encoder = torch.load(encoder_name)
saved_decoder = torch.load(decoder_name)
-> saved_encoder = torch.load(encoder_name, map_location=torch.device('cpu'))
-> saved_decoder = torch.load(decoder_name, map_location=torch.device('cpu'))

 

 전부 수정하고 다시 돌려보면 다음과 같은 에러가 나옵니다.

AttributeError: module 'numpy' has no attribute 'float'.
`np.float` was a deprecated alias for the builtin `float`. To avoid this error in existing code, use `float` by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, use `np.float64` here.
The aliases was originally deprecated in NumPy 1.20; for more details and guidance see the original release note at:
    https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations

 

 해당 오류는 np.float를 지원하지 않는다는 것이므로 아래 소스코드 파일의 np.float를 np.float32로 변환해주고 다시 돌려봅니다. 

utils/inference_sketch_processing.py
# `np.float` was a deprecated alias for the builtin `float`
np.float -> np.float32

 

가운데의 UserWarning의 경우 아직까지는 오류가 아니므로, 넘어가도 괜찮습니다.

 

 이렇게 drawing airplane 1, 2,... 가 나오기 시작했다는 것은...! Lmser pix2seq 모델을 돌리는데 성공했습니다!


 

2. 졸업 프로젝트 데모 실행

 그렇다면 이제 저희 모델을 실행하는 방법을 알아보겠습니다. 간단해요!

 git 압축 파일 링크를 통해 zip 파일을 다운로드 받고, 압축을 풀어주면 StrokeCollaborativeDrawing-main 이라는 폴더가 나옵니다. 앞에서처럼 seed.npy 파일을 다운로드 받아, 메인 폴더 아래 lmser/utils 폴더 내부에 넣어줍니다. 해당 폴더의 경로에서 터미널을 열어, conda 환경을 활성화 해줍니다.

conda activate vSCD
# 비활성화를 위해서는 
# conda deactivate

(base)라고 적혀있던 메인 환경 대신 (vSCD)라는 가상환경이 작동된 것을 확인할 수 있습니다.

 터미널에 이 명령어를 입력하면, 데모 UI 창이 열리게 됩니다.

python demoUI.py

 

Demo 용 그래픽 유저 인터페이스 창

 

 이제 Drawing with Mouse라고 적혀 있는 부분에 그림을 그릴 수 있습니다. 사용자가 그림을 그리고, Save Deltas 버튼을 누르면 모든 획이 저장되어 npy 형태의 파일로 저장됩니다. Next Step 버튼을 누르면, 사용자가 그린 그림이 RDP algorithm을 통해서 선이 간단해집니다. 다만 이 과정에서도, 원형은 보존됩니다. 그리고 다시 Next Step 버튼을 누르면, AI 가 그림을 이어서 그립니다. 그려진 그림에 획을 추가해서 그리고 싶다면 다시 Next Step을 눌러줍니다. 그리고 그림을 계속 이어서 그리는 과정을 반복해주면 됩니다. 

 

[ demo 예시 ]

사용자가 그린 그림이 Drawing with Mouse이다. / 전처리 후 그림이 Drawing after RDP에 나와있다.
AI가 전처리 된 데이터를 받아서 추가로 획을 그린다. / 이후 사람이 다시 AI가 그린 그림에 획을 추가한다.

 


 

3. UI 설계

 처음에는 UI 부분을 맡아서 진행할 생각은 아니었어요. 가장 신경 쓸 내용은 자잘하게 많은데 어려운 부분이 없을 것 같아 흥미롭지 않았기 때문입니다... 하지만 다들 본인이 AI 파트를 맡고 싶어 하였고, 저도 방학기간 동안 바쁠 예정이였어서 UI를 맡게 되었습니다. 따라서 저는 AI의 설계에는 관여하지 않았으며, 기존의 모델을 돌리는 과정과, 코드 작성 시 어려움을 느끼는 부분을 의사코드를 통해 구현 방법을 설명하는 정도로만 도움을 주었어요. 그래서 UI에 대해서만 설명을 조금 해 볼까 합니다.

 

 UI를 무에서부터 설계하는 것은 처음이었기 때문에 우선 어디서부터 시작해야 할지 전혀 감이 잡히지 않아서 어떤 모듈을 사용할지, 그림을 어떻게 그릴지를 먼저 생각해야 했습니다. 그래서 처음에는 화상캠을 통해 사람이 든 펜을 트래킹하고, 사람이 보이는 카메라 화면 상에 그림을 그리는 방법을 생각했습니다. 하지만 펜의 색에 따라 추적되는 방법이어서 빛에 따라 성능의 차이가 너무 났어요. 그래서 기존 quickdraw dataset이 만들어지는 과정과 어느정도 비슷하게 마우스로 그림을 그리는 방식으로 변경했습니다.

 

 그 이후에는 필요한 기능을 나열하고 각자 구현해서 검증을 먼저 했어요. Npz와 Npy 형식을 서로 바꿀 수 있어야 하기 때문에 npz2npy, npy2npz 함수를, 마우스 변화값에 따라 deltaX, deltaY, penstate를 받을 수 있어야 하므로 draw_deltas, 그런데 중간에 coordinate으로도 데이터가 필요할 거 같다는 주장에 draw_coords를 구현했습니다. 또한 데이터 전처리를 하기 위한 rdp 알고리즘 모듈은 옛날에 구현된 것이라 파이썬 최신 버전을 지원하지 않으므로 다시 구현해야 했고, 존재하는 데이터를 다시 그림으로 옮기는 함수도 필요하기 때문에 reconstruct_deltas, reconstruct_coords 함수를 구현했습니다. 그리고 input과 output 데이터 형식이 어떻게 생겼는지 이해가 안된다고 하여 view_npy 함수를 만들었습니다. 이 함수들은 전부 CapUI/tmp/ 내부에 참고용으로 넣어놨습니다.

 

 이제 이것들을 합치기 위해서 class를 사용하고자 했고, 마우스로 그림을 그리는 것이 이 UI의 주 목적이기 때문에 MousePainter class를 구현하기로 했습니다. 필요한 기능들 중 직접적으로 그림과 관련된 것들은 MousePainter 클래스 내부에 포함했어요. 그래서 아래의 의사 코드에는 나타나지 않지만, reconstruct, draw와 같은 함수들과 추가로 tkinter를 이용한 canvas를 해당 클래스 내부에서 작동하도록 개발했습니다. 해당 클래스 내부에 frame을 사용해서 GUI를 직접적으로 구현했고, 그 외에 자잘한 데이터 처리 함수들은 utils의 misc.py에 넣었어요. rdp 알고리즘도 misc.py에서 import한 뒤 작동하도록 설계했습니다.

 

 그 이후에 해당 클래스와 함께, 외부의 ai 모델의 메소드와도 서로 상호작용을 할 수 있는 메인 함수 역할이 필요했습니다. 그래서 demoUI.py를 통해서 각 단계마다 필요한 메소드를 실행할 수 있도록 구현해 주었습니다. 

 

 다음은 전반적인 코드 설명입니다.

 

1. demoUI.py

: 메인 함수로, painter 객체와 파일들을 접근하고 ai 모델의 메소드를 부르는 역할입니다.

if __name__ == "__main__":
    painter = MP.MousePainter(args)  # MousePainter 클래스의 객체 선언
    while not painter.exit:
        painter.run()
        # self.current_frame_index == 0 : AI -> Mouse Drawing
        # self.current_frame_index == 1 : Mouse Drawing -> RDP
        # self.current_frame_index == 2 : RDP algorithm -> AI
        if painter.current_frame_index == 0:  # (AI가 완료된 이후) 그림을 그리는 단계
            painter.reflect_ai()  # AI 모델이 저장한 데이터 가져옴
            painter.load_and_reconstruct(filename=painter.save_file_name) # 캔버스 업데이트 후 입력받음
        elif painter.current_frame_index == 1:  # RDP 알고리즘으로 전처리 후 캔버스 업데이트
            painter.load_and_reconstruct(filename=painter.rdp_file_name)
        else:  	# AI 모델 사용
            misc.npy2npz(painter.rdp_file_name, painter.ai_file_name)  # 전처리한 데이터 형식 변경
            stroke.run(painter.ai_index, misc.just_name(painter.ai_file_name))  # AI 모델을 통해 더해진 획을 파일로 저장
            painter.load_and_reconstruct(filename=painter.ai_file_name)  # 저장된 파일의 데이터로 캔버스 업데이트
            painter.ai_index = painter.ai_index + 1  # 저장을 위한 인덱스 업데이트
        painter.running = True  # if 문이 실행하는 동안 painter의 스레드가 멈추므로 다시 작동시킴

 

 

2. MousePainter class

: 모든 인터페이스(canvas, button) 등을 구현한 클래스입니다.

class MousePainter:
    def __init__(self, args):  # 모든 UI의 구성과 data 저장 파일들을 초기화하는 생성자
 
    # load npy file and draw
    def load_and_reconstruct(self, filename='mouse_deltas.npy'):
        deltas = npy.load(filename)  # npy 파일을 불러온다
        if deltas :  # 데이터가 있는지 확인한다
        	reconstruct_drawing(filename)  # 데이터를 통해 그림을 그린다
	
    # paint(event), start_paint(event), stop_paint(event) 
    # 사용자가 그림을 그리는 과정에서
    # 마우스가 눌리는 경우 start_paint, 이동하는 경우 paint, 그리고 누른 것을 떼는 경우 stop_paint가 실행
    # 그림을 그리고, (delta_x, delta_y, penstate) 상태의 행렬 데이터 저장

    def paint(self, event):      # paint with a mouse
    def start_paint(self, event):      # called when mouse button first pressed
    def stop_paint(self, event):      # called when mouse button unpressed

    # save_deltas_button(), erase_button(), next_button(), save_button(), exit_button() 
    # 각 버튼이 눌렸을 때 각자의 기능을 수행한다. 
    # save_deltas_button : 사용자가 그린 그림이 저장된 배열을 npy파일로 저장 
    # erase_button : 그려진 그림을 지움과 동시에 기존의 배열도 초기화
    # next_button : 다음 단계로 넘어감
    # save_button : 현재 각 단계에서 마지막으로 생성된 파일들을 다른 폴더에 저장
    # exit_button : 모든 과정을 마무리하고 프로그램을 종료

    def save_deltas_button(self):    # 사용자가 그린 그림이 저장된 배열을 npy파일로 저장 
    def erase_button(self):      # 그려진 그림을 지움과 동시에 기존의 배열도 초기화
    def next_button(self):      # 다음 단계로 넘어감
    def save_button(self):      # 현재 각 단계에서 마지막으로 생성된 파일들을 다른 폴더에 저장
    def exit_button(self):      # 모든 과정을 마무리하고 프로그램을 종료

 

3. RDP algorithm

: 선형 단순화 알고리즘으로, quickdraw dataset을 전처리하기 위해 사용된 알고리즘입니다. 해당 알고리즘을 동일한 조건으로 사람이 그린 데이터에도 적용해 주었습니다.

"""
rdp
~~~
Python implementation of the Ramer-Douglas-Peucker algorithm.
:copyright: 2014-2016 Fabian Hirschmann <fabian@hirschmann.email>
:license: MIT, see LICENSE.txt for more details.
"""
# 기존 파이썬 모듈 중 공개된 코드를 많이 차용하였습니다.

# recursive하거나 iterative하게 rdp를 계산할 수 있습니다.
# 저희의 코드에서는 rdp가 사용되는 것만이 중요해서 방법은 수정하지 않고 함수를 실행하였습니다.
def rdp(M, epsilon=0, dist=pldist, algo="iter", return_mask=False):
    """
    Simplifies a given array of points using the Ramer-Douglas-Peucker
    algorithm.
    """
    return algo(np.array(M), epsilon, dist) # return npy array of lines

# 기존의 data 형태는 line을 구성하는 좌표를 받아 그것을 epsilon 값에 따라 수정하는 rdp 알고리즘에 맞지 않았습니다.
# 따라서 npy의 데이터를 penstate가 변화하는 시점에서 끊어 각자 line으로 저장하여, line의 집합인 lines를 return하는 함수를 만들었습니다.
# 이후, Misc.py에서 이렇게 받은 lines의 각 line에 대해 별도로 rdp 알고리즘을 실행한 후,
# 다시 원래의 형태로 변형하여 데이터 전처리를 진행합니다.
def extract_lines_from_npy(npy_file):
    # Load the npy file
    data = np.load(npy_file, allow_pickle=True, encoding='latin1')

    # (dx, dy, penstate) -> (x, y, penstate)
    # Initialize an empty list to store lines
    lines = []
    current_line = []

    # Iterate through the data
    # At start of a new line, if there's a current line, append it to lines
    # Append the last line

    return lines

 

 UI를 통해 결국 모든 데이터의 흐름을 제어한 것이나 다름없기 때문에, 데이터 Flow 다이어그램을 통해서 마지막으로 저희 모듈에서의 데이터 이동을 설명하고 이 글을 마무리할까 합니다.

Data Flow Diagram

 

1. User Interface에서 사람이 그린 Stroke Input을 받아 저장한다. (Input file : 마우스로 그린 그림, output file : .npy 파일)
2. 마우스로 그린 입력을 기존 모델의 입력 형식과 맞춰주기 위한 중간 과정으로, 선형 단순화 알고리즘인 RDP 알고리즘을 적용하여 선을 단순하게 해준다. (Input : .npy 파일, Output :  .npy 파일) 이후, 결과를 UI 디스플레이에 업데이트 해주어, 본인이 그린 그림의 단순화 버전을 보여준다.
3. 모델의 입력 형식과 맞춰주기 위해서 .npy파일의 데이터를 .npz파일로 저장한다. (Input : .npy파일, Output : .npz파일)
4. 모델의 입력 형식에 맞춘 데이터를 모델에 입력한다.
5. Image Completion 미완성된 입력 이미지를 완성된 이미지로 만들어준다.  (Input : .npz파일, Output : .npz 파일) 해당 결과를 Stroke Ordering 모델로 넘겨주어 내부 알고리즘에 따라, 어떤 획을 그릴지 선택하고 이를 출력한다.  (Input : numpy array 데이터, Output : .npy 파일)
6. 가장 처음에 사람이 그린 획에 5번을 통해 선택한 획을 추가한다. 이후, 모델이 그린 부분이 추가된 그림을 화면에 보여주며 다음 입력을 기다린다. (Input : .npy파일, Output : .npy파일)
7. 1-6의 과정을 반복한다.

 

 

 긴 글 읽어주셔서 감사합니다.