[프로젝트] 안면 인식 기반 추천 프로그램 - (6)

julian·2025년 7월 11일

python

목록 보기
65/74
post-thumbnail

1. 📌 주요 변경사항

이 프로젝트의 마지막 단계인 음식점 추천 부분에 대한 코드만 남은 상태여서 이 추천 시스템을 어떤식으로 작동하게 할지에 대한 고민을 상당히 많이 하게됐다.

처음 프로젝트 구상단계에서 네이버 지도나, 카카오 맵에서 정보를 얻어서 TOP 5를 제공한다고 했었다.

그런데 몇가지 걸리는 점이 있었다.

  1. API 연동을 통한 real time으로 제공 -> 유료
  2. 키워드 기반 검색 -> 크롤링? -> non real time -> 무료 -> DB에 매일 정보 갱신?
  3. 인스타? -> 해시태그 크롤링 -> 검색 후 추천?
  4. 작은 규모이기 때문에 무료로라도 API를 사용해보는?
  5. 광고성 글에 대한 문제

이런 고민을 하다보니 한가지 의문점이 들었다.
그냥 키워드 기반 검색으로 한다고 가정했을 때, 아래와 같은 흐름일 것이다.

20대 남성과 20대 여성 예측 -> 커플 -> "분위기 좋은 맛집" -> 키워드 검색

그런데 과연 이게, 그냥 네이버 지도 앱을 켜서 분위기 좋은 맛집을 검색하는 것보다 좋은 점이 한가지로 있는가?

아무리 생각해봐도 너무 의미가 없는 거 같다고 생각했다.
단 하나, 이런 저런 기술들을 접목해보고 연습 해봤다는 그런 의미밖에 없다고 생각했다.

그래서 결론적으로 목표를 바꾸자고 마음먹었다.

지금 성별과 연령대 예측 기능까지는 만들어져있다.
이를 잘 활용한다면 의미있게 사용할만하다고 느꼈다.

그렇게 결정하된 시스템은,

2. 스마트 매장 분석 및 메뉴 최적화 시스템

  • 카페에 키오스크가 있다고 가정한다.
  • 방문자가 카페에 방문해 음료를 주문할 때 키오스크에 카메라가 작동한다.
  • 이 카메라를 통해 정보를 수집해 DB에 저장한다.
    • id(주문번호)
    • timestamp
    • 성별
    • 연령대
    • 메뉴
    • 수량
  • 이 DB 정보를 통해 다음과 같은 전략을 짠다.
    • "오후 6시에 10대들이 학교 끝나고 카페에 들려 주로 뭘 시키는가?"
    • 쿼리문을 통해 DB에서 결과를 수집(COUNT)한다.
      아메리카노 200잔  
      카페라떼 150잔  
      ...  
      녹차라떼 7잔  
    • 오후 6시에는 10대들을 위해 가격이 싸고 판매량이 많은 아메리카노를 키오스크에서 가장 첫번째로 배치시킨다.
    • 잘 팔리지 않는 녹차라떼를 그 시간대에 학생들을 위해 할인하며 추천 메뉴로 넣는다.

이처럼 메뉴판 순서 자동 최적화, 가장 잘 팔리는 메뉴 상단 배치, 연령+시간대 조합별 베스트메뉴 분석 및 추천 메뉴 배치 등 다양하게 매장 마케팅에 응용이 가능해보인다고 생각했다.

그래서 전체적으로 보면 웹앱을 하나 만들 생각이다.
0. 접속 후
1. 예를들어 기록시작, 기록중지, 분석 이라는 세가지 버튼을 두고
2. 영업 시작과 동시에 기록시작 버튼을 통해 실시간으로 감지하며 DB에 기록을 시작한다.
3. 하루 영업이 마무리된다면 기록중지를 버튼을 통해 기록을 중지한다.
4. 통계량 등 비교 분석을 할때는 분석 버튼을 통해 확인한다.

이런 흐름을 구상하고 있다.

따라서 가장 먼저 데이터베이스부터 세팅을 진행해보자.

3. Database

