A ship in harbor is safe, but that is not what ships are built for.

개발일지/AI 캠프

내일배움캠프 AI - 56일차 TIL, 2022.11.18

플리피나리 2022. 11. 19. 10:39
반응형

스파르타 코딩클럽 내일배움캠프 AI 웹개발자양성과정 3회차

2022.11.18. 56일차 - TIL

 

 

1. 타임어택

문제) Docker를 활용해 서비스 배포하기(docker-compose 활용)

1. github에서 timeattack project를 clone해주세요.

2. django의 runserver 명령어를 사용해 배포해주세요.(사용 이미지: python:3.10.8)

3. postgresql을 연동한 후 배포해주세요.(사용 이미지: postgres:14.5)

4. nginx를 연동한 후 배포해주세요.(사용 이미지: nginx:1.23.2)

5. gunicorn을 사용해 배포해주세요.

 

 

풀이)

먼저 aws ubuntu에 접속한다.

1. AWS EC2에 SSH 연결

- ssh -i 키페어드래그 ubuntu@인스턴스IPv4 > 연결 확인

 

 

저번 타임어택에서 도커 패키지를 설치했기 때문에 해당 과정은 생략하겠다. 이제 django 프로젝트를 clone하려고 하는데 backend 폴더 밑에 django 폴더를 만들어 clone하도록 하겠다.

2. backend 폴더 만들고 그 아래 django 폴더를 만들어 project clone하기

- backend 폴더 만들고(mkdir backend) 해당 폴더로 이동하기(cd backend)

- git clone 깃헙주소 ./django : django 폴더 아래에 clone하기

 

 

3. 2번을 위한 Dockerfile 작성하기(django runserver)

# backend/Dockerfile

FROM python:3.10.8  # 빌드할 때 사용할 이미지 지정

ENV PYTHONDONTWRITEBYTECODE 1  # .pyc 파일을 생성하지 않도록 설정

ENV PYTHONUNBUFFERED 1  # 파이썬 로그가 버퍼링 없이 즉각적으로 출력하도록 설정

RUN mkdir /app/  # /app/ 디렉토리를 생성

WORKDIR /app/  # app/ 경로를 작업 디렉토리로 설정

COPY ./django/requirements.txt .  # requirements.txt 파일을 작업 디렉토리(/app/)로 복사

RUN pip install --no-cache-dir -r requirements.txt  # 프로젝트 실행에 필요한 패키지들을 설치

Dockerfile이란 docker의 이미지를 직접 생성하기 위한 용도로 작성하는 파일을 말한다. Dockerfile은 사용자가 개발한 프로젝트 혹은 설정파일 등을 이미지에 포함시키거나 이미지에 기본적으로 특정 패키지를 설치하고 싶을 때 다양한 용도로 사용된다. 지금은 docker에서 django를 배포하는 과정에서 기본 python 이미지를 불러온 후 django 패키지를 pip install한 후 이미지를 생성해야 하기 때문에 Dockerfile을 작성한다.

 

 

docker-compose로 배포하는게 학습 목표이기 때문에 docker-compose.yml을 작성한다.

4. 2번을 위한 docker-compose.yml 작성하기(django runserver)

version: '3.8'  # docker-compose.yml에 사용될 문법 버전을 정의

services:
  backend:  # 서비스 이름 지정, 서비스 이름은 컨테이너끼리 통신할 때 사용됨
    container_name: backend  # 컨테이너 이름 지정
    build: ./backend/  # 해당 경로(./backend/)에 있는 Dockerfile을 사용해 이미지 생성
    entrypoint: sh -c "python manage.py collectstatic --no-input && python manage.py migrate && python manage.py runserver 0.0.0.0:8000"
    # sh -c: 컨테이너에서 뒤에 작성할 명령어를 실행시킴
    # python manage.py collectstatic --no-input: 배포를 위해 static 파일을 모음
    # &&: 특정 명령어를 실행한 이후 다음 명령어 실행
    # python manage.py migrate: django에 연결된 db를 migrate
    # python manage.py runserver 0.0.0.0:8000: runserver을 이용해 django 프로젝트를 실행
    ports:  # 포트포워딩 설정
      - "80:8000"  # 외부에서 80포트로 접속했을 때 컨테이너의 8000번 포트(django)로 연결
    volumes:  # volume을 설정
      - ./backend/django/:/app/  # 정의한 volume의 mount할 경로를 지정
      - /etc/localtime:/etc/localtime:ro  # host의 timezone 설정을 컨테이너에 적용
      # ro 은 읽기 전용(read only) 속성으로 볼륨을 설정하는 것을 의미
    restart: always  # 컨테이너가 종료됐을 때 다시 실행

