CoolFace
Apppublic

tae3lee/VisionVelocity

sourceHugging Faceupdated 2mo agoView on Hugging Face
0likes
app.py692 linesDownload Raw Back to root
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