학기가 마무리되었습니다.

물론 재미는 있었지만 저를 무려 4달동안 괴롭힌 프로젝트가 끝났어요..

우리 되게 잘했거든요? 근데.. 세상이 억까해서..

아니 데모 2시간전에 납땜한거 끊어지는 거 있기없기?

그냥 코드가 에러난거면 어떻게든 고치겠는데.. 정통 3명이 모여서 납땜한거 끊어진거 어떻게 고쳐요..

설상가상으로 3D프린팅도 망해서 뚜껑도 제대로 못열고.. 진짜 생각할 수록 열받네..

 

그래서 데모도 제대로 못하고 덕분에 우리 프로젝트 되게 잘했는데도 만족스러운 성적이 나오지는 않았습니다..

아 또 생각하니 열받아.. 최소 A+이었는데..

암튼, 그래도 끝이 났으니, 우리 프로젝트를 좀 정리해볼까 하는데요,
이걸 정리하는 이유는 혹시 나중에 누가 우리 아이디어 훔치면 우리꺼라고 주장하기 위해...
사실 뻥이고, 그래도 블로그에 이것저것 많이 올렸는데, 마무리는 해둘까 싶어서 올립니당.😊

 

우선 결론부터 보여드리자면, 우리 프로젝트 시연 영상입니다.

앞에서 계속 보여드리던 얼굴인식 부분을 보고 싶으시면 대충 5:25쯤 가시면 나올겁니당

https://youtu.be/GoyFj_HKImU

Doggy Dine 시연 영상

저희 전체 source 코드입니다.

https://github.com/PAWSITIVE2024마지막에 급하게 수정한 코드들은 아마 업데이트가 안되어있을텐데...

 

PAWSITIVE

2024 정보통신공학전공 캡스톤디자인1 PAWSITIVE팀. PAWSITIVE has 4 repositories available. Follow their code on GitHub.

github.com

 

저는 종강했으니 모르는 일입니다 ㅎㅎㅎ... 아마 에러 수정일텐데.. 뭐, 어차피 또 안쓸건데.. 어차피 아무도 안돌려보실거잖아요 ㅋㅋㅋㅋㅋ

 

초기 화면

로그인 및 회원가입부터 환경설정까지 입니다.

로그인을 하고 환경설정에 들어가면 강아지를 추가할 수 있게 됩니다.

각 강아지에 대한 정보는 사용자의 편리를 고려하여 제작되었고,,, 이런 사용자 편리요소는 대부분 제가 제작하였답니다..후훗.. 물론.. 다 하고 선배님께 데이터 베이스 수정을 떠넘겨버렸죠.. 이제 생각하니 너무 죄송하네요..

갑자기 이렇게 수정하고 있으니 바꿔주세욥! 하면 얼마나 빡칠까.. 우리 선배 알고보니 보살..?

아래에는 연결하기 버튼이랑 로그아웃 버튼이 있는데요 이거는 제가 했답니다 ㅎㅎ.

아무래도 저희는 IoT기기이다보니, 기기와 통신을 위해 만들었습니다.

사실 처음에는 WIFI나 Bluetooth를 이용하려고 했는데, 좀.. 잘 안돼서.. 어차피 기기는 사용자가 누군지만 알면 서버에 자동으로 접속해 알아서 작동하도록 설계되어있고, 앞에 카메라도 달려있겠다! User ID를 그냥 QR로 쏴줬습니다.. 근데.. 인식 속도도 빠르고 기능도 잘 되는것 같아서 쾌재를 불렀답니다 ㅎㅎ..

 

사료 추천 및 비교

이번엔 저희 앱의 초기 목적!! 사료 비교 및 추천 항목입니당.

사용자가 바코드를 통해 간편하게 사료 정보를 찾을 수도 있고, 제외해야 하는 알러지 항목을 선택하면, 해당 강아지가 먹어도 되는 사료 리스트가 쭉 나오죠. 사용자는 이 리스트에서 비교를 진행할 수 있답니다!

이부분은 진짜 저희 선배님께서 전적으로 담당해주신 부분으로.. 감사하다는 말씀밖에 못드리겠네요...

저요..? 바코드 읽어오는 것 정도는 제가 했습니다. 깔깔..

 

서비스

다음은 서비스 부분입니다!
앱의 재미를 위해 추가한 항목이지요! 다시 말해 없어도 되는 부분이란 소리랍니다!!

이건 제가 했어요.. ㅋㅋㅋㅋㅋ 물론.. 다이어리의 데이터베이스는.. 이 역시 선배님께 떠넘겨버렸답니다..

UI/UX 다 되었으니 연결해 주세욤!!!ㅎㅎㅎ... 죄송하네요..

그래도 GPT부분은 프롬프트 엔지니어링까지 제가 100% 완벽히 한겁니다?!?!?

알람같은 부분은 선배님이 데이터베이스에 시간들 저장하고, 가장 빠른 시간 찾을 수 있게 만들어 주신거 가져와서 제가 알람으로 띄우도록 구성 했어욤 ㅎㅎ..

 

딥러닝

다음은 대망의...! 강아지 얼굴 인식 및 밥주는 부분입니다!!

밥주기 버튼을 클릭하면 기기에서 카메라를 통해 앞에 있는 강아지의 얼굴을 인식하고, 그 결과를 사용자에게 알려주게 됩니다! 그리고, 인식된 강아지 이름을 바탕으로 데이터베이스에서 해당 강아지가 먹고 있는 사료의 칼로리, 강아지의 활동 수치 및 몸무게 등을 가져와서 종합적으로 고려해, 이번에 얼마의 사료를 줘야 하는지 계산합니다!!

여기서 만약 캡처된 사진과 인식된 이름이 다르다면 '다시하기' 버튼으로 인식을 다시 진행하도록 하였고, 만약 맞다면 급여하기 버튼을 통해 강아지가 밥을 얻을 수 있게 하였답니다.

ㅎㅎ.. 제가 만든 프로세스 설명 및 얼굴 인식 딥러닝 자랑이에요.. 저 잘했죠..?
마지막 사진은.. 인스타 셀럽 찌나랑 빵지인데, 견주분께서 잠깐 발표에 사용하는 것은 괜찮지만 지속적으로는 사용 안해주셨으면 좋겠다고 하셔서 모자이크 해드렸습니다~ 왼쪽 강아지 두마리는 저희 집 강아지라서 초상권? 그딴거 없어요. ㅎㅎ

 

하드웨어

하드웨어 및 센서

다음은 하드웨어입니다. 사실, 앱이랑 딥러닝 부분을 제가, 데이터베이스와 관련된 모든 작업을 선배님께서 담당해주셨고, 나머지 한 친구는 앱을 다뤄본적도 AI를 다뤄본적도 없기 때문에 하드웨어를 맡게 되었습니다. 사실상 저희는 앉아서 컴퓨터만 뚱땅대면 됐지만, 이 친구는 그것도 아니라 제일 고생했죠.. 마지막에 제대로 되지 않은 부분이 하드웨어라 더 마음이 불편했을 겁니다. 3D 프린팅 한 부분은 온도가 안맞아서인지 우그러지고 센서는 계속 끊어지고.. 갑자기 라즈베리파이 안켜지고.. 회의 때도 저랑 선배님은 앱 개발 관련해서 계속 얘기하는데, 그 친구는 혼자 하드웨어하는 거라 회의때도 할 말도 없고 외로웠을 겁니다.. 암튼 고생했다..

 

GUI for Raspberry Pi

GUI를 개발하는 부분은 앞의 포스팅에서 다뤘었습니당. 우리 송이.. 귀엽죠...?

