<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>RaspberryPi 보관 - 하우인포-IT·테크</title>
	<atom:link href="https://howinfo.kr/tag/raspberrypi/feed/" rel="self" type="application/rss+xml" />
	<link>https://howinfo.kr/tag/raspberrypi/</link>
	<description>IT·AI 자동화 &#38; 인프라 전문 블로그 (하우인포)</description>
	<lastBuildDate>Mon, 09 Feb 2026 06:50:42 +0000</lastBuildDate>
	<language>ko-KR</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://howinfo.kr/wp-content/uploads/2026/02/cropped-ChatGPT-Image-2026년-2월-12일-오후-05_39_40-32x32.png</url>
	<title>RaspberryPi 보관 - 하우인포-IT·테크</title>
	<link>https://howinfo.kr/tag/raspberrypi/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>[라즈베리파이] 나만의 스마트 보안 카메라 만들기 (Python + OpenCV + Edge-TTS)</title>
		<link>https://howinfo.kr/%eb%9d%bc%ec%a6%88%eb%b2%a0%eb%a6%ac%ed%8c%8c%ec%9d%b4-%eb%82%98%eb%a7%8c%ec%9d%98-%ec%8a%a4%eb%a7%88%ed%8a%b8-%eb%b3%b4%ec%95%88-%ec%b9%b4%eb%a9%94%eb%9d%bc-%eb%a7%8c%eb%93%a4%ea%b8%b0-python-op/</link>
					<comments>https://howinfo.kr/%eb%9d%bc%ec%a6%88%eb%b2%a0%eb%a6%ac%ed%8c%8c%ec%9d%b4-%eb%82%98%eb%a7%8c%ec%9d%98-%ec%8a%a4%eb%a7%88%ed%8a%b8-%eb%b3%b4%ec%95%88-%ec%b9%b4%eb%a9%94%eb%9d%bc-%eb%a7%8c%eb%93%a4%ea%b8%b0-python-op/#respond</comments>
		
		<dc:creator><![CDATA[hong]]></dc:creator>
		<pubDate>Mon, 09 Feb 2026 06:45:24 +0000</pubDate>
				<category><![CDATA[개발·코딩]]></category>
		<category><![CDATA[AI음성]]></category>
		<category><![CDATA[DIY프로젝트]]></category>
		<category><![CDATA[EdgeTTS]]></category>
		<category><![CDATA[OpenCV]]></category>
		<category><![CDATA[python]]></category>
		<category><![CDATA[RaspberryPi]]></category>
		<category><![CDATA[라즈베리파이]]></category>
		<category><![CDATA[모션감지]]></category>
		<category><![CDATA[보안카메라]]></category>
		<category><![CDATA[스마트홈]]></category>
		<category><![CDATA[알림시스템]]></category>
		<category><![CDATA[임베디드]]></category>
		<category><![CDATA[파이썬]]></category>
		<category><![CDATA[홈네트워크]]></category>
		<category><![CDATA[홈캠만들기]]></category>
		<guid isPermaLink="false">https://howinfo.kr/?p=1462</guid>

					<description><![CDATA[<p>집을 비울 때 누군가 들어오는지 궁금하신가요? 시중의 비싼 홈캠 대신, 라즈베리파이와 파이썬을 활용해 움직임을 감지하고 목소리로 경고를 날리는 스마트 감시...</p>
<p>게시물 <a href="https://howinfo.kr/%eb%9d%bc%ec%a6%88%eb%b2%a0%eb%a6%ac%ed%8c%8c%ec%9d%b4-%eb%82%98%eb%a7%8c%ec%9d%98-%ec%8a%a4%eb%a7%88%ed%8a%b8-%eb%b3%b4%ec%95%88-%ec%b9%b4%eb%a9%94%eb%9d%bc-%eb%a7%8c%eb%93%a4%ea%b8%b0-python-op/">[라즈베리파이] 나만의 스마트 보안 카메라 만들기 (Python + OpenCV + Edge-TTS)</a>이 <a href="https://howinfo.kr">하우인포-IT·테크</a>에 처음 등장했습니다.</p>
]]></description>
										<content:encoded><![CDATA[
<p>집을 비울 때 누군가 들어오는지 궁금하신가요? 시중의 비싼 홈캠 대신, 라즈베리파이와 파이썬을 활용해 <strong>움직임을 감지하고 목소리로 경고를 날리는 스마트 감시 시스템</strong>을 직접 만들어보았습니다. AI를 활용한 고품질 TTS 기능까지 더해 더욱 강력해진 &#8216;모션 가드&#8217; 제작기를 공유합니다.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">1. 주요 기능 및 특징: 이 프로젝트가 특별한 이유</h3>



<p>단순히 녹화만 하는 카메라가 아닙니다. 상황에 맞춰 즉각 대응하는 지능형 시스템입니다.</p>



<ul class="wp-block-list">
<li><strong>실시간 모션 감지:</strong> OpenCV를 활용해 지정된 ROI(관심 영역) 내의 움직임을 픽셀 단위로 분석하여 작은 변화도 놓치지 않습니다.</li>



<li><strong>고품질 AI 음성 안내:</strong> <code>edge-tts</code>를 연동하여 기계음이 아닌 자연스러운 한국어 목소리로 침입 경고 멘트를 송출합니다.</li>



<li><strong>오탐 방지 알고리즘:</strong> 연속 프레임 감지(Confirm Frames)와 쿨다운 타임을 적용해 조명 변화나 미세한 노이즈로 인한 오작동을 최소화했습니다.</li>



<li><strong>강력한 비프음 발생:</strong> 경고 멘트 후 강렬한 &#8216;삐삐삐&#8217; 패턴의 비프음을 재생해 청각적인 보안 효과를 극대화합니다.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">2. 준비물 및 환경 설정: 시작하기 전에</h3>



<p>이 프로젝트를 실행하기 위해 라즈베리파이에 몇 가지 하드웨어와 라이브러리 설치가 필요합니다.</p>



<ul class="wp-block-list">
<li><strong>하드웨어:</strong>
<ul class="wp-block-list">
<li>Raspberry Pi (Zero W, 3, 4 등 모든 모델 가능)</li>



<li>USB 웹캠 또는 라즈베리파이 카메라 모듈</li>



<li>스피커 (3.5mm 오디오 잭 또는 USB 스피커)</li>
</ul>
</li>



<li><strong>소프트웨어 설치:</strong>Bash<code># 1. 시스템 의존성 설치 (음성 재생을 위한 mpg123, alsa-utils) sudo apt-get update &amp;&amp; sudo apt-get install -y mpg123 alsa-utils # 2. 파이썬 라이브러리 설치 (OpenCV, NumPy, Edge-TTS, Asyncio) pip install opencv-python numpy edge-tts asyncio </code><strong>💡 Tip:</strong> <code>pip</code> 명령어가 오류난다면 <code>pip3 install ...</code>을 시도해 보세요.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">3. 핵심 코드 분석: 어떻게 움직임을 감지할까?</h3>



<p>코드의 핵심은 <strong>이전 프레임과 현재 프레임 간의 픽셀 차이를 계산하여 움직임을 수치화</strong>하는 것입니다.</p>



<h4 class="wp-block-heading">🔍 모션 감지 알고리즘 (<code>motion_ratio</code> 함수)</h4>



<p>Python</p>



<pre class="wp-block-code"><code>import cv2
import numpy as np
import os
import asyncio
import edge_tts
import time

# --- 환경 변수 설정 (값을 변경하여 감지 민감도를 조절할 수 있습니다) ---
MOTION_RATIO_THRESH = float(os.environ.get("MOTION_RATIO_THRESH", "0.03")) # 움직임 감지 임계값 (0.01~0.1 사이 권장)
CONFIRM_FRAMES = int(os.environ.get("CONFIRM_FRAMES", "5"))                 # 연속 감지 확인 프레임 수
ALERT_COOLDOWN_SEC = int(os.environ.get("ALERT_COOLDOWN_SEC", "30"))        # 경고 후 쿨다운 시간(초)
TTS_MP3_PATH = os.environ.get("TTS_MP3_PATH", "/tmp/alert.mp3")            # TTS 음성 파일 저장 경로
ALERT_MESSAGE = os.environ.get("ALERT_MESSAGE", "경고! 움직임이 감지되었습니다.") # 경고 음성 메시지
ROI_X, ROI_Y, ROI_W, ROI_H = map(int, os.environ.get("ROI", "0,0,0,0").split(',')) # 관심 영역 (x,y,width,height)

async def tts_save_mp3(text, mp3_path, voice="ko-KR-SunHiNeural"):
    """
    Edge-TTS를 사용하여 텍스트를 mp3 파일로 변환하여 저장합니다.
    """
    try:
        communicate = edge_tts.Communicate(text=text, voice=voice)
        await communicate.save(mp3_path)
    except Exception as e:
        print(f"TTS 생성 중 오류 발생: {e}")

def motion_ratio(prev_gray, gray):
    """
    두 회색조 이미지 간의 움직임 비율을 계산합니다.
    """
    diff = cv2.absdiff(prev_gray, gray) # 이전 프레임과 현재 프레임의 픽셀 차이 계산
    _, th = cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY) # 임계값 처리 (차이가 25 이상인 픽셀만 흰색으로)
    th = cv2.medianBlur(th, 5) # 노이즈 제거를 위한 미디언 블러 적용
    changed_pixels = np.count_nonzero(th) # 변경된 픽셀 수 계산
    return changed_pixels / th.size # 전체 픽셀 대비 변경된 픽셀 비율 반환

def play_alert_sound(tts_path, beep_count=3, beep_duration=0.2):
    """
    경고 음성 메시지와 비프음을 재생합니다.
    """
    print("경고음 재생...")
    if os.path.exists(tts_path):
        os.system(f"mpg123 {tts_path}") # TTS 음성 재생
    
    # 비프음 재생
    for _ in range(beep_count):
        os.system(f"aplay -q -c 1 -t raw -f S16_LE -r 44100 /dev/zero") # 기본 비프음 (라즈비안에서 작동 확인)
        time.sleep(beep_duration)
        os.system(f"aplay -q -c 1 -t raw -f S16_LE -r 44100 /dev/zero") # 종료 비프음
        time.sleep(beep_duration)
    print("경고음 재생 완료.")

async def main():
    cap = cv2.VideoCapture(0) # 웹캠 (0번 장치) 초기화
    if not cap.isOpened():
        print("카메라를 열 수 없습니다.")
        return

    ret, frame = cap.read()
    if not ret:
        print("첫 프레임을 읽을 수 없습니다.")
        cap.release()
        return

    # ROI 설정이 유효하면 해당 영역으로 프레임을 자름
    if ROI_W &gt; 0 and ROI_H &gt; 0:
        frame = frame&#91;ROI_Y:ROI_Y+ROI_H, ROI_X:ROI_X+ROI_W]
        
    prev_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    
    motion_detected_count = 0
    last_alert_time = 0

    # TTS 파일 미리 생성
    await tts_save_mp3(ALERT_MESSAGE, TTS_MP3_PATH)

    print(f"모션 감지 시작. 임계값: {MOTION_RATIO_THRESH}, 확인 프레임: {CONFIRM_FRAMES}, 쿨다운: {ALERT_COOLDOWN_SEC}초")
    print(f"관심 영역(ROI): X={ROI_X}, Y={ROI_Y}, W={ROI_W}, H={ROI_H}")

    try:
        while True:
            ret, frame = cap.read()
            if not ret:
                break

            display_frame = frame.copy() # 화면 표시용 원본 프레임 복사

            # ROI 설정이 유효하면 해당 영역으로 프레임을 자르고 ROI 표시
            if ROI_W &gt; 0 and ROI_H &gt; 0:
                frame_for_detection = frame&#91;ROI_Y:ROI_Y+ROI_H, ROI_X:ROI_X+ROI_W]
                cv2.rectangle(display_frame, (ROI_X, ROI_Y), (ROI_X+ROI_W, ROI_Y+ROI_H), (0, 255, 0), 2) # ROI 박스 그리기
            else:
                frame_for_detection = frame

            gray = cv2.cvtColor(frame_for_detection, cv2.COLOR_BGR2GRAY)
            
            ratio = motion_ratio(prev_gray, gray)
            
            current_time = time.time()

            if ratio &gt; MOTION_RATIO_THRESH:
                motion_detected_count += 1
                if motion_detected_count &gt;= CONFIRM_FRAMES and (current_time - last_alert_time) &gt; ALERT_COOLDOWN_SEC:
                    print(f"!!! 움직임 감지됨 (비율: {ratio:.4f}) !!!")
                    play_alert_sound(TTS_MP3_PATH)
                    last_alert_time = current_time
                    motion_detected_count = 0 # 알림 후 카운트 초기화
            else:
                motion_detected_count = 0 # 움직임이 없으면 카운트 초기화

            # 프레임에 감지 정보 표시 (선택 사항, 라즈베리파이 성능 고려하여 주석 처리 가능)
            # cv2.putText(display_frame, f"Motion Ratio: {ratio:.4f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
            # cv2.putText(display_frame, f"Alerts: {current_time - last_alert_time:.0f}s cooldown", (10, 70), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)

            # cv2.imshow('Motion Guard Cam', display_frame) # 화면에 표시 (GUI 환경에서만 작동)
            
            prev_gray = gray # 현재 프레임을 다음 반복의 이전 프레임으로 저장

            if cv2.waitKey(1) &amp; 0xFF == ord('q'):
                break

    finally:
        cap.release()
        cv2.destroyAllWindows()
        print("프로그램 종료.")

if __name__ == '__main__':
    asyncio.run(main())

</code></pre>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><strong>코드 설명:</strong> 단순 픽셀 차이 외에도 <code>cv2.medianBlur</code>를 적용해 미세한 노이즈를 제거하여 오작동을 줄였습니다. 또한 <code>CONFIRM_FRAMES</code>로 여러 프레임에 걸쳐 움직임이 지속될 때만 감지하도록 설정하여 신뢰도를 높였습니다.</p>
</blockquote>



<h4 class="wp-block-heading">🗣 AI 음성 경고 (<code>edge_tts</code> 활용)</h4>



<p>구글 TTS(gTTS)보다 훨씬 자연스러운 Microsoft Edge의 TTS 엔진을 활용하여 고품질의 한국어 음성 경고를 구현했습니다. <code>asyncio</code>를 통해 비동기적으로 음성을 생성합니다.</p>



<p>Python</p>



<pre class="wp-block-code"><code>async def tts_save_mp3(text, mp3_path, voice="ko-KR-SunHiNeural"):
    """
    Edge-TTS를 사용하여 텍스트를 mp3 파일로 변환하여 저장합니다.
    """
    try:
        communicate = edge_tts.Communicate(text=text, voice=voice)
        await communicate.save(mp3_path)
    except Exception as e:
        print(f"TTS 생성 중 오류 발생: {e}")
</code></pre>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><strong>코드 설명:</strong> <code>ko-KR-SunHiNeural</code>은 한국어 여성 목소리입니다. 이 외에도 다양한 목소리가 있으니 <code>edge-tts --list-voices</code> 명령어로 확인 후 변경해 볼 수 있습니다.</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">4. 실제 구동 팁: 나만의 환경에 최적화하기</h3>



<p>환경에 따라 설정을 미세하게 조정하면 훨씬 똑똑해집니다. 코드 상단의 <strong>환경 변수</strong>를 통해 조절할 수 있습니다.</p>



<ul class="wp-block-list">
<li><strong>민감도 조절 (<code>MOTION_RATIO_THRESH</code>):</strong> 기본값 <code>0.03</code>은 실내에서 적합합니다. 바람에 흔들리는 나뭇잎이 보이거나 외부 환경이라면 <code>0.05</code> ~ <code>0.1</code> 정도로 높여 오탐을 줄일 수 있습니다.</li>



<li><strong>관심 영역 지정 (<code>ROI_X, ROI_Y, ROI_W, ROI_H</code>):</strong>
<ul class="wp-block-list">
<li><code>ROI="0,0,0,0"</code> (기본값) : 전체 화면을 감지합니다.</li>



<li><code>ROI="100,50,400,300"</code> : X좌표 100, Y좌표 50에서 시작하여 가로 400, 세로 300 픽셀 영역만 감지합니다. 이 기능은 문이나 창문 쪽만 집중적으로 감시하게 설정할 수 있어 효율적입니다.</li>
</ul>
</li>



<li><strong>쿨다운 시간 (<code>ALERT_COOLDOWN_SEC</code>):</strong> 한 번 알림이 울린 후 지정된 시간 동안은 재알림을 하지 않습니다. 반복적인 알림으로 인한 소음 공해를 방지해 줍니다.</li>



<li><strong>연속 감지 프레임 (<code>CONFIRM_FRAMES</code>):</strong> 짧은 순간의 노이즈로 인한 오탐을 줄이기 위해, 움직임이 N 프레임 이상 연속될 때만 실제 움직임으로 간주합니다.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">5. 마치며: 나만의 스마트 홈 시큐리티</h3>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><strong>💡 직접 구현해보니:</strong> 처음에는 <code>cv2.medianBlur</code>나 <code>CONFIRM_FRAMES</code>를 적용하지 않아 바람에 흔들리는 커튼, 혹은 갑작스러운 조명 변화 때문에 알람이 계속 울려 고생했습니다. 하지만 이러한 &#8216;오탐 방지&#8217; 로직을 추가하니 시스템의 신뢰도가 비약적으로 향상되었습니다. 여러분도 환경에 맞춰 임계값이나 ROI를 조금씩 바꿔보며 최적의 보안 환경을 구축해 보세요!</p>
</blockquote>



<p>이 프로젝트는 간단하지만 매우 실용적인 라즈베리파이 활용 예시입니다. 여기에서 더 나아가 감지된 영상을 텔레그램으로 전송하거나, 특정 시간대에만 작동하도록 스케줄링하는 등 다양한 기능으로 확장할 수 있습니다.</p>



<p></p>



<p>전체소스 참고</p>



<p><strong>&#8220;이 코드는 별도의 유료 API 키 없이도 작동하며, 환경 변수만으로 간편하게 설정할 수 있도록 설계했습니다.&#8221;</strong></p>



<pre class="wp-block-code"><code>#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import time
import asyncio
import tempfile
import subprocess

import cv2
import numpy as np
import edge_tts

# =========================
# 설정(튜닝 포인트)
# =========================
CAM_INDEX = int(os.environ.get("CAM_INDEX", "0"))     # /dev/video0
FRAME_W   = int(os.environ.get("FRAME_W", "640"))
FRAME_H   = int(os.environ.get("FRAME_H", "480"))

# 모션 감지 ROI (전체 화면이면 기본값 그대로 두세요)
# x,y,w,h
ROI = (
    int(os.environ.get("ROI_X", "0")),
    int(os.environ.get("ROI_Y", "0")),
    int(os.environ.get("ROI_W", str(FRAME_W))),
    int(os.environ.get("ROI_H", str(FRAME_H))),
)

# 모션 민감도: "변화한 픽셀 비율"
# - 너무 잘 울리면 ↑ (0.08~0.20)
# - 잘 안 울리면 ↓ (0.01~0.06)
MOTION_RATIO_THRESH = float(os.environ.get("MOTION_RATIO_THRESH", "0.03"))

# 픽셀 차이 임계치(조명 변화에 민감하면 ↑)
DIFF_THRESH = int(os.environ.get("DIFF_THRESH", "25"))

# 모션을 몇 프레임 연속 감지해야 트리거할지(오탐 줄임)
MOTION_CONFIRM_FRAMES = int(os.environ.get("MOTION_CONFIRM_FRAMES", "3"))

# 경고 후 쿨다운(연속 재생 방지)
ALERT_COOLDOWN_SEC = float(os.environ.get("ALERT_COOLDOWN_SEC", "15.0"))

# 프레임 처리 간격(부하 조절)
SLEEP_SEC = float(os.environ.get("SLEEP_SEC", "0.01"))

# TTS
TTS_VOICE  = os.environ.get("TTS_VOICE", "ko-KR-SunHiNeural")
TTS_RATE   = os.environ.get("TTS_RATE", "+0%")
TTS_VOLUME = os.environ.get("TTS_VOLUME", "+0%")

ALERT_TEXT = "여기 들어오시면 안됩니다. 허가된 주인님만 입장 가능합니다."

# 비프 패턴: "3초짜리 삐삐삐"를 3번 반복
BEEP_FREQ = int(os.environ.get("BEEP_FREQ", "1100"))     # Hz
BEEP_MS   = int(os.environ.get("BEEP_MS", "180"))        # beep 1회 길이
BEEP_GAP_MS = int(os.environ.get("BEEP_GAP_MS", "120"))  # beep 사이 간격
BEEP_CYCLE_SEC = float(os.environ.get("BEEP_CYCLE_SEC", "3.0"))  # 3초
BEEP_REPEAT = int(os.environ.get("BEEP_REPEAT", "3"))    # 3회 반복

# =========================
# 오디오 유틸
# =========================
def require_bins():
    for binname, pkg in &#91;("mpg123", "mpg123"), ("aplay", "alsa-utils")]:
        try:
            subprocess.run(&#91;binname, "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False)
        except FileNotFoundError:
            raise RuntimeError(f"{binname}가 없습니다. 설치: sudo apt-get install -y {pkg}")

def play_mp3(path: str):
    subprocess.run(&#91;"mpg123", "-q", path], check=False)

async def tts_save_mp3(text: str, mp3_path: str):
    communicate = edge_tts.Communicate(text=text, voice=TTS_VOICE, rate=TTS_RATE, volume=TTS_VOLUME)
    await communicate.save(mp3_path)

def speak(text: str):
    text = " ".join(text.split()).strip()
    if not text:
        return
    with tempfile.NamedTemporaryFile(suffix=".mp3", delete=True) as tf:
        asyncio.run(tts_save_mp3(text, tf.name))
        play_mp3(tf.name)

def gen_beep_wav(path: str, freq_hz: int, ms: int, volume: float = 0.4, sr: int = 16000):
    t = np.linspace(0, ms/1000.0, int(sr*ms/1000.0), endpoint=False)
    wave = (np.sin(2*np.pi*freq_hz*t) * volume).astype(np.float32)
    pcm_i16 = (np.clip(wave, -1.0, 1.0) * 32767).astype(np.int16)

    import wave as _wave
    with _wave.open(path, "wb") as wf:
        wf.setnchannels(1)
        wf.setsampwidth(2)
        wf.setframerate(sr)
        wf.writeframes(pcm_i16.tobytes())

def play_wav(path: str):
    subprocess.run(&#91;"aplay", "-q", path], check=False)

def beep_3sec_triple_repeat3():
    """
    3초 사이클 안에 '삐삐삐'(3회) 후 남는 시간 쉬기.
    그 3초 사이클을 3번 반복.
    """
    with tempfile.NamedTemporaryFile(suffix=".wav", delete=True) as tf:
        gen_beep_wav(tf.name, BEEP_FREQ, BEEP_MS)

        for _ in range(BEEP_REPEAT):
            start = time.time()

            # 삐삐삐
            for i in range(3):
                play_wav(tf.name)
                if i &lt; 2:
                    time.sleep(BEEP_GAP_MS / 1000.0)

            # 3초 맞추기
            elapsed = time.time() - start
            remain = max(0.0, BEEP_CYCLE_SEC - elapsed)
            time.sleep(remain)

# =========================
# 모션 감지 유틸
# =========================
def motion_ratio(prev_gray: np.ndarray, gray: np.ndarray) -&gt; float:
    diff = cv2.absdiff(prev_gray, gray)
    _, th = cv2.threshold(diff, DIFF_THRESH, 255, cv2.THRESH_BINARY)

    # 작은 노이즈 제거(조금만)
    th = cv2.medianBlur(th, 5)

    changed = np.count_nonzero(th)
    total = th.size
    return changed / max(1, total)

# =========================
# main
# =========================
def main():
    require_bins()

    cap = cv2.VideoCapture(CAM_INDEX)
    if not cap.isOpened():
        raise RuntimeError(f"카메라 열기 실패: CAM_INDEX={CAM_INDEX} (/dev/video{CAM_INDEX})")

    cap.set(cv2.CAP_PROP_FRAME_WIDTH, FRAME_W)
    cap.set(cv2.CAP_PROP_FRAME_HEIGHT, FRAME_H)

    x, y, w, h = ROI

    print("=== Motion Guard (Camera) ===")
    print("CAM_INDEX:", CAM_INDEX)
    print("FRAME:", FRAME_W, "x", FRAME_H)
    print("ROI:", ROI)
    print("MOTION_RATIO_THRESH:", MOTION_RATIO_THRESH, "DIFF_THRESH:", DIFF_THRESH)
    print("CONFIRM_FRAMES:", MOTION_CONFIRM_FRAMES, "COOLDOWN:", ALERT_COOLDOWN_SEC)
    print("종료: Ctrl+C\n")

    prev_gray = None
    motion_hits = 0
    last_alert = 0.0

    try:
        while True:
            ok, frame = cap.read()
            if not ok or frame is None:
                time.sleep(0.05)
                continue

            roi = frame&#91;y:y+h, x:x+w]
            gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)

            # 조명변화 완화(살짝 블러)
            gray = cv2.GaussianBlur(gray, (5, 5), 0)

            if prev_gray is None:
                prev_gray = gray
                time.sleep(SLEEP_SEC)
                continue

            ratio = motion_ratio(prev_gray, gray)
            prev_gray = gray

            if ratio &gt;= MOTION_RATIO_THRESH:
                motion_hits += 1
            else:
                motion_hits = max(0, motion_hits - 1)

            now = time.time()

            # 트리거 조건: 모션 연속 감지 + 쿨다운 지난 후
            if motion_hits &gt;= MOTION_CONFIRM_FRAMES and (now - last_alert) &gt; ALERT_COOLDOWN_SEC:
                last_alert = now
                motion_hits = 0

                print(f"&#91;ALERT] motion detected! ratio={ratio:.4f}")

                # 1) 경고 멘트
                speak(ALERT_TEXT)

                # 2) 삐삐삐(3초) x 3회
                beep_3sec_triple_repeat3()

            time.sleep(SLEEP_SEC)

    except KeyboardInterrupt:
        print("\n종료합니다.")
    finally:
        cap.release()

if __name__ == "__main__":
    main()

</code></pre>
<p>게시물 <a href="https://howinfo.kr/%eb%9d%bc%ec%a6%88%eb%b2%a0%eb%a6%ac%ed%8c%8c%ec%9d%b4-%eb%82%98%eb%a7%8c%ec%9d%98-%ec%8a%a4%eb%a7%88%ed%8a%b8-%eb%b3%b4%ec%95%88-%ec%b9%b4%eb%a9%94%eb%9d%bc-%eb%a7%8c%eb%93%a4%ea%b8%b0-python-op/">[라즈베리파이] 나만의 스마트 보안 카메라 만들기 (Python + OpenCV + Edge-TTS)</a>이 <a href="https://howinfo.kr">하우인포-IT·테크</a>에 처음 등장했습니다.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://howinfo.kr/%eb%9d%bc%ec%a6%88%eb%b2%a0%eb%a6%ac%ed%8c%8c%ec%9d%b4-%eb%82%98%eb%a7%8c%ec%9d%98-%ec%8a%a4%eb%a7%88%ed%8a%b8-%eb%b3%b4%ec%95%88-%ec%b9%b4%eb%a9%94%eb%9d%bc-%eb%a7%8c%eb%93%a4%ea%b8%b0-python-op/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