SQLite DB Browser를 이용해서 진행해보고자 한다.

다운받고나서 DB Browser for SQLite.exe 를 실행한다.

이후 새 데이터베이스를 클릭하고서 작업 중인 vscode 루트에 database 폴더를 만들고 visitor_logs.db 파일을 생성한다.

생성을 완료하고 나면 테이블을 정의해줘야 하는데 GUI툴을 이용하면 CREATE TABLE을 통해 Field 들을 직접 작성하지 않고 클릭 몇번 만으로도 편하게 할 수 있다.

  • id: 주문 식별자 (PK + 자동 증가)
  • timestamp: 일시 (YYYY-MM-DD HH:MM:SS)
  • gender: 성별
  • age_group: 연령대
  • menu_name: 메뉴명
  • quantity: 수량

이렇게 생성완료했다면 이제 vscode에서 test폴더를 만들고 insert가 잘 되는 지 확인해보자.

3.1. Insert

# DB 입력 테스트
import os
import sqlite3
from datetime import datetime

base_dir=os.path.dirname(os.path.abspath(__file__))
db_path=os.path.join(base_dir, "../database/visitor_logs.db")

conn=sqlite3.connect(db_path)
cursor=conn.cursor()

# 테스트 데이터
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
gender="male"
age_group="20s"
menu_name="아메리카노"
quantity=2
# INSERT 문
cursor.execute("""
    INSERT INTO visitor_logs (timestamp, gender, age_group, menu_name, quantity)
    VALUES (?, ?, ?, ?, ?)
""", (timestamp, gender, age_group, menu_name, quantity))

# 저장 및 종료
conn.commit()
conn.close()

print("입력완료")
입력완료

이후 DB Browser에서 쿼리문을 던져보면 다음과 같이 나온다.

4. 흐름

자 이제 흐름을 정리하자면 다음과 같다.

  1. 사용자가 키오스크에서 주문하기 버튼 클릭
  2. 웹캠 ON
  3. 3초 카운트다운 -> 얼굴 인식 및 캡처
  4. 성별+연령대 예측
  5. 메뉴 선택
  6. 주문 완료 -> 예측 정보 + 메뉴 정보 DB 저장
  7. 웹캠 종료 + 초기화 상태

따라서 카메라 연동과 예측 결과 DB 저장을 연결하는 작업부터 진행하면 된다.
그 전에 구조부터 조금 손보자.

  📦ADFOU  
   ┣ 📂data  
   ┃ ┣ 📂processed  
   ┃ ┗ 📂raw  
   ┣ 📂notebooks  
   ┃ ┣ 📂data_preprocessing  
   ┃ ┣ 📂model_pred  
   ┃ ┗ 📂model_training  
   ┣ 📂src  
   ┃ ┣ 📂models  
   ┃ ┃ ┗ 📜09_efficientnetb0_best.pth  
   ┃ ┣ 📜db_utils.py  # DB 저장 함수
   ┃ ┗ 📂results  
   ┣ 📂frontend  # 버튼, 페이지 등
   ┃ ┣ 📜analyze.html  
   ┃ ┣ 📜index.html  
   ┃ ┣ 📜order_done.html  
   ┃ ┗ 📜order.html
   ┣ 📂backtend  # 이벤트 처리
   ┃ ┗ 📜routers.py  
   ┣ 📂database  
   ┃ ┗ 📜visitor_logs.db  
   ┣ 📂test  
   ┃ ┗ 📜test_insert.py  
   ┣ 📂webcam_app  
   ┃ ┣ 📂result  
   ┃ ┃ ┣ 📜captured_face.jpg  
   ┃ ┃ ┗ 📜prediction.txt  
   ┃ ┣ 📂test  
   ┃ ┃ ┣ 📂image  
   ┃ ┃ ┣ 📂result  
   ┃ ┃ ┗ 📜run_test_image.py  
   ┃ ┣ 📂utils
   ┃ ┃ ┣ 📜image_utils.py  
   ┃ ┃ ┗ 📜__init__.py  
   ┃ ┣ 📜predictor.py  # 성별+연령대 예측
   ┃ ┣ 📜face_capture.py  # 3초 타이머 -> 얼굴 캡처
   ┃ ┗ 📜__init__.py  
   ┣ 📜create_db.py  # db 생성용 파일
   ┗ 📜main.py  # 메인 실행 파일
   