docker-compose란 docker 2개 이상의 컨테이너를 더욱 간편하게 관리하기 위해 사용되는 툴이다. 이를 통해 여러 컨테이너를 더 간편하고 직관적으로 관리할 수 있다. 또한 entrypoiint란 docker 컨테이너가 생성될 때 기본적으로 실행할 명령어를 지정하는 옵션을 말한다.

결과 화면은 다음과 같다.

runserver로 배포 성공

잠시 발생한 에러 상황을 이야기하자면 다른 거 다 똑같이 작성하고, docker-compose.yml의 entrypoint에서 마지막 부분에 python manage.py runserver -d 0.0.0.0:8000 이라고 작성했다가 웹화면이 실행되지 않았다. 나는 백엔드로 실행되니까 -d 옵션을 사용해도 된다고 생각했다.(실제로 gunicorn을 이용해 실행할 때는 저렇게 -d를 쓰길래...;;;)

 

 

다음에는 postgresql을 써야 하기 때문에 Dockerfile 내용을 수정해야한다. 마지막에 postgresql을 사용하기 위한 패키지 설치 코드를 추가한다. postgresql이란 오픈 소스 데이터베이스로, Oracle DB, MySQL 등 상용 라이센스를 가지고 있는 데이터베이스와는 다르게 무료로 사용이 가능하다. 또한 장고에서 기본 데이터 베이스로 권장되고 있는 데이터베이스이다.

5. 3번을 위한 Dockerfile 작성하기(postgresql)

#  backend/Dockerfile

FROM python:3.10.8  # 빌드할 때 사용할 이미지 지정

ENV PYTHONDONTWRITEBYTECODE 1  # .pyc 파일을 생성하지 않도록 설정

ENV PYTHONUNBUFFERED 1   # 파이썬 로그가 버퍼링 없이 즉각적으로 출력하도록 설정

RUN mkdir /app/  # /app/ 디렉토리를 생성

WORKDIR /app/  # app/ 경로를 작업 디렉토리로 설정

COPY ./django/requirements.txt .  # requirements.txt 파일을 작업 디렉토리(/app/)로 복사

RUN pip install --no-cache-dir -r requirements.txt  # 프로젝트 실행에 필요한 패키지들을 설치  

RUN pip install psycopg2  # postgresql을 사용하기 위한 패키지를 설치

 

 

postgresql을 위한 docker-compose.yml을 수정한다. postgresql 컨테이너를 추가한다.

6. 3번을 위한 docker-compose.yml 작성하기(postgresql)

# docker-compose.yml

version: '3.8'  # docker-compose.yml에 사용될 문법 버전을 정의

volumes:
  postgres: {}  # postgresql에서 사용할 볼륨 지정

services: 
  postgres:  # 서비스 이름 지정, 서비스 이름은 컨테이너끼리 통신할 때 사용됨
    container_name: postgres  # 컨테이너 이름 지정
    image: postgres:14.5  # 사용할 이미지 설정
    volumes:  # volume을 설정
      - postgres:/var/lib/postgresql/data/
    environment:  # postgresql 컨테이너에서 사용할 환경변수 지정
      - POSTGRES_USER=user  # 데이터베이스 사용자 지정
      - POSTGRES_PASSWORD=P@ssw0rd  # 사용자 비밀번호 지정
      - POSTGRES_DB=django  # 데이터베이스 이름 지정
    restart: always  # 컨테이너가 종료됐을 때 다시 실행

  backend:  # 서비스 이름 지정, 서비스 이름은 컨테이너끼리 통신할 때 사용됨
    container_name: backend  # 컨테이너 이름 지정
    build: ./backend/  # 해당 경로(./backend/)에 있는 Dockerfile을 사용해 이미지 생성
    entrypoint: sh -c "python manage.py collectstatic --no-input && python manage.py migrate && python manage.py runserver 0.0.0.0:8000"
    # sh -c: 컨테이너에서 뒤에 작성할 명령어를 실행시킴
    # python manage.py collectstatic --no-input: 배포를 위해 static 파일을 모음
    # &&: 특정 명령어를 실행한 이후 다음 명령어 실행
    # python manage.py migrate: django에 연결된 db를 migrate
    # python manage.py runserver 0.0.0.0:8000: runserver을 이용해 django 프로젝트를 실행
    ports:  # 포트포워딩 설정
      - "80:8000"  # 외부에서 80포트로 접속했을 때 컨테이너의 8000번 포트(django)로 연결
    volumes:  # volume을 설정
      - ./backend/django/:/app/  # 정의한 volume의 mount할 경로를 지정
      - /etc/localtime:/etc/localtime:ro  # host의 timezone 설정을 컨테이너에 적용
      # ro 은 읽기 전용(read only) 속성으로 볼륨을 설정하는 것을 의미
    environment:  # 컨테이너에서 사용할 환경변수 지정
      - DEBUG=1
      # 데이터베이스 설정을 위한 값들(settings.py)
      - POSTGRES_DB=django  # 데이터베이스 이름(NAME)
      - POSTGRES_USER=user  # 데이터베이스 사용자(USER)
      - POSTGRES_PASSWORD=P@ssw0rd  # 사용자 비밀번호(PASSWORD)
      - POSTGRES_HOST=postgres  # 데이터베이스 호스트(HOST)
      - POSTGRES_PORT=5432  # 데이터베이스 포트번호(PORT)
    depends_on:
      - postgres  # backend 컨테이너보다 먼저 postres 컨테이너를 실행
    restart: always  # 컨테이너가 종료됐을 때 다시 실행