사실 데모날 교수님 앞에서 멋있게 하려고 이쁜 한복까지 입고 갔었는데...

그냥 발표해야 되는데 강아지 데리고 온 사람이 되었습니다.. 속상..

사실 이 GUI는 시연영상을 만들기 위해 급하게 만들어진 아이로.. 아니, 원래 교수님 앞에서 보여드리면 이런거 필요 없거든요? 그래서 안만들었죠.. 근데 못했으니, 영상으로라도 성공해야 한다.. 라는 생각으로 급하게 만든거랍니다... 하...

그래서 좀 조잡하지만.. 첫 작품부터 잘 되기를 바라는 건 욕심이죠.. 다음에는 더 아름다운 GUI.. 만들어주마..

 

여기까지 제 졸업 프로젝트 소개였습니다!!

사실 저희끼리 저희 프로젝트 이렇게 부릅니다.

"다재다능 개쩌는 앱"

사실 맞는 말이죠.. 좀 기발하지 않나요? 온갖 기능 다 때려넣었답니다 하하하.

사실 이번 프로젝트에 제가 앱으로 할 수 있는 모든 걸 한 것 같아요.

누가 제게 '너 안드 얼마나 할 줄 알아?'라고 물어보면, 딱 여기까지 할 줄 안다고 말할 것 같습니다.

물론, 이후에 앱 개발자로 가게 되어서 더 다양한 툴도 써보고 한다면 얘기는 달라지겠지만,, 그러지는 않을 것 같습니다.

저 이제 졸업해요... 대학 4년 너무 길었다..

 

졸업 프로젝트를 끝내면서..

이제 한달 뒤 졸업생의 관점으로 제 대학시절을 돌아보자면.. 사실 너무 겁을 먹었던 것 같아요.

1-2학년을 좀 더 도전하면서 살았다면, 제 포스팅의 수준이 한 두단계는 높았을까요..?

 

이제와서 생각해보면, 혼자서 코딩 완벽하게 못하는 게 정상인데.. 취업 때문에 코딩 테스트 준비 하는 거 아닌 이상, 혼자 모든 걸 완벽하게 할 필요가 없었는데, 구글 도움도 좀 받고, 동기들이랑 의논도 좀 해야 되는게 맞는거였는데...
혼자 모든걸 완벽하게 했던 고등학교때를 계속 회상하면서, '아, 이것도 못하는 나는 코딩이 길이 아니구나, 나 못하는구나'라는 생각에 빠져있었습니다. 중간에 전과할까 진지하게 생각한 적도 많아요.

 

1학년 때, 코딩 동아리를 들어가기는 했지만, 알고리즘 스터디를 따라가기 벅찼고, 동기로부터 다른 코딩 동아리를 추천 받았지만, 앞에서 했던 동아리에서 제가 잘 못했기 때문에 '아, 내가 그걸 어떻게 해... 나 못해..' 하면서 그저 넘겼습니다.

2학년 때, 지금 하는 프로젝트들 홈페이지 만들어서 포트폴리오로 정리하라고 말을 들었을 때, '나 코딩도 못해, 웹 개발 배운적도 없어. 그걸 내가 어떻게 해..' 하면서 그저 흘렸습니다. 근데, 지금 아무한테도 안배웠지만 혼자 만들었죠...

2학년 마지막에는 유니스트 연구원으로부터 앱 개발 협업 제안까지 왔었지만, 나는 못한다는 생각에 사로잡혀서 그저 기회를 보내버렸어요.

 

내가 뭘 좀 모르더라도, 못하더라도, 일단 도전하고 계속 공부하면서 치열하게 싸워왔다면, 저는 지금이랑 좀 다른 사람이었을 거라는 생각이 듭니다. 어쨌던 저는 학생이었고, 학생은 계속 배우는 사람인거니까요.

앞으로는 좀 더 배우고, 도전하는 사람이 되고 싶습니다. 많이 응원해주세요😉

개발을 진행하면서 발생하는 에러들을 Terminal에서 print문들을 보면서 해결했다.

그러다 보니, 현재 어느 단계를 processing중인지까지 모두 print문으로 띄우게 되었는데... 사실 사용자에게 '모니터 하나 연결하시고 print문 보세요' 할 수는 없지 않은가.. 

그래서 개발 마지막에 GUI를 도입하였다. 결론만 말하면 GUI는 아주 훌륭하다.

그러나, 문제가 하나 있는데... 이걸 왜 미리 도입하지 않았는지 후회가 막심했다는 것이다.

현재 어느 단계에 있는지, 카메라는 잘 켜졌는지 모두 한번에 확인이 가능하다.

그래서 앞으로 개발할 때, process가 복잡한 코드들은 모두 GUI를 만들어서 돌릴 예정이다. 

그래서 GUI를 만드는 법을 정리해 보려고 한다.

 

Desktop 생성

우선 GUI를 위해 desktop을 생성해줘야 한다.

sudo nano ~/.local/share/application/DoggyDine.desktop

아래 source를 입력하고 저장해주자.

[Desktop Entry]
Name=Doggy Dine
Comment=Run My Python GUI Application
Exec=/home/yunjijeong/DogRecognition/run_gui.sh
Icon=/home/yunjijeong/DogRecognition/images/final_icon.png
Terminal=false
Type=Application
Encoding=UTF-8
Categories=None;

자 이렇게 하면, 내가 설정한 Icon으로 이쁜 애플리케이션이 생기게 된다.

이미지가 깨져있기는 한데 우리 Icon은 귀여운 강아지다...

 

Shell Script 생성

그러나 연결되어있는 shell script가 없다면 아무것도 일어나지 않겠지요?

설정한 Directory아래에 run_gui.sh파일을 만들어주고, 아래를 입력한 뒤 저장해 주자.

#!/bin/bash

# GUI 애플리케이션 실행
python3 /home/yunjijeong/DogRecognition/DoggyDine.py

그러면 자동으로 애플리케이션을 시작시키면 아래 파이썬이 작동될 것이다.

이 shell script의 장점 중 하나는 USB 권한 부여나 launch파일이나 다른 파이썬 파일 등, 여러 개를 힘들이지 않고 동시에 실행시킬 수 있다는 것이다.

이거 알게 되면서 가장 후회했던 거... 작년에 MACARON 할 때 shell script 쓸껄... 아니, 그 때 실행시켜야 되는거 진짜 너무 많았음... launch파일만 한 3개 됐던가... 사실 shell script 임베디드때 짜봐서 작년에도 짤 줄은 알았지만, 이걸 카롱이에도 적용시킬 생각을 못했음... 바보였던 정윤지...

 

GUI 코드 생성

그럼 이제 실행시킬 파이썬 코드를 짜면 된다.

사실 GUI는 굳이 파이썬으로 짤 필요는 없다. 다만, 나는 이거 real-time으로 파일을 저장하고 이들을 다시 불러와야 하기 때문에 프로그램 특성상 파이썬으로 진행했다. 물론 openCV 쓰려고 하는 것도 있지만 말이다. ㅎㅎ..

 

여기서부터는 UI꾸미는 영역이라 이걸 보는 사람은 자기 코드에 맞는 GUI를 개발하시길 바랍니다.

UI안꾸미고 그냥 실행시키면 진짜 빈 화면이 나온답니다. 깔깔

나는 미리 실행시킬 run.py를 다 짜뒀고, 이걸 실행만 시키면 됐었고, 카메라 부분만 조금 고생했다...