4.1. src/db_utils.py

먼저 log 기록용 함수를 src/db_utils.py 로 하나 만들어놓자.

import sqlite3

def insert_visitor_log(db_path, table_name, timestamp, gender, age_group, menu_name, quantity):
    conn=sqlite3.connect(db_path)
    cursor=conn.cursor()

    cursor.execute(f"""
        INSERT INTO "{table_name}" (timestamp, gender, age_group, menu_name, quantity)
        VALUES (?, ?, ?, ?, ?)
    """, (timestamp, gender, age_group, menu_name, quantity))

    conn.commit()
    conn.close()

4.2. webcam_app/face_capture.py

기존 run_webcam.py 였던 파일이 실시간 감지가 아니기 때문에 face_capture.py라고 변경하였고, 지금 스크립트로 동작하는 구조에서 flask에서 import해서 사용하기 위해 함수형으로 바꿨다. 크게 코드는 변경된 부분이 없으며 대신 predict_face로 return 받는 변수들 부분만 조금 변경되었다.

...
...

def capture_face_from_webcam():
    # 웹캠 시작
    cap=cv2.VideoCapture(0)
    ...
    ...
    while True:
        ret, frame=cap.read()
        if not ret:
            break
        # 좌우 반전 복원
        frame=cv2.flip(frame, 1)
        # 예측 및 얼굴 감지
        result_img, sex_text, age_text, label_text, has_face=predict_face(frame.copy())
        result_img_with_countdown=result_img.copy()  # 숫자 덧씌울 용
        ...
        ...
    # 자원 해제
    cap.release()
    cv2.destroyAllWindows()
    # 예측 결과 반환
    return sex_text, age_text

4.3. webcam_app/predictor.py

이 redictor.py 에서도 앞선 4.2.에서 언급한던 부분과 동일하게 return값 부분을 신경써줘야한다.

...
...
# 클래스 예측 ==========================================================================
# -> image_utils
def predict_face(image):
    bboxes=detect_faces(image)
    has_face=bool(bboxes)  # 얼굴 탐지 여부

    # 예측값 return용
    predictions=[]

    for box in bboxes:
        x, y, w_box, h_box=box
        face=image[y:y+h_box, x:x+w_box]
        if face.size == 0:
            continue
        
        # 전처리 및 예측
        input_tensor=preprocess_face_rgb(face).to(device)
        with torch.no_grad():
            outputs=model(input_tensor)
            preds=torch.sigmoid(outputs).cpu().numpy()[0]

        # 성별 예측
        sex_preds=preds[:2]
        sex_label=np.argmax(sex_preds)
        sex_text="male" if sex_label==0 else "female"

        # 연령대 예측
        age_preds=preds[2:]
        age_label=np.argmax(age_preds)
        age_text=LABEL_COLS[2:][age_label]

        # 저장
        label_text=f"{'남성' if sex_text == 'male' else '여성'}, {age_text}대"
        predictions.append((sex_text, age_text, label_text))  # 튜플로 저장
        image=draw_prediction(image, box, label_text)

    if predictions:
        sex, age, label=predictions[0]
        return image, sex, age, label, has_face
    else:
        return image, "Unknown", "Unknown", "Unknown", False

4.4. create_db.py

이제 구상하고 있는 테이블은 다음과 같기에,

  • master: 전체 기록
  • 2025master: 2025년 전체 기록
  • 202501~202512: 2025년 월별 기록

create_db.py 라는 파일을 하나 만들어서 원하는 년도를 입력하면 해당 구조를 가진 테이블을 생성하도록했다.

먼저 테이블 구조는 다음과 같이 정의했다.

