본문 바로가기
공부/AI

[PyTorch] MNIST로 학습한 CNN 모델로 웹 캠 손 글씨 숫자 인식하기 (2)

by 웅대 2023. 9. 10.
728x90
반응형

https://growth-coder.tistory.com/245

 

[AI] MNIST로 학습한 CNN 모델로 웹 캠 손 글씨 숫자 인식하기 (1)

웹 캠을 켜고 앞에 손 글씨 숫자를 갖다대면 인식한 숫자를 띄워주는 프로젝트를 진행해보려고 한다. 우선 이번 포스팅에서는 CNN 모델의 특성과 학습을 진행해보자. CNN (Convolution Neural Network)의

growth-coder.tistory.com

이전 포스팅에서 CNN 모델에 대해 배우고 MNIST 손글씨 데이터를 학습시켜 보았다.

 

이번 포스팅에서는 웹 캠을 통해 읽어온 이미지를 모델로 보내서 손 글씨 숫자를 인식하는 코드를 작성해보려고 한다.

 

우선 학습한 모델을 저장하고 불러오는 방법부터 알아보자.

 

학습한 모델을 저장할 때는 모델 자체를 저장하는 방법이 있고 학습한 모델의 파라미터만 저장하는 방법이 있다.

 

두 방법 모두 알아보자.

 

개발 환경은 PyCharm을 사용하였다.

 

모델 저장 및 불러오기

파라미터만 저장

학습을 마치고 나서 모델 안의 state_dict 파일을 저장한다.

 

# 파라미터만 저장
torch.save(
    obj=model.state_dict(),
    f='cnn_parameters.pth'
)

 

이후 모델의 파라미터를 불러와야 하는데 이 때 모델 클래스가 구현되어 있어야 한다.

 

import torch
import torch.nn as nn


class CNN(nn.Module):

    def __init__(self):
        super(CNN, self).__init__()

        # 첫 번째 층
        self.layer1 = nn.Sequential(
            nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )

        # 두 번째 층
        self.layer2 = nn.Sequential(
            nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )

        # 전결합 층
        self.fc = nn.Linear(7 * 7 * 64, 10, bias=True)

        # 전결합 층 가중치 초기화
        nn.init.xavier_uniform(self.fc.weight)

    def forward(self, x):
        out = self.layer1(x)

        out = self.layer2(out)
        # 평평하게 펼침
        out = out.view(out.size(0), -1)
        out = self.fc(out)
        return out
model = CNN()
model.load_state_dict(
    torch.load(f='model_parameters.pth')
)

 

모델 자체를 저장

모델 자체를 저장하면 된다.

# 모델 자체를 저장
torch.save(
    obj=model,
    f='cnn_model.pth'
)

 

모델 자체를 저장했기 때문에 파라미터만 저장했을 때와 달리 이 모델을 불러올 때는 클래스가 없어도 된다.

 

model = torch.load(f='cnn_model.pth')

 

손글씨 이미지 숫자 예측하기

이 부분부터는 opencv를 사용한다.

 

혹시 opencv를 잘 모른다면 아래 포스팅을 보고 오길 바란다.

https://growth-coder.tistory.com/239

 

[Computer Vision] 파이썬 OpenCV 기본 사용법

컴퓨터 비전(Computer Vision)이란 인간의 시각이 할 수 있는 일들을 수행하는 자율적인 시스템을 만드는 것을 목표로 하는 컴퓨터 과학의 연구 분야이다. 카메라와 동영상에서 디지털 이미지와 딥

growth-coder.tistory.com

 

이제 본격적으로 이미지 숫자를 예측해보자.

 

우선 모델을 생성하고 파라미터를 불러오자.

import torch
import torch.nn as nn
import cv2