import sys
import subprocess
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel, QTextEdit, QSpacerItem, QSizePolicy
from PyQt5.QtGui import QPixmap, QImage
from PyQt5.QtCore import QThread, pyqtSignal, Qt
import cv2

class Worker(QThread):
    output = pyqtSignal(str)
    process_done = pyqtSignal()

    def __init__(self):
        super().__init__()
        self.process = None

    def run(self):
        self.process = subprocess.Popen(['python3', 'run.py'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
        for line in iter(self.process.stdout.readline, ''):
            self.output.emit(line.strip())
        self.process.stdout.close()
        self.process.wait()
        self.process_done.emit()

class CameraFeed(QThread):
    change_pixmap_signal = pyqtSignal(QImage)

    def __init__(self, label_size):
        super().__init__()
        self._run_flag = True
        self.label_size = label_size

    def run(self):
        cap = cv2.VideoCapture(0)
        while self._run_flag:
            ret, frame = cap.read()
            if not ret:
                break

            frame = cv2.resize(frame, self.label_size)
            rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            h, w, ch = rgb_image.shape
            bytes_per_line = ch * w
            convert_to_qt_format = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888)
            self.change_pixmap_signal.emit(convert_to_qt_format)
            QThread.msleep(33)  # approximately 30 frames per second
        cap.release()

    def stop(self):
        self._run_flag = False
        self.wait()

class MyApp(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
        self.step = -1
        self.worker = Worker()
        self.worker.output.connect(self.append_output)
        self.worker.process_done.connect(self.next_step)
        self.camera_feed = None

    def initUI(self):
        self.setWindowTitle('Doggy Dine')
        self.setGeometry(300, 300, 800, 600)

        vbox = QVBoxLayout()

        self.label = QLabel(self)
        self.default_pixmap = QPixmap('/home/yunjijeong/DogRecognition/images/final_icon.png')
        self.label.setPixmap(self.default_pixmap.scaled(400, 300, Qt.KeepAspectRatio))
        vbox.addWidget(self.label, alignment=Qt.AlignCenter)

        self.run_button = QPushButton('연결하기', self)
        self.run_button.clicked.connect(self.run_script)
        vbox.addWidget(self.run_button, alignment=Qt.AlignCenter)

        spacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
        vbox.addItem(spacer)

        self.text_edit = QTextEdit(self)
        self.text_edit.setReadOnly(True)
        self.text_edit.setFixedHeight(self.height() // 2)
        vbox.addWidget(self.text_edit)

        self.setLayout(vbox)
        self.show()

    def run_script(self):
        self.step = 0
        self.clear_output()
        self.append_output("시작")
        self.worker.start()

    def next_step(self):
        self.step += 1
        if self.step == 1:
            self.clear_output()
        	self.worker.start()
        elif self.step == 2:
            self.clear_output()
            self.append_output("얼굴 학습 중")
            self.worker.start()
        elif self.step == 3:
            self.clear_output()
            self.append_output("대기 중")
            self.worker.start()
        elif self.step == 4:
            self.clear_output()
            self.append_output("카메라 피드 시작")
            self.start_camera_feed()
        elif self.step == 5:
            self.label.setPixmap(QPixmap('/home/yunjijeong/DogRecognition/images/result.jpg').scaled(400, 300, Qt.KeepAspectRatio))
            self.clear_output()
            self.append_output("인식된 강아지")
            self.worker.start()
        elif self.step == 6:
            self.clear_output()
            self.label.setPixmap(self.default_pixmap.scaled(400, 300, Qt.KeepAspectRatio))
            self.append_output("사료 급여가 완료되었습니다")
            self.worker.start()

    def append_output(self, text):
        self.text_edit.append(text)

    def clear_output(self):
        self.text_edit.clear()

    def start_camera_feed(self):
        if self.camera_feed is not None and self.camera_feed.isRunning():
            self.camera_feed.stop()
        self.camera_feed = CameraFeed((self.label.width(), self.label.height()))
        self.camera_feed.change_pixmap_signal.connect(self.update_image)
        self.camera_feed.start()

    def update_image(self, qt_image):
        self.label.setPixmap(QPixmap.fromImage(qt_image))

    def closeEvent(self, event):
        if self.camera_feed is not None and self.camera_feed.isRunning():
            self.camera_feed.stop()
        event.accept()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = MyApp()
    sys.exit(app.exec_())

물론, 내 코드를 그대로 실행시킨다고 나랑 같은 결과가 나오지는 않는다.

나는 미리 짜둔 다른 코드에서 카메라로 들어오는 영상에 cv2로 그림그리는 함수같은 것들을 불러와서 띄웠기 때문에, 아마 이걸 그대로 실행하면 그냥 영상만 들어올 것이다.

 

결과

자 이제 결론을 보여주면 아래처럼 나온다.

사실 아래 print문 없애도 되고, 만약 나에게 시간이 많았다면 진짜 버튼과 이미지, GIF만 가지고 이쁘게 꾸몄을 텐데..

저 이거 3시간만에 한거라.. 좀 조잡하다.

그래도 너무 직관적이고 편리해서,, 나중에 다시 할 생각이다.. ㅎㅎ

 

만약 내 프로젝트가 너무 복잡하고 process를 확인하면서 해야 한다? 

코드가 한 80% 정도로 어느 정도 완성이 되면 그때부터 GUI로 하는 거 추천드립니다.

처음부터 GUI로 하지 않는 이유요..? 에러나면 절대 못찾아요,, ㅋㅋㅋㅋㅋ... print를 안해주거든요... 아이 슬퍼라...

지난번 시간에 다룬 강아지 얼굴 인식하기 2탄을 진행해보자!

원래 중간고사 끝나고 하려고 했는데, 내가 너무 공부가 하기 싫은 관계로.. 그냥 지금 올려본다..

2024.04.07 - [정보통신공학전공/캡스톤 디자인] - [Python] 강아지 얼굴 인식하고 구분하기 - 1

 

[Python] 강아지 얼굴 인식하고 구분하기

이번에는 강아지 얼굴은 인식하고 구분하는 코드를 만들어 보겠다. 원래 자동 라벨링 기능을 공부했기 때문에 이를 이용해서 지난번 캡스톤 때 공부한 Yolo를 사용할까 했으나, 같은 종을 인식시

yunji00.tistory.com

지난 번 포스팅에서는 사진에서 인식을 하는 것이었다. 글 마지막에 얘기 했듯이, 학습 데이터가 하나 뿐이라 정확도도 많이 낮고, 표정이나 각도에 따라 인지하지 못하는 것을 확인할 수 있었다.

 

이번에는 영상에서 인식하는 것을 진행하려고 한다. 우선, 후에는 웹캠을 연결해 real time으로 진행할 예정이나, 아직 주문도 안한 관계로 영상을 돌리고, 결과를 영상으로 받았다.

 

결과만 보여주자면 아래 유튜브에 올려두었다. 나름 잘한 것 같다.

https://youtube.com/shorts/S_AJKAfYSPE?feature=share

Source Code는 아래 GitHub에 계속 업데이트 하는 중이다.

https://github.com/yunjiJ00/dog_face_recognition

 

GitHub - yunjiJ00/dog_face_recognition

Contribute to yunjiJ00/dog_face_recognition development by creating an account on GitHub.

github.com

 

자 이제 코드를 설명해보도록 하겠다.

우선 얼굴 자체를 인지하는 것은 사실 별로 필요는 없어서 변형된 부분이 없다. 대신 add_dog_face부분이 조금 바뀌었는데, 지난번에 언급한 학습데이터가 적어서 생기는 부정확성을 여러 사진을 학습하는 것으로 어느 정도 해결하였다.

import cv2
import dlib
import imutils
from imutils import face_utils
import numpy as np
import matplotlib.pyplot as plt
import face_recognition
from find_dog_face import Find_dog_face
import time

face_landmark_detector_path = 'library/dogHeadDetector.dat'
face_landmark_predictor_path = 'library/landmarkDetector.dat'

detector = dlib.cnn_face_detection_model_v1(face_landmark_detector_path)
predictor = dlib.shape_predictor(face_landmark_predictor_path)

known_face = [(("images/coco1.jpg", "images/coco2.jpeg", "images/coco6.jpg", "images/coco12.jpg", "images/coco7.jpg"), "coco"),
              (("images/song2.jpg", "images/song4.jpg", "images/song5.jpg", "images/song7.jpg", "images/song9.jpg"), "song")]
class Add_dog_face:
    def __init__(self):
        self.known_face_encodings = []   
        self.known_face_names = []
        self.face_specifics = []
        self.DONE = False
    
    def add_known_face(self, known_face):
        Finding = Find_dog_face()
        target_width = 200
        name_len = len(known_face)
        
        for i in range(name_len):
            face_image_paths = []
            name = None
            face_image_paths, name = known_face[i]
            face_specific = []
            
            for face_image_path in face_image_paths:
                print(face_image_path)
                image = Finding.resize_image(face_image_path, target_width)
                dets_locations = face_locations(image, 1)
                face_encodings = face_recognition.face_encodings(image, dets_locations)
                print('face_encodings', face_encodings)

                for face_encoding, location in zip(face_encodings, dets_locations):
                    detected_face_image = draw_label(image, location, name)
                    face_specific.append(face_encoding)
                    self.known_face_encodings.append(face_encoding)
                    self.known_face_names.append(name)
            self.face_specifics.append(face_specific)
            
        np.save('library/known_faces.npy', self.known_face_encodings)
        np.save('library/known_names.npy', self.known_face_names)
        np.save('library/face_specifics.npy', self.face_specifics)

def _trim_css_to_bounds(css, image_shape):
    return max(css[0], 0), min(css[1], image_shape[1]), min(css[2], image_shape[0]), max(css[3], 0)

def _rect_to_css(rect):
    return rect.top(), rect.right(), rect.bottom(), rect.left()

def _raw_face_locations(img, number_of_times_to_upsample=1):
    return detector(img, number_of_times_to_upsample)

def face_locations(img, number_of_times_to_upsample=1):
    return [_trim_css_to_bounds(_rect_to_css(face.rect), img.shape) for face in _raw_face_locations(img, number_of_times_to_upsample)]    

def draw_label(input_image, coordinates, label):
    image = input_image.copy()
    (top, right, bottom, left) = coordinates
    cv2.rectangle(image, (left, top), (right, bottom), (0, 255, 0), 5)
    cv2.putText(image, label, (left - 10, top - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 3)
    return image

def main():
    adding = Add_dog_face()
    adding.add_known_face(known_face)
    
if __name__ == '__main__':
    main()

후에 Firebase와 연동했을 때 더 수월하게 접근하기 위한 초석을 다져두었다.

 

이렇게 해서 생성된 새로운 numpy파일을 가지고 결과를 보면, 기존 정확도 43%에서 72%로 확실히 끌어올릴 수 있었다. 또한, Unkown 횟수가 확실히 줄은 것을 확인할 수 있었다.

 

또한, 이 방법을 이용하니, 다양한 표정의 강아지 얼굴을 인지할 수 있고, 미용을 했어도, 잘 인식하는 것을 볼 수 있었다.

 

다음은 영상에서 구분해 내는 코드이다.

# 영상에서 강아지 구분하기
import cv2
import dlib
import face_recognition
import numpy as np
from find_dog_face import Find_dog_face

video_path = 'images/song2.mp4'
output_path = 'results/song2.mp4'

face_landmark_detector_path = 'library/dogHeadDetector.dat'
face_landmark_predictor_path = 'library/landmarkDetector.dat'

detector = dlib.cnn_face_detection_model_v1(face_landmark_detector_path)
predictor = dlib.shape_predictor(face_landmark_predictor_path)

class Dog_facial_recognition:
    def __init__(self, known_face_encodings, known_face_names):
        self.known_face_encodings = known_face_encodings
        self.known_face_names = known_face_names
        self.current_name = None
        self.possible_names = set(self.known_face_names)
        self.counts = {name : 0 for name in self.possible_names}
        self.detected_name = None
    
    def detection(self, video_path, output_path):
        finding = Find_dog_face()
        
        cap = cv2.VideoCapture(video_path)
        if not cap.isOpened():
            print("Error: Couldn't open the video.")
            return
        
        frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
        frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
        fps = int(cap.get(cv2.CAP_PROP_FPS))
        out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (frame_width, frame_height))

        while cap.isOpened():
            ret, frame = cap.read()
            if not ret:
                break
            
            processed_frame = self.process_frame(frame)
            out.write(processed_frame)

        cap.release()
        out.release()
        cv2.destroyAllWindows()

    def process_frame(self, frame):
        dets_locations = self.face_locations(frame)
        face_encodings = face_recognition.face_encodings(frame, dets_locations)
        
        face_names = []

        for face_encoding in face_encodings:
            matches = face_recognition.compare_faces(self.known_face_encodings, face_encoding, tolerance=0.4)
            name = "Unknown"

            face_distances = face_recognition.face_distance(self.known_face_encodings, face_encoding)
            best_match_index = np.argmin(face_distances)

            if matches[best_match_index]:
                name = self.known_face_names[best_match_index]

            face_names.append(name)
            if name in self.counts:
                self.counts[name] += 1
                if self.counts[name] > 30:
                    print(f"{name} 카운트가 넘었습니다.")
                    self.counts = {name : 0 for name in self.possible_names}
                    self.detected_name = name
                    
        # for name, count in self.counts.items():
        #     print(f"{name}: {count}")

        for (top, right, bottom, left), name in zip(dets_locations, face_names):
            if name != "Unknown":
                color = (0, 255, 0)
            else:
                color = (0, 0, 255)

            cv2.rectangle(frame, (left, top), (right, bottom), color, 1)
            cv2.rectangle(frame, (left, bottom - 30), (right, bottom), color, cv2.FILLED)
            font = cv2.FONT_HERSHEY_DUPLEX
            cv2.putText(frame, name, (left + 3, bottom - 3), font, 2, (0, 0, 0), 1)

        return frame

    def face_locations(self, img, number_of_times_to_upsample=1):
        def _trim_css_to_bounds(css, image_shape):
            return max(css[0], 0), min(css[1], image_shape[1]), min(css[2], image_shape[0]), max(css[3], 0)

        def _rect_to_css(rect):
            return rect.top(), rect.right(), rect.bottom(), rect.left()

        def _raw_face_locations(img, number_of_times_to_upsample=1):
            return detector(img, number_of_times_to_upsample)

        raw_locations = _raw_face_locations(img, number_of_times_to_upsample)
        return [_trim_css_to_bounds(_rect_to_css(face.rect), img.shape) for face in raw_locations]

def main():
    known_face_encodings = np.load('numpy/known_faces.npy')
    known_face_names = np.load('numpy/known_names.npy')
    detect = Dog_facial_recognition(known_face_encodings, known_face_names)
    detect.detection(video_path, output_path)

if __name__ == '__main__':
    main()

frame별로 나눠서 인지하도록 하였고, 정확도를 높이기 위해 count를 넣어 보았다. count가 넘으면 print를 한 다음 count를 전부 초기화 시키는 방식이다. 나중에는 이렇게 카운트가 넘었을 때, 안드로이드로 signal을 보내는 방식으로 해서 오차를 줄여볼 예정이다.

count는 실험값으로 구하고 있는데 결과가 다음과 같다.

count가 30 이상일 때부터는 거의 100% 확률로 인식해 내지만, 한번 인지하는데에 시간이 많이 걸려서 적당한 타협점을 찾고, 거기서 더 개선하는 방법을 찾아 보려고 한다. 

사실 강아지 사진 10장을 받아오는 방법도 있기는 한데.. 마지막에 마지막 수단으로 생각 중이다.

 

영상에서 인식하는 것은 이미지에서 하는 것보다 GPU를 많이 사용하기는 했다.

그래도 이정도는 나쁘지 않다고 생각하고, Jetson Nano에서도 Raspberry Pi에서도 잘 돌 것 같다.

 

그럼 오늘 강의실까지 최소 15분 걸리는데 수업 시작까지 26분 남은 관계로 오늘 포스팅은 여기서 마무리 하도록 하겠다.

이번에는 강아지 얼굴은 인식하고 구분하는 코드를 만들어 보겠다.
원래 자동 라벨링 기능을 공부했기 때문에 이를 이용해서 지난번 캡스톤 때 공부한 Yolo를 사용할까 했으나, 같은 종을 인식시키면 그리 높지 않은 정확도를 보였고, 무엇보다 생성된 pt파일을 내가 학습시키고 직접 첨부하면 모를까..  자동으로 적용시킬 방법을 찾지 못했다. 또한 정확도를 높이기 위해서는 많은 custom data가 필요한데.. 사용자에게 강아지 사진을 100장을 넣으라고는 할 수 없지 않겠는가...
그래서 적은 data로 real time으로 학습하고 인지까지 해줄 수 있는 방법을 찾다가 강아지 얼굴에 landmark를 생성하고 이 특징을 이용해 비교하는 방법을 사용하기로 했다. 일단 비교는 우리 집 코코 사진과 송이 사진으로 진행했는데, 만약 가능하다면, 같은 종 같은 나이대의 같은 색상의 강아지 사진을 이용해서 진행해볼 예정이다.
아래는 완성된 코드들을 올려둔 github이다.
https://github.com/yunjiJ00/dog_face_recognition/tree/main

 

GitHub - yunjiJ00/dog_face_recognition

Contribute to yunjiJ00/dog_face_recognition development by creating an account on GitHub.

github.com

 
이 링크를 참고해서 만들었고, 나한테 맞게 수정을 진행했다.
 
우선 참고자료와의 비교점을 얘기하자면 나는 class로 구분해서 파일을 나누었다. 참고 코드를 보면 처음 사용하는 find_dog_face 함수가 다른 기능에서는 전혀 역할을 하지 않는 것을 확인했고, IoT를 진행하는 나한테는 한정된 RAM과 GPU로 진행해야 하기에, 필요없는 기능을 밖으로 뺴버렸다.
그리고 나는 이미 기억했던 데이터를 numpy 파일로 저장하고, 판단 부분에서 다시 불러와 수행 시간과 GPU사용량을 최대로 줄였다.
아래는 내가 만든 find_dog_face.py이다.

import cv2
import dlib
import imutils
from imutils import face_utils
import numpy as np
import matplotlib.pyplot as plt
import face_recognition
import time

face_landmark_detector_path = 'dogHeadDetector.dat'
face_landmark_predictor_path = 'landmarkDetector.dat'

detector = dlib.cnn_face_detection_model_v1(face_landmark_detector_path)
predictor = dlib.shape_predictor(face_landmark_predictor_path)

target_path = 'images/song_coco10.jpg'

class Find_dog_face:
    def __init__(self):
        pass
    
    def finding(self, org_image, debug=False):
        image = self.resize_image(org_image, target_width=200)
        gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        dets = detector(gray_image, 1)
        print('Found {} faces.'.format(len(dets)))
        face_images = []
        for (i, det) in enumerate(dets):
            shape = predictor(gray_image, det.rect)
            shape = face_utils.shape_to_np(shape)
            (x, y, w, h) = face_utils.rect_to_bb(det.rect)
            face_images.append(image[y:y+h, x:x+w].copy())

            cv2.rectangle(image, (x, y), (x + w, y + h), (0, 255, 0), 2)
            cv2.putText(image, "Face #{}".format(i + 1), (x - 10, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)

            if debug:
                for (i, (x, y)) in enumerate(shape):
                    cv2.circle(image, (x, y), int(image.shape[1]/250), (0, 0, 255), -1)
                    
        self.plt_imshow(["Original", "Find Faces"], [image, image], figsize=(16,10), result_name='find_face.jpg')
        return face_images
    
    def resize_image(self, image_path, target_width=200):
        img = cv2.imread(image_path)
        height, width = img.shape[:2]
        new_height = int((target_width / width) * height)
        resized_img = cv2.resize(img, (target_width, new_height), interpolation=cv2.INTER_AREA)
        return resized_img

    def plt_imshow(self, title='image', img=None, figsize=(8 ,5), result_name='result.jpg'):
        plt.figure(figsize=figsize)

        if type(img) == list:
            if type(title) == list:
                titles = title
            else:
                titles = []

                for i in range(len(img)):
                    titles.append(title)

            for i in range(len(img)):
                plt.subplot(1, len(img), i + 1), plt.imshow(cv2.cvtColor(img[i], cv2.COLOR_BGR2RGB))
                plt.title(titles[i])
                plt.xticks([]), plt.yticks([])

            plt.show()
        else:
            plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
            plt.title(title)
            plt.xticks([]), plt.yticks([])
            plt.show()

def main():
    finding = Find_dog_face()
    finding.finding(target_path, debug=True)

if __name__ == '__main__':
    main()

위 코드를 실행하면 아래와 같이 나온다.

 
빨간 점이 찾은 landmark이다. 다른 참고자료를 찾아보면, 저 랜드마크를 이용해서 스티커나 이모티콘을 생성하기도 하는 것을 확인할 수 있었다. 이 부분은 나중에 심심하면 진행하기로 하자.
 
두번째로는 내가 사진과 이름을 한번씩 넣어주면, 이 특징점을 찾아 npy파일로 저장하는 add_dog_face.py이다.

import cv2
import dlib
import imutils
from imutils import face_utils
import numpy as np
import matplotlib.pyplot as plt
import face_recognition
from find_dog_face import Find_dog_face
import time

face_landmark_detector_path = 'dogHeadDetector.dat'
face_landmark_predictor_path = 'landmarkDetector.dat'

detector = dlib.cnn_face_detection_model_v1(face_landmark_detector_path)
predictor = dlib.shape_predictor(face_landmark_predictor_path)

known_face = [("images/coco1.jpg", "coco"), ("images/song5.jpg", "song")]

class Add_dog_face:
    def __init__(self):
        self.known_face_encodings = []   
        self.known_face_names = []
    
    def add_known_face(self, known_face):
        Finding = Find_dog_face()
        target_width = 200
        for face_image_path, name in known_face:   
            image = Finding.resize_image(face_image_path, target_width)
            dets_locations = face_locations(image, 1)
            face_encodings = face_recognition.face_encodings(image, dets_locations)

            for face_encoding, location in zip(face_encodings, dets_locations):
                detected_face_image = draw_label(image, location, name)
                self.known_face_encodings.append(face_encoding)
                self.known_face_names.append(name)
                
            Finding.plt_imshow(["Input Image", "Detected Face"], [image, detected_face_image], result_name='known_face.jpg')
        
        np.save('known_faces.npy', self.known_face_encodings)
        np.save('known_names.npy', self.known_face_names)

def _trim_css_to_bounds(css, image_shape):
    return max(css[0], 0), min(css[1], image_shape[1]), min(css[2], image_shape[0]), max(css[3], 0)

def _rect_to_css(rect):
    return rect.top(), rect.right(), rect.bottom(), rect.left()

def _raw_face_locations(img, number_of_times_to_upsample=1):
    return detector(img, number_of_times_to_upsample)

def face_locations(img, number_of_times_to_upsample=1):
    return [_trim_css_to_bounds(_rect_to_css(face.rect), img.shape) for face in _raw_face_locations(img, number_of_times_to_upsample)]    

def draw_label(input_image, coordinates, label):
    image = input_image.copy()
    (top, right, bottom, left) = coordinates
    cv2.rectangle(image, (left, top), (right, bottom), (0, 255, 0), 5)
    cv2.putText(image, label, (left - 10, top - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 3)
    return image

def main():
    # finding = Find_dog_face(target_image)
    adding = Add_dog_face()
    # finding.finding(debug=True)
    adding.add_known_face(known_face)
    
if __name__ == '__main__':
    main()

이러면 detect된 이미지들을 보여주고, numpy파일로 저장해서, 나중에 구분할 때 똑같은 작업을 또 해야 하는 것을 막아준다. GPU가 연약한 나에게 꼭 필요한 작업이었다.
위 파일을 실행하면 아래와 같이 나온다.

... 폰트 굵기 조정이 좀 필요할 것 같기는 하다..
 
다음은 이렇게 저장된 numpy파일을 불러와서 새 이미지에서 구분해 내는 dog_facial_recognition.py이다.

import cv2
import dlib
import imutils
from imutils import face_utils
import numpy as np
import matplotlib.pyplot as plt
from find_dog_face import Find_dog_face
from add_dog_face import Add_dog_face
import face_recognition
import time

face_landmark_detector_path = 'dogHeadDetector.dat'
face_landmark_predictor_path = 'landmarkDetector.dat'

detector = dlib.cnn_face_detection_model_v1(face_landmark_detector_path)
predictor = dlib.shape_predictor(face_landmark_predictor_path)

image_path = 'images/song_coco10.jpg'

class Dog_facial_recognition:
    def __init__(self):
        self.known_face_encodings = np.load('known_faces.npy')
        self.known_face_names = np.load('known_names.npy')
    
    def detection(self, image_path, size=None):
        finding = Find_dog_face()
        image = finding.resize_image(image_path, target_width=200)
        dets_locations = face_locations(image)
        face_encodings = face_recognition.face_encodings(image, dets_locations)
        
        face_names = []

        for face_encoding in face_encodings:
            matches = face_recognition.compare_faces(self.known_face_encodings, face_encoding, tolerance=0.4)
            name = "Unknown"

            face_distances = face_recognition.face_distance(self.known_face_encodings, face_encoding)
            best_match_index = np.argmin(face_distances)

            if matches[best_match_index]:
                name = self.known_face_names[best_match_index]

            face_names.append(name)

        for (top, right, bottom, left), name in zip(dets_locations, face_names):
            if name != "Unknown":
                color = (0, 255, 0)
            else:
                color = (0, 0, 255)

            cv2.rectangle(image, (left, top), (right, bottom), color, 1)
            cv2.rectangle(image, (left, bottom - 10), (right, bottom), color, cv2.FILLED)
            font = cv2.FONT_HERSHEY_DUPLEX
            cv2.putText(image, name, (left + 3, bottom - 3), font, 0.5, (0, 0, 0), 1)

        finding.plt_imshow("Output", image, figsize=(24, 15), result_name='output.jpg')
        
def _trim_css_to_bounds(css, image_shape):
    return max(css[0], 0), min(css[1], image_shape[1]), min(css[2], image_shape[0]), max(css[3], 0)

def _rect_to_css(rect):
    return rect.top(), rect.right(), rect.bottom(), rect.left()

def _raw_face_locations(img, number_of_times_to_upsample=1):
    return detector(img, number_of_times_to_upsample)

def face_locations(img, number_of_times_to_upsample=1):
    return [_trim_css_to_bounds(_rect_to_css(face.rect), img.shape) for face in _raw_face_locations(img, number_of_times_to_upsample)]

def main():
    detect = Dog_facial_recognition()
    detect.detection(image_path)

if __name__ == '__main__':
    main()

결과가 아주 만족스럽게 나오는 것을 확인할 수 있었다.

이미지를 이용해 구분해 내는 것은 아주 잘 작동하는 것을 볼 수 있었다.
모든 수행을 다했을 때, RAM과 GPU사용량은 아래와 같았다.

그러나 여기에도 단점은 있었는데, 사람 얼굴 인식보다 훨씬 적은 랜드마크 사용 때문에, 각도나 표정에 따라 구분을 못하는 경우가 있었다. 특히 옆모습인 경우에는 아예 강아지라고 인식 자체를 못하는 모습도 보았다.

예를 들면 송이는 아직 어려서 표정이 많이 없어서 항상 송이라고 판단을 했는데, 코코의 경우 웃는 얼굴과 같이 표정이 있는 얼굴은 코코라고 판단하지 못하고 Unkown을 return하기도 했다. 
 
다른 딥러닝 모델들에 비해 학습 데이터가 하나여도 된다는 장점이 있는 만큼, 단점도 확실한 듯 했다.
 
이미지를 구분해 내는 것은 여기서 마치고 다음에는 영상에서 구분해 내는 것을 진행해보도록 하겠다.
곧 시험기간이라 아마 시험 기간 후에 포스팅을 하지 않을까 싶다.

이번에는 안드로이드 스튜디오를 이용해 챗봇 만들기를 진행해 보도록 하자.

우선 이번에는 이미 여러 자료들이 있었고, 그중 한 유튜브 강의를 보면서 주로 진행했는데, 마찬가지로 2024년이 되면서 메세지 요청 방식이 조금 달라져서, 그대로 따라했다가 에러가 났었다.

이에 python으로 진행했을 때처럼 openai의 사이트를 보면서 수정했고, 성공적으로 실행되는 것을 확인할 수 있었다.

 

그럼 이제 AI 챗봇 만들기를 진행해보자.

 

우선 AndroidManifest.xml에서 인터넷 퍼미션을 추가해 주어야 한다.

<uses-permission android:name="android.permission.INTERNET" />

 

그 다음 layout을 꾸며보자.

나는 챗봇이 메인 엑티비티가 아니기 때문에 empty activity를 생성하고 ChatBot이라고 명명했다,

따라서 activity_chat_bot.xml에 들어가서 아래와 같이 진행해주자.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ChatBot">
  <androidx.recyclerview.widget.RecyclerView
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:id="@+id/chat_view"
      android:layout_above="@id/bottom_layout"
      android:layout_below="@id/doggy_dine" />
  <TextView
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:id="@+id/welcome_text"
      android:layout_centerInParent="true"
      android:text="궁금한것을 물어보세요!\n예)강아지가 사과 먹어도 돼?"
      android:gravity="center"
      android:textSize="20dp" />
  <RelativeLayout
      android:layout_width="match_parent"
      android:layout_height="80dp"
      android:layout_alignParentBottom="true"
      android:padding="8dp"
      android:id="@+id/bottom_layout">
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/meeage_edit_text"
        android:layout_centerInParent="true"
        android:hint="Write here"
        android:padding="16dp"
        android:layout_toLeftOf="@id/send_btn"
        android:background="@drawable/rounder_coner"
        />
    <ImageButton
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:id="@+id/send_btn"
        android:layout_alignParentEnd="true"
        android:layout_centerInParent="true"
        android:layout_marginStart="10dp"
        android:padding="8dp"
        android:src="@drawable/baseline_send_24"
        android:background="?attr/selectableItemBackgroundBorderless"
      />
  </RelativeLayout>
  <ImageView
      android:id="@+id/doggy_dine"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintHorizontal_bias="0.12"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent"
      app:srcCompat="@drawable/doggy_dine" />

</RelativeLayout>

 

이번에는 drawable파일을 추가해 보자.

위에서 rounder_coner.xml을 불러왔는데, 이를 위해서는 새로 생성해줘야 한다.

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <corners android:radius="10dip"/>
    <stroke android:width="1dp"/>
    <solid android:color="@color/white"/>
</shape>

이제 EditText 부분이 둥글게 생기게 되었을 것이다.

 

다음은 채팅에서 기본인 chat box를 생성해 보자.

이를 위해서는 새로 layout을 생성해 주어야 한다. 나는 chat_item.xml을 생성해 아래와 같이 코드를 넣어주었다.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="8dp">
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/left_chat_view"
        android:background="@drawable/rounder_coner"
        android:backgroundTint="#673AB7"
        android:padding="8dp"
        android:layout_marginEnd="80dp">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/left_chat_text_view"
            android:textColor="@color/white"
            android:textSize="18sp"
            />
    </LinearLayout>
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/right_chat_view"
        android:layout_alignParentEnd="true"
        android:background="@drawable/rounder_coner"
        android:backgroundTint="#1F97F6"
        android:padding="8dp"
        android:layout_marginStart="80dp">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/right_chat_text_view"
            android:textColor="@color/white"
            android:textSize="18sp"
            />
    </LinearLayout>
</RelativeLayout>

그러면 이제 bot이 보내는 메세지는 왼쪽에, 내가 보내는 오른쪽에 위치하도록 되었을 것이다.

 

이번에는 새 java class를 생성해서 message를 잘 처리하도록 하자.

나는 Message.java를 생성해서 아래와 같이 코드를 넣어주었다.

package com.example.doggydine;

public class Message {
    public static String SENT_BY_ME = "me";
    public static String SENT_BY_BOT="bot";
    String message;
    String sentBy;
    public String getMessage(){
        return message;
    }
    public void setMessage(String message){
        this.message = message;
    }
    public String getSentBy(){
        return sentBy;
    }
    public void setSentBy(String sentBy){
        this.sentBy = sentBy;
    }
    public Message(String message, String sentBy) {
        this.message = message;
        this.sentBy = sentBy;
    }
}

 

이번에는 MessageAdapter.java를 생성해서 아래와 같이 넣어주었다.

package com.example.doggydine;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import java.util.List;

public class MessageAdapter extends RecyclerView.Adapter<MessageAdapter.MyViewHolder>{
    List<Message> messageList;
    public MessageAdapter(List<Message> messageList){
        this.messageList = messageList;
    }
    @NonNull
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View chatView = LayoutInflater.from(parent.getContext()).inflate(R.layout.chat_item,null);
        MyViewHolder myViewHolder =new MyViewHolder(chatView);
        return myViewHolder;
    }
    @Override
    public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
        Message message = messageList.get(position);
        if (message.getSentBy().equals(Message.SENT_BY_ME)) {
            holder.leftChatView.setVisibility(View.GONE);
            holder.rightChatView.setVisibility(View.VISIBLE);
            holder.rightTextView.setText(message.getMessage());
        }else{
            holder.rightChatView.setVisibility(View.GONE);
            holder.leftChatView.setVisibility(View.VISIBLE);
            holder.leftTextView.setText(message.getMessage());
        }
    }
    @Override
    public int getItemCount() {
        return messageList.size();
    }
    public class MyViewHolder extends RecyclerView.ViewHolder{
        LinearLayout leftChatView, rightChatView;
        TextView leftTextView, rightTextView;
        public MyViewHolder(@NonNull View itemView){
            super(itemView);
            leftChatView = itemView.findViewById(R.id.left_chat_view);
            rightChatView = itemView.findViewById(R.id.right_chat_view);
            leftTextView = itemView.findViewById(R.id.left_chat_text_view);
            rightTextView = itemView.findViewById(R.id.right_chat_text_view);

        }
    }
}

위의 코드들은 만약 메세지 박스 안에 메세지가 길어지면 자동으로 줄바꿈이 되고, 누가 보내는지에 따라 왼쪽에 배치할 것인지 오른쪽에 배치할 것인지 등을 결정하게 해준다.

 

다음으로 build.gradle(Module)에 들어가서 dependencies에 okhttp 라이브러리를 넣어주었다.

implementation("com.squareup.okhttp3:okhttp:4.12.0")

 

자 이제 마지막으로 핵심인 ChatBot.java를 완성해 보자.

package com.example.doggydine;

import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;

import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

public class ChatBot extends AppCompatActivity {
    RecyclerView recyclerView;
    TextView welcomeTextView;
    EditText messageEditText;
    ImageButton sendButton;
    List<Message> messageList;
    MessageAdapter messageAdapter;
    public static final MediaType JSON = MediaType.get("application/json");
    OkHttpClient client;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_chat_bot);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });
        messageList = new ArrayList<>();
        recyclerView = findViewById(R.id.chat_view);
        welcomeTextView = findViewById(R.id.welcome_text);
        messageEditText = findViewById(R.id.meeage_edit_text);
        sendButton = findViewById(R.id.send_btn);
        messageAdapter = new MessageAdapter(messageList);
        recyclerView.setAdapter(messageAdapter);
        LinearLayoutManager llm = new LinearLayoutManager(this);
        llm.setStackFromEnd(true);
        recyclerView.setLayoutManager(llm);
        sendButton.setOnClickListener((v)->{
            String question = messageEditText.getText().toString().trim();
            addToChat(question, Message.SENT_BY_ME);
            messageEditText.setText("");
            callAPI(question);
            welcomeTextView.setVisibility(View.GONE);
        });
        client = new OkHttpClient().newBuilder()
                .connectTimeout(60, TimeUnit.SECONDS)
                .writeTimeout(120, TimeUnit.SECONDS)
                .readTimeout(60, TimeUnit.SECONDS)
                .build();
    }
    void addToChat(String message, String sentBy){
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                messageList.add(new Message(message, sentBy));
                messageAdapter.notifyDataSetChanged();
                recyclerView.smoothScrollToPosition(messageAdapter.getItemCount());
            }
        });
    }
    void addResponse(String response){
        messageList.remove(messageList.size()-1);
        addToChat(response, Message.SENT_BY_BOT);
    }

    void callAPI(String question) {
        messageList.add(new Message("... ", Message.SENT_BY_BOT));
        JSONArray arr = new JSONArray();
        JSONObject baseAi = new JSONObject();
        JSONObject userMsg = new JSONObject();
        try{
            baseAi.put("role", "user");
            baseAi.put("content", "You are sweet and bright AI Assistant.");
            userMsg.put("role", "user");
            userMsg.put("content", question);
            arr.put(baseAi);
            arr.put(userMsg);
        }catch (JSONException e){
            throw new RuntimeException(e);
        }

        JSONObject object = new JSONObject();
        try{
            object.put("model", "gpt-3.5-turbo");
            JSONObject messageObj = new JSONObject();
            messageObj.put("role", "user");
            messageObj.put("content", question);
            JSONArray messagesArray = new JSONArray();
            messagesArray.put(messageObj);
            object.put("messages", messagesArray);
        }catch (JSONException e) {
            e.printStackTrace();
        }

        RequestBody body = RequestBody.create(object.toString(), JSON);
        Request request = new Request.Builder()
                .url("https://api.openai.com/v1/chat/completions")
                .header("Authorization", "Bearer MY_SECRET_KEY")
                .post(body)
                .build();

        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(@NonNull Call call, @NonNull IOException e) {
                addResponse("Failed to load response due to "+e.getMessage());
            }

            @Override
            public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
                if(response.isSuccessful()){
                    JSONObject jsonObject = null;
                    try {
                        jsonObject = new JSONObject(response.body().string());
                        JSONArray jsonArray = jsonObject.getJSONArray("choices");
                        String result = jsonArray.getJSONObject(0).getJSONObject("message").getString("content");
                        addResponse(result.trim());
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                }else{
                    addResponse("Failed to load response due to "+response.body().string());
                }
            }
        });
    }
}

 

