피피 - 인공지능 소변분석 웹서비스 프로젝트

피피의 시작

서버 그룹 스터디를 마무리할 때쯤 학교측에서 서버를 제공해서 이용할 수 있을 때 많이 써먹어보고 싶어서 만들기로 한 것은 소변 색으로 건강을 분석해주는 프로그램. 한창 웹으로 뭔가를 서비스해보고 싶었는데 재미있겠다 싶었다. 나는 뭔가를 만들 때 이름부터 짓는다. 소변분석기...같은 식상한 이름보다는 Pee(소변)를 두번 말해서 은근히 귀여운 피피로 지어서 유저에게 거부감을 그나마 적게 주고 싶었다.

우선 제작에 들어가는 기술들을 나열하자면,

웹을 위한 HTML/CSS/JS 삼형제, 파이썬과 웹 연동을 위한 Flask, 이미지 처리를 위한 opencv, 웹 서비스를 위한 nginx, 배포를 위한 docker를 사용했다. 서버는 학교측에서 빌려준 NHN Toast Cloud 사용.

프론트엔드 - 보기 좋은 떡이 먹기에도 좋다

디자인에 원래 신경을 많이 썼지만 이번에는 더더욱 신중을 가했다. 본격적으로 서비스할 계획은 없지만, 서비스 하더라도 사용자가 거부감을 느끼지 않게 세련되고 귀엽게 디자인하려고 노력했다.


프론트엔드 구상은 간단했다. 사진을 form 메소드를 통해서 submit받고, 그 사진을 벡엔드로 넘기고 다시 그 결과(소변의 type)를 프론트로 받아오면 되는 것이었다. 위에서 사진을 업로드하고 업로드/분석 버튼을 누르면,


이렇게 받아온 분석 결과를 숫자로 받아 (사진의 경우는 5) 자바스크립트에 선언해놓은 문자열을 출력하면 된다. 계획짜는 것은 역시 재밌고 흥미롭다. 머리에서 떠오르는 생각대로 뭐든지 술술 될 것 같다. 코드 작성을 시작하기 전까지는..

프로그램 알고리즘 끄적이기

우선 프로젝트 폴더의 파일 구성은 다음과 같다.


프로그램은 다음 순서로 돌아가면 된다.

1. main.py에서 flask 기반의 웹을 구동하고 index.html으로 메인화면을 렌더링한다.
2. 메인화면에서 사진을 업로드하면 파이썬 라이브러리 opencv를 사용하여 이미지의 색상을 추출한다.
3. 픽셀 값을 기준으로 소변의 타입을 나눈다.
4. 소변의 타입을 result.html로 데이터와 함께 렌더링한다.

 + Dockerfile을 작성해서 도커로 배포도 쉽게 해보고 싶었다.

여기서 의문이 드는 점은,

1. html이랑 python의 데이터(함수의 리턴값)를 어떻게 주고 받을건데?
2. 사진마다 소변 영역의 부분이 다를텐데 어떻게 맞출건데?
3. 소변 색상의 타입은 어떻게 나눌건데?

자문과 검색을 통해 알아낸 답은,