결과 화면을 확인하면 다음과 같다.

postgresql로 배포 성공!

 

 

nginx는 클라이언트의 request 요청을 처리해주는 웹서버이다. reverse proxy(클라이언트의 서버 직접 접근 차단), 로드밸런싱(트래픽 분산), 캐싱(동일한 요청에 대한 더 빠른 처리) 등의 기능을 지원하며 클라이언트의 요청을 받은 후 service(django) 데이터를 넘겨주는 역할을 한다.

7. 4번을 위한 nginx 설정파일 만들기(nginx)

- 먼저 nginx 디렉토리를 만들고(mkdir nginx) 해당 폴더로 이동해(cd nginx) default.conf를 작성한다.(vi default.conf)

# nginx/default.conf

server {
  listen 80;
  server_name _; # 모든 도메인 혹은 ip로 들어오는 요청에 대해 처리

  location / { # nginx로 요청이 들어왔을 때
    proxy_pass http://backend:8000/; # backend 컨테이너의 8000번 포트로 전달
  }

  location /static/ { # 브라우저에서 /static/ 경로로 요청이 들어왔을 때
    alias /static/; # /static/ 경로에 있는 파일들을 보여줌
  }

  location /media/ { # 브라우저에서 /media/ 경로로 요청이 들어왔을 때
    alias /media/; # /media/ 경로에 있는 파일들을 보여줌
  }
}

 

Dockerfile의 내용은 변하지 않고 기존 그대로 사용한다.

8. 4번을 위한 docker-compose.yml 작성하기(nginx)

# docker-compose.yml

version: '3.8'  # docker-compose.yml에 사용될 문법 버전을 정의

volumes:
  postgres: {}  # postgresql에서 사용할 볼륨 지정
  django_media: {}
  django_static: {}

services: 
  postgres:  # 서비스 이름 지정, 서비스 이름은 컨테이너끼리 통신할 때 사용됨
    container_name: postgres  # 컨테이너 이름 지정
    image: postgres:14.5  # 사용할 이미지 설정
    volumes:  # volume을 설정
      - postgres:/var/lib/postgresql/data/
    environment:  # postgresql 컨테이너에서 사용할 환경변수 지정
      - POSTGRES_USER=user  # 데이터베이스 사용자 지정
      - POSTGRES_PASSWORD=P@ssw0rd  # 사용자 비밀번호 지정
      - POSTGRES_DB=django  # 데이터베이스 이름 지정
    restart: always  # 컨테이너가 종료됐을 때 다시 실행

  backend:  # 서비스 이름 지정, 서비스 이름은 컨테이너끼리 통신할 때 사용됨
    container_name: backend  # 컨테이너 이름 지정
    build: ./backend/  # 해당 경로(./backend/)에 있는 Dockerfile을 사용해 이미지 생성
    entrypoint: sh -c "python manage.py collectstatic --no-input && python manage.py migrate && python manage.py runserver 0.0.0.0:8000"
    # sh -c: 컨테이너에서 뒤에 작성할 명령어를 실행시킴
    # python manage.py collectstatic --no-input: 배포를 위해 static 파일을 모음
    # &&: 특정 명령어를 실행한 이후 다음 명령어 실행
    # python manage.py migrate: django에 연결된 db를 migrate
    # python manage.py runserver 0.0.0.0:8000: runserver을 이용해 django 프로젝트를 실행
    volumes:  # volume을 설정
      - ./backend/django/:/app/  # 정의한 volume의 mount할 경로를 지정
      - /etc/localtime:/etc/localtime:ro  # host의 timezone 설정을 컨테이너에 적용
      # ro 은 읽기 전용(read only) 속성으로 볼륨을 설정하는 것을 의미
      - django_media:/app/media/  # nginx에서 media를 사용할 수 있도록 volume을 지정
      - django_static:/app/static/  # nginx에서 static을 사용할 수 있도록 volume을 지정
    environment:  # 컨테이너에서 사용할 환경변수 지정
      - DEBUG=1
      # 데이터베이스 설정을 위한 값들(settings.py)
      - POSTGRES_DB=django  # 데이터베이스 이름(NAME)
      - POSTGRES_USER=user  # 데이터베이스 사용자(USER)
      - POSTGRES_PASSWORD=P@ssw0rd  # 사용자 비밀번호(PASSWORD)
      - POSTGRES_HOST=postgres  # 데이터베이스 호스트(HOST)
      - POSTGRES_PORT=5432  # 데이터베이스 포트번호(PORT)
    depends_on:
      - postgres  # backend 컨테이너보다 먼저 postres 컨테이너를 실행
    restart: always  # 컨테이너가 종료됐을 때 다시 실행
    
    nginx:  # 서비스 이름
    container_name: nginx  # 컨테이너 이름
    image: nginx:1.23.2  # 컨테이너가 사용할 이미지 지정
    ports:  # 포트포워딩 설정
      - "80:80"  # http 포트포워딩
      - "443:443"  # https 포트포워딩
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
      - django_media:/media/  # django의 media를 사용할 수 있도록 volume을 지정
      - django_static:/static/  # django의 static 사용할 수 있도록 volume을 지정
    depends_on:
      - backend  # nginx 컨테이너를 실행하기 전에 backend 컨테이너를 먼저 실행
    restart: always   # 컨테이너가 종료됐을 때 다시 실행