class CNN(nn.Module):

    def __init__(self):
        super(CNN, self).__init__()

        # 첫 번째 층
        self.layer1 = nn.Sequential(
            nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )

        # 두 번째 층
        self.layer2 = nn.Sequential(
            nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )

        # 전결합 층
        self.fc = nn.Linear(7 * 7 * 64, 10, bias=True)

        # 전결합 층 가중치 초기화
        nn.init.xavier_uniform(self.fc.weight)

    def forward(self, x):
        out = self.layer1(x)

        out = self.layer2(out)
        # 평평하게 펼침
        out = out.view(out.size(0), -1)
        out = self.fc(out)
        return out
model = CNN()
model.load_state_dict(
    torch.load(f='cnn_parameters.pth')
)

그리고 이미지를 불러오자. 나는 아래처럼 이진화가 되어있는 28 * 28 이미지를 사용했다.

 

혹시 테스트하고 싶다면 아래 이미지 파일을 받아서 사용하면 좋을 것 같다.

1.png
0.00MB
0.png
0.00MB

우선 0을 테스트 해보자. cv2를 사용하여 이미지를 불러오고 shape와 값 타입을 확인해보자.

 

img = cv2.imread("1.png")
print(img.shape)
print(type(img[0][0][0]))

이진화 처리가 되어있는 이미지임에도 channel이 3인 모습을 확인할 수 있고 저장되어있는 데이터는 unit8 형식이다.

 

이제 이 데이터를 학습한 데이터와 동일한 형식으로 바꿔줘야 한다.

 

학습 시킬 때 X의 shape와 요소 타입을 출력해보면 다음과 같다.

 

즉 (batch size, channel, height, width) shape를 갖도록 바꿔주고 numpy 배열을 tensor로 바꿔준 후 uint8이 아닌 float32 타입을 갖도록 바꿔줘야 한다.

 

다음 과정을 통해 입력 가능한 형태로 바꿔주고 shape과 값 타입을 출력해보자.

# 채털 수가 3개라서 gray scale로 변경
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img = cv2.resize(img, (28, 28))

# 이미지를 [0, 1] 범위로 스케일링
img = img/255

