Skip to content

Commit 3fc2a22

Browse files
authored
Merge pull request #11 from DDD-Community/feat/#3
[feat#3] 자세 추적 기능 고도화
2 parents 2c215f8 + 098bd6e commit 3fc2a22

File tree

5 files changed

+382
-75
lines changed

5 files changed

+382
-75
lines changed

src/components/PoseDetector.tsx

+96-69
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import { useState, useEffect, useRef } from "react"
1+
import { useState, useEffect, useRef, useCallback } from "react"
22
import { Camera } from "."
33
import type { pose } from "@/utils/detector"
44
import { detectSlope, detectTextNeck } from "@/utils/detector"
55
import { drawPose } from "@/utils/drawer"
66
import usePushNotification from "@/hooks/usePushNotification"
77
import { worker } from "@/utils/worker"
88

9-
declare let ml5: any
10-
119
const PoseDetector: React.FC = () => {
1210
const [isScriptLoaded, setIsScriptLoaded] = useState<boolean>(false)
1311
const [isScriptError, setIsScriptError] = useState<boolean>(false)
@@ -28,7 +26,7 @@ const PoseDetector: React.FC = () => {
2826
const requestApi = (delay: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, delay))
2927

3028
const setup = async (): Promise<void> => {
31-
ml5.bodyPose(
29+
window.ml5.bodyPose(
3230
"MoveNet",
3331
{
3432
modelType: "SINGLEPOSE_THUNDER",
@@ -63,45 +61,51 @@ const PoseDetector: React.FC = () => {
6361

6462
// webgl설정
6563
const initializeBackend = async (): Promise<void> => {
66-
await ml5.setBackend("webgl")
64+
await window.ml5.setBackend("webgl")
6765
}
6866

69-
const detect = (results: pose[]): void => {
70-
resultRef.current = results
71-
if (canvasRef.current) drawPose(results, canvasRef.current)
72-
if (snapRef.current) {
73-
const _slope = detectSlope(snapRef.current, results, mode === "snapshot")
74-
const _isTextNeck = detectTextNeck(snapRef.current, results)
75-
if (_slope !== null) setSlope(_slope)
76-
if (_isTextNeck !== null) setIsTextNeck(_isTextNeck)
77-
78-
if (_isTextNeck) {
79-
if (!textNeckStartTime || !textNeckStartTime.current) {
80-
textNeckStartTime.current = Date.now()
81-
// 거북목 자세 3초 유지 시, api 요청을 보내게 (콘솔 로그에서 확인)
82-
} else if (Date.now() - textNeckStartTime.current >= 3000) {
83-
if (!timer.current) {
84-
timer.current = setInterval(() => {
85-
requestApi(1000).then(() => console.log("api request"))
86-
showNotification()
87-
}, 2000)
67+
const detect = useCallback(
68+
(results: pose[]): void => {
69+
resultRef.current = results
70+
if (canvasRef.current) drawPose(results, canvasRef.current)
71+
if (snapRef.current) {
72+
const _slope = detectSlope(snapRef.current, results, mode === "snapshot")
73+
const _isTextNeck = detectTextNeck(snapRef.current, results, mode === "snapshot")
74+
if (_slope !== null) setSlope(_slope)
75+
if (_isTextNeck !== null) setIsTextNeck(_isTextNeck)
76+
77+
if (_isTextNeck) {
78+
if (!textNeckStartTime || !textNeckStartTime.current) {
79+
textNeckStartTime.current = Date.now()
80+
// 거북목 자세 3초 유지 시, api 요청을 보내게 (콘솔 로그에서 확인)
81+
} else if (Date.now() - textNeckStartTime.current >= 3000) {
82+
if (!timer.current) {
83+
timer.current = setInterval(() => {
84+
requestApi(1000).then(() => console.log("api request"))
85+
showNotification()
86+
}, 2000)
87+
}
8888
}
89+
} else {
90+
clearInterval(timer.current)
91+
timer.current = null
92+
textNeckStartTime.current = null
8993
}
90-
} else {
91-
clearInterval(timer.current)
92-
timer.current = null
93-
textNeckStartTime.current = null
9494
}
95-
}
96-
}
95+
},
96+
[mode, setSlope, setIsTextNeck, showNotification]
97+
)
9798

98-
const detectStart = async (video: HTMLVideoElement): Promise<void> => {
99-
worker.onmessage = ({ }: any) => {
100-
if (modelRef.current) {
101-
modelRef.current.detect(video, detect)
99+
const detectStart = useCallback(
100+
async (video: HTMLVideoElement): Promise<void> => {
101+
worker.onmessage = ({ data }: any) => {
102+
if (modelRef.current) {
103+
modelRef.current.detect(video, detect)
104+
}
102105
}
103-
}
104-
}
106+
},
107+
[detect]
108+
)
105109

106110
const getInitSnap = (): void => {
107111
if (modelRef && modelRef.current) snapRef.current = resultRef.current
@@ -112,12 +116,32 @@ const PoseDetector: React.FC = () => {
112116
getScript()
113117
}, [])
114118

119+
useEffect(() => {
120+
if (isModelLoaded) {
121+
const video = document.querySelector("video")
122+
if (video) {
123+
detectStart(video)
124+
}
125+
}
126+
}, [isModelLoaded, detectStart])
127+
128+
const initializePoseMonitoring = () => {
129+
setIsTextNeck(null)
130+
setSlope(null)
131+
snapRef.current = null
132+
}
133+
115134
const onChangeMode = (e: React.ChangeEvent<HTMLSelectElement>) => {
116135
if (e.target.value) {
117136
setMode(e.target.value)
137+
initializePoseMonitoring()
118138
}
119139
}
120140

141+
const onCancelAutoPoseMonitoring = () => {
142+
initializePoseMonitoring()
143+
}
144+
121145
return (
122146
<div>
123147
{isScriptError ? (
@@ -141,53 +165,56 @@ const PoseDetector: React.FC = () => {
141165
{isModelLoaded && (
142166
<>
143167
<div className="font-bold text-red-500">본 화면은 좌우가 반대로 보이고 있으니 주의하세요!</div>
144-
<div className="w-1/2">
145-
<select
146-
className="w-full appearance-none rounded border border-gray-400 bg-white p-2"
147-
onChange={onChangeMode}
148-
>
168+
<div>
169+
<select className="rounded border border-gray-400 bg-white p-2" onChange={onChangeMode}>
149170
<option value={"snapshot"}>스냅샷 모드 (올바른 자세 촬영 후, 해당 자세 기준으로 측정)</option>
150-
<option value={"skeleton"}>스켈레톤 모드 (올바른 자세 제시 후, 해당 자세 기준으로 측정)</option>
171+
<option value={"skeleton"}>자동 모드 (올바른 자세 기준으로 측정)</option>
151172
</select>
152173
</div>
153174
{mode === "snapshot" && (
154175
<>
155-
<button
156-
className="rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700"
157-
onClick={getInitSnap}
158-
>
176+
<div className="p-10 font-bold">
177+
스냅샷 모드입니다. 올바른 자세를 하신 후에, 버튼을 눌러 촬영을 하면 해당 자세를 기준으로 부적절한
178+
자세를 추적합니다!
179+
</div>
180+
<button className="rounded bg-blue-500 px-4 py-2 font-bold text-white" onClick={getInitSnap}>
159181
올바른 자세를 촬영한 후 자세 측정 시작!
160182
</button>
161-
<div>
162-
거북목 상태:&nbsp;
163-
{isTextNeck === null
164-
? "상태를 확인할 수 없습니다."
165-
: isTextNeck
166-
? "거북목 상태 입니다"
167-
: "정상적인 자세 입니다"}
168-
</div>
169-
<div>어깨 기울기: {slope === null ? "상태를 확인할 수 없습니다." : slope}</div>
170183
</>
171184
)}
172185
{mode === "skeleton" && (
173186
<>
174-
<button
175-
className="rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700"
176-
onClick={getInitSnap}
177-
>
178-
기준 그림에 몸을 맞춘 후 측정 시작!
179-
</button>
180-
<div>
181-
거북목 상태:&nbsp;
182-
{isTextNeck === null
183-
? "상태를 확인할 수 없습니다."
184-
: isTextNeck
185-
? "거북목 상태 입니다"
186-
: "정상적인 자세 입니다"}
187+
<div className="p-10 font-bold">자동 모드입니다. 자동으로 부적절한 자세를 추적합니다.</div>
188+
<div className="flex gap-10">
189+
<button
190+
className="rounded bg-blue-500 px-4 py-2 font-bold text-white disabled:bg-gray-400"
191+
onClick={getInitSnap}
192+
// disabled={snapShoptPose.length > 0}
193+
>
194+
자세 모니터링 시작!
195+
</button>
196+
<button
197+
className="rounded bg-red-500 px-4 py-2 font-bold text-white disabled:bg-gray-400"
198+
onClick={onCancelAutoPoseMonitoring}
199+
// disabled={snapShoptPose.length === 0}
200+
>
201+
자세 모니터링 취소
202+
</button>
187203
</div>
188-
<div>어깨 기울기: {slope === null ? "상태를 확인할 수 없습니다." : slope}</div>
189204
</>
190205
)}
206+
207+
<div>
208+
거북목 상태:&nbsp;
209+
{isTextNeck === null ? (
210+
"상태를 확인할 수 없습니다."
211+
) : isTextNeck ? (
212+
<span className="font-extrabold text-red-500">"거북목 상태 입니다"</span>
213+
) : (
214+
"정상적인 자세 입니다"
215+
)}
216+
</div>
217+
<div>어깨 기울기: {slope === null ? "상태를 확인할 수 없습니다." : slope}</div>
191218
</>
192219
)}
193220
</>

0 commit comments

Comments
 (0)