nginx 컨테이너에서 포트포워딩을 설정했기 때문에 backend 컨테이너에서 설정한 포트포워딩은 삭제한다.

결과를 확인하면 다음과 같다.

nginx 배포 성공!

 

 

gunicorn이란 django 프로젝트를 실행할 때 사용되는 runserver와 같이, 사용자의 요청을 받아 django에 작성한 코드를 실행시켜 주도록 하는 역할을 한다. 배포용 runserver라고 생각해도 무관하다.

9. 5번을 위한 Dockerfile 작성하기(gunicorn)

# backend/Dockerfile

# python 3.10.8버전 이미지를 사용해 빌드
FROM python:3.10.8

# .pyc 파일을 생성하지 않도록 설정합니다.
ENV PYTHONDONTWRITEBYTECODE 1

# 파이썬 로그가 버퍼링 없이 즉각적으로 출력하도록 설정합니다.
ENV PYTHONUNBUFFERED 1

# /app/ 디렉토리를 생성합니다.
RUN mkdir /app/

# /app/ 경로를 작업 디렉토리로 설정합니다.
WORKDIR /app/

# requirments.txt를 작업 디렉토리(/app/) 경로로 복사합니다.
COPY ./django/requirements.txt .

# 프로젝트 실행에 필요한 패키지들을 설치합니다.
RUN pip install --no-cache-dir -r requirements.txt

# gunicorn과 postgresql을 사용하기 위한 패키지를 설치합니다.
RUN pip install gunicorn psycopg2

 

 

이제 entrypoint에 python runserver가 아닌 gunicorn으로 서버를 실행하는 명령어를 작성한다.

10. 5번을 위한 docker-compose.yml 작성하기(gunicorn)

# docker-compose.yml

version: '3.8'  # docker-compose.yml에 사용될 문법 버전을 정의

volumes:
  postgres: {}  # postgresql에서 사용할 볼륨 지정
  django_media: {}
  django_static: {}