여기까지 진행해주면, 아래와 같이 이쁘게 AI chatbot과 대화가 가능해진다...!!!

2024년 3월 기준으로 모든 버전을 최신으로 업데이트 한 코드이니, 만약 openai에서 추가 업데이트를 진행하지 않는 이상 잘 작동할 것이다..!!

다음에는 프롬프트 엔지니어링을 공부하고 이 챗봇에 적용시켜보겠다!

오랜만에 쓰는 블로그에는 제일 먼저 Python으로 AI챗봇 만들기를 진행해 보겠다!

이번에 진행하는 캡스톤 디자인에서 챗봇 구현을 맡았는데, 마지막에는 프롬프트 엔지니어링까지 진행할 계획이다.

그러기 위해서는 우선 간단한 챗봇을 구현해보기 위해 python으로 진행했다.

 

chatGPT API를 받아와서 진행할 건데, 사실 api-key 받아오는 거는 어렵지 않게 진행할 수 있었지만, 2024년이 되면서 openai의 메세지 요청 방식이 달라졌고, 이를 잘 반영하고 있는 자료가 많이 없었다...

그래서 openai에서 주는 안내를 보면서 코드를 짰고, 잘 작동하는 지까지 확인을 했다.

아래는 openai의 chat completion이다.

https://platform.openai.com/docs/guides/text-generation/chat-completions-api

 