# tb 정의
def create_table(cursor, table_name):
    cursor.execute(f"""
        CREATE TABLE IF NOT EXISTS "{table_name}" (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            timestamp TEXT,
            gender TEXT,
            age_group TEXT,
            menu_name TEXT,
            quantity INTEGER
        )
    """)

그리고 initialize하는 함수는 다음과 같다.

# db 초기화
def initialize_db(year, create_yearly=True, create_monthly=True):
    DB_DIR="database"
    os.makedirs(DB_DIR, exist_ok=True)
    DB_PATH=os<.path.join(DB_DIR, "visitor_logs.db")

    conn=sqlite3.connect(DB_PATH)
    cursor=conn.cursor()

    # 전체 master tb
    create_table(cursor, "master")

    # 연도별 master tb
    # 2025master
    if create_yearly:
        create_table(cursor, f"{year}master")

    # 월별 tb
    # 202501
    if create_monthly:
        for m in range(1, 13):
            table_name=f"{year}{m:02d}"
            create_table(cursor, table_name)

    conn.commit()
    conn.close()
    print(f"{year}년 기준 테이블 생성 완료 ({DB_PATH})")

이렇게 정의를 했고 이제 main에서 년도를 입력받아서 진행하도록 했다.

if __name__=="__main__":
    year=input("생성할 연도를 입력하세요 (YYYY): ").strip()
    
    if not year.isdigit() or len(year) != 4:
        print("!!! 4자리 숫자로 입력하세요 !!!")
    else:
        initialize_db(year)

4.5. backend/routes.py