services: 
  postgres:  # 서비스 이름 지정, 서비스 이름은 컨테이너끼리 통신할 때 사용됨
    container_name: postgres  # 컨테이너 이름 지정
    image: postgres:14.5  # 사용할 이미지 설정
    volumes:  # volume을 설정
      - postgres:/var/lib/postgresql/data/
    environment:  # postgresql 컨테이너에서 사용할 환경변수 지정
      - POSTGRES_USER=user  # 데이터베이스 사용자 지정
      - POSTGRES_PASSWORD=P@ssw0rd  # 사용자 비밀번호 지정
      - POSTGRES_DB=django  # 데이터베이스 이름 지정
    restart: always  # 컨테이너가 종료됐을 때 다시 실행

  backend:  # 서비스 이름 지정, 서비스 이름은 컨테이너끼리 통신할 때 사용됨
    container_name: backend  # 컨테이너 이름 지정
    build: ./backend/  # 해당 경로(./backend/)에 있는 Dockerfile을 사용해 이미지 생성
    entrypoint: sh -c "python manage.py collectstatic --no-input && python manage.py migrate && gunicorn timeattack.wsgi --workers=5 -b 0.0.0.0:8000"
    # sh -c: 컨테이너에서 뒤에 작성할 명령어를 실행시킴
    # python manage.py collectstatic --no-input: 배포를 위해 static 파일을 모음
    # &&: 특정 명령어를 실행한 이후 다음 명령어 실행
    # python manage.py migrate: django에 연결된 db를 migrate
    # gunicorn timeattack.wsgi --workers=5 -b 0.0.0.0:8000: gunicorn을 이용해 django 프로젝트를 실행
    # timeattack: django 프로젝트 이름
    # --workers=5: django를 실행시킬 process 갯수(cpu코어개수*2+1)
    # -b 0.0.0.0:8000: 8000번 포트로 실행
    volumes:  # volume을 설정
      - ./backend/django/:/app/  # 정의한 volume의 mount할 경로를 지정
      - /etc/localtime:/etc/localtime:ro  # host의 timezone 설정을 컨테이너에 적용
      # ro 은 읽기 전용(read only) 속성으로 볼륨을 설정하는 것을 의미
      - django_media:/app/media/  # nginx에서 media를 사용할 수 있도록 volume을 지정
      - django_static:/app/static/  # nginx에서 static을 사용할 수 있도록 volume을 지정
    environment:  # 컨테이너에서 사용할 환경변수 지정
      - DEBUG=1
      # 데이터베이스 설정을 위한 값들(settings.py)
      - POSTGRES_DB=django  # 데이터베이스 이름(NAME)
      - POSTGRES_USER=user  # 데이터베이스 사용자(USER)
      - POSTGRES_PASSWORD=P@ssw0rd  # 사용자 비밀번호(PASSWORD)
      - POSTGRES_HOST=postgres  # 데이터베이스 호스트(HOST)
      - POSTGRES_PORT=5432  # 데이터베이스 포트번호(PORT)
    depends_on:
      - postgres  # backend 컨테이너보다 먼저 postres 컨테이너를 실행
    restart: always  # 컨테이너가 종료됐을 때 다시 실행
    
    nginx:  # 서비스 이름
    container_name: nginx  # 컨테이너 이름
    image: nginx:1.23.2  # 컨테이너가 사용할 이미지 지정
    ports:  # 포트포워딩 설정
      - "80:80"  # http 포트포워딩
      - "443:443"  # https 포트포워딩
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
      - django_media:/media/  # django의 media를 사용할 수 있도록 volume을 지정
      - django_static:/static/  # django의 static 사용할 수 있도록 volume을 지정
    depends_on:
      - backend  # nginx 컨테이너를 실행하기 전에 backend 컨테이너를 먼저 실행
    restart: always   # 컨테이너가 종료됐을 때 다시 실행

결과를 확인하면 다음과 같다.

gunicorn 배포 성공!

 

이미 settings.py가 알맞게 설정되어있어 변경하지 않아도 괜찮았지만 원래는 DEBUG값, 접속 허용 host, DB 설정 변경 등을 위해 settings,py를 아래와 같이 고쳐야 한다.

# settings.py

import os

# 환경변수에 따라 DEBUG모드 여부를 결정합니다.
DEBUG = os.environ.get('DEBUG', '0') == '1'

# 접속을 허용할 host를 설정합니다.
ALLOWED_HOSTS = ['backend', ]

# postgres 환경변수가 존재 할 경우에 postgres db에 연결을 시도합니다.
POSTGRES_DB = os.environ.get('POSTGRES_DB', '')
if POSTGRES_DB:
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.postgresql',
            'NAME': POSTGRES_DB,
            'USER': os.environ.get('POSTGRES_USER', ''),
            'PASSWORD': os.environ.get('POSTGRES_PASSWORD', ''),
            'HOST': os.environ.get('POSTGRES_HOST', ''),
            'PORT': os.environ.get('POSTGRES_PORT', ''),
        }
    }

# 환경변수가 존재하지 않을 경우 sqlite3을 사용합니다.
else:
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.sqlite3',
            'NAME': BASE_DIR / 'db.sqlite3',
        }
    }


# CORS 허용 목록에 ec2 ip를 추가합니다.
CORS_ORIGIN_WHITELIST = ['http://$ec2_public_ip']
# ex) CORS_ORIGIN_WHITELIST = ['http://43.201.72.190']

