tae3lee/VisionVelocity
0
1"""2강의 유속 측정 앱3River Velocity Measurement Web App4 5Streamlit을 이용한 웹 애플리케이션6비디오에서 물의 흐름 속도를 자동으로 측정합니다.7"""8 9import streamlit as st10import cv211import numpy as np12import tempfile13from pathlib import Path14import matplotlib.pyplot as plt15import os16 17# 페이지 설정18st.set_page_config(19 page_title="강의 유속 측정",20 page_icon="🌊",21 layout="wide",22 initial_sidebar_state="expanded"23)24 25# 스타일링26st.markdown("""27<style>28 .main {29 padding: 0rem 1rem;30 }31 .stTabs [data-baseweb="tab-list"] button {32 font-size: 1.2em;33 }34</style>35""", unsafe_allow_html=True)36 37 38class RiverVelocityTracker:39 """강의 유속 추적 클래스"""40 41 def __init__(self, video_path, fps=30, scale=0.01):42 self.video_path = video_path43 self.fps = fps44 self.scale = scale # m/pixel45 self.time_per_frame = 1.0 / fps46 47 self.velocities = []48 self.timestamps = []49 self.frames_info = []50 51 def calculate_optical_flow(self, prev_frame, frame, progress_placeholder=None):52 """광학 유동 계산"""53 gray_prev = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY)54 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)55 56 # 코너점 감지57 corners = cv2.goodFeaturesToTrack(58 gray_prev,59 maxCorners=200,60 qualityLevel=0.01,61 minDistance=1062 )63 64 if corners is None or len(corners) == 0:65 return None66 67 # Lucas-Kanade 광학 유동68 lk_params = dict(69 winSize=(15, 15),70 maxLevel=2,71 criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03)72 )73 74 next_points, status, error = cv2.calcOpticalFlowPyrLK(75 gray_prev, gray, corners, None, **lk_params76 )77 78 if next_points is None:79 return None80 81 good_prev = corners[status == 1]82 good_next = next_points[status == 1]83 84 if len(good_prev) == 0:85 return None86 87 displacements = np.linalg.norm(good_next - good_prev, axis=1)88 avg_displacement = np.median(displacements)89 90 velocity = (avg_displacement * self.scale) / self.time_per_frame91 92 return velocity, avg_displacement93 94 def process_video(self, progress_placeholder, status_placeholder, skip_frames=1, max_frames=None):95 """비디오 처리"""96 cap = cv2.VideoCapture(self.video_path)97 98 if not cap.isOpened():99 st.error("비디오를 열 수 없습니다!")100 return False101 102 frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))103 prev_frame = None104 frame_idx = 0105 processed_count = 0106 107 while True:108 ret, frame = self.cap.read()109 110 if not ret:111 break112 113 if max_frames and processed_count >= max_frames:114 break115 116 if frame_idx % skip_frames != 0:117 frame_idx += 1118 continue119 120 processed_count += 1121 timestamp = frame_idx * self.time_per_frame122 123 if prev_frame is None:124 prev_frame = frame.copy()125 frame_idx += 1126 continue127 128 result = self.calculate_optical_flow(prev_frame, frame)129 130 if result is not None:131 velocity, displacement = result132 133 if 0 < velocity < 5: # 유효 범위134 self.velocities.append(velocity)135 self.timestamps.append(timestamp)136 self.frames_info.append({137 'frame': frame_idx,138 'time': timestamp,139 'velocity': velocity,140 'displacement': displacement141 })142 143 # 진행률 업데이트144 progress = min(frame_idx / frame_count, 1.0)145 progress_placeholder.progress(progress)146 status_placeholder.text(f"처리 중: {frame_idx}/{frame_count} 프레임")147 148 prev_frame = frame.copy()149 frame_idx += 1150 151 cap.release()152 status_placeholder.success(f"✅ 처리 완료: {len(self.velocities)}개 데이터 수집")153 return True154 155 def get_statistics(self):156 """통계 계산"""157 if not self.velocities:158 return None159 160 velocities = np.array(self.velocities)161 return {162 'mean': np.mean(velocities),163 'median': np.median(velocities),164 'min': np.min(velocities),165 'max': np.max(velocities),166 'std': np.std(velocities),167 'count': len(velocities)168 }169 170 171def main():172 st.title("🌊 강의 유속 측정 앱")173 st.markdown("---")174 175 # 사이드바 설정176 st.sidebar.header("⚙️ 설정")177 178 # 탭 구성179 tab1, tab2, tab3, tab4 = st.tabs(["📹 업로드 & 측정", "📊 결과 분석", "🎥 라이브 측정", "📖 가이드"])180 181 # ========== 탭 1: 업로드 & 측정 ==========182 with tab1:183 st.header("비디오 업로드 및 유속 측정")184 185 col1, col2 = st.columns([2, 1])186 187 with col1:188 uploaded_file = st.file_uploader(189 "비디오 파일 선택 (MP4, AVI, MOV 등)",190 type=['mp4', 'avi', 'mov', 'mkv']191 )192 193 with col2:194 st.info("💡 **팁**: 강물 표면의 움직임이 명확한 영상을 사용하세요")195 196 if uploaded_file is not None:197 # 임시 파일 저장198 with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as tmp_file:199 tmp_file.write(uploaded_file.getbuffer())200 video_path = tmp_file.name201 202 # 측정 파라미터203 st.subheader("📏 측정 파라미터")204 205 col1, col2, col3 = st.columns(3)206 207 with col1:208 fps = st.slider(209 "프레임 속도 (FPS)",210 min_value=10,211 max_value=120,212 value=30,213 step=5,214 help="비디오의 실제 프레임 속도를 입력하세요"215 )216 217 with col2:218 scale_cm = st.number_input(219 "스케일 (cm/픽셀)",220 min_value=0.1,221 max_value=10.0,222 value=1.0,223 step=0.1,224 help="1미터가 몇 픽셀인지 계산해서 입력하세요"225 )226 scale = scale_cm / 100 # cm -> m227 228 with col3:229 skip_frames = st.slider(230 "프레임 간격",231 min_value=1,232 max_value=10,233 value=1,234 help="1 = 모든 프레임, 2 = 1개씩 건너뜀"235 )236 237 # 측정 시작 버튼238 if st.button("🚀 유속 측정 시작", key="measure_btn", use_container_width=True):239 st.session_state.processing = True240 st.session_state.skip_frames = skip_frames241 st.session_state.fps = fps242 st.session_state.scale = scale243 st.session_state.video_path = video_path244 245 # 측정 진행246 if 'processing' in st.session_state and st.session_state.processing:247 with st.spinner("📹 비디오 처리 중..."):248 progress_placeholder = st.empty()249 status_placeholder = st.empty()250 251 tracker = RiverVelocityTracker(252 video_path=video_path,253 fps=st.session_state.fps,254 scale=st.session_state.scale255 )256 257 # 비디오 처리 (수정된 부분)258 cap = cv2.VideoCapture(video_path)259 frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))260 prev_frame = None261 frame_idx = 0262 processed_count = 0263 264 while True:265 ret, frame = cap.read()266 if not ret:267 break268 269 if frame_idx % st.session_state.skip_frames != 0:270 frame_idx += 1271 continue272 273 processed_count += 1274 timestamp = frame_idx * (1.0 / st.session_state.fps)275 276 if prev_frame is None:277 prev_frame = frame.copy()278 frame_idx += 1279 continue280 281 result = tracker.calculate_optical_flow(prev_frame, frame)282 283 if result is not None:284 velocity, displacement = result285 if 0 < velocity < 5:286 tracker.velocities.append(velocity)287 tracker.timestamps.append(timestamp)288 289 progress = min(frame_idx / frame_count, 1.0)290 progress_placeholder.progress(progress)291 status_placeholder.text(f"처리 중: {frame_idx}/{frame_count} 프레임")292 293 prev_frame = frame.copy()294 frame_idx += 1295 296 cap.release()297 298 # 결과 저장299 st.session_state.tracker = tracker300 st.session_state.stats = tracker.get_statistics()301 st.session_state.processing = False302 status_placeholder.success(f"✅ 처리 완료: {len(tracker.velocities)}개 데이터 수집")303 304 # ========== 탭 2: 결과 분석 ==========305 with tab2:306 st.header("📊 결과 분석")307 308 if 'tracker' in st.session_state and st.session_state.stats:309 stats = st.session_state.stats310 tracker = st.session_state.tracker311 312 # 통계 카드313 col1, col2, col3, col4 = st.columns(4)314 315 with col1:316 st.metric(317 "평균 유속",318 f"{stats['mean']:.3f} m/s",319 delta=f"{stats['mean']*100:.0f} cm/s"320 )321 322 with col2:323 st.metric("중앙값", f"{stats['median']:.3f} m/s")324 325 with col3:326 st.metric("최소 유속", f"{stats['min']:.3f} m/s")327 328 with col4:329 st.metric("최대 유속", f"{stats['max']:.3f} m/s")330 331 st.write("")332 333 # 그래프334 col1, col2 = st.columns(2)335 336 with col1:337 # 시간별 유속338 fig, ax = plt.subplots(figsize=(10, 5))339 ax.plot(tracker.timestamps, tracker.velocities, 'b-o', linewidth=2, markersize=4)340 ax.axhline(y=stats['mean'], color='r', linestyle='--',341 label=f"평균: {stats['mean']:.3f} m/s", linewidth=2)342 ax.set_xlabel("시간 (초)", fontsize=11)343 ax.set_ylabel("유속 (m/s)", fontsize=11)344 ax.set_title("강의 유속 (시간별)", fontsize=12, fontweight='bold')345 ax.grid(True, alpha=0.3)346 ax.legend()347 st.pyplot(fig)348 349 with col2:350 # 히스토그램351 fig, ax = plt.subplots(figsize=(10, 5))352 ax.hist(tracker.velocities, bins=20, color='skyblue', edgecolor='black', alpha=0.7)353 ax.axvline(x=stats['mean'], color='r', linestyle='--',354 linewidth=2, label=f"평균: {stats['mean']:.3f} m/s")355 ax.axvline(x=stats['median'], color='g', linestyle='--',356 linewidth=2, label=f"중앙값: {stats['median']:.3f} m/s")357 ax.set_xlabel("유속 (m/s)", fontsize=11)358 ax.set_ylabel("빈도", fontsize=11)359 ax.set_title("유속 분포", fontsize=12, fontweight='bold')360 ax.legend()361 ax.grid(True, alpha=0.3, axis='y')362 st.pyplot(fig)363 364 # 상세 통계 테이블365 st.subheader("📈 상세 통계")366 stats_data = {367 '항목': ['평균', '중앙값', '최소값', '최대값', '표준편차', '데이터 개수'],368 '값': [369 f"{stats['mean']:.4f}",370 f"{stats['median']:.4f}",371 f"{stats['min']:.4f}",372 f"{stats['max']:.4f}",373 f"{stats['std']:.4f}",374 f"{stats['count']}"375 ],376 '단위': ['m/s', 'm/s', 'm/s', 'm/s', 'm/s', '-']377 }378 379 st.table(stats_data)380 381 # 다운로드 버튼382 col1, col2 = st.columns(2)383 384 with col1:385 # CSV 다운로드386 csv_data = "시간(초),유속(m/s)\n"387 for time, vel in zip(tracker.timestamps, tracker.velocities):388 csv_data += f"{time:.3f},{vel:.6f}\n"389 390 st.download_button(391 label="📥 CSV로 다운로드",392 data=csv_data,393 file_name="river_velocity_data.csv",394 mime="text/csv",395 use_container_width=True396 )397 398 with col2:399 # 리포트 다운로드400 report = f"""401강의 유속 측정 리포트402{'='*50}403 404측정 설정:405- 프레임 속도: {st.session_state.fps} fps406- 스케일: {st.session_state.scale*100} cm/픽셀407- 프레임 간격: {st.session_state.skip_frames}408 409결과:410- 평균 유속: {stats['mean']:.4f} m/s411- 중앙값: {stats['median']:.4f} m/s412- 최소 유속: {stats['min']:.4f} m/s413- 최대 유속: {stats['max']:.4f} m/s414- 표준편차: {stats['std']:.4f} m/s415- 데이터 개수: {stats['count']}416"""417 st.download_button(418 label="📄 리포트 다운로드",419 data=report,420 file_name="river_velocity_report.txt",421 mime="text/plain",422 use_container_width=True423 )424 425 else:426 st.info("📹 먼저 비디오를 업로드하고 측정을 시작하세요!")427 428 # ========== 탭 3: 라이브 측정 ==========429 with tab3:430 st.header("🎥 실시간 유속 측정")431 st.markdown("웹캠으로 촬영하면서 실시간으로 유속을 측정하고 영상에 표시합니다")432 433 col1, col2 = st.columns([2, 1])434 435 with col1:436 st.subheader("📏 측정 파라미터")437 col_a, col_b = st.columns(2)438 439 with col_a:440 fps_live = st.slider(441 "프레임 속도 (FPS)",442 min_value=10,443 max_value=60,444 value=30,445 help="웹캠의 프레임 속도"446 )447 448 with col_b:449 scale_cm_live = st.number_input(450 "스케일 (cm/픽셀)",451 min_value=0.1,452 max_value=10.0,453 value=1.0,454 step=0.1,455 key="scale_live"456 )457 458 scale_live = scale_cm_live / 100459 460 with col2:461 st.info("💡 **웹캠 연결 필수**\n\n웹캠이 컴퓨터에 연결되어 있어야 합니다")462 463 col1, col2 = st.columns([1, 1])464 465 with col1:466 start_live = st.button("▶️ 측정 시작", use_container_width=True, key="start_live")467 with col2:468 stop_live = st.button("⏹️ 측정 중지", use_container_width=True, key="stop_live")469 470 if start_live:471 st.session_state.live_measuring = True472 473 if stop_live:474 st.session_state.live_measuring = False475 476 # 라이브 측정 실행477 if st.session_state.get('live_measuring', False):478 video_placeholder = st.empty()479 status_placeholder = st.empty()480 stats_placeholder = st.empty()481 482 cap = cv2.VideoCapture(0)483 484 if not cap.isOpened():485 st.error("❌ 웹캠을 연결할 수 없습니다. 웹캠이 연결되어 있는지 확인하세요.")486 else:487 # 웹캠 해상도 설정488 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)489 cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)490 cap.set(cv2.CAP_PROP_FPS, fps_live)491 492 prev_frame = None493 live_velocities = []494 frame_count = 0495 stop_flag = False496 output_frames = []497 498 status_placeholder.info("🔴 라이브 측정 중...")499 500 try:501 while st.session_state.get('live_measuring', False) and not stop_flag:502 ret, frame = cap.read()503 504 if not ret:505 break506 507 frame = cv2.resize(frame, (640, 480))508 frame_count += 1509 510 display_frame = frame.copy()511 velocity = None512 513 # Optical flow 계산514 if prev_frame is not None:515 gray_prev = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY)516 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)517 518 corners = cv2.goodFeaturesToTrack(519 gray_prev, maxCorners=200, qualityLevel=0.01, minDistance=10520 )521 522 if corners is not None and len(corners) > 0:523 lk_params = dict(524 winSize=(15, 15), maxLevel=2,525 criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03)526 )527 528 next_points, status, error = cv2.calcOpticalFlowPyrLK(529 gray_prev, gray, corners, None, **lk_params530 )531 532 if next_points is not None:533 good_prev = corners[status == 1]534 good_next = next_points[status == 1]535 536 if len(good_prev) > 0:537 displacements = np.linalg.norm(good_next - good_prev, axis=1)538 avg_displacement = np.median(displacements)539 velocity = (avg_displacement * scale_live) / (1.0 / fps_live)540 541 # 유효 범위 체크542 if 0 < velocity < 5:543 live_velocities.append(velocity)544 545 # 영상에 유속 표시546 if velocity is not None and 0 < velocity < 5:547 text = f"Velocity: {velocity:.3f} m/s"548 color = (0, 255, 0) # 녹색549 else:550 text = "Velocity: N/A"551 color = (0, 0, 255) # 빨간색552 553 # 텍스트 배경554 cv2.rectangle(display_frame, (10, 10), (350, 50), (0, 0, 0), -1)555 cv2.putText(556 display_frame, text, (15, 40),557 cv2.FONT_HERSHEY_SIMPLEX, 1.0, color, 2558 )559 560 # 통계 표시561 if live_velocities:562 stats_text = f"Mean: {np.mean(live_velocities):.3f} m/s | Count: {len(live_velocities)}"563 cv2.putText(564 display_frame, stats_text, (15, 80),565 cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1566 )567 568 output_frames.append(display_frame.copy())569 570 # 영상 표시571 display_frame_rgb = cv2.cvtColor(display_frame, cv2.COLOR_BGR2RGB)572 video_placeholder.image(display_frame_rgb, use_column_width=True)573 574 prev_frame = frame.copy()575 576 # 상태 업데이트577 if frame_count % 10 == 0:578 if live_velocities:579 status_placeholder.info(580 f"⏱️ 프레임: {frame_count} | 평균 유속: {np.mean(live_velocities):.3f} m/s"581 )582 583 finally:584 cap.release()585 586 # 측정 완료587 st.session_state.live_measuring = False588 status_placeholder.success(f"✅ 측정 완료: {len(live_velocities)}개 데이터 수집")589 590 # 결과 저장591 if live_velocities:592 col1, col2 = st.columns(2)593 594 with col1:595 # 영상 저장596 if output_frames:597 output_path = "/tmp/river_velocity_live.mp4"598 fourcc = cv2.VideoWriter_fourcc(*'mp4v')599 out = cv2.VideoWriter(output_path, fourcc, fps_live, (640, 480))600 601 for frame in output_frames:602 out.write(frame)603 out.release()604 605 with open(output_path, 'rb') as f:606 st.download_button(607 label="🎬 영상 다운로드",608 data=f.read(),609 file_name="river_velocity_live.mp4",610 mime="video/mp4",611 use_container_width=True612 )613 614 with col2:615 # CSV 저장616 csv_data = "프레임,유속(m/s)\n"617 for i, vel in enumerate(live_velocities):618 csv_data += f"{i},{vel:.6f}\n"619 620 st.download_button(621 label="📥 데이터 다운로드",622 data=csv_data,623 file_name="river_velocity_live.csv",624 mime="text/csv",625 use_container_width=True626 )627 628 # ========== 탭 4: 가이드 ==========629 with tab4:630 st.header("📖 사용 가이드")631 632 st.subheader("1️⃣ 스케일 측정 방법")633 st.markdown("""634 유속 측정을 위해서는 **픽셀과 실제 거리의 비율(스케일)**을 알아야 합니다.635 636 **방법:**637 1. 강에 **1미터 길이의 자** 또는 **알려진 길이의 물체**를 놓습니다638 2. 그 물체가 비디오에서 **몇 픽셀**인지 측정합니다 (이미지 편집 프로그램 사용)639 3. 스케일 = 실제 거리(미터) / 픽셀 수 를 계산합니다640 641 **예시:**642 - 1미터가 100픽셀 → 스케일 = 1/100 = 0.01 m/px = 1 cm/픽셀643 - 1미터가 50픽셀 → 스케일 = 1/50 = 0.02 m/px = 2 cm/픽셀644 """)645 646 st.subheader("2️⃣ 프레임 속도(FPS) 확인")647 st.markdown("""648 비디오의 정확한 프레임 속도를 알아야 합니다.649 650 **방법:**651 - 스마트폰: 보통 30fps 또는 60fps652 - 드론: 보통 24fps, 30fps, 60fps653 - 카메라: 설정에 따라 다름654 655 **확인 방법:**656 - 파일 정보 확인 (Windows 속성, macOS 정보 보기)657 - ffmpeg 사용: `ffmpeg -i video.mp4`658 """)659 660 st.subheader("3️⃣ 영상 촬영 팁")661 st.markdown("""662 ✅ **좋은 촬영 조건:**663 - 명확한 흐름이 보이도록 정면에서 촬영664 - 스케일이 명확한 물체 포함665 - 충분한 조명666 - 안정적인 카메라 (삼각대 사용 권장)667 - 최소 10초 이상 촬영668 669 ❌ **피해야 할 것:**670 - 흐릿한 영상671 - 카메라 흔들림672 - 반사가 많은 장면673 - 너무 빠르거나 느린 속도 설정674 """)675 676 st.subheader("4️⃣ 결과 해석")677 st.markdown("""678 **일반적인 강의 유속:**679 - 0.1~0.3 m/s: 매우 느린 흐름680 - 0.3~0.5 m/s: 보통 흐름681 - 0.5~1.0 m/s: 빠른 흐름682 - 1.0~2.0 m/s: 매우 빠른 흐름 (위험)683 684 **표준편차가 크면:**685 - 흐름이 불안정686 - 영상 품질 재검토 필요687 """)688 689 690if __name__ == "__main__":691 main()692 