자 이제 코드를 짜보자!!

우선 openai 라이브러리를 설치해줘야 한다.

pip install openai

아주 간단하다..

자 이제 파이썬 코드로 넘어가자.

from openai import OpenAI
api_key = 'MY_API-KEY'

# new openai 반영.
client = OpenAI()
client.api_key = api_key

def ask_openai(question, chat_log=None):
    if chat_log is None:
        chat_log = []
    messages = [{"role": "user", "content": msg} for msg in chat_log]
    messages.append({"role": "user", "content": question})
    # new openai 반영.
    response = client.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=messages
    )
    # new openai 반영.
    answer = response.choices[0].message.content
    return answer

def chat():
    print("chatting is started. If you want to exit, enter 'exit'.")
    chat_log = []
    while True:
        user_input = input("User: ")
        if user_input.lower() == 'exit':
            print("Exiting...")
            break
        response = ask_openai(user_input, chat_log)
        print(f"AI bot: {response}")
        chat_log.extend([user_input, response])

if __name__ == "__main__":
    chat()

올해 새로 적용된 openai 사이트를 보면서 만든 부분은 # new openai 반영. 이라고 표시해 두었다!

 

openai에서 제공하는 api는 무료다. 그렇지만, 계정을 가입하고 한달동안 무료인거라 나는 아주 옛날에 만료가 되어버렸다... api 제공을 위해서는 휴대폰 본인인증이 필요해서 처음에는 랩실 폰으로 가입해서 무료로 쓸까 하다가 어차피 한달 뒤 만료인데, 나는 3달을 더 써야하니 그냥 5달러 결제했다.. 과금 안되도록 조심해야 한다.

 