1. json 타입의 데이터를 렌더링, 함수 호출 시 사용한다.
2. 사진의 중간 100px * 100px 부분만 추출하여 opencv 처리한다.
3. "11가지 소변의 색으로 알아보는 내 건강상태"라는 기사가 있다. (https://news.joins.com/article/21381482)

결국 핵심적인 코드는 opencv 처리와 이미지 rgb값마다 타입을 나누는 코드이다.


우선 파일 업로드부터 시작하자

확장자까지 검사하고 싶었다. 이미지 파일만 받을 수 있게!

ALLOWED_EXTENSIONS = set(['jpg''jpeg''png''gif''bmp''JPG''JPEG''PNG''GIF''BMP'])
app = Flask(__name__)
# 업로드 HTML 렌더링
@app.route('/')
def upload():
    return render_template('index.html')
# 파일 업로드 처리
@app.route('/fileUpload', methods=['GET''POST'])
def upload_file():
    if request.method == 'POST':
        file = request.files['file']
        #확장자 이미지파일인 경우
        if file and allowed_file(file.filename):
            file.save('uploads/' + 'image.' + 'jpg')
            return redirect(url_for('run_anal'))
        #확장자 이미지파일 아닐 경우
        return render_template('index.html', data="이미지 파일만 업로드하세요.")
#파일 확장자 검사
def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.'1)[1in ALLOWED_EXTENSIONS
cs

이제 opencv 처리 코드를 작성하자

이미지 파일을 rgb값으로 추출 처리 후 중간의 100px*100px만 dst에 copy한 후 가장 많이 쓰인 rgb값을 col값으로 리턴한다.

#CV2 처리
def load_n_crop():
    img_bgr = cv2.imread("uploads/image.jpg", cv2.IMREAD_COLOR)
    img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
    # crop the image    x, y, channel = img_bgr.shape
    x = int(x / 2)  # width    y = int(y / 2)  # height    dst = img_rgb.copy()
    dst = dst[(x - 50):(x + 50), (y - 50):(y + 50)]
 
    # use K-mean algorithm to find mean value of colors    new_rgb = dst.reshape((dst.shape[0] * dst.shape[1], 3))
    clt = KMeans(n_clusters=1)
    clt.fit(new_rgb)
 
    col = clt.cluster_centers_.astype("uint8").flatten().tolist()
    return col
cs

rgb값을 타입으로 분류하자

이 부분이 가장 시간이 오래 걸리고 골치 아팠다. 그래도 왠만한 소변의 색상은 다 구분할 수 있게 rgb 중 r에 따라 세세하게 경우를 나눴다.

#rgb에 따른 소변유형 분류
def calc_type(color):
    r, g, b = color
    result = 0
    if r >= 200 and r <= 255:
        if g >= 200 and g <= 225 and b >= 200 and b <= 225:
            result = 1
        elif g >= 226 and g <= 240 and b >= 160 and b <= 199:
            result = 2
        elif g >= 180 and g <= 199 and b >= 100 and b <= 159:
            result = 3
        elif g >= 180 and g <= 220 and b >= 70 and b <= 100:
            result = 4
        elif g >= 180 and g <= 220 and b >= 40 and b < 70:
            result = 5
        elif g >= 120 and g <= 160 and b >= 100 and b <= 140:
            result = 7
        elif g >= 150 and g <= 190 and b >= 100 and b <= 130:
            result = 8
    elif r >= 150 and r <= 199:
        if g >= 140 and g <= 180 and b >= 100 and b <= 140:
            result = 6
        elif g >= 170 and g <= 200 and b >= 120 and b <= 150:
            result = 9
    elif r >= 100 and r <= 150:
        if g >= 90 and g <= 140 and b >= 80 and b <= 130:
            result = 1
        elif g > 113 and g <= 140 and b >= 0 and b <= 40:
            result = 2
        elif g >= 60 and g <= 113 and b >= 0 and b <= 40:
            result = 3
        elif g >= 90 and g <= 130 and b >= 40 and b <= 90:
            result = 4
        elif g >= 90 and g <= 130 and b >= 20 and b < 40:
            result = 5
    return result
cs

HTML과 파이썬을 연동하자

jinja2로 렌더링하면 타입의 값만 데이터로 보내면 된다.

#분석 실행
@app.route('/mod')
def run_anal():
    result = calc_type(load_n_crop())
    return render_template('result.html', peeValue=result)
cs

이제 결과를 표시하는 result.html에서 javascript를 작성한다.

    <script>
        var peeValue = {{peeValue|tojson}};
        function printResult() {
            var typeText = document.getElementById('pPeeType');
            var resText = document.getElementById('pRes');
            var div = document.getElementById('divRes');
            if (peeValue > 9 || peeValue < 0) {
                resText.innerHTML = "파일 분석 중 오류가 발생했습니다.";
                return false;
            }
            typeText.innerHTML = "소변 타입: " + typeWord[peeValue];
            resText.innerHTML = res[peeValue];
            div.style.background = colorSet[peeValue];
        }
    </script>
cs

{{ value || tojson }}을 통해 렌더링할 때 데이터를 json으로 넘겨받을 수 있다. 이 부분에서 한참을 검색했다. 원래는 js 파일로 따로 분류하고 싶었는데, 그러면 렌더링할 때 문제가 생겼다..

코드를 정리하고 css, js까지 다듬고 나니 그럴듯한 소변 분석기가 만들어졌다.

이제 배포를 하려고 하다가 nginx, flask, uwsgi가 한번에 세팅된 도커이미지를 찾아서 도커 공부한거 좀 써먹어보자 생각했다. 서버는 학교측에서 빌려준 NHN Toast 서버를 활용했다. 써먹을 수 있을 때 써먹자!

아 여기서 uwsgi는 웹과 flask 애플리케이션을 연결해주는 역할을 한다. flask로 구현된 파이썬 언어를 웹 프레임워크(nginx)에서는 무슨 말인지 알아 듣지 못하기 때문.

nginx-flask-uwsgi 도커이미지를 활용한 서비스 과정은 따로 포스팅해뒀다. 참고하면 나처럼 flask 웹을 서비스하고 싶은 초보자에게 많은 도움이 될 것 같다. (http://zinirun.blogspot.com/2020/02/docker-flask-nginx-uwsgi.html)

Dockerfile을 작성한다.

FROM tiangolo/uwsgi-nginx-flask:python3.7
COPY ./app /app
RUN pip install -r requirements.txt
cs

간단하다. app 호스트 디렉토리의 파일을 도커의 os(우분투)로 복사하고, 파이썬을 requirements.txt를 참고하여 인스톨한다. requirements.txt에는 이번 flask에 쓰였던 라이브러리들이 포함되어 있다.

opencv-python
scikit-learn
cs

다른 라이브러리는 도커 이미지에 포함되어 있다.
이제 docker image를 만들고 본격적으로 서비스한다.

docker build -t pp-app .
docker run -d --name pp-app -p 80:80 pp-app:latest
cs
도커 이미지를 성공적으로 만들었고 호스트의 80포트와 도커의 80포트(nginx 프레임워크는 기본적으로 80포트 사용!)를 연결하고 백그라운드 실행했다.


다 했다!

들뜬 마음으로 브라우저로 호스트의 주소를 입력한다.



내 소변은 정상이란다. 다행이다

일주일간 새벽까지 프론트 만지고 백 만지느라 고생한 친구와 나에게 박수.
성공적으로 우리끼리의 프로젝트를 끝냈다. 역시 뭔가 공부하면 써먹고 만들어봐야 제대로 공부한 느낌이다.

이번 코드를 써먹으면 다른 이미지 처리에 대한 서비스도 수월하게 할 수 있을 것 같다.

프로젝트 폴더는 Github에 업로드해두었다.

Github Repository: https://github.com/zinirun/PeePee-App

댓글 없음:

Powered by Blogger.