# CSRF 허용 목록을 CORS와 동일하게 설정합니다.
CSRF_TRUSTED_ORIGINS = CORS_ORIGIN_WHITELIST

 

 

 

2. 딥러닝 원격강의

프로젝트를 준비할 때 가상환경(미리 만들었던 'sparta_project': venv)이 잘 설정되어 있는지 확인하고, models폴더와 imgs폴더를 만들고 main.py 파일을 만들어 진행하는 것이 좋다.

 

models에는 딥러닝 모델을 넣는다. 여기서는 ecv16 폴더와 instance_norm 폴더에 다양한 모델들을 담아놓았다.

imgs 에는 앞으로 테스트할 이미지들을 넣는다.

 

1) 이미지 출력하기

먼저 필요한 패키지들을 불러오고

import cv2  # opencv를 임포트
import numpy as np  # numpy를 임포트

사용할 딥러닝 모델을 로드한다.

net = cv2.dnn.readNetFromTorch('models/eccv16/starry_night.t7')

그리고 단순히 이미지를 불러와 윈도우에 띄우는 코드는 다음과 같다.

img = cv2.imread('imgs/01.jpg')

cv2.imshow('img', img)
cv2.waitKey(0)

 

 

2) 전처리(Preprocessing)

모델을 성능을 높이기 위한 방법으로, 연구원들이 사용한 전처리 방법을 그대로 따라하면 된다.

h, w, c = img.shape  # 이미지의 높이, 너비, 채널

img = cv2.resize(img, dsize=(500, int(h / w * 500)))  # 비율을 유지한 채 너비를 500으로 이미지 크기 변형

MEAN_VALUE = [103.939, 116.779, 123.680]  # 연구원들이 사용했던 전처리방법 > 이미지의 각 픽셀에 해당 값을 빼서 성능을 높임
blob = cv2.dnn.blobFromImage(img, mean=MEAN_VALUE)  # MEAN_VALUE 빼기 연산, 이미지 데이터 차원 변형

차원 변형 후 이미지의 shape은 (325, 500, 3=높이, 너비, 채널)에서 (1, 3, 325, 500=1, 채널, 높이, 너비)으로 변형된 것을 확인할 수 있다. 이때 변형 후 1은 딥러닝에서 배치 사이즈를 의미한다. 배치 사이즈란 딥러닝 모델을 학습시키는 과정에서 한번에 학습시키는 자료의 개수를 말하는데(예를 들어 이미지를 한번에 32개씩 묶어서 학습시킨다면 배치사이즈는 32...) 실제 이 모델을 연구한 연구원들이 사용한 배친 사이즈에 관계없이 1개의 테스트 이미지를 사용할 것이기 때문에 배치 사이즈는 1이 된다.

 

 

3) 추론 결과 보기(Inference)

전처리한 이미지(blob)를 모델에 넣고 추론(forward)한다. 이후 output 변수에 추론한 결과를 저장한다.

net.setInput(blob)
output = net.forward()

output은 컴퓨터가 인식하는 이미지로 숫자 형태이다. 따라서 해당 숫자를 우리가 보는 이미지로 변환하는 과정이 필요한데 이것을 후처리(Postprocessing)라 한다.

output = output.squeeze().transpose((1, 2, 0))
# squeeze: 첫번째 차원을 삭제한다.(차원줄이기)
# transpose: (채널, 높이, 너비) -> (높이, 너비, 채널) 형태로 변환(순서 바꾸기)
output += MEAN_VALUE  # MEAN_VALUE 다시 더하기

output = np.clip(output, 0, 255)  # 이미지 픽셀값 범위 넘어가는 값 잘라내기
output = output.astype('uint8')  # 이미지 자료형을 일반 이미지에서 사용되는 정수형으로 변형

마지막으로 결과 이미지를 출력한다.

cv2.imshow('img', img)
cv2.imshow('result', output)
cv2.waitKey(0)

 

 

4) 새로운 이미지로 다시 추론하기

이번에는 새로운 이미지에서 액자 부분만 잘라낸 후 해당 부분만 추론하려한다. 먼저 액자 부분만 잘라내야 하는데 y축으로 162~513까지 자르고, x축으로 185~428까지 자른다.

img = cv2.imread('imgs/02.jpg')

h, w, c = img.shape

img = cv2.resize(img, dsize=(500, int(h / w * 500)))

img = img[162:513, 185:428]  # 자를 때는 y축, x축 순서

이제 잘라낸 이미지로 추론한다. 만약 모델을 바꾸고 싶다면 readNetFromTorch에서 모델 경로만 수정하면 된다.

net = cv2.dnn.readNetFromTorch('models/eccv16/la_muse.t7')

 

 