아래는 위 코드를 돌려서 AI와 대화해 본 것이다! 아주 친절하게 잘 된 것 같다.

지난번에 Unity에서 ROS로 topic을 publish해주고 파이썬 코드를 이용해 띄워보는 것까지 했다.

그런데 문제가 하나 생겼는데.. UNIYT에서 이미지를 Publish해줄때, Message type이 Compressed Image로 고정이 되어 있다는 점이다.

이게 뭐가 문제이냐 하면, Compressed Image는 rviz에도, rqt에도 띄울 수가 없다.

Compressed Image를 이용해 파이썬에서 코드를 돌리고 딥러닝을 돌리는 것에도 문제가 없지만, 파이썬 코드에서 여러대의 카메라 Subscribe를 받고 여러 window를 띄우게 되면.. 내 노트북이 너무 아파해서 문제가 되었다.. 또한, 그 외에 여러 기능들을 사용하지 못한다는게 너무 아쉬워서 메세지 타입을 바꿔주는 코드를 만들었다.

물론 Publish를 할 때 Image message type을 바꾼채로 보낼 수 있지 않을까? 라는 생각으로 script를 수정해보았으나.. message type을 변경하고 run을 하면 unity내에서 계속 에러가 나는 바람에 포기했다...

#!/usr/bin/env python
#-*-coding:utf-8-*-