router를 통해 동작할 각 기능들을 구현해줬다.
이 부분은 html과의 연동이 있으므로 frontend/templates/* 도 함께 정리하겠다.

4.5.1. index

메인 홈으로, 주문하기 와 분석 보기 버튼 두개를 뒀다.

    @app.route("/")
        def index():
            return render_template("index.html")
  • index.html
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>스마트 매장</title>
</head>
<body>
    <h1>스마트 매장 분석 시스템</h1>
    <form action="/order" method="get">
        <button type="submit">주문하기</button>
    </form>
    <form action="/analyze" method="get">
        <button type="submit">분석 보기</button>
    </form>
</body>
</html>

4.5.2. order

이제 주문하기 버튼을 통해 들어왔을 때 카메라로 촬영하고, 메뉴를 주문하게 되면 해당 내용들이 모두 db로 저장되도록 했다.
또한 주문이 완료됐다면 3초 후 홈으로 자동 이동되도록 js를 통해 구현했다.

    @app.route("/order", methods=["GET", "POST"])
        def order():
            if request.method=="GET":
                gender, age_group=capture_face_from_webcam()
                return render_template("order.html", gender=gender, age_group=age_group)

            elif request.method=="POST":
                gender=request.form["gender"]
                age_group=request.form["age_group"]
                menu_name=request.form["menu"]
                quantity=int(request.form["quantity"])

                db_path=os.path.join("database", "visitor_logs.db")
                timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S")

                # 동적으로 연도 및 월 추출
                year=datetime.now().strftime("%Y")
                year_month=datetime.now().strftime("%Y%m")

                # master, YYYYmaster, YYYYMM에 모두 insert
                for table in ["master", f"{year}master", year_month]:
                    insert_visitor_log(
                        db_path=db_path,
                        table_name=table,
                        timestamp=timestamp,
                        gender=gender,
                        age_group=age_group,
                        menu_name=menu_name,
                        quantity=quantity
                    )

                return render_template("order_done.html")
  • order.html
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>주문 페이지</title>
</head>
<body>
    <!-- <h2>예측된 손님 정보: {{ gender }}, {{ age_group }}</h2> -->
    <form method="post">
        <input type="hidden" name="gender" value="{{ gender }}">
        <input type="hidden" name="age_group" value="{{ age_group }}">

        <label>메뉴:
            <select name="menu">
                <option value="아메리카노">아메리카노</option>
                <option value="카페라떼">카페라떼</option>
                <option value="녹차라떼">녹차라떼</option>
            </select>
        </label>
        <br>
        <label>수량:
            <input type="number" name="quantity" value="1" min="1">
        </label>
        <br>
        <button type="submit">주문 완료</button>
    </form>
</body>
</html>
  • order_done.html
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>주문 완료</title>

    <!--3초 후 홈으로 자동 이동-->
    <script>
        let seconds = 3;
        function updateCountdown() {
            const countdownEl=document.getElementById("countdown");
            countdownEl.textContent=`${seconds}초 후 메인화면으로 이동합니다.`;
            if (seconds === 0) {
                window.location.href = "/";
            } else {
                seconds--;
                setTimeout(updateCountdown, 1000);
            }
        }
        window.onload=updateCountdown;
    </script>
</head>
<body>
    <h1>주문이 완료되었습니다!</h1>
    <p id="countdown"></p>
</body>
</html>

4.5.3. analyze

마지막 분석 하기 부분이며, 지금은 자세한 기능으로 구현하지는 않았으며 select all하도록 했다.

    @app.route("/analyze")
    def analyze():
        db_path=os.path.join("database", "visitor_logs.db")
        conn=sqlite3.connect(db_path)
        conn.row_factory=sqlite3.Row
        cursor=conn.cursor()

        cursor.execute("SELECT * FROM master ORDER BY timestamp DESC")
        logs=cursor.fetchall()
        conn.close()

        return render_template("analyze.html", logs=logs)
  • analyze.html
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>분석 페이지</title>
</head>
<body>
    <h1>방문자 분석 결과</h1>

    {% if logs %}
        <table border="1">
            <tr>
                <th>시간</th>
                <th>성별</th>
                <th>연령대</th>
                <th>메뉴</th>
                <th>수량</th>
            </tr>
            {% for log in logs %}
            <tr>
                <td>{{ log.timestamp }}</td>
                <td>{{ log.gender }}</td>
                <td>{{ log.age_group }}</td>
                <td>{{ log.menu_name }}</td>
                <td>{{ log.quantity }}</td>
            </tr>
            {% endfor %}
        </table>
    {% else %}
        <p>저장된 기록이 없습니다!</p>
    {% endif %}
  
    <a href="/">홈으로 돌아가기</a>
</body>
</html>

4.6. main.py

이제 최종으로 불러오기만 하면 된다.

from flask import Flask
from backend.routes import init_routes

app=Flask(__name__, template_folder="frontend/templates")

# 본인이 직접 등록하면 된다.
# 개인 프로젝트이므로 간단하게 abc123으로 적음
app.config["SECRET_KEY"]="abc123"

# router 등록
init_routes(app)

if __name__=="__main__":
    app.run(debug=True)

이렇게하여 기본적인 기능이 동작하도록 구현을 완료했다.

5. 동작

따라서 기능을 보면 다음과 같다.

  1. python create_db.py를 통해 DB 초기화를 해준다.

  2. main.py를 실행한다.

  3. 실행되었다면 http://127.0.0.1:5000 을 통해 접속한다.

  4. 분석 보기 버튼을 클릭하면 현재 비어있기에 저장된 기록이 없다고 나온다.

  5. 주문하기 버튼을 클릭하면 웹캠이 동작한다.

  6. 카운트가 종료되고 촬영이 완료되면 메뉴를 선택하는 화면으로 이동한다.

  7. 메뉴를 선택하고 주문 완료 버튼을 누르면 주문이 완료되었다는 메세지와 함께 3초 카운트 후 메인화면으로 자동 이동한다.

  8. 이후 돌아온 메인화면에서 분석 보기 을 클릭하면 분석 화면으로, 즉 지금은 저장된 데이터를 전체 조회하도록 해놨으니 저장된 내용을 볼 수 있다.

6. 정리

이렇게하여 기본적인 기능 구현은 완료했다.
이제 웹캠 동작과정을 비동기처리하고, 분석 페이지를 월별, 년도별 비교 할 수 있게 다양하게 바꾸고, UI 개선 정도를 개선할 거 같다.
생각보다 목표하는 방향을 바꿨더니 수정해야하는 부분이 많은 거 같다..

profile
AI Model Developer

0개의 댓글