# 이미지를 텐서로 변경
img_tensor = torch.tensor(img, dtype=torch.float32).unsqueeze(0).unsqueeze(0

# 입력 데이터의 shape과 타입을 출력
print(img_tensor.shape)
print(type(img_tensor[0][0][0][0]))

 이제 입력 가능한 형식이 되었다. 생성한 모델 인스턴스에 이 값을 넣어보자.

 

res = model(img_tensor)
print(res.argmax().item())

우리는 0에서 9까지 분류를 해야하기 때문에 res의 값은 아래와 같이 확률을 요소로 가지는 길이가 10인 리스트가 반환된다.

이제 여기서 가장 값이 큰 인덱스를 구하면 이 값이 곧 예측한 값이 된다.

 

우리는 1 이미지를 넣었고 인덱스 1의 값이 가장 큰 것을 확인할 수 있다.

 

argmax 메소드를 통해서 가장 큰 값의 인덱스를 출력하면 그 값이 예측한 값이다.

 

0과 1을 각각 불러와서 전처리 후 모델에 넣어서 잘 예측하는지 확인하자.

 

웹 캠을 통해 손글씨 숫자 인식하기

이제 웹 캠을 통해서 손글씨 숫자를 인식해보자.

 

다음은 웹 캠 설정이다.

# 웹 캠 설정
webcam = cv2.VideoCapture(0)
webcam.set(3, 640) # width 세팅
webcam.set(4, 480) # height 세팅

VideoCapture에 0을 넣으면 기본 캠으로 설정한다.

 

그리고 width, height를 각각 설정하자.

 

다음은 캠으로 프레임을 읽어 띄우는 코드이다.

while True:
    # success는 성공 여부, img는 이미지
    success, img = webcam.read()
    print(success)
    cv2.imshow("cam", img)
    
    # q를 누르면 무한 반복에서 빠져나옴
    if cv2.waitKey(1)&0xFF == ord('q'):
        break

read를 사용하면 성공 여부와 이미지를 함께 반환해준다.

 

무한 반복문으로 한 프레임씩 읽어서 띄워주기 때문에 q를 누르면 반복문에서 빠져나오는 코드도 작성해준다.

 

벽에 붙여둔 손글씨가 보이는 모습을 확인할 수 있다.

지금부터는 opencv를 사용할 예정이다.

 

사용법을 모른다면 아래 링크를 참고하는 것을 추천한다.

 

https://growth-coder.tistory.com/239

 

[Computer Vision] 파이썬 OpenCV 기본 사용법

컴퓨터 비전(Computer Vision)이란 인간의 시각이 할 수 있는 일들을 수행하는 자율적인 시스템을 만드는 것을 목표로 하는 컴퓨터 과학의 연구 분야이다. 카메라와 동영상에서 디지털 이미지와 딥

growth-coder.tistory.com

https://growth-coder.tistory.com/243

 

[Computer Vision] 원하는 색상을 가진 객체만 가져오기

https://growth-coder.tistory.com/239 [Computer Vision] 파이썬 OpenCV 기본 사용법 컴퓨터 비전(Computer Vision)이란 인간의 시각이 할 수 있는 일들을 수행하는 자율적인 시스템을 만드는 것을 목표로 하는 컴퓨터

growth-coder.tistory.com

 

https://growth-coder.tistory.com/244

 

[Computer Vision] 원하는 색상을 가진 객체의 윤곽선과 외접 사각형 그리기

https://growth-coder.tistory.com/243 이전 포스팅에서 이미지에서 원하는 색상만 검출해내는 방법을 알아보았다. 이번 포스팅에서는 원본 이미지에서 원하는 색상의 객체 주변에 contour와 bounding rectangle

growth-coder.tistory.com

가장 먼저 도형의 mask를 추출해보자.

 

위 링크 중 두 번째 링크에서 원하는 색상의 mask를 얻어내는 방법에 대해 배웠다.

 

그 링크에서는 색상을 감지하기 위해 HSV 색 공간으로 변환 후 범위를 통해 mask를 얻어냈는데 이번에는 threshold 값을 지정하는 방식을 사용하려고 한다.

 

threshold 방식을 사용하기 위해서는 먼저 이미지를 grayscale로 변경한다.

 

그리고 cv2.threshole 메소드를 통해 원하는 임계점을 지정하고 이 임계점을 넘어가면 0(검은색)으로 만들고 넘지 못하면 최대값(흰 색)으로 만든다.

retval, thr = cv2.threshold(이미지, 임계값, 임계값을 넘으면 바꿀 값, 옵션)

옵션 종류
1. THRESH_BINARY: 임계 값보다 크면 위에서 지정한 바꿀 값으로 변경 작으면 0으로 변경
2. THRESH_BINARY_INV: 임계값보다 크면 0으로 변경 작으면 위에서 지정한 바꿀 값으로 변경
등등..

두 개의 반환 값 중 두 번째 값이 이진화 처리를 진행한 이미지

 

우선 어느 정도의 값을 threshold로 정해야 mask를 얻어낼 수 있는지 tracking bar를 생성해서 확인해보자.

 

# 윈도우 생성
cv2.namedWindow("track bar")
# 윈도우 크기 지정
cv2.resizeWindow("track bar", 640, 480)

# 트랙바가 바뀔 때마다 실행될 함수
def empty(a):
    pass

# (트랙바 이름, 윈도우 이름, 첫 값, 최대 값, 함수)
cv2.createTrackbar("threshold", "track bar", 0, 255, empty)
while True:
    # success는 성공 여부, img는 이미지
    success, img = webcam.read()
    img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    threshold = cv2.getTrackbarPos("threshold", "track bar")
    _, mask = cv2.threshold(img, threshold, 255, cv2.THRESH_BINARY_INV)
    cv2.imshow("cam", mask)

    # q를 누르면 무한 반복에서 빠져나옴
    if cv2.waitKey(1)&0xFF == ord('q'):
        break

나는 110정도가 숫자 mask를 적절하게 얻어낼 수 있었다.

 

이제 tracking bar 코드는 없애자.

 

숫자 mask를 얻어냈다면 contour 정보를 얻어내서 bounding rectangle을 원본 이미지에 그려보자.

숫자를 잘 인식하고 있다.

 

이제 우리는 모델에 넣을 수 있게 이미지를 변형해야 한다.

 

이미지를 torch.tensor([1, 1, 28, 28])로 변형해보자.

 

while True:
    # success는 성공 여부, img는 이미지
    success, img = webcam.read()
    img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    _, mask = cv2.threshold(img, 110, 255, cv2.THRESH_BINARY_INV)
    cv2.imshow("cam mask", mask)

    # contour 얻기
    contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    for cnt in contours:
        # bounding rectangle 좌표 얻기
        x, y, w, h = cv2.boundingRect(cnt)
        cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 255), thickness=2)
    cv2.imshow("cam", img)
    # q를 누르면 무한 반복에서 빠져나옴
    if cv2.waitKey(1)&0xFF == ord('q'):
        break

