version: v0.0.1
This commit is contained in:
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"python-envs.defaultEnvManager": "ms-python.python:conda",
|
||||
"python-envs.defaultPackageManager": "ms-python.python:conda"
|
||||
}
|
||||
337
cctv.py
Normal file
337
cctv.py
Normal file
@@ -0,0 +1,337 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@file : cctv.py
|
||||
@author: hsj100
|
||||
@license: A2TEC
|
||||
@brief: multi cctv
|
||||
|
||||
@section Modify History
|
||||
- 2025-11-24 오후 1:38 hsj100 base
|
||||
|
||||
"""
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import threading
|
||||
import time
|
||||
import os
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from demo_detect import DemoDetect
|
||||
from utils import get_monitorsize
|
||||
from config_loader import CFG
|
||||
from parsing_msg import ParsingMsg
|
||||
from mqtt import mqtt_publisher
|
||||
|
||||
# ==========================================================================================
|
||||
# [Configuration Section]
|
||||
# ==========================================================================================
|
||||
|
||||
_cctv_cfg = CFG.get('cctv', {})
|
||||
|
||||
# 1. Camera Switch Interval (Seconds)
|
||||
SWITCH_INTERVAL = _cctv_cfg.get('switch_interval', 5.0)
|
||||
|
||||
# 2. Camera Sources List
|
||||
SOURCES = _cctv_cfg.get('sources', [])
|
||||
if not SOURCES:
|
||||
# 기본값 (설정이 없을 경우)
|
||||
SOURCES = [
|
||||
{"src": "rtsp://192.168.200.231:50199/wd", "name": "dev1_stream"},
|
||||
{"src": "rtsp://192.168.200.232:50299/wd", "name": "dev2_stream"},
|
||||
]
|
||||
|
||||
# ==========================================================================================
|
||||
|
||||
class CameraStream:
|
||||
"""
|
||||
개별 카메라 스트림을 관리하는 클래스.
|
||||
별도의 스레드에서 프레임을 계속 읽어와서 '최신 프레임(latest_frame)' 변수에 저장합니다.
|
||||
"""
|
||||
def __init__(self, src, name="Camera"):
|
||||
self.src = src
|
||||
self.name = name
|
||||
|
||||
# RTSP를 TCP로 강제 설정 (461 Unsupported Transport, Packet Loss 방지)
|
||||
os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp"
|
||||
self.cap = cv2.VideoCapture(self.src, cv2.CAP_FFMPEG)
|
||||
|
||||
# OpenCV 버퍼 사이즈를 최소화하여 레이턴시 감소 (백엔드에 따라 동작 안 할 수도 있음)
|
||||
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
||||
|
||||
self.ret = False
|
||||
self.latest_frame = None
|
||||
self.running = False
|
||||
self.is_active = False # [최적화] 현재 화면 표시 여부
|
||||
self.lock = threading.Lock() # 프레임 읽기/쓰기 충돌 방지
|
||||
self.last_read_time = time.time() # 마지막 프레임 수신 시간 초기화
|
||||
self.frame_skip_count = 0
|
||||
|
||||
# 연결 확인
|
||||
if not self.cap.isOpened():
|
||||
print(f"[{self.name}] 접속 실패: {self.src}")
|
||||
else:
|
||||
print(f"[{self.name}] 접속 성공")
|
||||
self.running = True
|
||||
# 스레드 시작
|
||||
self.thread = threading.Thread(target=self.update, args=())
|
||||
self.thread.daemon = True # 메인 프로그램 종료 시 스레드도 강제 종료
|
||||
self.thread.start()
|
||||
|
||||
def update(self):
|
||||
"""백그라운드에서 계속 프레임을 읽어오는 함수 (grab + retrieve 분리)"""
|
||||
while self.running:
|
||||
# 1. 버퍼에서 압축된 데이터 가져오기 (grab은 항상 수행하여 버퍼가 쌓이지 않게 함)
|
||||
if self.cap.grab():
|
||||
# [최적화 로직] 활성 상태일 때만 디코딩(retrieve) 수행
|
||||
# 비활성 상태일 때는 30프레임에 한 번만 디코딩하여 리소스 절약
|
||||
self.frame_skip_count += 1
|
||||
if self.is_active or (self.frame_skip_count % 30 == 0):
|
||||
# 2. 실제 이미지로 디코딩 (무거움)
|
||||
ret, frame = self.cap.retrieve()
|
||||
|
||||
if ret:
|
||||
with self.lock:
|
||||
self.ret = ret
|
||||
self.latest_frame = frame
|
||||
self.last_read_time = time.time() # 수신 시간 갱신
|
||||
else:
|
||||
# 디코딩을 건너뛰는 동안 CPU 부하를 아주 약간 줄이기 위해 미세한 대기
|
||||
time.sleep(0.001)
|
||||
else:
|
||||
# 스트림이 끊기거나 끝난 경우 재접속 로직
|
||||
print(f"[{self.name}] 신호 없음 (grab failed). 2초 후 재접속 시도...")
|
||||
self.cap.release()
|
||||
time.sleep(2.0)
|
||||
|
||||
# 재접속 시도 (TCP 강제 설정 유지)
|
||||
os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp"
|
||||
self.cap = cv2.VideoCapture(self.src, cv2.CAP_FFMPEG)
|
||||
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
||||
|
||||
if self.cap.isOpened():
|
||||
print(f"[{self.name}] 재접속 성공")
|
||||
else:
|
||||
print(f"[{self.name}] 재접속 실패")
|
||||
|
||||
def get_frame(self):
|
||||
"""현재 저장된 가장 최신 프레임을 반환 (타임아웃 체크 포함)"""
|
||||
with self.lock:
|
||||
# 마지막 수신 후 3초 이상 지났으면 신호 없음(False) 처리
|
||||
if time.time() - self.last_read_time > 3.0:
|
||||
return False, None
|
||||
return self.ret, self.latest_frame
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
if self.thread.is_alive():
|
||||
self.thread.join()
|
||||
self.cap.release()
|
||||
|
||||
def main():
|
||||
|
||||
MQTT_TOPIC = "/hospital/ai1"
|
||||
|
||||
parser = ParsingMsg()
|
||||
|
||||
# Get Monitor Size inside main or top level if safe
|
||||
MONITOR_RESOLUTION = get_monitorsize()
|
||||
|
||||
# 2. 객체탐지 모델 초기화
|
||||
detector = DemoDetect()
|
||||
print("모델 로딩 완료!")
|
||||
|
||||
# 3. 카메라 객체 생성 및 백그라운드 수신 시작
|
||||
cameras = []
|
||||
for i, s in enumerate(SOURCES):
|
||||
cam = CameraStream(s["src"], s["name"])
|
||||
# 첫 번째 카메라만 활성 상태로 시작
|
||||
if i == 0:
|
||||
cam.is_active = True
|
||||
cameras.append(cam)
|
||||
|
||||
DISPLAY_TURN_ON = CFG.get('display').get('turn_on') # display 출력 여부
|
||||
|
||||
current_cam_index = 0 # 현재 보고 있는 카메라 인덱스
|
||||
last_switch_time = time.time() # 마지막 전환 시간
|
||||
switch_interval = SWITCH_INTERVAL # 5초마다 자동 전환
|
||||
|
||||
# FPS 계산을 위한 변수
|
||||
prev_frame_time = time.time()
|
||||
fps = 0
|
||||
|
||||
# FPS 통계 변수
|
||||
fps_min = float('inf')
|
||||
fps_max = 0
|
||||
fps_sum = 0
|
||||
fps_count = 0
|
||||
warmup_frames = 30 # 초반 불안정한 프레임 제외
|
||||
|
||||
print("\n=== CCTV 시스템 시작 ===")
|
||||
print(f"{switch_interval}초마다 자동으로 카메라가 전환됩니다.")
|
||||
if DISPLAY_TURN_ON:
|
||||
print("[Q] 또는 [ESC] : 종료\n")
|
||||
else:
|
||||
print("화면 출력이 꺼져 있습니다. 시스템은 백그라운드에서 계속 실행됩니다.")
|
||||
print("종료하려면 터미널에서 Ctrl+C를 누르세요.\n")
|
||||
|
||||
if DISPLAY_TURN_ON:
|
||||
# 전체화면 윈도우 설정 (프레임 없이)
|
||||
window_name = "CCTV Control Center"
|
||||
cv2.namedWindow(window_name, cv2.WND_PROP_FULLSCREEN)
|
||||
cv2.setWindowProperty(window_name, cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
|
||||
|
||||
while True:
|
||||
# 1. 카메라 전환 처리 (자동)
|
||||
current_time = time.time()
|
||||
need_switch = False
|
||||
|
||||
if DISPLAY_TURN_ON:
|
||||
# 키 입력 처리 (종료만 체크)
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
if key == ord('q') or key == 27: # 종료
|
||||
break
|
||||
|
||||
# 자동 전환 타이머 체크
|
||||
if current_time - last_switch_time >= switch_interval:
|
||||
need_switch = True
|
||||
|
||||
if need_switch:
|
||||
# 기존 카메라 비활성화 (리소스 절약 모드 진입)
|
||||
cameras[current_cam_index].is_active = False
|
||||
|
||||
# 다음 카메라 인덱스 계산
|
||||
current_cam_index = (current_cam_index + 1) % len(cameras)
|
||||
|
||||
# 새 카메라 활성화 (풀 디코딩 시작)
|
||||
cameras[current_cam_index].is_active = True
|
||||
|
||||
last_switch_time = current_time
|
||||
print(f"-> 자동 전환: {cameras[current_cam_index].name}")
|
||||
|
||||
# 2. 현재 선택된 카메라의 최신 프레임 가져오기
|
||||
target_cam = cameras[current_cam_index]
|
||||
ret, frame = target_cam.get_frame()
|
||||
|
||||
if ret and frame is not None:
|
||||
# FPS 계산
|
||||
current_frame_time = time.time()
|
||||
time_diff = current_frame_time - prev_frame_time
|
||||
fps = 1 / time_diff if time_diff > 0 else 0
|
||||
prev_frame_time = current_frame_time
|
||||
|
||||
# FPS 통계 갱신 (워밍업 이후)
|
||||
if fps > 0:
|
||||
fps_count += 1
|
||||
if fps_count > warmup_frames:
|
||||
fps_sum += fps
|
||||
real_count = fps_count - warmup_frames
|
||||
|
||||
fps_avg = fps_sum / real_count
|
||||
fps_min = min(fps_min, fps)
|
||||
fps_max = max(fps_max, fps)
|
||||
|
||||
# 객체탐지 인퍼런스 수행 (원본 해상도로 추론하여 불필요한 리사이즈 제거)
|
||||
# HPE
|
||||
hpe_message = detector.inference_hpe(frame, False)
|
||||
|
||||
#NOTE(jwkim): od모델 사용 안함
|
||||
# Helmet 탐지
|
||||
# helmet_message = detector.inference_helmet(frame, False)
|
||||
|
||||
# 객체 탐지
|
||||
# od_message_raw = detector.inference_od(frame, False, class_name_view=True)
|
||||
|
||||
# 필터링
|
||||
# od_message = detector.od_filtering(frame, od_message_raw, helmet_message, hpe_message, add_siginal_man=False)
|
||||
|
||||
od_message = []
|
||||
|
||||
parser.set(msg=hpe_message,img=frame)
|
||||
parsing_msg = parser.parse()
|
||||
|
||||
#mqtt publish
|
||||
if parsing_msg is not None:
|
||||
mqtt_publisher.client.publish(MQTT_TOPIC, parsing_msg, qos=1)
|
||||
|
||||
# 탐지 결과 라벨링 (원본 좌표 기준)
|
||||
if DISPLAY_TURN_ON:
|
||||
detector.hpe_labeling(frame, hpe_message)
|
||||
detector.od_labeling(frame, od_message)
|
||||
detector.border_labeling(frame, hpe_message, od_message)
|
||||
|
||||
# 모니터 해상도에 맞춰 리사이즈 (라벨링 완료 후 화면 표시용)
|
||||
frame = cv2.resize(frame, MONITOR_RESOLUTION)
|
||||
|
||||
# 화면에 카메라 이름과 시간 표시 (UI 효과)
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# 카메라 이름 (테두리 + 본문)
|
||||
cam_text = f"CH {current_cam_index+1}: {target_cam.name}"
|
||||
cv2.putText(frame, cam_text, (20, 40),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 5, cv2.LINE_AA) # 검은색 테두리 (진하게)
|
||||
cv2.putText(frame, cam_text, (20, 40),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2, cv2.LINE_AA) # 초록색 본문
|
||||
|
||||
# 시간 (테두리 + 본문)
|
||||
cv2.putText(frame, timestamp, (20, 80),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 5, cv2.LINE_AA) # 검은색 테두리
|
||||
cv2.putText(frame, timestamp, (20, 80),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2, cv2.LINE_AA) # 초록색 본문
|
||||
|
||||
# FPS (테두리 + 본문)
|
||||
fps_text = f"FPS: {fps:.1f}"
|
||||
cv2.putText(frame, fps_text, (20, 120),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 5, cv2.LINE_AA) # 검은색 테두리
|
||||
cv2.putText(frame, fps_text, (20, 120),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2, cv2.LINE_AA) # 초록색 본문
|
||||
|
||||
# FPS 통계 (AVG, MIN, MAX)
|
||||
stat_val_min = fps_min if fps_min != float('inf') else 0
|
||||
stat_val_avg = fps_sum / (fps_count - warmup_frames) if fps_count > warmup_frames else 0
|
||||
|
||||
stats_text = f"AVG: {stat_val_avg:.1f} MIN: {stat_val_min:.1f} MAX: {fps_max:.1f}"
|
||||
cv2.putText(frame, stats_text, (20, 160),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 5, cv2.LINE_AA)
|
||||
cv2.putText(frame, stats_text, (20, 160),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2, cv2.LINE_AA) # 노란색
|
||||
|
||||
cv2.imshow(window_name, frame)
|
||||
|
||||
|
||||
elif DISPLAY_TURN_ON:
|
||||
# 신호가 없을 때 보여줄 대기 화면
|
||||
# MONITOR_RESOLUTION은 (width, height)이므로 numpy shape는 (height, width, 3)이어야 함
|
||||
blank_screen = np.zeros((MONITOR_RESOLUTION[1], MONITOR_RESOLUTION[0], 3), dtype=np.uint8)
|
||||
|
||||
# "NO SIGNAL" 텍스트 설정
|
||||
text = f"NO SIGNAL ({target_cam.name})"
|
||||
font = cv2.FONT_HERSHEY_SIMPLEX
|
||||
font_scale = 1.5
|
||||
thickness = 3
|
||||
|
||||
# 텍스트 중앙 정렬 계산
|
||||
text_size = cv2.getTextSize(text, font, font_scale, thickness)[0]
|
||||
text_x = (blank_screen.shape[1] - text_size[0]) // 2
|
||||
text_y = (blank_screen.shape[0] + text_size[1]) // 2
|
||||
|
||||
# 텍스트 그리기 (빨간색)
|
||||
cv2.putText(blank_screen, text, (text_x, text_y), font, font_scale, (0, 0, 255), thickness, cv2.LINE_AA)
|
||||
|
||||
cv2.imshow(window_name, blank_screen)
|
||||
|
||||
# 4. 키 입력 처리 (종료만 처리)
|
||||
if DISPLAY_TURN_ON:
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
if key == ord('q') or key == 27: # 종료
|
||||
break
|
||||
|
||||
# 종료 처리
|
||||
print("시스템 종료 중...")
|
||||
for cam in cameras:
|
||||
cam.stop()
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
280
cctv_origin.py
Normal file
280
cctv_origin.py
Normal file
@@ -0,0 +1,280 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@file : cctv.py
|
||||
@author: hsj100
|
||||
@license: A2TEC
|
||||
@brief: multi cctv
|
||||
|
||||
@section Modify History
|
||||
- 2025-11-24 오후 1:38 hsj100 base
|
||||
|
||||
"""
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import threading
|
||||
import time
|
||||
import os
|
||||
from datetime import datetime
|
||||
from demo_detect import DemoDetect
|
||||
from utils import get_monitorsize
|
||||
from config_loader import CFG
|
||||
|
||||
# ==========================================================================================
|
||||
# [Configuration Section]
|
||||
# ==========================================================================================
|
||||
|
||||
_cctv_cfg = CFG.get('cctv', {})
|
||||
|
||||
# 1. Camera Switch Interval (Seconds)
|
||||
SWITCH_INTERVAL = _cctv_cfg.get('switch_interval', 5.0)
|
||||
|
||||
# 2. Camera Sources List
|
||||
SOURCES = _cctv_cfg.get('sources', [])
|
||||
if not SOURCES:
|
||||
# 기본값 (설정이 없을 경우)
|
||||
SOURCES = [
|
||||
{"src": "rtsp://192.168.200.231:50199/wd", "name": "dev1_stream"},
|
||||
{"src": "rtsp://192.168.200.232:50299/wd", "name": "dev2_stream"},
|
||||
]
|
||||
|
||||
# ==========================================================================================
|
||||
|
||||
class CameraStream:
|
||||
"""
|
||||
개별 카메라 스트림을 관리하는 클래스.
|
||||
별도의 스레드에서 프레임을 계속 읽어와서 '최신 프레임(latest_frame)' 변수에 저장합니다.
|
||||
"""
|
||||
def __init__(self, src, name="Camera"):
|
||||
self.src = src
|
||||
self.name = name
|
||||
|
||||
# RTSP를 TCP로 강제 설정 (461 Unsupported Transport, Packet Loss 방지)
|
||||
os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp"
|
||||
self.cap = cv2.VideoCapture(self.src, cv2.CAP_FFMPEG)
|
||||
|
||||
# OpenCV 버퍼 사이즈를 최소화하여 레이턴시 감소 (백엔드에 따라 동작 안 할 수도 있음)
|
||||
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
||||
|
||||
self.ret = False
|
||||
self.latest_frame = None
|
||||
self.running = False
|
||||
self.lock = threading.Lock() # 프레임 읽기/쓰기 충돌 방지
|
||||
self.last_read_time = time.time() # 마지막 프레임 수신 시간 초기화
|
||||
|
||||
# 연결 확인
|
||||
if not self.cap.isOpened():
|
||||
print(f"[{self.name}] 접속 실패: {self.src}")
|
||||
else:
|
||||
print(f"[{self.name}] 접속 성공")
|
||||
self.running = True
|
||||
# 스레드 시작
|
||||
self.thread = threading.Thread(target=self.update, args=())
|
||||
self.thread.daemon = True # 메인 프로그램 종료 시 스레드도 강제 종료
|
||||
self.thread.start()
|
||||
|
||||
def update(self):
|
||||
"""백그라운드에서 계속 프레임을 읽어오는 함수 (grab + retrieve 분리)"""
|
||||
while self.running:
|
||||
# 1. 버퍼에서 압축된 데이터 가져오기 (가볍고 빠름)
|
||||
if self.cap.grab():
|
||||
# 2. 실제 이미지로 디코딩 (무거움)
|
||||
ret, frame = self.cap.retrieve()
|
||||
|
||||
if ret:
|
||||
with self.lock:
|
||||
self.ret = ret
|
||||
self.latest_frame = frame
|
||||
self.last_read_time = time.time() # 수신 시간 갱신
|
||||
else:
|
||||
# grab은 성공했으나 디코딩 실패 (드문 경우)
|
||||
continue
|
||||
else:
|
||||
# 스트림이 끊기거나 끝난 경우 재접속 로직
|
||||
print(f"[{self.name}] 신호 없음 (grab failed). 2초 후 재접속 시도...")
|
||||
self.cap.release()
|
||||
time.sleep(2.0)
|
||||
|
||||
# 재접속 시도 (TCP 강제 설정 유지)
|
||||
os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp"
|
||||
self.cap = cv2.VideoCapture(self.src, cv2.CAP_FFMPEG)
|
||||
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
||||
|
||||
if self.cap.isOpened():
|
||||
print(f"[{self.name}] 재접속 성공")
|
||||
else:
|
||||
print(f"[{self.name}] 재접속 실패")
|
||||
|
||||
def get_frame(self):
|
||||
"""현재 저장된 가장 최신 프레임을 반환 (타임아웃 체크 포함)"""
|
||||
with self.lock:
|
||||
# 마지막 수신 후 3초 이상 지났으면 신호 없음(False) 처리
|
||||
if time.time() - self.last_read_time > 3.0:
|
||||
return False, None
|
||||
return self.ret, self.latest_frame
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
if self.thread.is_alive():
|
||||
self.thread.join()
|
||||
self.cap.release()
|
||||
|
||||
def main():
|
||||
# Get Monitor Size inside main or top level if safe
|
||||
MONITOR_RESOLUTION = get_monitorsize()
|
||||
|
||||
# 2. 객체탐지 모델 초기화
|
||||
detector = DemoDetect()
|
||||
print("모델 로딩 완료!")
|
||||
|
||||
# 3. 카메라 객체 생성 및 백그라운드 수신 시작
|
||||
cameras = []
|
||||
for s in SOURCES:
|
||||
cam = CameraStream(s["src"], s["name"])
|
||||
cameras.append(cam)
|
||||
|
||||
current_cam_index = 0 # 현재 보고 있는 카메라 인덱스
|
||||
last_switch_time = time.time() # 마지막 전환 시간
|
||||
switch_interval = SWITCH_INTERVAL # 5초마다 자동 전환
|
||||
|
||||
# FPS 계산을 위한 변수
|
||||
prev_frame_time = time.time()
|
||||
fps = 0
|
||||
|
||||
# FPS 통계 변수
|
||||
fps_min = float('inf')
|
||||
fps_max = 0
|
||||
fps_sum = 0
|
||||
fps_count = 0
|
||||
warmup_frames = 30 # 초반 불안정한 프레임 제외
|
||||
|
||||
print("\n=== CCTV 관제 시스템 시작 ===")
|
||||
print("5초마다 자동으로 카메라가 전환됩니다.")
|
||||
print("[Q] 또는 [ESC]를 눌러 종료합니다.\n")
|
||||
|
||||
# 전체화면 윈도우 설정 (프레임 없이)
|
||||
window_name = "CCTV Control Center"
|
||||
cv2.namedWindow(window_name, cv2.WND_PROP_FULLSCREEN)
|
||||
cv2.setWindowProperty(window_name, cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
|
||||
|
||||
while True:
|
||||
# 자동 전환 로직: 5초마다 다음 카메라로 전환
|
||||
current_time = time.time()
|
||||
if current_time - last_switch_time >= switch_interval:
|
||||
current_cam_index = (current_cam_index + 1) % len(cameras)
|
||||
last_switch_time = current_time
|
||||
print(f"-> 자동 전환: {cameras[current_cam_index].name}")
|
||||
|
||||
# 3. 현재 선택된 카메라의 최신 프레임 가져오기
|
||||
target_cam = cameras[current_cam_index]
|
||||
ret, frame = target_cam.get_frame()
|
||||
|
||||
if ret and frame is not None:
|
||||
# FPS 계산
|
||||
current_frame_time = time.time()
|
||||
time_diff = current_frame_time - prev_frame_time
|
||||
fps = 1 / time_diff if time_diff > 0 else 0
|
||||
prev_frame_time = current_frame_time
|
||||
|
||||
# FPS 통계 갱신 (워밍업 이후)
|
||||
if fps > 0:
|
||||
fps_count += 1
|
||||
if fps_count > warmup_frames:
|
||||
fps_sum += fps
|
||||
real_count = fps_count - warmup_frames
|
||||
|
||||
fps_avg = fps_sum / real_count
|
||||
fps_min = min(fps_min, fps)
|
||||
fps_max = max(fps_max, fps)
|
||||
|
||||
# 객체탐지 인퍼런스 수행 (원본 해상도로 추론하여 불필요한 리사이즈 제거)
|
||||
# HPE
|
||||
hpe_message = detector.inference_hpe(frame, False)
|
||||
|
||||
# Helmet 탐지
|
||||
helmet_message = detector.inference_helmet(frame, False)
|
||||
|
||||
# 객체 탐지
|
||||
od_message_raw = detector.inference_od(frame, False, class_name_view=True)
|
||||
|
||||
# 필터링
|
||||
od_message = detector.od_filtering(frame, od_message_raw, helmet_message, hpe_message, add_siginal_man=False)
|
||||
|
||||
# 탐지 결과 라벨링 (원본 좌표 기준)
|
||||
detector.hpe_labeling(frame, hpe_message)
|
||||
detector.od_labeling(frame, od_message)
|
||||
detector.border_labeling(frame, hpe_message, od_message)
|
||||
|
||||
# 모니터 해상도에 맞춰 리사이즈 (라벨링 완료 후 화면 표시용)
|
||||
frame = cv2.resize(frame, MONITOR_RESOLUTION)
|
||||
|
||||
# 화면에 카메라 이름과 시간 표시 (UI 효과)
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# 카메라 이름 (테두리 + 본문)
|
||||
cam_text = f"CH {current_cam_index+1}: {target_cam.name}"
|
||||
cv2.putText(frame, cam_text, (20, 40),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 5, cv2.LINE_AA) # 검은색 테두리 (진하게)
|
||||
cv2.putText(frame, cam_text, (20, 40),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2, cv2.LINE_AA) # 초록색 본문
|
||||
|
||||
# 시간 (테두리 + 본문)
|
||||
cv2.putText(frame, timestamp, (20, 80),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 5, cv2.LINE_AA) # 검은색 테두리
|
||||
cv2.putText(frame, timestamp, (20, 80),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2, cv2.LINE_AA) # 초록색 본문
|
||||
|
||||
# FPS (테두리 + 본문)
|
||||
fps_text = f"FPS: {fps:.1f}"
|
||||
cv2.putText(frame, fps_text, (20, 120),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 5, cv2.LINE_AA) # 검은색 테두리
|
||||
cv2.putText(frame, fps_text, (20, 120),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2, cv2.LINE_AA) # 초록색 본문
|
||||
|
||||
# FPS 통계 (AVG, MIN, MAX)
|
||||
stat_val_min = fps_min if fps_min != float('inf') else 0
|
||||
stat_val_avg = fps_sum / (fps_count - warmup_frames) if fps_count > warmup_frames else 0
|
||||
|
||||
stats_text = f"AVG: {stat_val_avg:.1f} MIN: {stat_val_min:.1f} MAX: {fps_max:.1f}"
|
||||
cv2.putText(frame, stats_text, (20, 160),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 5, cv2.LINE_AA)
|
||||
cv2.putText(frame, stats_text, (20, 160),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2, cv2.LINE_AA) # 노란색
|
||||
|
||||
cv2.imshow(window_name, frame)
|
||||
else:
|
||||
# 신호가 없을 때 보여줄 대기 화면
|
||||
# MONITOR_RESOLUTION은 (width, height)이므로 numpy shape는 (height, width, 3)이어야 함
|
||||
blank_screen = np.zeros((MONITOR_RESOLUTION[1], MONITOR_RESOLUTION[0], 3), dtype=np.uint8)
|
||||
|
||||
# "NO SIGNAL" 텍스트 설정
|
||||
text = f"NO SIGNAL ({target_cam.name})"
|
||||
font = cv2.FONT_HERSHEY_SIMPLEX
|
||||
font_scale = 1.5
|
||||
thickness = 3
|
||||
|
||||
# 텍스트 중앙 정렬 계산
|
||||
text_size = cv2.getTextSize(text, font, font_scale, thickness)[0]
|
||||
text_x = (blank_screen.shape[1] - text_size[0]) // 2
|
||||
text_y = (blank_screen.shape[0] + text_size[1]) // 2
|
||||
|
||||
# 텍스트 그리기 (빨간색)
|
||||
cv2.putText(blank_screen, text, (text_x, text_y), font, font_scale, (0, 0, 255), thickness, cv2.LINE_AA)
|
||||
|
||||
cv2.imshow(window_name, blank_screen)
|
||||
|
||||
# 4. 키 입력 처리 (종료만 처리)
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
|
||||
# 종료
|
||||
if key == ord('q') or key == 27:
|
||||
break
|
||||
|
||||
# 종료 처리
|
||||
print("시스템 종료 중...")
|
||||
for cam in cameras:
|
||||
cam.stop()
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
63
color_table.py
Normal file
63
color_table.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import project_config
|
||||
|
||||
TEXT_COLOR_WHITE = [255,255,255]
|
||||
TEXT_COLOR_BLACK = [0,0,0]
|
||||
|
||||
class ColorInfo:
|
||||
def __init__(self,label_color,font_color):
|
||||
self.label_color = label_color
|
||||
self.font_color = font_color
|
||||
|
||||
dev_color_table = {
|
||||
"person" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [183, 183, 183],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"truck" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [4, 63, 120],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [255, 255, 255]),
|
||||
"safety_helmet_off" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [193, 193, 255],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"safety_helmet_on" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [0, 0, 255],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"safety_gloves_off" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [184,255,205],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"safety_gloves_work_on" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [70,251,124],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"safety_gloves_insulated_on_1" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [59,216,106],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [255, 255, 255]),
|
||||
"safety_gloves_insulated_on_2" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [45,169,82],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [255, 255, 255]),
|
||||
"safety_gloves_insulated_on_3" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [29,114,55],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [255, 255, 255]),
|
||||
"safety_gloves_insulated_on_4" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [14, 54, 26],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [255, 255, 255]),
|
||||
"safety_rubber_insulated_sleeve_off" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [233, 210, 217],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"safety_rubber_insulated_sleeve_on" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [195, 124, 142],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [255, 255, 255]),
|
||||
"safety_boots_off" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [204, 242, 255],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"safety_boots_on" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [50, 194, 241],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"safety_belt_off" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [209,165,253],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"safety_belt_basic_on_1" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [158,68,248],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [255, 255, 255]),
|
||||
"safety_belt_basic_on_2" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [130,54,206],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [255, 255, 255]),
|
||||
"safety_belt_xband_on" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [102,40,164],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [255, 255, 255]),
|
||||
"safety_belt_swing_on" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [71, 27, 116],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [255, 255, 255]),
|
||||
"safety_vest_off" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [219,205,143],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"safety_vest_on" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [142, 129, 69],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [255, 255, 255]),
|
||||
"safety_suit_top_off" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [116, 186, 255],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"safety_suit_top_on" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [0, 128, 255],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"safety_suit_bottom_off" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [255, 255, 208],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"safety_suit_bottom_on" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [255, 255, 0],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"equipment_smartstick" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [116, 134, 217],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"equipment_con_switch" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [12, 32, 133],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [255, 255, 255]),
|
||||
"sign_board_information" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [0, 255, 255],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"sign_board_construction" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [0, 209, 209],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"sign_board_traffic" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [0, 114, 114],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [255, 255, 255]),
|
||||
"sign_traffic_cone" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [255, 81, 255],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"truck_lift_bucket" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [175, 57, 175],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [255, 255, 255]),
|
||||
"wheel_chock" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [86, 9, 86],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [255, 255, 255]),
|
||||
"signal_light_stick" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [212,59,8],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [255, 255, 255])
|
||||
}
|
||||
|
||||
|
||||
demo_color_table = {
|
||||
"person" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [183, 183, 183],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"safety_helmet_off" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [193, 193, 255],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"safety_helmet_on" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [0, 0, 255],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"safety_gloves_off" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [184,255,205],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"safety_gloves_insulated_on_1" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [59,216,106],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [255, 255, 255]),
|
||||
"safety_gloves_insulated_on_2" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [45,169,82],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [255, 255, 255]),
|
||||
"safety_boots_off" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [204, 242, 255],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"safety_boots_on" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [50, 194, 241],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"safety_belt_off" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [209,165,253],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"safety_belt_swing_on" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [71, 27, 116],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [255, 255, 255]),
|
||||
"sign_board_information" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [0, 255, 255],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"sign_traffic_cone" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [255, 81, 255],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [0, 0, 0]),
|
||||
"signal_light_stick" : ColorInfo(label_color=TEXT_COLOR_WHITE if project_config.LABEL_ALL_WHITE else [212,59,8],font_color=TEXT_COLOR_BLACK if project_config.LABEL_ALL_WHITE else [255, 255, 255])
|
||||
}
|
||||
87
config.yaml
Normal file
87
config.yaml
Normal file
@@ -0,0 +1,87 @@
|
||||
# ===================================================
|
||||
# KEPCO DEMO 설정 파일
|
||||
# 이 파일을 수정하여 프로그램 동작을 변경할 수 있습니다.
|
||||
# ===================================================
|
||||
|
||||
# --- 모델 설정 ---
|
||||
pt_type: "dev" # "dev" 또는 "demo"
|
||||
use_hpe_person: true # Human Pose Estimation 사용 여부
|
||||
use_helmet_model: true # 헬멧 전용 모델 사용 여부
|
||||
|
||||
# --- 라벨 설정 ---
|
||||
view_conf_score: false # confidence score 표시 여부
|
||||
show_gloves: true # glove on/off 라벨링 여부
|
||||
label_all_white: true # bounding box 색상 전부 흰색 여부
|
||||
use_hpe_frame_check: false # n개 프레임 체크 후 HPE 위험 판단
|
||||
|
||||
# --- 디버그 ---
|
||||
add_cross_arm: false # cross arm 디버그 모드
|
||||
|
||||
# --- 소스 설정 ---
|
||||
# RTSP 카메라 프리셋 (source에서 프리셋 이름으로 사용 가능)
|
||||
cameras:
|
||||
MG: "rtsp://admin:admin1263!@10.20.10.99:28554/onvif/media?profile=Profile1"
|
||||
LF: "rtsp://admin:!1q2w3e4r@10.20.10.100:50554/profile1/media.smp"
|
||||
FC: "rtsp://daool:Ekdnfeldpsdptm1@192.168.200.101/axis-media/media.amp"
|
||||
MG_74_LTE: "rtsp://admin:admin1263!@223.171.48.74:28554/trackID=1"
|
||||
FERMAT: "rtsp://192.168.200.233:50399/wd"
|
||||
|
||||
source: "FC" # 카메라 프리셋 이름 또는 파일/RTSP 경로
|
||||
save_path: "./250630_result"
|
||||
|
||||
# --- 모델 가중치 파일 ---
|
||||
weights:
|
||||
pose: "yolov8l-pose.pt"
|
||||
helmet: "yolov8_dev1_97.pt"
|
||||
# pt_type별 OD 모델
|
||||
od_dev: "yolov11m_dev1_8.pt"
|
||||
od_demo: "yolov8_dev1_66.pt"
|
||||
|
||||
# --- 모델 파라미터 ---
|
||||
model_confidence: 0.5
|
||||
model_image_size: 640
|
||||
kpt_min_confidence: 0.5
|
||||
|
||||
# --- HPE 임계값 ---
|
||||
hpe:
|
||||
falldown_tilt_ratio: 0.80
|
||||
falldown_tilt_angle: 15
|
||||
body_tilt_ratio: 0.30
|
||||
cross_arm_ratio_threshold: 0.10
|
||||
cross_arm_angle_threshold: [15.0, 165.0]
|
||||
arm_angle_threshold_cross: [80.0, 135.0] # add_cross_arm: true 일 때
|
||||
arm_angle_threshold_default: [150.0, 195.0] # add_cross_arm: false 일 때
|
||||
elbow_angle_threshold: [150.0, 185.0]
|
||||
hpe_frame_check_max_count: 3
|
||||
|
||||
# --- 디스플레이 설정 ---
|
||||
display:
|
||||
turn_on: false # 화면 표시 여부
|
||||
border_thickness: 40
|
||||
border_thickness_half: 20
|
||||
normal_thickness: 2
|
||||
warning_thickness: 4
|
||||
hpe_thickness_ratio: 1
|
||||
text_size: 1
|
||||
text_thickness: 0
|
||||
ppe_union_min_percent: 0.9
|
||||
loadstreams_img_buffer: 5
|
||||
fhd_resolution: [1920, 1080]
|
||||
|
||||
# --- CCTV 관제 설정 ---
|
||||
cctv:
|
||||
switch_interval: 5.0
|
||||
sources:
|
||||
# - {src: 0, name: "Realtek_Webcam_0"}
|
||||
# - {src: "rtsp://daool:Ekdnfeldpsdptm1@192.168.200.101/axis-media/media.amp", name: "ipcam_1"}
|
||||
# - {src: "rtsp://daool:Ekdnfeldpsdptm1@192.168.200.102/axis-media/media.amp", name: "ipcam_2"}
|
||||
- {src: "rtsp://192.168.200.231:50199/wd", name: "dev1_stream"}
|
||||
- {src: "rtsp://192.168.200.232:50299/wd", name: "dev2_stream"}
|
||||
# - {src: "rtsp://192.168.200.236:8554/videodevice", name: "boss_webcam"}
|
||||
# - {src: "rtsp://192.168.200.236:8554/1.mp4", name: "boss_stream1"}
|
||||
# - {src: "rtsp://192.168.200.214/1.mp4", name: "sgm_stream1"}
|
||||
# - {src: "rtsp://192.168.200.214/2.mp4", name: "sgm_stream2"}
|
||||
# - {src: "rtsp://192.168.200.111/1.mp4", name: "hp_stream1"}
|
||||
# - {src: "rtsp://192.168.200.111/2.mp4", name: "hp_stream2"}
|
||||
# - {src: "rtsp://192.168.200.215/1.mp4", name: "msk_stream1"}
|
||||
# - {src: "rtsp://192.168.200.215/2.mp4", name: "msk_stream2"}
|
||||
35
config_loader.py
Normal file
35
config_loader.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
|
||||
|
||||
def _get_base_dir():
|
||||
"""exe 실행 시 exe 옆, 개발 시 스크립트 옆에서 config.yaml을 찾는다."""
|
||||
if getattr(sys, 'frozen', False):
|
||||
# PyInstaller exe
|
||||
return os.path.dirname(sys.executable)
|
||||
else:
|
||||
return os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def load_config():
|
||||
base_dir = _get_base_dir()
|
||||
config_path = os.path.join(base_dir, 'config.yaml')
|
||||
|
||||
if not os.path.exists(config_path):
|
||||
raise FileNotFoundError(f"config.yaml을 찾을 수 없습니다: {config_path}")
|
||||
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
|
||||
def get_weights_dir():
|
||||
"""weights 폴더 경로. exe 내부 번들 또는 exe 옆 폴더."""
|
||||
if getattr(sys, 'frozen', False):
|
||||
# PyInstaller 번들 내부 (--add-data로 포함된 경우)
|
||||
return os.path.join(sys._MEIPASS, 'weights')
|
||||
else:
|
||||
return os.path.join(os.path.dirname(os.path.abspath(__file__)), 'weights')
|
||||
|
||||
|
||||
CFG = load_config()
|
||||
127
demo_const.py
Normal file
127
demo_const.py
Normal file
@@ -0,0 +1,127 @@
|
||||
import project_config
|
||||
from config_loader import CFG, get_weights_dir
|
||||
from color_table import dev_color_table, demo_color_table
|
||||
|
||||
# === 카메라 프리셋 ===
|
||||
_cameras = CFG.get('cameras', {})
|
||||
MG = _cameras.get('MG', '')
|
||||
LF = _cameras.get('LF', '')
|
||||
FC = _cameras.get('FC', '')
|
||||
MG_74_LTE = _cameras.get('MG_74_LTE', '')
|
||||
FERMAT = _cameras.get('FERMAT', '')
|
||||
|
||||
# === 소스 ===
|
||||
_source_raw = CFG.get('source', '')
|
||||
SOURCE = _cameras.get(_source_raw, _source_raw) # 프리셋 이름이면 URL로, 아니면 그대로
|
||||
|
||||
SAVE_PATH = CFG.get('save_path', './250630_result')
|
||||
|
||||
# === 모델 가중치 경로 ===
|
||||
WEIGHTS_PATH = get_weights_dir()
|
||||
_weights = CFG.get('weights', {})
|
||||
WEIGHTS_POSE = WEIGHTS_PATH + "/" + _weights.get('pose', 'yolov8l-pose.pt')
|
||||
WEIGHTS_YOLO_HELMET = WEIGHTS_PATH + "/" + _weights.get('helmet', 'yolov8_dev1_97.pt')
|
||||
|
||||
# PT
|
||||
HELMET_KPT = [0, 1, 2]
|
||||
RUBBER_INSULATED_SLEEVE_KPT = [7, 8]
|
||||
SUIT_TOP_KPT = [6, 5]
|
||||
SUIT_BOTTOM_KPT = [13, 14]
|
||||
HELMET_CLASS_NAME = ['safety_helmet_off', 'safety_helmet_on']
|
||||
|
||||
# PT dev
|
||||
if project_config.PT_TYPE == 'dev':
|
||||
color_table = dev_color_table
|
||||
|
||||
WEIGHTS_YOLO = WEIGHTS_PATH + "/" + _weights.get('od_dev', 'yolov11m_dev1_8.pt')
|
||||
UNVISIBLE_CLS = [4, 5, 6, 7, 8, 9]
|
||||
OFF_CLASS_LIST = [2, 4, 12, 14]
|
||||
OFF_TRIGGER_CLASS_LIST = [2, 4] # (helmet, gloves)
|
||||
PPE_CLASS_LIST = list(range(2, 19))
|
||||
|
||||
HELMET_CID = [2, 3] # OFF, ON
|
||||
RUBBER_INSULATED_SLEEVE_CID = [10, 11] # OFF, ON
|
||||
SUIT_TOP_CID = [] # OFF, ON
|
||||
SUIT_BOTTOM_CID = [] # OFF, ON
|
||||
|
||||
HELMET_ON_CID = 3
|
||||
GLOVES_WORK_ON_CID = 6
|
||||
BOOTS_ON_CID = 13
|
||||
TRAFFIC_CONE_CID = 21
|
||||
LIGHT_STICK_CLS_ID = 23
|
||||
|
||||
# PT DEMO
|
||||
elif project_config.PT_TYPE == 'demo':
|
||||
color_table = demo_color_table
|
||||
|
||||
WEIGHTS_YOLO = WEIGHTS_PATH + "/" + _weights.get('od_demo', 'yolov8_dev1_66.pt')
|
||||
|
||||
UNVISIBLE_CLS = [3, 4, 5]
|
||||
OFF_CLASS_LIST = [1, 3, 6, 8]
|
||||
OFF_TRIGGER_CLASS_LIST = [1, 3] # (helmet, gloves)
|
||||
PPE_CLASS_LIST = list(range(1, 9))
|
||||
|
||||
HELMET_CID = [1, 2] # OFF, ON
|
||||
RUBBER_INSULATED_SLEEVE_CID = []
|
||||
SUIT_TOP_CID = [] # OFF, ON
|
||||
SUIT_BOTTOM_CID = [6, 7] # OFF, ON
|
||||
|
||||
HELMET_ON_CID = 2
|
||||
GLOVES_WORK_ON_CID = 4
|
||||
BOOTS_ON_CID = 7
|
||||
TRAFFIC_CONE_CID = 11
|
||||
|
||||
# === 모델 파라미터 ===
|
||||
_display = CFG.get('display', {})
|
||||
|
||||
HPE_FRAME_CHECK_MAX_COUNT = CFG.get('hpe', {}).get('hpe_frame_check_max_count', 3)
|
||||
PPE_UNION_MIN_PERCENT = _display.get('ppe_union_min_percent', 0.9)
|
||||
|
||||
MODEL_CONFIDENCE = CFG.get('model_confidence', 0.5)
|
||||
MODEL_IMAGE_SIZE = CFG.get('model_image_size', 640)
|
||||
|
||||
WD_BORDER_COLOR = [0, 255, 255]
|
||||
WD_THICKNESS_ON = False
|
||||
|
||||
BORDER_OD_TEXT = "Danger(OD)"
|
||||
BORDER_HPE_TEXT = "Danger(HPE)"
|
||||
|
||||
KPT_MIN_CONFIDENCE = CFG.get('kpt_min_confidence', 0.5)
|
||||
|
||||
# loadstreams
|
||||
LOADSTREAMS_IMG_BUFFER = _display.get('loadstreams_img_buffer', 5)
|
||||
|
||||
_fhd = _display.get('fhd_resolution', [1920, 1080])
|
||||
FHD_RESOLUTION = tuple(_fhd)
|
||||
|
||||
BORDER_THICKNESS = _display.get('border_thickness', 40)
|
||||
BORDER_THICKNESS_HALF = _display.get('border_thickness_half', 20)
|
||||
NORMAL_THICKNESS = _display.get('normal_thickness', 2)
|
||||
WARNING_THICKNESS = _display.get('warning_thickness', 4)
|
||||
|
||||
HPE_THICKNESS_RAITO = _display.get('hpe_thickness_ratio', 1)
|
||||
|
||||
TEXT_SIZE = _display.get('text_size', 1)
|
||||
TEXT_THICKNESS = _display.get('text_thickness', 0)
|
||||
TEXT_OD_STARTING_POINT = (20, 30)
|
||||
TEXT_HPE_STARTING_POINT = (20, 30)
|
||||
|
||||
# POSE HUMAN LABEL COLOR (BGR)
|
||||
POSE_NORMAL_COLOR = [0, 0, 0]
|
||||
POSE_CROSS_COLOR = [0, 0, 255]
|
||||
POSE_FALL_COLOR = [0, 0, 255]
|
||||
POSE_ANGLE_ARM_COLOR = [0, 51, 102]
|
||||
|
||||
TEXT_COLOR_WHITE = [255, 255, 255]
|
||||
TEXT_COLOR_BLACK = [0, 0, 0]
|
||||
|
||||
text_color_white_list = [
|
||||
"truck",
|
||||
"safety_gloves_insulated_on_1",
|
||||
"safety_gloves_insulated_on_2",
|
||||
"safety_rubber_insulated_sleeve_on",
|
||||
"safety_belt_swing_on",
|
||||
"safety_vest_on",
|
||||
"safety_suit_top_on",
|
||||
"sign_board_traffic"
|
||||
]
|
||||
797
demo_detect.py
Normal file
797
demo_detect.py
Normal file
@@ -0,0 +1,797 @@
|
||||
import os
|
||||
import cv2
|
||||
import random
|
||||
import numpy as np
|
||||
import imghdr
|
||||
|
||||
import project_config
|
||||
import demo_const as AI_CONST
|
||||
|
||||
from predict import ObjectDetect, PoseDetect
|
||||
from load_models import model_manager
|
||||
|
||||
from ultralytics.data.loaders import LOGGER
|
||||
from ultralytics.data.loaders import LoadImagesAndVideos
|
||||
|
||||
from utils import LoadStreamsDaool,CustomVideoCapture,CLASS_INFORMATION,CLASS_SWAP_INFO,get_monitorsize,img_resize
|
||||
|
||||
LOGGER.setLevel("ERROR")
|
||||
|
||||
MONITOR_RESOLUTION = get_monitorsize()
|
||||
|
||||
class DemoDetect:
|
||||
|
||||
POSETYPE_NORMAL = int(0x0000)
|
||||
POSETYPE_FALL = int(0x0080)
|
||||
POSETYPE_CROSS = int(0x0100)
|
||||
"""
|
||||
시연용
|
||||
"""
|
||||
def __init__(self):
|
||||
self.image_data = None
|
||||
self.crop_img = False
|
||||
|
||||
self.model = model_manager.get_od()
|
||||
self.object_detect = ObjectDetect()
|
||||
self.object_detect.set_model(self.model)
|
||||
|
||||
# helmet
|
||||
if project_config.USE_HELMET_MODEL:
|
||||
self.helmet_model = model_manager.get_helmet()
|
||||
self.helmet_detect = ObjectDetect()
|
||||
self.helmet_detect.set_model(self.helmet_model)
|
||||
|
||||
# HPE
|
||||
self.pose_model = model_manager.get_hpe()
|
||||
self.pose_predict = PoseDetect()
|
||||
self.pose_predict.set_model(self.pose_model)
|
||||
self.hpe_frame_count = 0
|
||||
|
||||
self.color_table = AI_CONST.color_table
|
||||
|
||||
self.source =AI_CONST.SOURCE
|
||||
self.save = False
|
||||
self.ext = None
|
||||
self.video_capture = None
|
||||
|
||||
def set(self, args):
|
||||
self.source = args.source
|
||||
self.save = args.save
|
||||
self.clahe = args.clahe
|
||||
|
||||
def run(self):
|
||||
if os.path.exists(self.source):
|
||||
dataset = LoadImagesAndVideos(path=self.source)
|
||||
else:
|
||||
if self.save:
|
||||
raise Exception("스트림영상은 저장 불가")
|
||||
dataset = LoadStreamsDaool(sources=self.source)
|
||||
|
||||
if self.save:
|
||||
if imghdr.what(self.source):
|
||||
#img
|
||||
self.ext = ".jpg"
|
||||
else:
|
||||
#video
|
||||
self.ext = ".mp4"
|
||||
|
||||
os.makedirs(AI_CONST.SAVE_PATH, exist_ok=True)
|
||||
|
||||
for _, image, *_ in dataset:
|
||||
|
||||
image = image[0]
|
||||
#image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE) ##영상이 좌로 90도 회전되어 들어오는 경우가 있어 임시로 추가함
|
||||
image = self.image_calibration(image)
|
||||
|
||||
if self.ext == ".mp4" and not self.video_capture:
|
||||
self.video_capture = CustomVideoCapture()
|
||||
self.video_capture.set_frame_size(image=image)
|
||||
# _video_path = os.path.join(AI_CONST.SAVE_PATH,f"{os.path.splitext(os.path.split(self.source)[-1])[0]}_detect{self.ext}")
|
||||
_video_path = os.path.join(AI_CONST.SAVE_PATH,f"{os.path.splitext(os.path.split(self.source)[-1])[0]}_detect{'.avi'}")
|
||||
if self.clahe:
|
||||
_video_path = os.path.join(AI_CONST.SAVE_PATH,f"{os.path.splitext(os.path.split(self.source)[-1])[0]}_detect_clahe{'.avi'}")
|
||||
self.video_capture.set_video_writer(path=_video_path)
|
||||
print(_video_path)
|
||||
|
||||
# hpe person detect
|
||||
hpe_message = self.inference_hpe(image, self.crop_img) if project_config.USE_HPE_PERSON else []
|
||||
|
||||
if project_config.USE_HPE_FRAME_CHECK:
|
||||
hpe_message = self.hpe_frame_check(hpe_message)
|
||||
|
||||
# helmet detect
|
||||
_helmet_message = self.inference_helmet(image, self.crop_img) if project_config.USE_HELMET_MODEL else []
|
||||
|
||||
# object detect
|
||||
od_message_raw = self.inference_od(image, self.crop_img,class_name_view=True)
|
||||
# od_message_raw = []
|
||||
|
||||
# od filtering
|
||||
od_message = self.od_filtering(image,od_message_raw,_helmet_message,hpe_message,add_siginal_man=False)
|
||||
|
||||
self.hpe_labeling(image,hpe_message)
|
||||
|
||||
# od_message = [] # NOTE(jwkim) od 라벨링 막음 다시 그리고싶다면 주석
|
||||
self.od_labeling(image,od_message)
|
||||
self.border_labeling(image,hpe_message,od_message)
|
||||
|
||||
if self.save:
|
||||
if imghdr.what(self.source):
|
||||
#img
|
||||
_video_path = os.path.join(AI_CONST.SAVE_PATH,f"{os.path.splitext(os.path.split(self.source)[-1])[0]}_detect{self.ext}")
|
||||
cv2.imwrite(_video_path,image)
|
||||
else:
|
||||
#video
|
||||
self.video_capture.write_video(image)
|
||||
else:
|
||||
cv2.namedWindow("image", cv2.WND_PROP_FULLSCREEN)
|
||||
cv2.setWindowProperty("image", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
|
||||
|
||||
if MONITOR_RESOLUTION != AI_CONST.FHD_RESOLUTION:
|
||||
image = img_resize(image,MONITOR_RESOLUTION)
|
||||
cv2.imshow("image", image)
|
||||
|
||||
# Hit 'q' on the keyboard to quit!
|
||||
if cv2.waitKey(1) & 0xFF == ord('q'):
|
||||
break
|
||||
|
||||
def inference_hpe(self, image, crop_image):
|
||||
"""
|
||||
inference 완료 후 off class 필터링
|
||||
|
||||
:param frame_count:현재 프레임 수
|
||||
:param image: 이미지 데이터
|
||||
:param crop_image: crop_image 유무
|
||||
|
||||
:return: 탐지 결과
|
||||
"""
|
||||
# 초기화
|
||||
message = []
|
||||
self.pose_predict.set_image(image)
|
||||
message = self.pose_predict.predict(working=True,crop_image=crop_image)
|
||||
|
||||
return message
|
||||
|
||||
def inference_helmet(self, image, crop_image):
|
||||
"""
|
||||
inference 완료후 탐지된 리스트 반환
|
||||
|
||||
:param image: 이미지 데이터
|
||||
:param crop_image: crop_image 유무
|
||||
:return: 탐지 결과
|
||||
"""
|
||||
# 초기화
|
||||
message = []
|
||||
self.helmet_detect.set_image(image)
|
||||
raw_message = self.helmet_detect.predict(crop_image=crop_image, class_name=True)
|
||||
|
||||
for i in raw_message:
|
||||
#ON
|
||||
if i["class_name"] == 'head' or i["class_name"] == 'safety_helmet_off':
|
||||
i["class_id"] = CLASS_SWAP_INFO['safety_helmet_off']
|
||||
i["class_name"] = CLASS_INFORMATION[i["class_id"]]
|
||||
message.append(i)
|
||||
#OFF
|
||||
elif i["class_name"] == 'helmet' or i["class_name"] == 'safety_helmet_on':
|
||||
i["class_id"] = CLASS_SWAP_INFO['safety_helmet_on']
|
||||
i["class_name"] = CLASS_INFORMATION[i["class_id"]]
|
||||
message.append(i)
|
||||
else:
|
||||
continue
|
||||
|
||||
return message
|
||||
|
||||
def inference_od(self, image, crop_image, class_name_view=False):
|
||||
"""
|
||||
inference 완료후 탐지된 리스트 반환
|
||||
|
||||
:param image: 이미지 데이터
|
||||
:param crop_image: crop_image 유무
|
||||
:return: 탐지 결과
|
||||
"""
|
||||
# 초기화
|
||||
message = []
|
||||
self.object_detect.set_image(image)
|
||||
message = self.object_detect.predict(crop_image=crop_image, class_name=class_name_view)
|
||||
|
||||
return message
|
||||
|
||||
def od_filtering(self,image,od_raw_message,helmet_message,hpe_message,add_siginal_man=True):
|
||||
"""
|
||||
여러 모델 inference 결과를 조합하여 od 결과 추출
|
||||
|
||||
:param image: 원본 이미지
|
||||
:param od_raw_message: od inference 결과
|
||||
:param helmet_message: helmet inference 결과
|
||||
:param hpe_message: hpe inference 결과
|
||||
:param add_siginal_man: 신호수 추가 유무
|
||||
:return: 필터링된 od inference 결과
|
||||
"""
|
||||
ppe_filtered_message = []
|
||||
|
||||
#od helmet 제거 , helmet 추가
|
||||
helmet_changed_message = self.update_helmet(helmet_message, od_raw_message)
|
||||
|
||||
#od person 제거, hpe_person 추가
|
||||
person_changed_message = self.update_person(hpe_message, helmet_changed_message)
|
||||
|
||||
# 필터링 작업 (현재 box 겹침만 사용)
|
||||
ppe_filtered_message = self.od_ppe_class_filter(person_changed_message,hpe_message)
|
||||
|
||||
# signalman 추가
|
||||
if add_siginal_man:
|
||||
signal_man = self.signal_man_message(image, ppe_filtered_message)
|
||||
if signal_man:
|
||||
ppe_filtered_message.append(signal_man)
|
||||
|
||||
return ppe_filtered_message
|
||||
|
||||
def update_helmet(self, helmet_message, od_message):
|
||||
"""
|
||||
helmet message 파싱 후 , od message 에서 helmet 제거
|
||||
그후 합친 결과 return
|
||||
|
||||
:param helmet_message: helmet detect result
|
||||
:param od_message: od detect result
|
||||
:return: result
|
||||
"""
|
||||
_result = []
|
||||
if not helmet_message:
|
||||
return od_message
|
||||
elif project_config.USE_HELMET_MODEL is False:
|
||||
return od_message
|
||||
else:
|
||||
#parsing helmet msg
|
||||
_result = _result + helmet_message
|
||||
|
||||
#remove od helmet
|
||||
for _od in od_message:
|
||||
if _od['class_id'] not in AI_CONST.HELMET_CID:
|
||||
_result.append(_od)
|
||||
|
||||
return _result
|
||||
|
||||
def update_person(self, hpe_message, od_message):
|
||||
"""
|
||||
hpe message 파싱 후 , od message 에서 person 제거
|
||||
그후 합친 결과 return
|
||||
|
||||
:param hpe_message: _description_
|
||||
:param od_message: _description_
|
||||
:return: _description_
|
||||
"""
|
||||
_result = []
|
||||
if not hpe_message:
|
||||
return od_message
|
||||
elif project_config.USE_HPE_PERSON is False:
|
||||
return od_message
|
||||
else:
|
||||
#parsing hpe msg
|
||||
for _hpe in hpe_message:
|
||||
_person_info = {k: v for k, v in _hpe['result'].items() if k not in ('pose_type', 'pose_level')}
|
||||
_result.append(_person_info)
|
||||
|
||||
#remove od person
|
||||
for _od in od_message:
|
||||
if _od['class_id'] != 0 :
|
||||
_result.append(_od)
|
||||
|
||||
return _result
|
||||
|
||||
def od_ppe_class_filter(self, od_message, kpt_message):
|
||||
"""
|
||||
PPE_CLASS_LIST 로 정의된 클래스가 사용할수 있는지 없는지 판단하여 결과물 return
|
||||
|
||||
- PPE_CLASS_LIST 중 helmet, rubber_insulated_sleeve, suit_top, suit_bottom class 경우 bbox가 keypoint에 포함되면 _result에 추가,
|
||||
포함되지 않는다면 person class와 겹치는지 확인후 겹치면 _result 에 추가
|
||||
|
||||
- PPE_CLASS_LIST 나머지 class 는 person class와 겹치면 _result 에 추가
|
||||
|
||||
- PPE_CLASS_LIST 가 아닐경우 _result에 추가
|
||||
|
||||
:param od_message: od list
|
||||
:param kpt_message: keypoint list
|
||||
:return: list
|
||||
"""
|
||||
_result = []
|
||||
_person = []
|
||||
_ppe_cls = []
|
||||
|
||||
# split classes
|
||||
for i in od_message:
|
||||
if i['class_id'] == 0:
|
||||
_person.append(i)
|
||||
_result.append(i)
|
||||
elif i['class_id'] in AI_CONST.PPE_CLASS_LIST:
|
||||
_ppe_cls.append(i)
|
||||
else:
|
||||
_result.append(i)
|
||||
|
||||
# filtering ppe classes
|
||||
for ppe in _ppe_cls:
|
||||
for kpt in kpt_message:
|
||||
if AI_CONST.HELMET_CID and ppe['class_id'] in AI_CONST.HELMET_CID:
|
||||
#HELMET
|
||||
kpt_include = self.check_keypoint_include(ppe_bbox=ppe['bbox'],kpt_list=kpt['keypoints'],kpt_index_list=AI_CONST.HELMET_KPT,type=1)
|
||||
|
||||
if kpt_include:
|
||||
_result.append(ppe)
|
||||
break
|
||||
else:
|
||||
intersect_area = self.check_union_area(person=kpt['person'],object=ppe['bbox'])
|
||||
if intersect_area >= AI_CONST.PPE_UNION_MIN_PERCENT:
|
||||
_result.append(ppe)
|
||||
break
|
||||
|
||||
elif AI_CONST.RUBBER_INSULATED_SLEEVE_CID and ppe['class_id'] in AI_CONST.RUBBER_INSULATED_SLEEVE_CID:
|
||||
#RUBBER_INSULATED_SLEEVE
|
||||
kpt_include = self.check_keypoint_include(ppe_bbox=ppe['bbox'],kpt_list=kpt['keypoints'],kpt_index_list=AI_CONST.RUBBER_INSULATED_SLEEVE_KPT,type=0)
|
||||
|
||||
if kpt_include:
|
||||
_result.append(ppe)
|
||||
break
|
||||
else:
|
||||
intersect_area = self.check_union_area(person=kpt['person'],object=ppe['bbox'])
|
||||
if intersect_area >= AI_CONST.PPE_UNION_MIN_PERCENT:
|
||||
_result.append(ppe)
|
||||
break
|
||||
|
||||
elif AI_CONST.SUIT_TOP_CID and ppe['class_id'] in AI_CONST.SUIT_TOP_CID:
|
||||
#SUIT_TOP
|
||||
kpt_include = self.check_keypoint_include(ppe_bbox=ppe['bbox'],kpt_list=kpt['keypoints'],kpt_index_list=AI_CONST.SUIT_TOP_KPT,type=1)
|
||||
|
||||
if kpt_include:
|
||||
_result.append(ppe)
|
||||
break
|
||||
else:
|
||||
intersect_area = self.check_union_area(person=kpt['person'],object=ppe['bbox'])
|
||||
if intersect_area >= AI_CONST.PPE_UNION_MIN_PERCENT:
|
||||
_result.append(ppe)
|
||||
break
|
||||
|
||||
elif AI_CONST.SUIT_BOTTOM_CID and ppe['class_id'] in AI_CONST.SUIT_BOTTOM_CID:
|
||||
#SUIT_BOTTOM
|
||||
kpt_include = self.check_keypoint_include(ppe_bbox=ppe['bbox'],kpt_list=kpt['keypoints'],kpt_index_list=AI_CONST.SUIT_BOTTOM_KPT,type=1)
|
||||
|
||||
if kpt_include:
|
||||
_result.append(ppe)
|
||||
break
|
||||
else:
|
||||
intersect_area = self.check_union_area(person=kpt['person'],object=ppe['bbox'])
|
||||
if intersect_area >= AI_CONST.PPE_UNION_MIN_PERCENT:
|
||||
_result.append(ppe)
|
||||
break
|
||||
|
||||
else:
|
||||
intersect_area = self.check_union_area(person=kpt['person'],object=ppe['bbox'])
|
||||
if intersect_area >= AI_CONST.PPE_UNION_MIN_PERCENT:
|
||||
_result.append(ppe)
|
||||
break
|
||||
|
||||
return _result
|
||||
|
||||
def check_union_area(self, person, object):
|
||||
"""
|
||||
두개의 bbox 가 겹치는지 확인후
|
||||
겹친다면 두개의 bbox 겹치는 영역 return
|
||||
아닐경우 false return
|
||||
:param person: bbox 좌표 x1,y1,x2,y2
|
||||
:param object: bbox 좌표 x1,y1,x2,y2
|
||||
"""
|
||||
person_left, person_top, person_right, person_bot = (
|
||||
int(person[0]),
|
||||
int(person[1]),
|
||||
int(person[2]),
|
||||
int(person[3]),
|
||||
)
|
||||
obj_left, obj_top, obj_right, obj_bot = (
|
||||
int(object[0]),
|
||||
int(object[1]),
|
||||
int(object[2]),
|
||||
int(object[3]),
|
||||
)
|
||||
|
||||
## case1 오른쪽으로 벗어나 있는 경우
|
||||
if person_right < obj_left:
|
||||
return 0
|
||||
|
||||
## case2 왼쪽으로 벗어나 있는 경우
|
||||
if person_left > obj_right:
|
||||
return 0
|
||||
|
||||
## case3 위쪽으로 벗어나 있는 경우
|
||||
if person_bot < obj_top:
|
||||
return 0
|
||||
|
||||
## case4 아래쪽으로 벗어나 있는 경우
|
||||
if person_top > obj_bot:
|
||||
return 0
|
||||
|
||||
# 교집합 영역 찾기
|
||||
modified_left, modified_top, modified_right, modified_bot = obj_left, obj_top, obj_right, obj_bot
|
||||
# x좌표 기준으로 이동
|
||||
if modified_left < person_left : # object 왼쪽 겹침
|
||||
modified_left = person_left
|
||||
elif modified_right > person_right : #object 오른쪽 겹침
|
||||
modified_right = person_right
|
||||
|
||||
# y좌표 기준으로 이동
|
||||
if modified_top < person_top : # object 위쪽 겹침
|
||||
modified_top = person_top
|
||||
elif modified_bot > person_bot : #object 아래쪽 겹침
|
||||
modified_bot = person_bot
|
||||
|
||||
width = modified_right - modified_left
|
||||
height = modified_bot - modified_top
|
||||
|
||||
if width * height > 0:
|
||||
return (width * height)/((obj_right-obj_left)*(obj_bot-obj_top))
|
||||
else:
|
||||
return 0
|
||||
|
||||
def plot_one_box(self, x, img, color=None, text_color=AI_CONST.TEXT_COLOR_WHITE, label=None, line_thickness=3):
|
||||
# Plots one bounding box on image img
|
||||
tl = line_thickness or round(0.002 * (img.shape[0] + img.shape[1]) / 2) + 1 # line/font thickness
|
||||
color = color or [random.randint(0, 255) for _ in range(3)]
|
||||
c1, c2 = (int(x[0]), int(x[1])), (int(x[2]), int(x[3]))
|
||||
cv2.rectangle(img, c1, c2, color, thickness=tl, lineType=cv2.LINE_AA)
|
||||
if label:
|
||||
|
||||
tf = max(tl - 1, 1) # font thickness
|
||||
t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0]
|
||||
c2 = c1[0] + t_size[0], c1[1] - t_size[1] - 3
|
||||
cv2.rectangle(img, c1, c2, color, -1, cv2.LINE_AA) # filled
|
||||
cv2.putText(
|
||||
img,
|
||||
label,
|
||||
(c1[0], c1[1] - 2),
|
||||
0,
|
||||
tl / 3,
|
||||
text_color,
|
||||
thickness=tf,
|
||||
lineType=cv2.LINE_AA,
|
||||
)
|
||||
|
||||
def plot_skeleton_kpts(self, im, kpts, steps, orig_shape=None):
|
||||
# Plot the skeleton and keypointsfor coco datatset
|
||||
palette = np.array(
|
||||
[
|
||||
[255, 128, 0],
|
||||
[255, 153, 51],
|
||||
[255, 178, 102],
|
||||
[230, 230, 0],
|
||||
[255, 153, 255],
|
||||
[153, 204, 255],
|
||||
[255, 102, 255],
|
||||
[255, 51, 255],
|
||||
[102, 178, 255],
|
||||
[51, 153, 255],
|
||||
[255, 153, 153],
|
||||
[255, 102, 102],
|
||||
[255, 51, 51],
|
||||
[153, 255, 153],
|
||||
[102, 255, 102],
|
||||
[51, 255, 51],
|
||||
[0, 255, 0],
|
||||
[0, 0, 255],
|
||||
[255, 0, 0],
|
||||
[255, 255, 255],
|
||||
]
|
||||
)
|
||||
|
||||
skeleton = [
|
||||
[16, 14],
|
||||
[14, 12],
|
||||
[17, 15],
|
||||
[15, 13],
|
||||
[12, 13],
|
||||
[6, 12],
|
||||
[7, 13],
|
||||
[6, 7],
|
||||
[6, 8],
|
||||
[7, 9],
|
||||
[8, 10],
|
||||
[9, 11],
|
||||
[2, 3],
|
||||
[1, 2],
|
||||
[1, 3],
|
||||
[2, 4],
|
||||
[3, 5],
|
||||
[4, 6],
|
||||
[5, 7],
|
||||
]
|
||||
|
||||
pose_limb_color = palette[
|
||||
[9, 9, 9, 9, 7, 7, 7, 0, 0, 0, 0, 0, 16, 16, 16, 16, 16, 16, 16]
|
||||
]
|
||||
pose_kpt_color = palette[[16, 16, 16, 16, 16, 0, 0, 0, 0, 0, 0, 9, 9, 9, 9, 9, 9]]
|
||||
radius = 4
|
||||
num_kpts = len(kpts) // steps
|
||||
|
||||
for kid in range(num_kpts):
|
||||
r, g, b = pose_kpt_color[kid]
|
||||
x_coord, y_coord = kpts[steps * kid], kpts[steps * kid + 1]
|
||||
if not (x_coord % 640 == 0 or y_coord % 640 == 0):
|
||||
if steps == 3:
|
||||
conf = kpts[steps * kid + 2]
|
||||
if conf < AI_CONST.KPT_MIN_CONFIDENCE:
|
||||
continue
|
||||
cv2.circle(
|
||||
im,
|
||||
(int(x_coord), int(y_coord)),
|
||||
radius,
|
||||
(int(r), int(g), int(b)),
|
||||
-1 * AI_CONST.HPE_THICKNESS_RAITO,
|
||||
)
|
||||
|
||||
for sk_id, sk in enumerate(skeleton):
|
||||
r, g, b = pose_limb_color[sk_id]
|
||||
pos1 = (int(kpts[(sk[0] - 1) * steps]), int(kpts[(sk[0] - 1) * steps + 1]))
|
||||
pos2 = (int(kpts[(sk[1] - 1) * steps]), int(kpts[(sk[1] - 1) * steps + 1]))
|
||||
if steps == 3:
|
||||
conf1 = kpts[(sk[0] - 1) * steps + 2]
|
||||
conf2 = kpts[(sk[1] - 1) * steps + 2]
|
||||
if conf1 < AI_CONST.KPT_MIN_CONFIDENCE or conf2 < AI_CONST.KPT_MIN_CONFIDENCE:
|
||||
continue
|
||||
if pos1[0] % 640 == 0 or pos1[1] % 640 == 0 or pos1[0] < 0 or pos1[1] < 0:
|
||||
continue
|
||||
if pos2[0] % 640 == 0 or pos2[1] % 640 == 0 or pos2[0] < 0 or pos2[1] < 0:
|
||||
continue
|
||||
cv2.line(im, pos1, pos2, (int(r), int(g), int(b)), thickness=2 * AI_CONST.HPE_THICKNESS_RAITO)
|
||||
|
||||
def hpe_labeling(self,image,hpe_data):
|
||||
for hpe in hpe_data:
|
||||
_kpt=[]
|
||||
for kpt, conf in zip(hpe["keypoints"], hpe["kpt_conf"]):
|
||||
if kpt == None:
|
||||
_kpt.append(0)
|
||||
_kpt.append(0)
|
||||
else:
|
||||
_kpt.append(kpt[0])
|
||||
_kpt.append(kpt[1])
|
||||
_kpt.append(conf)
|
||||
label_kpt = np.array(_kpt)
|
||||
self.plot_skeleton_kpts(im=image, kpts=np.array(label_kpt), steps=3)
|
||||
|
||||
def od_labeling(self,image,od_data):
|
||||
for od in od_data:
|
||||
if not project_config.SHOW_GLOVES:
|
||||
if od['class_id'] in AI_CONST.UNVISIBLE_CLS:
|
||||
continue
|
||||
_label_color = []
|
||||
_text_color = []
|
||||
|
||||
if od["class_name"] not in list(self.color_table.keys()):
|
||||
_label_color = AI_CONST.TEXT_COLOR_WHITE
|
||||
_text_color = AI_CONST.TEXT_COLOR_BLACK
|
||||
elif od["class_name"] in AI_CONST.text_color_white_list:
|
||||
_label_color = AI_CONST.TEXT_COLOR_WHITE
|
||||
_text_color = AI_CONST.TEXT_COLOR_BLACK
|
||||
else:
|
||||
_label_color = self.color_table[od["class_name"]].label_color
|
||||
_text_color = self.color_table[od["class_name"]].font_color
|
||||
|
||||
|
||||
self.plot_one_box(
|
||||
od["bbox"],
|
||||
image,
|
||||
label=f"{od['class_name']} {od['confidence']}" if project_config.VIEW_CONF_SCORE else f"{od['class_name']}",
|
||||
color=_label_color,
|
||||
text_color=_text_color,
|
||||
line_thickness=AI_CONST.NORMAL_THICKNESS,
|
||||
)
|
||||
|
||||
# # NOTE(gyong min) person만 bbox 표시
|
||||
# if od["class_name"] == 'person':
|
||||
|
||||
# self.plot_one_box(
|
||||
# od["bbox"],
|
||||
# image,
|
||||
# label=f"{od['class_name']} {od['confidence']}" if project_config.VIEW_CONF_SCORE else f"{od['class_name']}",
|
||||
# color=_label_color,
|
||||
# text_color=_text_color,
|
||||
# line_thickness=AI_CONST.NORMAL_THICKNESS,
|
||||
# )
|
||||
|
||||
def border_labeling(self,image,hpe_data,od_data):
|
||||
_current_pose_type = 0
|
||||
_border_color = []
|
||||
|
||||
_off_helmet = False
|
||||
_off_glove = False
|
||||
|
||||
#hpe warning
|
||||
for hpe in hpe_data:
|
||||
_current_pose_type = max(_current_pose_type, int(hpe['result']['pose_type']))
|
||||
|
||||
if _current_pose_type >= self.POSETYPE_CROSS:
|
||||
#cross
|
||||
_border_color = AI_CONST.POSE_CROSS_COLOR
|
||||
elif _current_pose_type < self.POSETYPE_CROSS and _current_pose_type >= self.POSETYPE_FALL:
|
||||
#fall
|
||||
_border_color = AI_CONST.POSE_FALL_COLOR
|
||||
|
||||
#NOTE(SGM)빨간 경고테두리 그리기
|
||||
if _current_pose_type>0:
|
||||
cv2.rectangle(
|
||||
image,
|
||||
pt1=(
|
||||
AI_CONST.BORDER_THICKNESS + AI_CONST.BORDER_THICKNESS_HALF,
|
||||
AI_CONST.BORDER_THICKNESS + AI_CONST.BORDER_THICKNESS_HALF,
|
||||
),
|
||||
pt2=(
|
||||
image.shape[1]
|
||||
- AI_CONST.BORDER_THICKNESS
|
||||
- AI_CONST.BORDER_THICKNESS_HALF,
|
||||
image.shape[0]
|
||||
- AI_CONST.BORDER_THICKNESS
|
||||
- AI_CONST.BORDER_THICKNESS_HALF,
|
||||
),
|
||||
color=_border_color,
|
||||
thickness=AI_CONST.BORDER_THICKNESS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
# cv2.putText(
|
||||
# image,
|
||||
# AI_CONST.BORDER_HPE_TEXT,
|
||||
# AI_CONST.TEXT_HPE_STARTING_POINT,
|
||||
# AI_CONST.TEXT_THICKNESS,
|
||||
# AI_CONST.TEXT_SIZE,
|
||||
# [0, 0, 0],
|
||||
# thickness=1,
|
||||
# lineType=cv2.LINE_AA,
|
||||
# )
|
||||
|
||||
# od warning
|
||||
for od in od_data:
|
||||
if od['class_id'] == AI_CONST.OFF_TRIGGER_CLASS_LIST[0]:
|
||||
_off_helmet = True
|
||||
if od['class_id'] == AI_CONST.OFF_TRIGGER_CLASS_LIST[1]:
|
||||
_off_glove = True
|
||||
|
||||
if _off_glove and _off_helmet :
|
||||
cv2.rectangle(
|
||||
image,
|
||||
pt1=(AI_CONST.BORDER_THICKNESS_HALF, AI_CONST.BORDER_THICKNESS_HALF),
|
||||
pt2=(
|
||||
image.shape[1] - AI_CONST.BORDER_THICKNESS_HALF,
|
||||
image.shape[0] - AI_CONST.BORDER_THICKNESS_HALF,
|
||||
),
|
||||
color=AI_CONST.WD_BORDER_COLOR,
|
||||
thickness=AI_CONST.BORDER_THICKNESS)
|
||||
|
||||
cv2.putText(
|
||||
image,
|
||||
AI_CONST.BORDER_OD_TEXT,
|
||||
AI_CONST.TEXT_OD_STARTING_POINT,
|
||||
AI_CONST.TEXT_THICKNESS,
|
||||
AI_CONST.TEXT_SIZE,
|
||||
[0, 0, 0],
|
||||
thickness=1,
|
||||
lineType=cv2.LINE_AA,
|
||||
)
|
||||
|
||||
def check_keypoint_include(self, ppe_bbox, kpt_list, kpt_index_list, type):
|
||||
"""
|
||||
bbox에 keypoint가 포함되어 있는지 확인
|
||||
|
||||
:param ppe_bbox: ppe xyxy 좌표
|
||||
:param kpt_list: keypoint 정보
|
||||
:param kpt_index_list: keypoint 인덱스 정보
|
||||
:param type: 0: null값을 제외한 keypoint가 최소 하나라도 있으면 인정, 1: null값을 제외한 keypoint가 전부 있어야 인정
|
||||
|
||||
:return: boolean
|
||||
"""
|
||||
include = True
|
||||
left, right = ppe_bbox[0], ppe_bbox[2]
|
||||
top, bottom = ppe_bbox[1], ppe_bbox[3]
|
||||
|
||||
if type == 0:
|
||||
# 최소 하나
|
||||
include = False
|
||||
for kpt_idx in kpt_index_list:
|
||||
kpt = kpt_list[kpt_idx]
|
||||
if kpt != None:
|
||||
if (kpt[0] >= left and kpt[0] <= right) and (kpt[1] >= top and kpt[1] <= bottom):
|
||||
include = True
|
||||
break
|
||||
else:
|
||||
# 전부
|
||||
_null_count = 0
|
||||
for kpt_idx in kpt_index_list:
|
||||
kpt = kpt_list[kpt_idx]
|
||||
if kpt != None:
|
||||
if (kpt[0] >= left and kpt[0] <= right) and (kpt[1] >= top and kpt[1] <= bottom):
|
||||
pass
|
||||
else:
|
||||
include = False
|
||||
break
|
||||
else:
|
||||
_null_count += 1
|
||||
|
||||
if _null_count == len(kpt_index_list):
|
||||
include = False
|
||||
|
||||
return include
|
||||
|
||||
def hpe_frame_check(self, hpe_message, threshold=int(0x0080), maxcount=AI_CONST.HPE_FRAME_CHECK_MAX_COUNT):
|
||||
_alert = False
|
||||
if hpe_message:
|
||||
for i in hpe_message:
|
||||
if i['result']['pose_type']>= threshold:
|
||||
self.hpe_frame_count += 1
|
||||
_alert = True
|
||||
break
|
||||
|
||||
if _alert:
|
||||
if self.hpe_frame_count == maxcount:
|
||||
self.hpe_frame_count = 0
|
||||
return hpe_message
|
||||
else:
|
||||
self.hpe_frame_count = 0
|
||||
|
||||
for j in hpe_message:
|
||||
j['result']['pose_type'] = 0
|
||||
j['result']['pose_level'] = 0
|
||||
|
||||
else:
|
||||
self.hpe_frame_count = 0
|
||||
return hpe_message
|
||||
|
||||
def image_calibration(self, image):
|
||||
"""
|
||||
이미지 역광 보정
|
||||
"""
|
||||
if self.clahe:
|
||||
# 이미지를 LAB 컬러 공간으로 변환 (L 채널에 CLAHE 적용)
|
||||
|
||||
lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
|
||||
l, a, b = cv2.split(lab)
|
||||
|
||||
# CLAHE 객체 생성 및 적용
|
||||
# clipLimit: 대비 제한 임계값, tileGridSize: 타일의 크기
|
||||
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))
|
||||
cl = clahe.apply(l)
|
||||
|
||||
# 보정된 L 채널과 기존 a, b 채널을 합쳐 다시 LAB 이미지 생성
|
||||
limg = cv2.merge((cl, a, b))
|
||||
|
||||
# LAB 이미지를 다시 BGR 컬러 공간으로 변환
|
||||
corrected_img = cv2.cvtColor(limg, cv2.COLOR_LAB2BGR)
|
||||
return corrected_img
|
||||
|
||||
else:
|
||||
return image
|
||||
|
||||
def label_color(model,table):
|
||||
label_colored=[]
|
||||
text_colored=[]
|
||||
model_key_list=list(model.model.names.values())
|
||||
|
||||
fixed_color=list(table.values())
|
||||
fixed_class=list(table.keys())
|
||||
|
||||
for cls in model_key_list:
|
||||
if cls in fixed_class:
|
||||
label_colored.append(table[cls])
|
||||
else:
|
||||
while True:
|
||||
_color = [random.randint(0, 255) for _ in range(3)]
|
||||
|
||||
if _color not in fixed_color and _color not in label_colored:
|
||||
break
|
||||
label_colored.append(_color)
|
||||
|
||||
if cls in AI_CONST.text_color_white_list:
|
||||
text_colored.append(AI_CONST.TEXT_COLOR_WHITE)
|
||||
else:
|
||||
text_colored.append(AI_CONST.TEXT_COLOR_BLACK)
|
||||
|
||||
return label_colored,text_colored
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
47
hpe_classification/README.md
Normal file
47
hpe_classification/README.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# UTILITY_YOLO_HPE_CLASSIFICATION
|
||||
YOLO Pose 좌표 정보를 이용한 자세 추정
|
||||
<br><br>
|
||||
### HPEClassification class
|
||||
HPEClassification(pose_info, cross_ratio_threshold=0.1, cross_angle_threshold=(15.0, 165.0), falldown_tilt_ratio=0.8, falldown_tilt_angle=15, body_tilt_ratio=0.5, yolo_version=8)
|
||||
- pose_info: pose keypoint 정보 (mandatory)
|
||||
```python
|
||||
pose_info: <dict> {
|
||||
'person': (cx, cy, w, h, c),
|
||||
'keypoints': [ (x, y, c), (x, y, c), ... ] # 1번이 index 0, 2번 index 1, ... 17번 index 16
|
||||
}
|
||||
```
|
||||
- cross_ratio_threshold: 팔 교차시 교차점의 최소 위치 비율. 생략시 0.1
|
||||
- cross_angle_threshold: 팔 교차시 교차각 범위 지정. 생략시 (15.0, 165.0)
|
||||
- falldown_tilt_ratio: 넘어짐/비틀거림 감지를 위한 body rect 비율 (세로/가로). 생략시 0.8
|
||||
- falldown_tilt_angle: 상체 기울어짐 판정각. 작업자가 '작업중'일때 넘어짐 판정에 반영됨 (생략시 15도)
|
||||
- body_tilt_ratio: 상체 기울어짐 판정을 위한 상체 비율 임계치 (가로/세로). 생략시 0.3
|
||||
- yolo_version: 사용 YOLO 버전. 생략시 8
|
||||
- v8 에서는 잡히지 않은 keypoint => (0,0)
|
||||
- v7 에서는 잡히지 않은 keypoint => None
|
||||
<br><br>
|
||||
#### cross arms
|
||||
- is_cross_arms() -> True/False
|
||||
- get_cross_point() -> (x, y)
|
||||
- get_cross_ratio() -> (ratio1, ratio2)
|
||||
- get_cross_angle() -> 교차각
|
||||
- set_cross_ratio_threshold(value)
|
||||
- set_cross_angle_threshold(angle)
|
||||
#### fall down
|
||||
- is_falldown(is_working_on) -> True/False
|
||||
- is_working_on: 현재 작업자가 작업중인지 여부.(True/False) 작업중인 경우에는 상체 구부러짐을 반영한다.
|
||||
- set_falldown_tilt_ratio(value)
|
||||
#### etc
|
||||
- get_hpe_type(is_working_on) -> 탐지된 type 종류가 모두 포함된 정보.
|
||||
- is_working_on: 현재 작업자가 작업중인지 여부.(True/False) 작업중인 경우에는 상체 구부러짐을 반영한다.
|
||||
- HPETypeMask.NORMAL = 0x0000
|
||||
- HPETypeMask.FALL_DOWN = 0x0080
|
||||
- HPETypeMask.CROSS_ARM = 0x0100
|
||||
- get_hpe_type(query, is_working_on) -> True/False
|
||||
- get_hpe_level(is_working_on) -> 위험도. 현재 0~9까지의 값.
|
||||
- is_working_on: 현재 작업자가 작업중인지 여부.(True/False) 작업중인 경우에는 상체 구부러짐을 반영한다.
|
||||
<br>
|
||||
### HPETypeMask class
|
||||
HPEClassification.get_hpe_type() 에서 return 되는 정보를 구분하는 mask 값
|
||||
- NORMAL = 0x0000
|
||||
- FALL_DOWN = 0x0080
|
||||
- CROSS_ARM = 0x0100
|
||||
0
hpe_classification/__init__.py
Normal file
0
hpe_classification/__init__.py
Normal file
33
hpe_classification/config.py
Normal file
33
hpe_classification/config.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@file : config.py
|
||||
@author: yunikim
|
||||
@license: A2TEC & DAOOLDNS
|
||||
@brief:
|
||||
|
||||
@section Modify History
|
||||
- 2023-11-21 yunikim base
|
||||
|
||||
"""
|
||||
import logging
|
||||
import project_config
|
||||
from config_loader import CFG
|
||||
|
||||
_hpe = CFG.get('hpe', {})
|
||||
|
||||
DEFAULT_YOLO_VERSION = 8
|
||||
|
||||
FALLDOWN_TILT_RATIO = _hpe.get('falldown_tilt_ratio', 0.80)
|
||||
FALLDOWN_TILT_ANGLE = _hpe.get('falldown_tilt_angle', 15)
|
||||
BODY_TILT_RATIO = _hpe.get('body_tilt_ratio', 0.30)
|
||||
|
||||
CROSS_ARM_RATIO_THRESHOLD = _hpe.get('cross_arm_ratio_threshold', 0.10)
|
||||
CROSS_ARM_ANGLE_THRESHOLD = tuple(_hpe.get('cross_arm_angle_threshold', [15.0, 165.0]))
|
||||
|
||||
if project_config.ADD_CROSS_ARM:
|
||||
ARM_ANGLE_THRESHOLD = tuple(_hpe.get('arm_angle_threshold_cross', [80.0, 135.0]))
|
||||
else:
|
||||
ARM_ANGLE_THRESHOLD = tuple(_hpe.get('arm_angle_threshold_default', [150.0, 195.0]))
|
||||
ELBOW_ANGLE_THRESHOLD = tuple(_hpe.get('elbow_angle_threshold', [150.0, 185.0]))
|
||||
|
||||
DEFAULT_LOG_LEVEL = logging.INFO
|
||||
581
hpe_classification/hpe_classification.py
Normal file
581
hpe_classification/hpe_classification.py
Normal file
@@ -0,0 +1,581 @@
|
||||
"""
|
||||
@file : hpe_classification.py
|
||||
@author: yunikim
|
||||
@license: DAOOLDNS
|
||||
@brief: HPE 자세 추정 알고리즘.
|
||||
작업중지동작(cross arm) 및 넘어짐 감지.
|
||||
기존의 두 선분의 교차여부, 교차점 좌표 및 해당 좌표의 각 선분 상 위치 비율을 얻는 로직을 통합함.
|
||||
|
||||
@section Modify History
|
||||
- 2023-06-21 yunikim base
|
||||
|
||||
"""
|
||||
|
||||
import math
|
||||
import numpy as np
|
||||
from hpe_classification import config
|
||||
import logging
|
||||
import project_config
|
||||
|
||||
|
||||
class HPETypeMask:
|
||||
NORMAL = 0x0000
|
||||
FALL_DOWN = 0x0080
|
||||
CROSS_ARM = 0x0100
|
||||
|
||||
|
||||
class HPEClassification:
|
||||
# keypoint는 실제 사용번호와 index가 다르다.
|
||||
# index 0~16 => keypoint 1~17
|
||||
class __KPIndex:
|
||||
NOSE = 0 # 코
|
||||
RIGHT_EYE = 1 # 눈
|
||||
LEFT_EYE = 2
|
||||
RIGHT_EAR = 3 # 귀
|
||||
LEFT_EAR = 4
|
||||
RIGHT_SHOULDER = 5 # 어깨
|
||||
LEFT_SHOULDER = 6
|
||||
RIGHT_ELBOW = 7 # 팔꿈치
|
||||
LEFT_ELBOW = 8
|
||||
RIGHT_WRIST = 9 # 손목
|
||||
LEFT_WRIST = 10
|
||||
RIGHT_HIP = 11 # 엉덩이
|
||||
LEFT_HIP = 12
|
||||
RIGHT_KNEE = 13 # 무릎
|
||||
LEFT_KNEE = 14
|
||||
RIGHT_FEET = 15 # 발
|
||||
LEFT_FEET = 16
|
||||
MAX = 17
|
||||
|
||||
class __HPELevel:
|
||||
NORMAL = 0
|
||||
FALLDOWN = 8
|
||||
CROSSARM = 9
|
||||
|
||||
# parameter:
|
||||
# pose_info: <dict> {
|
||||
# 'person': (cx, cy, w, h, c),
|
||||
# 'keypoints': [ (x, y, c), None, (x, y, c), ... ] # 1번이 index 0, 2번 index 1, ... 17번 index 16
|
||||
# }
|
||||
def __init__(self, pose_info: dict,
|
||||
cross_ratio_threshold: float = config.CROSS_ARM_RATIO_THRESHOLD,
|
||||
cross_angle_threshold: tuple = config.CROSS_ARM_ANGLE_THRESHOLD,
|
||||
arm_angle_threshold: tuple = config.ARM_ANGLE_THRESHOLD,
|
||||
elbow_angle_threshold: tuple = config.ELBOW_ANGLE_THRESHOLD,
|
||||
falldown_tilt_ratio: float = config.FALLDOWN_TILT_RATIO,
|
||||
falldown_tilt_angle: float = config.FALLDOWN_TILT_ANGLE,
|
||||
body_tilt_ratio: float = config.BODY_TILT_RATIO,
|
||||
yolo_version: int = config.DEFAULT_YOLO_VERSION):
|
||||
"""
|
||||
:param pose_info: HPE 좌표 정보
|
||||
:param cross_ratio_threshold: 팔 교차시 최소 비율
|
||||
:param cross_angle_threshold: 팔 교차시 검출 각도 범위. (min, max)
|
||||
:param arm_angle_threshold: 팔 각도 범위. (min, max)
|
||||
:param elbow_angle_threshold: 팔꿈치 각도 범위. (min, max)
|
||||
:param falldown_tilt_ratio: 넘어짐 감지를 위한 비율
|
||||
:param falldown_tilt_angle: 작업중 상체 구부러짐 판정각
|
||||
:param body_tilt_ratio: 작업중 상체 구부러짐 판정을 위한 비율 임계치
|
||||
:param yolo_version: yolo version
|
||||
"""
|
||||
|
||||
self.__raw_data = pose_info
|
||||
self.__bounding_box = pose_info['person']
|
||||
self.__keypoints = pose_info['keypoints']
|
||||
|
||||
# logger 설정
|
||||
self.__logger = self.__set_logger(self.__class__.__name__)
|
||||
|
||||
# yolov8 대응
|
||||
# v8 에서는 잡히지 않은 point => (0,0)
|
||||
# v7 에서는 잡히지 않은 point => None
|
||||
if yolo_version == 8:
|
||||
self.__convert_zero_point_to_none(self.__keypoints)
|
||||
|
||||
# 작업중지동작 detect 관련 변수
|
||||
self.__cross_ratio_threshold = cross_ratio_threshold
|
||||
self.__cross_angle_threshold = cross_angle_threshold
|
||||
self.__cross_point = None
|
||||
self.__cross_ratio = None
|
||||
self.__cross_angle = None
|
||||
self.__line1_point1 = self.__keypoints[self.__KPIndex.RIGHT_ELBOW][0:2] if self.__keypoints[self.__KPIndex.RIGHT_ELBOW] else None
|
||||
self.__line1_point2 = self.__keypoints[self.__KPIndex.RIGHT_WRIST][0:2] if self.__keypoints[self.__KPIndex.RIGHT_WRIST] else None
|
||||
self.__line2_point1 = self.__keypoints[self.__KPIndex.LEFT_ELBOW][0:2] if self.__keypoints[self.__KPIndex.LEFT_ELBOW] else None
|
||||
self.__line2_point2 = self.__keypoints[self.__KPIndex.LEFT_WRIST][0:2] if self.__keypoints[self.__KPIndex.LEFT_WRIST] else None
|
||||
|
||||
self.__detect_cross_arms() # 팔 교차 detect
|
||||
|
||||
# 팔각도 detect 관련 변수
|
||||
self.__cross_arm_angle_threshold = arm_angle_threshold
|
||||
self.__cross_left_arm_angle = None
|
||||
self.__cross_right_arm_angle = None
|
||||
|
||||
self.__cross_elbow_angle_threshold = elbow_angle_threshold
|
||||
self.__cross_left_elbow_angle = None
|
||||
self.__cross_right_elbow_angle = None
|
||||
|
||||
self.__left_wrist = self.__keypoints[self.__KPIndex.LEFT_WRIST][0:2] if self.__keypoints[self.__KPIndex.LEFT_WRIST] else None
|
||||
self.__left_elbow = self.__keypoints[self.__KPIndex.LEFT_ELBOW][0:2] if self.__keypoints[self.__KPIndex.LEFT_ELBOW] else None
|
||||
self.__left_shoulder = self.__keypoints[self.__KPIndex.LEFT_SHOULDER][0:2] if self.__keypoints[self.__KPIndex.LEFT_SHOULDER] else None
|
||||
|
||||
self.__right_wrist = self.__keypoints[self.__KPIndex.RIGHT_WRIST][0:2] if self.__keypoints[self.__KPIndex.RIGHT_WRIST] else None
|
||||
self.__right_elbow = self.__keypoints[self.__KPIndex.RIGHT_ELBOW][0:2] if self.__keypoints[self.__KPIndex.RIGHT_ELBOW] else None
|
||||
self.__right_shoulder = self.__keypoints[self.__KPIndex.RIGHT_SHOULDER][0:2] if self.__keypoints[self.__KPIndex.RIGHT_SHOULDER] else None
|
||||
|
||||
self.__detect_angle_arms() # 팔 각도 detect
|
||||
|
||||
# 넘어짐 detect 관련 변수
|
||||
self.__falldown_tilt_ratio = falldown_tilt_ratio
|
||||
self.__falldown_tilt_angle = falldown_tilt_angle
|
||||
self.__body_tilt_ratio = body_tilt_ratio
|
||||
self.__body_tilt_angle = None
|
||||
self.__overturn_upper = False
|
||||
self.__overturn_lower = False
|
||||
self.__badly_tilt = False
|
||||
self.__kp_rhip = self.__keypoints[self.__KPIndex.RIGHT_HIP][0:2] if self.__keypoints[self.__KPIndex.RIGHT_HIP] else None
|
||||
self.__kp_lhip = self.__keypoints[self.__KPIndex.LEFT_HIP][0:2] if self.__keypoints[self.__KPIndex.LEFT_HIP] else None
|
||||
self.__kp_rsldr = self.__keypoints[self.__KPIndex.RIGHT_SHOULDER][0:2] if self.__keypoints[self.__KPIndex.RIGHT_SHOULDER] else None
|
||||
self.__kp_lsldr = self.__keypoints[self.__KPIndex.LEFT_SHOULDER][0:2] if self.__keypoints[self.__KPIndex.LEFT_SHOULDER] else None
|
||||
self.__kp_rft = self.__keypoints[self.__KPIndex.RIGHT_FEET][0:2] if self.__keypoints[self.__KPIndex.RIGHT_FEET] else None
|
||||
self.__kp_lft = self.__keypoints[self.__KPIndex.LEFT_FEET][0:2] if self.__keypoints[self.__KPIndex.LEFT_FEET] else None
|
||||
|
||||
self.__detect_falldown() # 넘어짐 detect
|
||||
|
||||
@property
|
||||
def falldown_ratio(self):
|
||||
return self.__falldown_tilt_ratio
|
||||
|
||||
@property
|
||||
def cross_threshold(self):
|
||||
return self.__cross_ratio_threshold
|
||||
|
||||
@property
|
||||
def body_tilt_angle(self):
|
||||
return self.__body_tilt_angle
|
||||
|
||||
def __convert_zero_point_to_none(self, points):
|
||||
"""
|
||||
좌표가 (0,0)인 경우 None으로 대체
|
||||
(yolov8 대응)
|
||||
:param points: 좌표리스트
|
||||
"""
|
||||
for idx, point in enumerate(points):
|
||||
if point is not None and point[0] == 0 and point[1] == 0:
|
||||
points[idx] = None
|
||||
|
||||
def get_pose_info(self):
|
||||
return self.__raw_data
|
||||
|
||||
def get_keypoints(self):
|
||||
return self.__keypoints
|
||||
|
||||
def get_bounding_box(self):
|
||||
return self.__bounding_box
|
||||
|
||||
def is_cross_arms(self):
|
||||
"""
|
||||
팔이 cross 상태인지 여부 확인
|
||||
"""
|
||||
if self.__cross_point is not None and self.__cross_ratio is not None and self.__cross_angle is not None:
|
||||
if self.__cross_ratio[0] > self.__cross_ratio_threshold and self.__cross_ratio[1] > self.__cross_ratio_threshold:
|
||||
if self.__cross_angle_threshold[0] < self.__cross_angle < self.__cross_angle_threshold[1]:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def is_angle_arms(self):
|
||||
"""
|
||||
팔이 꺽여있는지 확인
|
||||
"""
|
||||
if project_config.ADD_CROSS_ARM:
|
||||
if self.__cross_left_arm_angle is not None and self.__cross_right_arm_angle is not None:
|
||||
if self.__cross_arm_angle_threshold[0] <= self.__cross_left_arm_angle and self.__cross_arm_angle_threshold[1] >= self.__cross_left_arm_angle and \
|
||||
self.__cross_arm_angle_threshold[0] <= self.__cross_right_arm_angle and self.__cross_arm_angle_threshold[1] >= self.__cross_right_arm_angle:
|
||||
return True
|
||||
else:
|
||||
if self.__cross_left_arm_angle is not None and self.__cross_right_arm_angle is not None and self.__cross_left_elbow_angle is not None and self.__cross_left_elbow_angle is not None:
|
||||
if (self.__cross_arm_angle_threshold[0] <= self.__cross_left_arm_angle and self.__cross_arm_angle_threshold[1] >= self.__cross_left_arm_angle) and \
|
||||
(self.__cross_arm_angle_threshold[0] <= self.__cross_right_arm_angle and self.__cross_arm_angle_threshold[1] >= self.__cross_right_arm_angle) and \
|
||||
(self.__cross_elbow_angle_threshold[0] <= self.__cross_left_elbow_angle and self.__cross_elbow_angle_threshold[1] >= self.__cross_left_arm_angle) and \
|
||||
(self.__cross_elbow_angle_threshold[0] <= self.__cross_right_elbow_angle and self.__cross_elbow_angle_threshold[1] >= self.__cross_right_arm_angle):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_cross_point(self):
|
||||
if self.__cross_point is None:
|
||||
self.__logger.debug("no cross info")
|
||||
return self.__cross_point
|
||||
|
||||
def get_cross_ratio(self):
|
||||
if self.__cross_ratio is None:
|
||||
self.__logger.debug("no cross info")
|
||||
return self.__cross_ratio
|
||||
|
||||
def get_cross_angle(self):
|
||||
if self.__cross_angle is None:
|
||||
self.__logger.debug("no cross info")
|
||||
return self.__cross_angle
|
||||
|
||||
def set_cross_ratio_threshold(self, threshold):
|
||||
"""
|
||||
교차여부를 결정할 때 사용하는 교차점 위치 최소 비율 설정
|
||||
:param threshold: 0.0 ~ 0.5 사이의 값
|
||||
"""
|
||||
self.__cross_ratio_threshold = threshold
|
||||
|
||||
def set_cross_angle_threshold(self, angle_range: tuple):
|
||||
"""
|
||||
교차여부를 결정할 때 사용하는 교차각 범위 설정
|
||||
:param angle_range: 교차각 범위, ex) (15.0, 165.0)
|
||||
"""
|
||||
self.__cross_angle_threshold = angle_range
|
||||
|
||||
def set_falldown_tilt_ratio(self, tilt_ratio):
|
||||
self.__falldown_tilt_ratio = tilt_ratio
|
||||
self.__detect_falldown()
|
||||
|
||||
def set_falldown_tilt_angle(self, tilt_angle):
|
||||
"""
|
||||
:param tilt_angle: 작업중 상체 구부러짐 판정각
|
||||
"""
|
||||
self.__falldown_tilt_angle = tilt_angle
|
||||
self.__detect_falldown()
|
||||
|
||||
def is_falldown(self, is_working_on=True):
|
||||
"""
|
||||
넘어짐 상태 여부 확인
|
||||
:param is_working_on: 현재 작업중인지 여부. 작업중인 상태에서는 상체 구부러짐 각도를 반영한다.
|
||||
"""
|
||||
result = True if self.__overturn_upper or self.__overturn_lower or self.__badly_tilt else False
|
||||
tilt_angle = False
|
||||
if self.__body_tilt_angle is not None:
|
||||
if (self.__body_tilt_angle[0] < self.__falldown_tilt_angle) or (self.__body_tilt_angle[1] < self.__falldown_tilt_angle):
|
||||
tilt_angle = True
|
||||
|
||||
return result or (is_working_on and tilt_angle)
|
||||
|
||||
def get_hpe_type(self, query=None, is_working_on=True):
|
||||
"""
|
||||
감지된 Pose의 bit masking 획득하거나 특정 pose 여부를 확인한다.
|
||||
:param query: 확인하고자 하는 Pose Mask. 생략시 Pose bit mask 리턴
|
||||
:param is_working_on: 현재 작업중인지 여부. 작업중인 상태에서는 상체 구부러짐 각도를 반영한다.
|
||||
:return: query 입력시 True/False, 미입력시 Pose bit mask
|
||||
"""
|
||||
result = 0x0000
|
||||
|
||||
if self.is_falldown(is_working_on):
|
||||
result |= HPETypeMask.FALL_DOWN
|
||||
if project_config.ADD_CROSS_ARM:
|
||||
if self.is_cross_arms():
|
||||
result |= HPETypeMask.CROSS_ARM
|
||||
elif self.is_angle_arms():
|
||||
result |= HPETypeMask.CROSS_ARM
|
||||
else:
|
||||
if self.is_angle_arms():
|
||||
result |= HPETypeMask.CROSS_ARM
|
||||
self.__logger.debug(msg=f"***angle arms -> left angle: {int(self.__cross_left_arm_angle)}, right angle: {int(self.__cross_right_arm_angle)}, left elbow: {int(self.__cross_left_elbow_angle)}, right elbow: {int(self.__cross_right_elbow_angle)}")
|
||||
|
||||
if query is None:
|
||||
return result
|
||||
|
||||
return (result & query) != 0
|
||||
|
||||
# 현재 type별 level값을 return 하고 있으나,
|
||||
# 추후 특정 계산식으로 level을 계산하게 될 예정
|
||||
def get_hpe_level(self, is_working_on=True):
|
||||
"""
|
||||
:param is_working_on: 현재 작업중인지 여부. 작업중인 상태에서는 상체 구부러짐 각도를 반영한다.
|
||||
"""
|
||||
result = 0
|
||||
|
||||
if self.is_falldown(is_working_on) and result < self.__HPELevel.FALLDOWN:
|
||||
result = self.__HPELevel.FALLDOWN
|
||||
|
||||
if (self.is_cross_arms() or self.is_angle_arms()) and result < self.__HPELevel.CROSSARM:
|
||||
result = self.__HPELevel.CROSSARM
|
||||
|
||||
return result
|
||||
|
||||
def __detect_cross_arms(self):
|
||||
self.__cross_point = self.__cross_ratio = self.__cross_angle = None
|
||||
|
||||
if not self.__line1_point1 or not self.__line1_point2 or not self.__line2_point1 or not self.__line2_point2:
|
||||
self.__logger.debug("need 4 points for detecting cross arms")
|
||||
return
|
||||
|
||||
# 손목이 팔꿈치 아래에 있는 경우는 제외
|
||||
if self.__line1_point1[1] < self.__line2_point2[1] and self.__line2_point1[1] < self.__line1_point2[1]:
|
||||
self.__logger.debug("both wrist is below elbow.")
|
||||
return
|
||||
|
||||
line1_ccw = self.__ccw(self.__line1_point1, self.__line1_point2, self.__line2_point1) * \
|
||||
self.__ccw(self.__line1_point1, self.__line1_point2, self.__line2_point2)
|
||||
line2_ccw = self.__ccw(self.__line2_point1, self.__line2_point2, self.__line1_point1) * \
|
||||
self.__ccw(self.__line2_point1, self.__line2_point2, self.__line1_point2)
|
||||
|
||||
if line1_ccw < 0 and line2_ccw < 0:
|
||||
self.__cross_point = self.__get_cross_point()
|
||||
self.__cross_ratio = self.__get_cross_ratio()
|
||||
self.__cross_angle = self.__get_cross_angle()
|
||||
|
||||
# 두 선분의 교차지점 좌표 획득.
|
||||
# 교차됨이 확인 된 후에 사용할 것.
|
||||
# 출력: 교차지점 좌표 tuple
|
||||
def __get_cross_point(self):
|
||||
"""
|
||||
두 직선의 교차점 좌표를 구함
|
||||
:return:
|
||||
"""
|
||||
x1, y1 = self.__line1_point1[0:2]
|
||||
x2, y2 = self.__line1_point2[0:2]
|
||||
x3, y3 = self.__line2_point1[0:2]
|
||||
x4, y4 = self.__line2_point2[0:2]
|
||||
|
||||
cx = ((x1 * y2 - x2 * y1) * (x3 - x4) - (x1 - x2) * (x3 * y4 - x4 * y3)) / \
|
||||
((x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4))
|
||||
cy = ((x1 * y2 - x2 * y1) * (y3 - y4) - (y1 - y2) * (x3 * y4 - x4 * y3)) / \
|
||||
((x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4))
|
||||
|
||||
return cx, cy
|
||||
|
||||
def __get_cross_angle(self, p1=None, p2=None, cross_point=None):
|
||||
"""
|
||||
교차하는 팔의 각도를 구함
|
||||
:param p1: p2: 구하고자 하는 vector 방향의 각 좌표
|
||||
:param cross_point: 교차점
|
||||
"""
|
||||
if cross_point is None and self.__cross_point is None:
|
||||
return None
|
||||
|
||||
p1 = self.__line1_point1[0:2] if p1 is None else p1 # 오른 팔꿈치
|
||||
p2 = self.__line2_point2[0:2] if p2 is None else p2 # 왼 손목
|
||||
cross_point = self.__cross_point if cross_point is None else cross_point
|
||||
|
||||
return self.three_point_angle(p1, p2, cross_point)
|
||||
|
||||
@staticmethod
|
||||
def three_point_angle(p1, p2, cross_point):
|
||||
"""
|
||||
세 점 사이의 각도를 구함
|
||||
"""
|
||||
x1, y1 = p1[0:2]
|
||||
x2, y2 = p2[0:2]
|
||||
cx, cy = cross_point[0:2]
|
||||
|
||||
radian = np.arctan2((y1-cy), (x1-cx)) - np.arctan2((y2-cy), (x2-cx))
|
||||
radian = np.fabs(radian * 180 / np.pi)
|
||||
if radian > 180:
|
||||
radian = 360 - radian
|
||||
|
||||
return radian
|
||||
|
||||
def __get_cross_ratio(self):
|
||||
"""
|
||||
선분 위의 점이 해당 선분의 어느정도 위치에 있는지 확인
|
||||
(끝점에서 얼마나 떨어져 있는지 선분에 대한 비율)
|
||||
:return: 작은 비율 기준으로 리턴됨. 0.0 ~ 0.5, 0.5 이면 가운데
|
||||
"""
|
||||
length_line1 = math.dist(self.__line1_point1[0:2], self.__line1_point2[0:2])
|
||||
length_point1 = math.dist(self.__line1_point1[0:2], self.__cross_point)
|
||||
|
||||
ratio1 = length_point1 / length_line1
|
||||
if ratio1 > 0.5:
|
||||
ratio1 = math.fabs(1.0 - ratio1)
|
||||
|
||||
length_line2 = math.dist(self.__line2_point1[0:2], self.__line2_point2[0:2])
|
||||
length_point2 = math.dist(self.__line2_point1[0:2], self.__cross_point)
|
||||
|
||||
ratio2 = length_point2 / length_line2
|
||||
if ratio2 > 0.5:
|
||||
ratio2 = math.fabs(1.0 - ratio2)
|
||||
|
||||
return ratio1, ratio2
|
||||
|
||||
# 입력: 3개의 point, 각 (x, y)
|
||||
# 출력: 0, 1, -1
|
||||
@staticmethod
|
||||
def __ccw(point1, point2, point3):
|
||||
"""
|
||||
3개의 점에 대한 CCW 계산 함수
|
||||
:return: -1, 0, 1
|
||||
"""
|
||||
x1, y1 = point1[0:2]
|
||||
x2, y2 = point2[0:2]
|
||||
x3, y3 = point3[0:2]
|
||||
cross_product = ((x2 - x1) * (y3 - y1)) - ((x3 - x1) * (y2 - y1))
|
||||
|
||||
if cross_product > 0:
|
||||
return 1 # 반시계방향 (counter clockwise)
|
||||
elif cross_product < 0:
|
||||
return -1 # 시계방향 (clockwise)
|
||||
|
||||
return 0 # 평행 (collinear)
|
||||
|
||||
def __detect_angle_arms(self):
|
||||
self.__cross_left_arm_angle = self.__cross_right_arm_angle = self.__cross_left_elbow_angle = self.__cross_right_elbow_angle = None
|
||||
|
||||
if project_config.ADD_CROSS_ARM:
|
||||
if self.__left_elbow is None or self.__left_shoulder is None or self.__right_elbow is None or self.__right_shoulder is None:
|
||||
self.__logger.debug("need 4 points for detecting angle arms")
|
||||
return
|
||||
else:
|
||||
if self.__left_elbow is None or self.__left_shoulder is None or self.__right_elbow is None or self.__right_shoulder is None or \
|
||||
self.__left_wrist is None or self.__right_wrist is None:
|
||||
self.__logger.debug("need 6 points for detecting angle arms")
|
||||
return
|
||||
|
||||
self.__cross_left_arm_angle, self.__cross_right_arm_angle, self.__cross_left_elbow_angle ,self.__cross_right_elbow_angle = self.__get_angle_arms()
|
||||
|
||||
@staticmethod
|
||||
def three_point_arm_angle(p1, p2, cross_point):
|
||||
"""
|
||||
세 점 사이(팔)의 각도를 구함
|
||||
p1: 몸쪽에서 가장 먼 keypoint
|
||||
p2: 몸쪽에서 가장 가까운 keypoint
|
||||
"""
|
||||
x1, y1 = p1[0:2]
|
||||
x2, y2 = p2[0:2]
|
||||
cx, cy = cross_point[0:2]
|
||||
|
||||
radian = np.arctan2((y1-cy), (x1-cx)) - np.arctan2((y2-cy), (x2-cx))
|
||||
radian = np.fabs(radian * 180 / np.pi)
|
||||
if y1 > cy or y1 > y2:
|
||||
return None
|
||||
|
||||
if radian > 180 :
|
||||
radian = 360 - radian
|
||||
|
||||
return radian
|
||||
|
||||
def __get_angle_arms(self):
|
||||
left_angle = self.three_point_arm_angle(p1=self.__left_elbow,cross_point=self.__left_shoulder,p2=self.__right_shoulder)
|
||||
right_angle = self.three_point_arm_angle(p1=self.__right_elbow,cross_point=self.__right_shoulder,p2=self.__left_shoulder)
|
||||
left_elbow = None if project_config.ADD_CROSS_ARM else self.three_point_arm_angle(p1=self.__left_wrist,cross_point=self.__left_elbow,p2=self.__left_shoulder)
|
||||
right_elbow = None if project_config.ADD_CROSS_ARM else self.three_point_arm_angle(p1=self.__right_wrist,cross_point=self.__right_elbow,p2=self.__right_shoulder)
|
||||
|
||||
return left_angle,right_angle,left_elbow,right_elbow
|
||||
|
||||
def __detect_falldown(self):
|
||||
"""
|
||||
인스턴스 생성시 주어진 key points를 바탕으로 falldown 판단
|
||||
- shoulder 좌표가 hip 좌표 아래에 있는 경우: __overturn_upper set
|
||||
- hip 좌표가 feet 좌표 아래에 있는 경우: __overturn_lower set
|
||||
- shoulder 좌표 및 feet 좌표의 ratio를 계산하여 임계치 이하인 경우: __badly_tilt set
|
||||
- shoulder 좌표 및 hip 좌표를 통해 body tilt angle 계산 __body_tilt_angle에 저장
|
||||
"""
|
||||
self.__overturn_upper = False
|
||||
self.__overturn_lower = False
|
||||
self.__badly_tilt = False
|
||||
self.__body_tilt_angle = None
|
||||
|
||||
# 1. 상체 뒤집힘
|
||||
if self.__kp_lhip and self.__kp_rhip and self.__kp_rsldr and self.__kp_lsldr:
|
||||
if self.__kp_lhip[1] < self.__kp_lsldr[1] and self.__kp_rhip[1] < self.__kp_rsldr[1]:
|
||||
self.__overturn_upper = True
|
||||
else:
|
||||
self.__logger.debug("need hip and shoulder points for detecting upper body falldown")
|
||||
|
||||
# 2. 하체 뒤집힘
|
||||
if self.__kp_rft and self.__kp_lft and self.__kp_lhip and self.__kp_rhip:
|
||||
if self.__kp_lhip[1] > self.__kp_lft[1] and self.__kp_rhip[1] > self.__kp_rft[1]:
|
||||
self.__overturn_lower = True
|
||||
else:
|
||||
self.__logger.debug("need feet points for detecting lower body falldown")
|
||||
|
||||
# 3. 신체의 기울어짐 (좌우 어깨, 좌우 발)
|
||||
if self.__kp_lft and self.__kp_rft and self.__kp_rsldr and self.__kp_lsldr:
|
||||
xmax = np.max([self.__kp_lsldr[0], self.__kp_rsldr[0], self.__kp_lft[0], self.__kp_rft[0]])
|
||||
ymax = np.max([self.__kp_lsldr[1], self.__kp_rsldr[1], self.__kp_lft[1], self.__kp_rft[1]])
|
||||
xmin = np.min([self.__kp_lsldr[0], self.__kp_rsldr[0], self.__kp_lft[0], self.__kp_rft[0]])
|
||||
ymin = np.min([self.__kp_lsldr[1], self.__kp_rsldr[1], self.__kp_lft[1], self.__kp_rft[1]])
|
||||
body_width = math.fabs(xmax - xmin)
|
||||
body_height = math.fabs(ymax - ymin)
|
||||
|
||||
if (body_width > 1e-4) and ((body_height / body_width) < self.__falldown_tilt_ratio):
|
||||
self.__badly_tilt = True
|
||||
else:
|
||||
self.__logger.debug("need feet and shoulder points for detecting badly tilt")
|
||||
|
||||
# 4. 상체 기울어짐 (좌우 어깨, 좌우 엉덩이)
|
||||
if self.__kp_lhip and self.__kp_rhip and self.__kp_rsldr and self.__kp_lsldr:
|
||||
angle_left = self.three_point_angle(self.__kp_lsldr, self.__kp_rhip, self.__kp_lhip)
|
||||
angle_right = self.three_point_angle(self.__kp_rsldr, self.__kp_lhip, self.__kp_rhip)
|
||||
self.__body_tilt_angle = (angle_left, angle_right)
|
||||
|
||||
xmax = np.max([self.__kp_lsldr[0], self.__kp_rsldr[0], self.__kp_lhip[0], self.__kp_rhip[0]])
|
||||
ymax = np.max([self.__kp_lsldr[1], self.__kp_rsldr[1], self.__kp_lhip[1], self.__kp_rhip[1]])
|
||||
xmin = np.min([self.__kp_lsldr[0], self.__kp_rsldr[0], self.__kp_lhip[0], self.__kp_rhip[0]])
|
||||
ymin = np.min([self.__kp_lsldr[1], self.__kp_rsldr[1], self.__kp_lhip[1], self.__kp_rhip[1]])
|
||||
body_width = math.fabs(xmax - xmin)
|
||||
body_height = math.fabs(ymax - ymin)
|
||||
|
||||
if (body_height > 1e-4) and ((body_width / body_height) < self.__body_tilt_ratio):
|
||||
self.__body_tilt_angle = None
|
||||
else:
|
||||
self.__logger.debug("need feet and shoulder points for calculating body tilt angle")
|
||||
|
||||
def __set_logger(self, name):
|
||||
logger = logging.getLogger(name)
|
||||
logger_handler = logging.StreamHandler()
|
||||
logger_handler.setFormatter(logging.Formatter("[%(levelname)s][%(asctime)s][%(module)s::%(funcName)s()] %(message)s"))
|
||||
logger.addHandler(logger_handler)
|
||||
logger.setLevel(config.DEFAULT_LOG_LEVEL)
|
||||
|
||||
return logger
|
||||
|
||||
def log(self, level, message):
|
||||
self.__logger.log(level, message)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# pose_info: <dict> {
|
||||
# 'person': (cx, cy, w, h, c),
|
||||
# 'keypoints': [ (x, y, c), None, (x, y, c), ... ] # 1번이 index 0, 2번 index 1, ... 17번 index 16
|
||||
# }
|
||||
pose_info1 = {
|
||||
'person': (0, 0, 0, 0, 0),
|
||||
'keypoints': [
|
||||
(0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0),
|
||||
(4.64, 3, 0), (-3.2, 2.85, 0), # 오른쪽, 왼쪽 어깨
|
||||
(-2.83, 2.2, 0), (5.45, 3.34, 0), # 오른쪽, 왼쪽 팔꿈치
|
||||
(6.5, 1.6, 0), (0.98, 1.77, 0), # 오른쪽, 왼쪽 손목
|
||||
(3.4, 3.9, 0), (-8.6, 3.2, 0), # 오른쪽, 왼쪽 엉덩이
|
||||
(0, 0, 0), (0, 0, 0),
|
||||
(0, 20, 0), (0, 20, 0) # 오른쪽, 왼쪽 발
|
||||
]
|
||||
}
|
||||
test1 = HPEClassification(pose_info1, cross_ratio_threshold=0.1)
|
||||
print(f'is_cross_arms({test1.cross_threshold}):', test1.is_cross_arms())
|
||||
print('angle:', test1.get_cross_angle())
|
||||
test1.set_cross_ratio_threshold(0.05)
|
||||
print(f'is_cross_arms({test1.cross_threshold}):', test1.is_cross_arms())
|
||||
print(test1.get_cross_point())
|
||||
print(test1.get_cross_ratio())
|
||||
print('angle:', test1.get_cross_angle())
|
||||
print('is_falldown:', test1.is_falldown(True))
|
||||
print(test1.get_hpe_type(HPETypeMask.CROSS_ARM))
|
||||
print(test1.get_hpe_type(HPETypeMask.FALL_DOWN))
|
||||
print(hex(test1.get_hpe_type()))
|
||||
print('hpe level:', test1.get_hpe_level())
|
||||
print('--------------------------')
|
||||
print('body tilt angle:', test1.body_tilt_angle)
|
||||
|
||||
print('--------------------------')
|
||||
|
||||
pose_info2 = {
|
||||
'person': (0, 0, 0, 0, 0),
|
||||
'keypoints': [
|
||||
(0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0),
|
||||
(5, 20, 0), (3, 17, 0), # 오른쪽, 왼쪽 어깨
|
||||
(-2.83, 2.2, 0), (0.98, 1.77, 0), # 오른쪽, 왼쪽 팔꿈치
|
||||
(6.5, 1.6, 0), (5.45, 3.34, 0), # 오른쪽, 왼쪽 손목
|
||||
(0, 30, 0), (0, 30, 0), # 오른쪽, 왼쪽 엉덩이
|
||||
(0, 0, 0), (0, 0, 0),
|
||||
(37, 31, 0), (40, 42, 0) # 오른쪽, 왼쪽 발
|
||||
]
|
||||
}
|
||||
test2 = HPEClassification(pose_info2, falldown_tilt_ratio=0.5)
|
||||
print('is_cross_arms:', test2.is_cross_arms())
|
||||
print(f'is_falldown({test2.falldown_ratio}):', test2.is_falldown())
|
||||
test2.set_falldown_tilt_ratio(0.7)
|
||||
print(f'is_falldown({test2.falldown_ratio}):', test2.is_falldown())
|
||||
print('hpe level:', test2.get_hpe_level())
|
||||
2
hpe_classification/requirements.txt
Normal file
2
hpe_classification/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
numpy
|
||||
opencv-python
|
||||
108
load_models.py
Normal file
108
load_models.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import numpy as np
|
||||
from ultralytics import YOLO
|
||||
|
||||
import demo_const as AI_CONST
|
||||
import project_config
|
||||
|
||||
# from ai_engine.custom_logger.custom_log import log
|
||||
|
||||
class AIObjectModel:
|
||||
"""
|
||||
Yolov8 OD model Load
|
||||
"""
|
||||
|
||||
MODEL_NAME = "OD"
|
||||
WEIGHTS = AI_CONST.WEIGHTS_YOLO
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.model = ""
|
||||
self.set_model(weights=self.WEIGHTS)
|
||||
|
||||
def set_model(self, weights):
|
||||
self.model = YOLO(weights)
|
||||
|
||||
# Check device status
|
||||
import torch
|
||||
device_status = "CUDA (GPU)" if torch.cuda.is_available() else "CPU"
|
||||
print(f"[{self.MODEL_NAME}] Model loaded. Using: {device_status}")
|
||||
|
||||
# warm up models (입력 영상 해상도가 다를경우 수정해야함)
|
||||
img = np.zeros([1080,1920,3],dtype=np.uint8) # black image FHD 1920x1080
|
||||
# img = np.zeros([360,640,3],dtype=np.uint8) # black image HD 640×360
|
||||
self.model.predict(source=img, verbose=False)
|
||||
|
||||
# log.info(f"YOLOV8 {self.MODEL_NAME} MODEL LOADED")
|
||||
|
||||
def get_model(self):
|
||||
return self.model
|
||||
|
||||
def get_class_info(self):
|
||||
return self.model.names
|
||||
|
||||
class AIHelmetObjectModel(AIObjectModel):
|
||||
"""
|
||||
Yolov8 OD model Load
|
||||
"""
|
||||
|
||||
MODEL_NAME = "OD-Helmet"
|
||||
WEIGHTS = AI_CONST.WEIGHTS_YOLO_HELMET
|
||||
|
||||
|
||||
class AIHPEModel(AIObjectModel):
|
||||
"""
|
||||
Yolov8 HPE model Load
|
||||
"""
|
||||
|
||||
MODEL_NAME = "HPE"
|
||||
WEIGHTS = AI_CONST.WEIGHTS_POSE
|
||||
|
||||
|
||||
class AIModelManager:
|
||||
def __init__(self) -> None:
|
||||
self.od_model_info = ""
|
||||
self.hpe_model_info = ""
|
||||
|
||||
self.helmet_model_info = ""
|
||||
|
||||
def set_od(self): # set od
|
||||
if not self.od_model_info:
|
||||
self.od_model_info = AIObjectModel()
|
||||
|
||||
def set_hpe(self): # set hpe
|
||||
if not self.hpe_model_info:
|
||||
self.hpe_model_info = AIHPEModel()
|
||||
|
||||
def set_helmet(self): # set od
|
||||
if not self.helmet_model_info:
|
||||
self.helmet_model_info = AIHelmetObjectModel()
|
||||
|
||||
def get_od(self):
|
||||
if not self.od_model_info:
|
||||
raise Exception("YOLO model not loaded")
|
||||
|
||||
return self.od_model_info
|
||||
|
||||
def get_hpe(self):
|
||||
if not self.hpe_model_info:
|
||||
raise Exception("HPE model not loaded")
|
||||
|
||||
return self.hpe_model_info
|
||||
|
||||
def get_helmet(self):
|
||||
if not self.helmet_model_info:
|
||||
raise Exception("helmet model not loaded")
|
||||
|
||||
return self.helmet_model_info
|
||||
|
||||
|
||||
model_manager = ""
|
||||
|
||||
if not model_manager:
|
||||
model_manager = AIModelManager()
|
||||
model_manager.set_od()
|
||||
model_manager.set_hpe()
|
||||
if project_config.USE_HELMET_MODEL:
|
||||
model_manager.set_helmet()
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
124
mqtt.py
Normal file
124
mqtt.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import threading
|
||||
import os, sys
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
"""
|
||||
todo
|
||||
|
||||
log 설정 별도 필요
|
||||
상수값 설정 필요
|
||||
"""
|
||||
|
||||
MQTT_USER_ID = "a2d2admin"
|
||||
MQTT_USER_PW = "a2d24992!"
|
||||
MQTT_HOST = "localhost"
|
||||
MQTT_PORT = 52002
|
||||
|
||||
class MQTTManager:
|
||||
"""
|
||||
MQTT manager
|
||||
subscribe 사용시 set_onmessage 함수 이용하여 on_message세팅
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
name="MQTT",
|
||||
id=MQTT_USER_ID,
|
||||
pw=MQTT_USER_PW,
|
||||
host=MQTT_HOST,
|
||||
port=MQTT_PORT) -> None:
|
||||
self.name = name
|
||||
self.id = id
|
||||
self.pw = pw
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
self.status = False
|
||||
self.status_subscribe = False
|
||||
|
||||
self.client = mqtt.Client()
|
||||
|
||||
self.client.username_pw_set(self.id, self.pw)
|
||||
self.client.on_connect = self.on_connect
|
||||
self.client.on_disconnect = self.on_disconnect
|
||||
self.client.on_subscribe = self.on_subscribe
|
||||
self.client.on_unsubscribe = self.on_unsubscribe
|
||||
self.client.on_message = self.on_message
|
||||
|
||||
self.mqtt_lock = threading.Lock()
|
||||
|
||||
def __del__(self):
|
||||
self.end()
|
||||
|
||||
def set_onmessage(self, function):
|
||||
self.client.on_message = function
|
||||
|
||||
def get_status(self):
|
||||
return self.status
|
||||
|
||||
def mqtt_connect(self):
|
||||
try:
|
||||
if self.status:
|
||||
pass #log.warning('Already connected to mqtt')
|
||||
else:
|
||||
self.client.connect(self.host, self.port)
|
||||
except Exception as e:
|
||||
pass #log.error((f"{self.name} connect fail"))
|
||||
raise ConnectionError(f"{self.name} connect fail")
|
||||
|
||||
def on_connect(self, client, userdata, flags, rc):
|
||||
"""
|
||||
mqtt 연결 확인 함수
|
||||
연결이 정상적으로 되었다면 rc = 0
|
||||
"""
|
||||
if rc == 0:
|
||||
self.mqtt_lock.acquire()
|
||||
self.status = True
|
||||
pass #log.info(f"{self.name} connected OK")
|
||||
self.mqtt_lock.release()
|
||||
else:
|
||||
pass #log.warning("Bad connection Returned code=", rc)
|
||||
|
||||
def on_disconnect(self, client, userdata, flags, rc=0):
|
||||
pass #log.info(f"{self.name} disconnected")
|
||||
self.mqtt_lock.acquire()
|
||||
self.status = False
|
||||
self.mqtt_lock.release()
|
||||
|
||||
def on_subscribe(self, client, userdata, mid, granted_qos):
|
||||
self.status_subscribe = True
|
||||
pass #log.info("subscribed: " + str(mid) + " " + str(granted_qos))
|
||||
|
||||
def on_unsubscribe(self, client, userdata, mid):
|
||||
self.status_subscribe = False
|
||||
pass #log.info("unsubscribed: " + str(mid))
|
||||
|
||||
def on_message(self, client, userdata, msg):
|
||||
"""
|
||||
구독중인 상태에서 메세지가 들어왔을때 처리 함수
|
||||
set_onmessage 에서 set 하여 사용
|
||||
"""
|
||||
pass
|
||||
|
||||
def start_pub(self):
|
||||
self.client.loop_start()
|
||||
|
||||
def start_sub(self, topic):
|
||||
self.client.subscribe(topic)
|
||||
self.client.loop_start()
|
||||
|
||||
def end_sub(self, topic):
|
||||
self.client.unsubscribe(topic)
|
||||
# self.client.loop_stop()
|
||||
|
||||
def end(self):
|
||||
self.client.loop_stop()
|
||||
self.client.disconnect()
|
||||
pass #log.info(f"{self.name} Ending Connection")
|
||||
|
||||
|
||||
mqtt_publisher = None
|
||||
|
||||
if mqtt_publisher is None:
|
||||
mqtt_publisher = MQTTManager()
|
||||
mqtt_publisher.mqtt_connect()
|
||||
mqtt_publisher.start_pub()
|
||||
147
parsing_msg.py
Normal file
147
parsing_msg.py
Normal file
@@ -0,0 +1,147 @@
|
||||
import cv2,json
|
||||
from base64 import b64encode
|
||||
|
||||
from schema import *
|
||||
|
||||
|
||||
class ParsingMsg():
|
||||
|
||||
IMAGE_FORMAT = 'jpg'
|
||||
|
||||
def __init__(self):
|
||||
self.msg = None
|
||||
self.img = None
|
||||
self.parsed_msg = None
|
||||
|
||||
def set(self, msg:list, img):
|
||||
self.msg = None
|
||||
self.img = None
|
||||
|
||||
self.msg = msg
|
||||
self.img = img
|
||||
self.parsed_msg = None
|
||||
|
||||
def get(self):
|
||||
result = self.parsed_msg
|
||||
return result
|
||||
|
||||
def image_encoding(self, img_data):
|
||||
"""
|
||||
이미지데이터(np.ndarray) 를 바이너리 데이터로 변환
|
||||
:param img_data: 이미지 데이터
|
||||
:return: base64 format
|
||||
"""
|
||||
_, JPEG = cv2.imencode(".jpg", img_data, [int(cv2.IMWRITE_JPEG_QUALITY), 80])
|
||||
# Base64 encode
|
||||
b64 = b64encode(JPEG)
|
||||
|
||||
return b64.decode("utf-8")
|
||||
|
||||
def image_cropping(self, img_data, bbox):
|
||||
"""
|
||||
이미지데이터(np.ndarray) 를 bbox 기준으로 크롭
|
||||
:param img_data: 이미지 데이터
|
||||
:param bbox: [x1, y1, x2, y2]
|
||||
:return: cropped image data
|
||||
"""
|
||||
x1, y1, x2, y2 = bbox
|
||||
cropped_img = img_data[y1:y2, x1:x2]
|
||||
return cropped_img
|
||||
|
||||
def parse_header(self):
|
||||
"""
|
||||
*camera_id, ward_id, frame_id 미구현
|
||||
"""
|
||||
msg = Header(
|
||||
camera_id= "CAMERA_001",
|
||||
ward_id= "WARD_001",
|
||||
timestamp= datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
frame_id= 0
|
||||
)
|
||||
return msg
|
||||
|
||||
def parse_summary(self, activate_cnt, total_cnt):
|
||||
"""
|
||||
*system_health 미구현
|
||||
"""
|
||||
msg = Summary(
|
||||
total_objects_count= total_cnt,
|
||||
active_alerts_count= activate_cnt,
|
||||
system_health= SyestemHealth.ok
|
||||
)
|
||||
return msg
|
||||
|
||||
def parse_object(self):
|
||||
"""
|
||||
*bbox,skeleton 제외 전부 미구현
|
||||
"""
|
||||
msg = []
|
||||
activate_cnt = 0
|
||||
total_cnt = 0
|
||||
|
||||
if self.msg:
|
||||
|
||||
for i in self.msg:
|
||||
|
||||
has_img = True if i.get('person') and self.img is not None else False
|
||||
b64_img = self.image_encoding(self.image_cropping(self.img, i.get('person'))) if has_img else ''
|
||||
if i['result']['pose_type'] > 0:
|
||||
activate_cnt += 1
|
||||
total_cnt += 1
|
||||
|
||||
sub_msg = DetectedObject(
|
||||
tracking_id= i['result']['object_id'],
|
||||
status= Status.stable if i['result']['pose_type'] == 0 else Status.fall_detected,
|
||||
status_detail= None if i['result']['pose_type'] == 0 else StatusDetail.sudden_fall,
|
||||
severity= None if i['result']['pose_type'] == 0 else Severity.critical,
|
||||
location_zone= 'ZONE_001',
|
||||
duration= 0.0,
|
||||
bbox= i.get('person'),
|
||||
skeleton= [[round(x, 0) for x in kpt] + [round(conf,2)] for kpt, conf in zip(i.get('keypoints'), i.get('kpt_conf'))],
|
||||
metrics=Metrics(
|
||||
velocity=0.0,
|
||||
angle=0.0
|
||||
),
|
||||
visual_data=ObjectVisualData(
|
||||
format=self.IMAGE_FORMAT,
|
||||
has_image= has_img,
|
||||
base64_str= b64_img
|
||||
)
|
||||
)
|
||||
msg.append(sub_msg)
|
||||
return msg, activate_cnt, total_cnt
|
||||
|
||||
def parse_virtual(self):
|
||||
has_img = True if self.img is not None else False
|
||||
b64_img = self.image_encoding(self.img) if has_img else ''
|
||||
|
||||
msg= VisualData(
|
||||
format=self.IMAGE_FORMAT,
|
||||
has_image= has_img,
|
||||
base64_str= b64_img
|
||||
)
|
||||
|
||||
return msg
|
||||
|
||||
def parse(self):
|
||||
"""
|
||||
* 위험note 여부는 수정 필요
|
||||
"""
|
||||
object_msg, activate_cnt, total_cnt = self.parse_object()
|
||||
|
||||
if activate_cnt == 0:
|
||||
return None
|
||||
|
||||
header_msg = self.parse_header()
|
||||
summary_msg = self.parse_summary(activate_cnt, total_cnt)
|
||||
visual_msg = self.parse_virtual()
|
||||
|
||||
parsing_msg = FallDetectionSchema(
|
||||
header= header_msg,
|
||||
summary= summary_msg,
|
||||
objects= object_msg,
|
||||
visual_data= visual_msg
|
||||
)
|
||||
|
||||
return parsing_msg.model_dump_json()
|
||||
|
||||
147
predict.py
Normal file
147
predict.py
Normal file
@@ -0,0 +1,147 @@
|
||||
import cv2
|
||||
import demo_const as AI_CONST
|
||||
from pydantic.main import BaseModel
|
||||
|
||||
# from DL.custom_utils import crop_image,image_encoding
|
||||
from hpe_classification.hpe_classification import HPEClassification
|
||||
from hpe_classification import config as hpe_config
|
||||
|
||||
|
||||
class HistoryInfo(BaseModel):
|
||||
"""
|
||||
history 에 저장될 정보
|
||||
"""
|
||||
class_name: str
|
||||
class_id: int
|
||||
object_id: int = None
|
||||
bbox: list
|
||||
bbox_conf: float
|
||||
keypoint: list = None
|
||||
kpt_conf: list = None
|
||||
|
||||
class ObjectDetect:
|
||||
"""
|
||||
ObjectDetect
|
||||
"""
|
||||
MODEL_CONFIDENCE = AI_CONST.MODEL_CONFIDENCE
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.image = ''
|
||||
self.model = ''
|
||||
self.class_info = ''
|
||||
|
||||
def predict(self,crop_image = True, class_name=True):
|
||||
od_predict = self.model.model.predict(source=self.image, show=False, stream=True, save=False, conf=self.MODEL_CONFIDENCE, verbose=False, imgsz=AI_CONST.MODEL_IMAGE_SIZE)
|
||||
message_list = []
|
||||
for object_list in od_predict:
|
||||
for box in object_list.boxes:
|
||||
_parsing_data = self.parsing(bbox=box)
|
||||
_name = _parsing_data.class_name if class_name else False
|
||||
message_list.append(self._od_mqtt_message(_parsing_data, image= self.image, crop=crop_image, name=_name))
|
||||
return message_list
|
||||
|
||||
def set_image(self,image):
|
||||
self.image = image
|
||||
|
||||
def set_model(self,model):
|
||||
self.model = model
|
||||
self.class_info = self.model.get_class_info()
|
||||
|
||||
def _od_mqtt_message(self,data,image,crop,name):
|
||||
message ={}
|
||||
|
||||
# if crop:
|
||||
# crop_img = image_encoding(crop_image(xyxy=data.bbox,img=image))
|
||||
# else:
|
||||
crop_img = None
|
||||
|
||||
if name:
|
||||
message = {
|
||||
"class_id" : data.class_id,
|
||||
"class_name" : name,
|
||||
"confidence" : data.bbox_conf,
|
||||
"bbox" : data.bbox,
|
||||
"object_id": data.object_id,
|
||||
"parent_object_id": None,
|
||||
"image": crop_img
|
||||
}
|
||||
|
||||
else:
|
||||
message = {
|
||||
"class_id" : data.class_id,
|
||||
"confidence" : data.bbox_conf,
|
||||
"bbox" : data.bbox,
|
||||
"object_id": data.object_id,
|
||||
"parent_object_id": None,
|
||||
"image": crop_img
|
||||
}
|
||||
|
||||
return message
|
||||
|
||||
def parsing(self, bbox, kpt=None):
|
||||
"""
|
||||
데이터 파싱
|
||||
kpt(pose) 정보가 없을시 keypoint 관련정보 None으로 처리
|
||||
object id가 없을시 None으로 처리
|
||||
|
||||
:param bbox: object detect 정보
|
||||
:param kpt: pose detect 정보
|
||||
:return: HistoryInfo
|
||||
"""
|
||||
|
||||
_cls_id = int(bbox.cls[0].item())
|
||||
_history_info = HistoryInfo(
|
||||
class_id = _cls_id,
|
||||
class_name = self.class_info[_cls_id],
|
||||
bbox = list(map(round, bbox.xyxy[0].detach().cpu().tolist())),
|
||||
bbox_conf = round(bbox.conf[0].item(), 2),
|
||||
)
|
||||
if bbox.id is not None:
|
||||
_history_info.object_id = int(bbox.id[0].item())
|
||||
|
||||
if kpt:
|
||||
_history_info.keypoint = kpt.xy[0].detach().cpu().tolist()
|
||||
_history_info.kpt_conf = kpt.conf[0].detach().cpu().tolist()
|
||||
|
||||
return _history_info
|
||||
|
||||
class PoseDetect(ObjectDetect):
|
||||
"""
|
||||
PoseDetect
|
||||
"""
|
||||
MODEL_CONFIDENCE = 0.5
|
||||
|
||||
FALLDOWN_TILT_RATIO = hpe_config.FALLDOWN_TILT_RATIO
|
||||
FALLDOWN_TILT_ANGLE = hpe_config.FALLDOWN_TILT_ANGLE
|
||||
CROSS_RATIO_THRESHOLD = hpe_config.CROSS_ARM_RATIO_THRESHOLD
|
||||
CROSS_ANGLE_THRESHOLD = hpe_config.CROSS_ARM_ANGLE_THRESHOLD
|
||||
|
||||
def predict(self,working,crop_image=True):
|
||||
|
||||
# pose_predict = self.model.model.predict(source=self.image, show=False, stream=True, save=False, conf=self.MODEL_CONFIDENCE, verbose=False, imgsz=AI_CONST.MODEL_IMAGE_SIZE)
|
||||
#NOTE(jwkim): pose predict
|
||||
pose_predict = self.model.model.track(source=self.image, show=False, stream=True, save=False, conf=self.MODEL_CONFIDENCE, verbose=False, imgsz=AI_CONST.MODEL_IMAGE_SIZE, persist=True)
|
||||
|
||||
message_list = []
|
||||
for object_list in pose_predict:
|
||||
for box, pose in zip(object_list.boxes, object_list.keypoints):
|
||||
_parsing_data = self.parsing(bbox=box, kpt=pose)
|
||||
current_pose={}
|
||||
current_pose["person"]= _parsing_data.bbox
|
||||
current_pose["keypoints"]= _parsing_data.keypoint
|
||||
current_pose["kpt_conf"] = _parsing_data.kpt_conf
|
||||
|
||||
# HPEClassification
|
||||
hpe_classification = HPEClassification(pose_info=current_pose)
|
||||
|
||||
current_pose["result"]=self._od_mqtt_message(_parsing_data, self.image, crop=crop_image, name=_parsing_data.class_name)
|
||||
current_pose["result"]['pose_type'] = hpe_classification.get_hpe_type(is_working_on=working)
|
||||
current_pose["result"]['pose_level'] = hpe_classification.get_hpe_level(is_working_on=working)
|
||||
|
||||
message_list.append(current_pose)
|
||||
return message_list
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
17
project_config.py
Normal file
17
project_config.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# === config.yaml에서 설정값 로드 ===
|
||||
from config_loader import CFG
|
||||
|
||||
PT_TYPE = CFG.get('pt_type', 'dev')
|
||||
|
||||
# === use model ===
|
||||
USE_HPE_PERSON = CFG.get('use_hpe_person', True)
|
||||
USE_HELMET_MODEL = CFG.get('use_helmet_model', True)
|
||||
|
||||
# === label ===
|
||||
VIEW_CONF_SCORE = CFG.get('view_conf_score', False)
|
||||
SHOW_GLOVES = CFG.get('show_gloves', True)
|
||||
LABEL_ALL_WHITE = CFG.get('label_all_white', True)
|
||||
USE_HPE_FRAME_CHECK = CFG.get('use_hpe_frame_check', False)
|
||||
|
||||
# === debug ===
|
||||
ADD_CROSS_ARM = CFG.get('add_cross_arm', False)
|
||||
18
requirements.txt
Normal file
18
requirements.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
# Core AI Framework
|
||||
ultralytics
|
||||
|
||||
# Image Processing & GUI
|
||||
opencv-python
|
||||
|
||||
# Configuration & Data Validation
|
||||
PyYAML
|
||||
pydantic
|
||||
|
||||
# System Utilities
|
||||
pyautogui
|
||||
|
||||
# Math & Array (usually installed with ultralytics, but good to have)
|
||||
numpy
|
||||
|
||||
#MQTT Client
|
||||
paho-mqtt
|
||||
86
schema.py
Normal file
86
schema.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from pydantic.main import BaseModel
|
||||
from enum import Enum
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
#=========================================
|
||||
class Header(BaseModel):
|
||||
camera_id: str
|
||||
ward_id: str
|
||||
timestamp: datetime
|
||||
frame_id: int
|
||||
#=========================================
|
||||
|
||||
|
||||
#=========================================
|
||||
class SyestemHealth(str,Enum):
|
||||
ok = "OK"
|
||||
overload = "OVERLOAD"
|
||||
critical = "CRITICAL"
|
||||
|
||||
|
||||
class Summary(BaseModel):
|
||||
active_alerts_count: int
|
||||
total_objects_count: int
|
||||
system_health: SyestemHealth
|
||||
#=========================================
|
||||
|
||||
|
||||
#=========================================
|
||||
class Status(str,Enum):
|
||||
stable = "STABLE"
|
||||
fall_detected = "FALL_DETECTED"
|
||||
|
||||
|
||||
class StatusDetail(str,Enum):
|
||||
sudden_fall = "SUDDEN_FALL"
|
||||
slip_down = "SLIP_DOWN"
|
||||
|
||||
|
||||
class Severity(str,Enum):
|
||||
low = "LOW"
|
||||
medium = "MEDIUM"
|
||||
critical = "CRITICAL"
|
||||
|
||||
|
||||
class Metrics(BaseModel):
|
||||
velocity: float
|
||||
angle: float
|
||||
|
||||
|
||||
class ObjectVisualData(BaseModel):
|
||||
format: str
|
||||
has_image: bool
|
||||
base64_str: str
|
||||
|
||||
|
||||
class DetectedObject(BaseModel):
|
||||
tracking_id: int|None
|
||||
status: Status
|
||||
status_detail: StatusDetail|None
|
||||
severity: Severity|None
|
||||
location_zone: str
|
||||
duration: float
|
||||
bbox: List[int] # [x1, y1, x2, y2]
|
||||
skeleton: Optional[List[List[float]]]
|
||||
metrics: Metrics
|
||||
visual_data: ObjectVisualData
|
||||
#=========================================
|
||||
|
||||
|
||||
#=========================================
|
||||
class VisualData(BaseModel):
|
||||
format: str
|
||||
has_image: bool
|
||||
base64_str: str
|
||||
#=========================================
|
||||
|
||||
|
||||
#=========================================
|
||||
class FallDetectionSchema(BaseModel):
|
||||
header: Header
|
||||
summary: Summary
|
||||
objects: List[DetectedObject]
|
||||
visual_data: VisualData
|
||||
#=========================================
|
||||
89
utils.py
Normal file
89
utils.py
Normal file
@@ -0,0 +1,89 @@
|
||||
import numpy as np
|
||||
import time
|
||||
import cv2
|
||||
import demo_const as AI_CONST
|
||||
import copy
|
||||
|
||||
from load_models import model_manager
|
||||
from ultralytics.data.loaders import LoadStreams,LOGGER
|
||||
|
||||
def class_info():
|
||||
od_model = model_manager.get_od() # pt파일에 따라 값이 달라짐
|
||||
|
||||
class_dict = copy.deepcopy(od_model.model.names)
|
||||
class_dict[1000] = 'signalman'
|
||||
return class_dict
|
||||
|
||||
|
||||
CLASS_INFORMATION = {}
|
||||
if not CLASS_INFORMATION:
|
||||
CLASS_INFORMATION = class_info()
|
||||
CLASS_SWAP_INFO = {v:k for k,v in CLASS_INFORMATION.items()}
|
||||
|
||||
class LoadStreamsDaool(LoadStreams):
|
||||
|
||||
def update(self, i, cap, stream):
|
||||
"""Read stream `i` frames in daemon thread."""
|
||||
n, f = 0, self.frames[i] # frame number, frame array
|
||||
while self.running and cap.isOpened() and n < (f - 1):
|
||||
if len(self.imgs[i]) < AI_CONST.LOADSTREAMS_IMG_BUFFER: # keep a <=30-image buffer
|
||||
n += 1
|
||||
cap.grab() # .read() = .grab() followed by .retrieve()
|
||||
if n % self.vid_stride == 0:
|
||||
success, im = cap.retrieve()
|
||||
if not success:
|
||||
im = np.zeros(self.shape[i], dtype=np.uint8)
|
||||
LOGGER.warning("WARNING ⚠️ Video stream unresponsive, please check your IP camera connection.")
|
||||
cap.open(stream) # re-open stream if signal was lost
|
||||
if self.buffer:
|
||||
self.imgs[i].append(im)
|
||||
else:
|
||||
self.imgs[i] = [im]
|
||||
else:
|
||||
time.sleep(0.01) # wait until the buffer is empty
|
||||
|
||||
class CustomVideoCapture:
|
||||
def __init__(self) -> None:
|
||||
self.fps = 30
|
||||
# self.fourcc = cv2.VideoWriter_fourcc(*'X264')
|
||||
self.fourcc = cv2.VideoWriter_fourcc(*'XVID')
|
||||
self.size = None
|
||||
self.video_writer = None
|
||||
|
||||
def set_frame_size(self, image):
|
||||
if isinstance(image, np.ndarray):
|
||||
height,width,ch = image.shape
|
||||
self.size = (int(width), int (height))
|
||||
|
||||
def set_video_writer(self, path):
|
||||
if self.size == None:
|
||||
raise
|
||||
|
||||
self.video_writer = cv2.VideoWriter(path, self.fourcc, self.fps, self.size)
|
||||
|
||||
def write_video(self,frame):
|
||||
if self.video_writer:
|
||||
self.video_writer.write(frame)
|
||||
|
||||
def release_video(self):
|
||||
if self.video_writer:
|
||||
self.video_writer.release()
|
||||
self.video_writer = None
|
||||
|
||||
|
||||
def get_monitorsize():
|
||||
width = 0
|
||||
height = 0
|
||||
try:
|
||||
import pyautogui #pip install pyautogui
|
||||
width = pyautogui.size()[0] # getting the width of the screen
|
||||
height = pyautogui.size()[1] # getting the height of the screen
|
||||
|
||||
except Exception:
|
||||
(width,height) = AI_CONST.FHD_RESOLUTION
|
||||
|
||||
finally:
|
||||
return (width,height)
|
||||
|
||||
def img_resize(image,size):
|
||||
return cv2.resize(image, dsize=size, interpolation=cv2.INTER_AREA)
|
||||
BIN
weights/yolov11m_dev1_8.pt
Normal file
BIN
weights/yolov11m_dev1_8.pt
Normal file
Binary file not shown.
BIN
weights/yolov8_dev1_66.pt
Normal file
BIN
weights/yolov8_dev1_66.pt
Normal file
Binary file not shown.
BIN
weights/yolov8_dev1_89.pt
Normal file
BIN
weights/yolov8_dev1_89.pt
Normal file
Binary file not shown.
BIN
weights/yolov8_dev1_97.pt
Normal file
BIN
weights/yolov8_dev1_97.pt
Normal file
Binary file not shown.
BIN
weights/yolov8l-pose.pt
Normal file
BIN
weights/yolov8l-pose.pt
Normal file
Binary file not shown.
Reference in New Issue
Block a user