import rospy
import cv2
import numpy as np
from sensor_msgs.msg import Image, CompressedImage
from cv_bridge import CvBridge

class ImageConverter:
    def __init__(self):
        self.cameras = {
            'front': {'pub': rospy.Publisher('/converted/front_cam', Image, queue_size=1),
                      'sub': rospy.Subscriber('/camera/front_cam', CompressedImage, partial(self.callback_compressed_image, 'front'))},
            'back': {'pub': rospy.Publisher('/converted/back_cam', Image, queue_size=1),
                     'sub': rospy.Subscriber('/camera/back_cam', CompressedImage, partial(self.callback_compressed_image, 'back'))},
            'left': {'pub': rospy.Publisher('/converted/left_cam', Image, queue_size=1),
                     'sub': rospy.Subscriber('/camera/left_cam', CompressedImage, partial(self.callback_compressed_image, 'left'))},
            'right': {'pub': rospy.Publisher('/converted/right_cam', Image, queue_size=1),
                      'sub': rospy.Subscriber('/camera/right_cam', CompressedImage, partial(self.callback_compressed_image, 'right'))}
        }
        self.bridge = CvBridge()

    def callback_compressed_image(self, camera_name, data):
        image = self.bridge.compressed_imgmsg_to_cv2(data)
        image_msg = self.bridge.cv2_to_imgmsg(image, encoding="bgr8")
        image_msg.header = data.header
        self.cameras[camera_name]['pub'].publish(image_msg)

def main():
    rospy.init_node('camera_input')
    image_converter = ImageConverter()
    rospy.spin()

if __name__ == '__main__':
    main()

Compressed Image로 들어오는 이미지를 바로 Image로 변환해서 다시 Subscribe를 해주는 코드이다.

callback함수를 여러개 만들자니 너무 지저분해서 딕셔너리 형태로 구성했다.

 

그리하여 아래의 rqt_graph와 같이 토픽을 잘 주고받을 수 있게 되었다.

'자율사물캡스톤디자인 > 알고리즘 정리' 카테고리의 다른 글

Yolo object tracking  (0) 2023.11.15

확률 및 랜덤 프로세스 6, 7장 - 통계, 추론

+ Recent posts