우선 정확히 숫자 이미지만 얻을 수 있게 이미지를 잘라보자.

 

mask[ y : y + h, x : x + w ]와 같이 이미지를 자르면 숫자 이미지만 얻어낼 수 있다.

 

참고로 원본 이미지가 아닌 mask에서 잘라야 한다. 결국 들어가는 이미지는 이진화 처리된 이미지이기 때문이다.

 

이미지만 잘라냈다면 해당 이미지를 정사각형으로 만들어야 한다.

 

width와 height 중 작은 값에 제로 패딩을 추가하면 된다.

 

def make_img_square(img):
    """
    이진화된 이미지를 받아서 검은색 padding을 넣어서 정사각형으로 만들어서 반환
    :param img: 이진화된 정사각형이 아닌 이미지
    :return:
    """
    h, w = img.shape

    # 높이와 너비 중 큰 값을 찾아 정사각형의 한 변의 길이로 설정
    max_dim = max(h, w)

    # 패딩 값 계산
    top_pad = (max_dim - h) // 2
    bottom_pad = max_dim - h - top_pad
    left_pad = (max_dim - w) // 2
    right_pad = max_dim - w - left_pad

    # 검은색으로 패딩 추가
    square_img = cv2.copyMakeBorder(img, top_pad, bottom_pad, left_pad, right_pad, cv2.BORDER_CONSTANT,
                                    value=[0, 0, 0])
    return square_img

정사각형으로 만들었다면 28*28 사이즈로 변경 후  torch.tensor([1, 1, 28, 28])로 변형 후 모델에 넣어주면 된다.

 

그리고 한 번 예측 값을 원본 이미지에 띄워보자.

 

 

<최종 코드>

import torch
import torch.nn as nn
import cv2


class CNN(nn.Module):

    def __init__(self):
        super(CNN, self).__init__()

        # 첫 번째 층
        self.layer1 = nn.Sequential(
            nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )

        # 두 번째 층
        self.layer2 = nn.Sequential(
            nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )

        # 전결합 층
        self.fc = nn.Linear(7 * 7 * 64, 10, bias=True)

        # 전결합 층 가중치 초기화
        nn.init.xavier_uniform(self.fc.weight)

    def forward(self, x):
        out = self.layer1(x)

        out = self.layer2(out)
        # 평평하게 펼침
        out = out.view(out.size(0), -1)
        out = self.fc(out)
        return out
model = CNN()
model.load_state_dict(
    torch.load(f='cnn_parameters.pth')
)

# 웹 캠 설정
webcam = cv2.VideoCapture(0)
webcam.set(3, 640) # width 세팅
webcam.set(4, 480) # height 세팅