5) 좌우 서로 다른 화풍 적용하기

서로 다른 모델을 2개 불러오고, 해당 모델로 각각 추론한 결과를 절반으로 잘라 이어 붙이면 된다. 이어붙이는 것은 numpy의 concatenate 함수를 사용한다.

import cv2
import numpy as np

net = cv2.dnn.readNetFromTorch('models/instance_norm/mosaic.t7')
net2 = cv2.dnn.readNetFromTorch('models/instance_norm/the_scream.t7')

img = cv2.imread('imgs/03.jpg')

h, w, c = img.shape

img = cv2.resize(img, dsize=(500, int(h / w * 500)))

MEAN_VALUE = [103.939, 116.779, 123.680]
blob = cv2.dnn.blobFromImage(img, mean=MEAN_VALUE)

net.setInput(blob)
net2.setInput(blob)
output = net.forward()
output2 = net2.forward()

output = output.squeeze().transpose((1, 2, 0))
output2 = output2.squeeze().transpose((1, 2, 0))

output += MEAN_VALUE
output2 = output2 + MEAN_VALUE

output = np.clip(output, 0, 255)
output2 = np.clip(output2, 0, 255)
output = output.astype('uint8')
output2 = output2.astype('uint8')

output3 = np.concatenate([output[:, :250], output2[:, 250:]], axis=1)
# output을 x축 0~250까지 자른 것과
# output2를 x축 250~끝까지 자른 것을
# 열 방향(좌 -> 우)으로 합친다.(axis=1)
cv2.imshow('img', img)  # 원본 이미지 출력
cv2.imshow('output', output)
cv2.imshow('output2', output2)
cv2.imshow('result', output3)  # 결과
cv2.waitKey(0)

추가적으로 concatenate 메소드는 선택한 축(axis)의 방향으로 배열을 연결해주는 메소드이다. 1차원 배열을 연결시킬 때는 방향이 하나밖에 없기 때문에 axis=0만 가능하다.(axis=1이면 오류가 발생한다..) 왜냐하면 1차원에서는 행방향, 열방향의 의미가 없으니까. 하지만 2차원 배열을 연결할 때 axis=0은 행방향(위->아래)을 의미하고, axis=1은 열방향(좌->우)을 의미한다. 참고로 3차원 배열에서 axis=0은 높이 방향으로 연결한 것이고, axis=1은 행방향, axis=2는 열방향을으로 연결한 것을 의미한다.

 

 

6) 2주차 과제

문제)

1. 액자 부분만 crop해서 추론하고 바뀐 이미지를 다시 액자 안에 집어넣기

2. 이미지를 가로 방향으로 3개로 잘라 적용하기

3. 동영상에 적용해보기

 

정답)

1. 액자 부분만 crop해서 추론하고 바뀐 이미지를 다시 액자 안에 집어넣기

# 패키지 불러오기
import cv2  # opnecv
import numpy as np  # numpy

# 딥러닝 모델 로드하기
net = cv2.dnn.readNetFromTorch('models/eccv16/starry_night.t7')

# 이미지 불러오기
img = cv2.imread('imgs/hw.jpg')
# 이미지 자르기(액자부분만-변형할 부분)
cropped_img = img[140:370, 480:810]

# 이미지 전처리하기
h, w, c = cropped_img.shape  # 이미지의 세로, 가로, 채널수
cropped_img = cv2.resize(cropped_img, dsize=(500, int(h/w*500))) # 가로를 500으로해 동일 비율로 세로 길리 설정
MEAN_VALUE = [103.939, 116.779, 123.680]  # 연구원이 사용했던 전처리 방법, 이미지 각 픽셀에 해당 값들을 빼서 성능을 높임
blob = cv2.dnn.blobFromImage(cropped_img, mean=MEAN_VALUE)  #MEAN_VALUE빼기 연산, 이미지 데이터 차원 변형 작업 처리

# 전처리한 이미지 넣고 결과 추론하기
net.setInput(blob)
output = net.forward()

# 후처리 코드(사람이 알아볼 수 있는 이미지로)
output = output.squeeze().transpose((1, 2, 0))  # 차원 다시 줄이고(sqeeze), (높이, 너비, 채널) 형태로 변형(transpose)
output += MEAN_VALUE  # MEAN_VALUE 다시 더하기
output = np.clip(output, 0, 255)  # 픽셀값이 0~255 밖 영역인 픽셀은 없애기
output = output.astype('uint8')  # 이미지 자료형을 일반 이미지에서 쓰이는 정수형으로 바꾸기
output = cv2.resize(output, (w, h))  # 처음 이미지 처리를 위해 resize했으니 다시 되돌리기