def make_img_square(img):
    """
    이진화된 이미지를 받아서 검은색 padding을 넣어서 정사각형으로 만들어서 반환
    :param img: 이진화된 정사각형이 아닌 이미지
    :return:
    """
    h, w = img.shape

    # 높이와 너비 중 큰 값을 찾아 정사각형의 한 변의 길이로 설정
    max_dim = max(h, w)

    # 패딩 값 계산
    top_pad = (max_dim - h) // 2
    bottom_pad = max_dim - h - top_pad
    left_pad = (max_dim - w) // 2
    right_pad = max_dim - w - left_pad

    # 검은색으로 패딩 추가
    square_img = cv2.copyMakeBorder(img, top_pad, bottom_pad, left_pad, right_pad, cv2.BORDER_CONSTANT,
                                    value=[0, 0, 0])
    return square_img
while True:
    # success는 성공 여부, img는 이미지
    success, img = webcam.read()
    img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    _, mask = cv2.threshold(img, 110, 255, cv2.THRESH_BINARY_INV)
    cv2.imshow("cam mask", mask)

    # contour 얻기
    contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    for cnt in contours:
        # bounding rectangle 좌표 얻기
        x, y, w, h = cv2.boundingRect(cnt)
        cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 255), thickness=2)

        # 숫자 이미지만 자르기
        cropped_img = mask[y:y+h, x:x+w]

        # 정사각형으로 만들기
        square_img = make_img_square(cropped_img)
        square_img = cv2.resize(square_img, (28, 28,))
        cv2.imshow("square", square_img)

        # 텐서로 변경
        # 이미지를 [0, 1] 범위로 스케일링
        square_img = square_img / 255

        # 이미지를 텐서로 변경
        img_tensor = torch.tensor(square_img, dtype=torch.float32).unsqueeze(0).unsqueeze(0)

        # 예측
        predicted = model(img_tensor)
        res = predicted.argmax().item()

        # 원본 이미지에 예측 값 그리기
        cv2.putText(img, str(res), (x+w//2, y+10), cv2.FONT_ITALIC, 1.5, (0, 255, 255), thickness=1)

    cv2.imshow("cam", img)
    # q를 누르면 무한 반복에서 빠져나옴
    if cv2.waitKey(1)&0xFF == ord('q'):
        break

 

 

<적절한 임계 값을 찾는 코드>

import cv2

# 웹 캠 설정
webcam = cv2.VideoCapture(0)
webcam.set(3, 640) # width 세팅
webcam.set(4, 480) # height 세팅

# 윈도우 생성
cv2.namedWindow("track bar")
# 윈도우 크기 지정
cv2.resizeWindow("track bar", 640, 480)

# 트랙바가 바뀔 때마다 실행될 함수
def empty(a):
    pass

# (트랙바 이름, 윈도우 이름, 첫 값, 최대 값, 함수)
cv2.createTrackbar("threshold", "track bar", 0, 255, empty)
while True:
    # success는 성공 여부, img는 이미지
    success, img = webcam.read()
    img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    threshold = cv2.getTrackbarPos("threshold", "track bar")
    _, mask = cv2.threshold(img, threshold, 255, cv2.THRESH_BINARY_INV)
    cv2.imshow("cam mask", mask)

이렇게 웹 캠을 통해 실시간으로 숫자 이미지를 예측하는 과정을 진행하였다.

 

간단하게 해보았지만 한계점이 존재한다.

 

먼저 단순하게 gray scale로 변경 후 threshold를 처리했기 때문에 흰 배경이 아닌 어두운 배경에서는 다른 객체를 인식할 가능성이 높다.

 

또한 조명, 각도, 환경에 따라 threshold 값이 달라질 수도 있다.

 

본인의 환경에 맞게끔 적절하게 이미지를 처리하는 과정을 추가하는 것이 좋다.

728x90
반응형

댓글