# 변형된 이미지 원본에 끼워 넣기
img[140:370, 480:810] = output

# 이미지 출력하기
cv2.imshow('img', img)  # 원본 이미지 출력
cv2.waitKey(0)

원본
이미지 추론 결과

2. 이미지를 가로 방향으로 3개로 잘라 적용하기

# 패키지 불러오기
import cv2  # opencv
import numpy as np  # numpy

# 딥러닝 모델 불러오기
net = cv2.dnn.readNetFromTorch('models/instance_norm/mosaic.t7')
net2 = cv2.dnn.readNetFromTorch('models/instance_norm/the_scream.t7')
net3 = cv2.dnn.readNetFromTorch('models/instance_norm/candy.t7')

# 이미지 불러오기
img = cv2.imread('imgs/03.jpg')

# 이미지 전처리하기
h, w, c = img.shape  # 이미지의 세로, 가로, 채널수
img = cv2.resize(img, dsize=(500, int(h/w*500)))  # 가로를 500으로해 동일 비율로 세로 길리 설정
MEAN_VALUE = [103.939, 116.779, 123.680]  # 연구원이 사용했던 전처리 방법, 이미지 각 픽셀에 해당 값들을 빼서 성능을 높임
blob = cv2.dnn.blobFromImage(img, mean=MEAN_VALUE)  #MEAN_VALUE빼기 연산, 이미지 데이터 차원 변형 작업 처리

# 전처리한 이미지 넣고 첫번째 모델 추론하기
net.setInput(blob)
output = net.forward()

# 후처리 코드(사람이 알아볼 수 있는 이미지로)
output = output.squeeze().transpose((1, 2, 0))
output += MEAN_VALUE
output = np.clip(output, 0, 255)
output = output.astype('uint8')

# 두번째 모델 추론하기
net2.setInput(blob)
output2 = net2.forward()

# 두번째 모델 적용한 이미지 후처리 코드
output2 = output2.squeeze().transpose((1, 2, 0))
output2 = output2 + MEAN_VALUE
output2 = np.clip(output2, 0, 255)
output2 = output2.astype('uint8')

# 세번째 모델 추론하기
net3.setInput(blob)
output3 = net3.forward()

# 두번째 모델 적용한 이미지 후처리 코드
output3 = output3.squeeze().transpose((1, 2, 0))
output3 = output3 + MEAN_VALUE
output3 = np.clip(output3, 0, 255)
output3 = output3.astype('uint8')

# 3개의 결과를 잘라 이어붙이기
output4 = np.concatenate([output[0:100, :], output2[100:200, :], output3[200:, :]], axis=0)

# 결과 이미지 출력하기
cv2.imshow('img', img)
cv2.imshow('output', output)
cv2.imshow('output2', output2)
cv2.imshow('output3', output3)
cv2.imshow('result', output4)
cv2.waitKey(0)

원본
이미지 추론 결과

3. 동영상에 적용해보기

import cv2
import numpy as np

# 모델 로드하기
net = cv2.dnn.readNetFromTorch('models/instance_norm/mosaic.t7')

# 비디오 가져오기
cap = cv2.VideoCapture('imgs/03.mp4')

# 동영상에서 이미지를 한 프레임씩 읽고 출력하는 코드 무한반복
while True:
    # 동영상 파일에서 한 개의 프레임 읽어오기
    # ret: 영상에서 제대로 프레임을 읽어왓는지 True or False
    # img: 읽어 온 1개 프레임 저장, ret이 False일 때 None 저장
    ret, img = cap.read() 
    
    # 동영상 프레임이 더이상 존재하지 않거나 에러 발생 시
    if ret == False:
        break
    
    # 이미지 전처리
    MEAN_VALUE = [103.939, 116.779, 123.680]
    blob = cv2.dnn.blobFromImage(img, mean=MEAN_VALUE)
    
    # 이미지 추론하기
    net.setInput(blob)
    output = net.forward()
    
    # 이미지 후처리
    output = output.squeeze().transpose((1, 2, 0))
    output += MEAN_VALUE
    output = np.clip(output, 0, 255)
    output = output.astype('uint8')
    
    cv2.imshow('result', output)
    # 1ms 동안 키보드의 입력을 기다림
    if cv2.waitKey(1)  == ord('q'):  # q버튼을 누르면 무한루프를 빠져나와 프로그램 종료
        break

성공은 했는데 결과 동영상 플레이가 너무 끊긴다. 좀 더 자연스러운 프레임으로 출력하려면 어떻게 해야할까....?

반응형