.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ mediapipe-endpoint/model/pose_landmarker_lite.task filter=lfs diff=lfs merge=lfs -text
mediapipe-endpoint/README.md ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MediaPipe HuggingFace Inference Endpoint
2
+
3
+ MediaPipe Pose Landmarker와 Image Classifier를 HuggingFace Inference Endpoint로 제공하는 커스텀 핸들러입니다.
4
+
5
+ ## 모델 파일
6
+
7
+ - `model/pose_landmarker_lite.task`: 포즈 랜드마크 추출 모델
8
+ - `model/efficientnet_lite0.tflite`: 이미지 분류 모델
9
+
10
+ ## 지원하는 엔드포인트
11
+
12
+ ### 1. `/extract_landmarks` - 포즈 랜드마크 추출
13
+
14
+ 이미지에서 33개의 포즈 랜드마크를 추출합니다.
15
+
16
+ **요청 형식:**
17
+ ```json
18
+ {
19
+ "endpoint": "/extract_landmarks",
20
+ "image": "base64_encoded_image_string"
21
+ }
22
+ ```
23
+
24
+ **응답 형식:**
25
+ ```json
26
+ {
27
+ "success": true,
28
+ "landmarks": [
29
+ {
30
+ "id": 0,
31
+ "x": 0.5,
32
+ "y": 0.3,
33
+ "z": 0.1,
34
+ "visibility": 0.9
35
+ },
36
+ ...
37
+ ]
38
+ }
39
+ ```
40
+
41
+ ### 2. `/classify_image` - 이미지 분류
42
+
43
+ 이미지를 ImageNet 1000개 클래스로 분류합니다.
44
+
45
+ **요청 형식:**
46
+ ```json
47
+ {
48
+ "endpoint": "/classify_image",
49
+ "image": "base64_encoded_image_string"
50
+ }
51
+ ```
52
+
53
+ **응답 형식:**
54
+ ```json
55
+ {
56
+ "success": true,
57
+ "categories": [
58
+ {
59
+ "category_name": "person",
60
+ "score": 0.95
61
+ },
62
+ ...
63
+ ]
64
+ }
65
+ ```
66
+
67
+ ### 3. `/is_person` - 사람 감지
68
+
69
+ 이미지에 사람이 있는지 판단합니다.
70
+
71
+ **요청 형식:**
72
+ ```json
73
+ {
74
+ "endpoint": "/is_person",
75
+ "image": "base64_encoded_image_string",
76
+ "threshold": 0.3
77
+ }
78
+ ```
79
+
80
+ **응답 형식:**
81
+ ```json
82
+ {
83
+ "success": true,
84
+ "is_person": true
85
+ }
86
+ ```
87
+
88
+ ## 사용 예시
89
+
90
+ ### Python 예시
91
+
92
+ ```python
93
+ import requests
94
+ import base64
95
+ from PIL import Image
96
+ import io
97
+
98
+ # 이미지 로드 및 base64 인코딩
99
+ image = Image.open("path/to/image.jpg")
100
+ buffer = io.BytesIO()
101
+ image.save(buffer, format="JPEG")
102
+ image_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
103
+
104
+ # 엔드포인트 호출
105
+ endpoint_url = "https://your-endpoint-url.hf.space"
106
+ response = requests.post(
107
+ endpoint_url,
108
+ json={
109
+ "endpoint": "/extract_landmarks",
110
+ "image": image_base64
111
+ },
112
+ headers={
113
+ "Authorization": "Bearer YOUR_HF_TOKEN"
114
+ }
115
+ )
116
+
117
+ result = response.json()
118
+ print(result)
119
+ ```
120
+
121
+ ## 배포 방법
122
+
123
+ 1. HuggingFace Hub에 모델 저장소 생성 (예: `jjunyuongv/mediapipe-endpoint`)
124
+ 2. 이 폴더의 모든 파일을 저장소에 업로드
125
+ 3. HuggingFace Inference Endpoints에서 Custom Endpoint 생성:
126
+ - Instance: CPU 1 vCPU
127
+ - Inference Engine: Custom
128
+ - Authentication: Private
129
+ - Autoscaling: Min 0 / Max 1
130
+ - Scale to zero: 1시간
131
+
132
+ ## 주의사항
133
+
134
+ - 이미지는 base64로 인코딩되어 전송되어야 합니다
135
+ - RGB 형식의 이미지를 권장합니다
136
+ - Private Endpoint의 경우 Authorization 헤더에 HuggingFace 토큰이 필요합니다
137
+
mediapipe-endpoint/handler.py ADDED
@@ -0,0 +1,377 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HuggingFace Inference Endpoint Handler for MediaPipe Models
3
+ MediaPipe Pose Landmarker와 Image Classifier를 제공하는 커스텀 핸들러
4
+ """
5
+ import os
6
+ import json
7
+ import base64
8
+ import io
9
+ from typing import Dict, Any, Optional, List
10
+ from pathlib import Path
11
+
12
+ import mediapipe as mp
13
+ from mediapipe.tasks import python
14
+ from mediapipe.tasks.python import vision
15
+ import numpy as np
16
+ from PIL import Image
17
+
18
+
19
+ # 모델 경로 설정
20
+ MODEL_DIR = Path(__file__).parent / "model"
21
+ POSE_MODEL_PATH = MODEL_DIR / "pose_landmarker_lite.task"
22
+ CLASSIFIER_MODEL_PATH = MODEL_DIR / "efficientnet_lite0.tflite"
23
+
24
+ # 전역 변수로 모델 저장
25
+ pose_landmarker = None
26
+ image_classifier = None
27
+
28
+
29
+ def init_models():
30
+ """모델 초기화"""
31
+ global pose_landmarker, image_classifier
32
+
33
+ try:
34
+ # Pose Landmarker 초기화
35
+ if POSE_MODEL_PATH.exists():
36
+ base_options = python.BaseOptions(model_asset_path=str(POSE_MODEL_PATH))
37
+ options = vision.PoseLandmarkerOptions(
38
+ base_options=base_options,
39
+ output_segmentation_masks=False,
40
+ min_pose_detection_confidence=0.5,
41
+ min_pose_presence_confidence=0.5,
42
+ min_tracking_confidence=0.5
43
+ )
44
+ pose_landmarker = vision.PoseLandmarker.create_from_options(options)
45
+ print("✅ Pose Landmarker 초기화 완료")
46
+ else:
47
+ print(f"⚠️ Pose 모델 파일을 찾을 수 없습니다: {POSE_MODEL_PATH}")
48
+
49
+ # Image Classifier 초기화
50
+ if CLASSIFIER_MODEL_PATH.exists():
51
+ base_options = python.BaseOptions(model_asset_path=str(CLASSIFIER_MODEL_PATH))
52
+ options = vision.ImageClassifierOptions(
53
+ base_options=base_options,
54
+ max_results=10,
55
+ score_threshold=0.1
56
+ )
57
+ image_classifier = vision.ImageClassifier.create_from_options(options)
58
+ print("✅ Image Classifier 초기화 완료")
59
+ else:
60
+ print(f"⚠️ Classifier 모델 파일을 찾을 수 없습니다: {CLASSIFIER_MODEL_PATH}")
61
+
62
+ except Exception as e:
63
+ print(f"❌ 모델 초기화 오류: {e}")
64
+ import traceback
65
+ traceback.print_exc()
66
+
67
+
68
+ def decode_image(image_data: str) -> Image.Image:
69
+ """
70
+ base64 인코딩된 이미지 데이터를 PIL Image로 변환
71
+
72
+ Args:
73
+ image_data: base64 인코딩된 이미지 문자열
74
+
75
+ Returns:
76
+ PIL Image 객체
77
+ """
78
+ try:
79
+ # base64 디코딩
80
+ image_bytes = base64.b64decode(image_data)
81
+ image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
82
+ return image
83
+ except Exception as e:
84
+ raise ValueError(f"이미지 디코딩 실패: {e}")
85
+
86
+
87
+ def extract_landmarks_handler(data: Dict[str, Any]) -> Dict[str, Any]:
88
+ """
89
+ 포즈 랜드마크 추출 핸들러
90
+
91
+ Args:
92
+ data: {
93
+ "image": base64 인코딩된 이미지 문자열
94
+ }
95
+
96
+ Returns:
97
+ {
98
+ "success": bool,
99
+ "landmarks": List[Dict] 또는 None,
100
+ "error": str (실패 시)
101
+ }
102
+ """
103
+ global pose_landmarker
104
+
105
+ if pose_landmarker is None:
106
+ return {
107
+ "success": False,
108
+ "error": "Pose Landmarker가 초기화되지 않았습니다."
109
+ }
110
+
111
+ try:
112
+ # 이미지 디코딩
113
+ image_data = data.get("image")
114
+ if not image_data:
115
+ return {
116
+ "success": False,
117
+ "error": "이미지 데이터가 제공되지 않았습니다."
118
+ }
119
+
120
+ image = decode_image(image_data)
121
+
122
+ # PIL Image를 numpy array로 변환
123
+ image_array = np.array(image)
124
+
125
+ # RGB 형식으로 변환
126
+ if len(image_array.shape) == 3 and image_array.shape[2] == 4:
127
+ image_array = image_array[:, :, :3]
128
+
129
+ # MediaPipe 형식으로 변환
130
+ mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=image_array)
131
+
132
+ # 랜드마크 추출
133
+ detection_result = pose_landmarker.detect(mp_image)
134
+
135
+ # 랜드마크가 없으면 None 반환
136
+ if not detection_result.pose_landmarks:
137
+ return {
138
+ "success": True,
139
+ "landmarks": None
140
+ }
141
+
142
+ # 첫 번째 포즈의 랜드마크 사용
143
+ pose_landmarks = detection_result.pose_landmarks[0]
144
+
145
+ # 랜드마크를 딕셔너리 리스트로 변환
146
+ landmarks = []
147
+ for idx, landmark in enumerate(pose_landmarks):
148
+ landmarks.append({
149
+ "id": idx,
150
+ "x": float(landmark.x),
151
+ "y": float(landmark.y),
152
+ "z": float(landmark.z),
153
+ "visibility": float(landmark.visibility)
154
+ })
155
+
156
+ return {
157
+ "success": True,
158
+ "landmarks": landmarks
159
+ }
160
+
161
+ except Exception as e:
162
+ import traceback
163
+ traceback.print_exc()
164
+ return {
165
+ "success": False,
166
+ "error": str(e)
167
+ }
168
+
169
+
170
+ def classify_image_handler(data: Dict[str, Any]) -> Dict[str, Any]:
171
+ """
172
+ 이미지 분류 핸들러
173
+
174
+ Args:
175
+ data: {
176
+ "image": base64 인코딩된 이미지 문자열
177
+ }
178
+
179
+ Returns:
180
+ {
181
+ "success": bool,
182
+ "categories": List[Dict] 또는 None,
183
+ "error": str (실패 시)
184
+ }
185
+ """
186
+ global image_classifier
187
+
188
+ if image_classifier is None:
189
+ return {
190
+ "success": False,
191
+ "error": "Image Classifier가 초기화되지 않았습니다."
192
+ }
193
+
194
+ try:
195
+ # 이미지 디코딩
196
+ image_data = data.get("image")
197
+ if not image_data:
198
+ return {
199
+ "success": False,
200
+ "error": "이미지 데이터가 제공되지 않았습니다."
201
+ }
202
+
203
+ image = decode_image(image_data)
204
+
205
+ # PIL Image를 numpy array로 변환
206
+ image_array = np.array(image)
207
+
208
+ # RGB 형식으로 변환
209
+ if len(image_array.shape) == 3 and image_array.shape[2] == 4:
210
+ image_array = image_array[:, :, :3]
211
+
212
+ # MediaPipe 형식으로 변환
213
+ mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=image_array)
214
+
215
+ # 이미지 분류
216
+ classification_result = image_classifier.classify(mp_image)
217
+
218
+ # 결과 추출
219
+ if not classification_result.classifications:
220
+ return {
221
+ "success": True,
222
+ "categories": None
223
+ }
224
+
225
+ categories = []
226
+ for category in classification_result.classifications[0].categories:
227
+ categories.append({
228
+ "category_name": category.category_name,
229
+ "score": float(category.score)
230
+ })
231
+
232
+ return {
233
+ "success": True,
234
+ "categories": categories
235
+ }
236
+
237
+ except Exception as e:
238
+ import traceback
239
+ traceback.print_exc()
240
+ return {
241
+ "success": False,
242
+ "error": str(e)
243
+ }
244
+
245
+
246
+ def is_person_handler(data: Dict[str, Any]) -> Dict[str, Any]:
247
+ """
248
+ 사람 감지 핸들러
249
+
250
+ Args:
251
+ data: {
252
+ "image": base64 인코딩된 이미지 문자열,
253
+ "threshold": float (기본값: 0.3)
254
+ }
255
+
256
+ Returns:
257
+ {
258
+ "success": bool,
259
+ "is_person": bool,
260
+ "error": str (실패 시)
261
+ }
262
+ """
263
+ global image_classifier
264
+
265
+ if image_classifier is None:
266
+ return {
267
+ "success": False,
268
+ "error": "Image Classifier가 초기화되지 않았습니다."
269
+ }
270
+
271
+ try:
272
+ # 이미지 분류 먼저 수행
273
+ classify_result = classify_image_handler(data)
274
+ if not classify_result.get("success"):
275
+ return classify_result
276
+
277
+ categories = classify_result.get("categories")
278
+ if not categories:
279
+ return {
280
+ "success": True,
281
+ "is_person": False
282
+ }
283
+
284
+ # threshold 가져오기
285
+ threshold = data.get("threshold", 0.3)
286
+
287
+ # 사람 관련 키워드
288
+ person_keywords = [
289
+ "person", "man", "woman", "girl", "boy", "child", "baby",
290
+ "people", "human", "bride", "groom", "bridegroom",
291
+ "lady", "gentleman", "adult", "teenager", "infant"
292
+ ]
293
+
294
+ # 동물 관련 키워드 (제외)
295
+ animal_keywords = [
296
+ "animal", "dog", "cat", "bear", "monkey", "ape", "gorilla",
297
+ "orangutan", "chimpanzee", "elephant", "lion", "tiger",
298
+ "bird", "fish", "horse", "cow", "pig", "sheep", "goat",
299
+ "rabbit", "mouse", "rat", "hamster", "squirrel", "deer",
300
+ "wolf", "fox", "panda", "koala", "kangaroo", "zebra",
301
+ "giraffe", "camel", "donkey", "mule", "llama", "alpaca"
302
+ ]
303
+
304
+ # 상위 결과 확인
305
+ for category in categories:
306
+ category_name_lower = category["category_name"].lower()
307
+ score = category["score"]
308
+
309
+ # 동물 관련 키워드가 포함되어 있으면 즉시 차단
310
+ if any(keyword in category_name_lower for keyword in animal_keywords):
311
+ return {
312
+ "success": True,
313
+ "is_person": False
314
+ }
315
+
316
+ # 사람 관련 키워드가 포함되어 있고 신뢰도가 임계값 이상이면 사람으로 판단
317
+ if any(keyword in category_name_lower for keyword in person_keywords):
318
+ if score >= threshold:
319
+ return {
320
+ "success": True,
321
+ "is_person": True
322
+ }
323
+
324
+ # 사람 관련 클래스가 없으면 차단
325
+ return {
326
+ "success": True,
327
+ "is_person": False
328
+ }
329
+
330
+ except Exception as e:
331
+ import traceback
332
+ traceback.print_exc()
333
+ return {
334
+ "success": False,
335
+ "error": str(e)
336
+ }
337
+
338
+
339
+ def handler(data: Dict[str, Any], context) -> Dict[str, Any]:
340
+ """
341
+ HuggingFace Inference Endpoint 메인 핸들러
342
+
343
+ Args:
344
+ data: 요청 데이터
345
+ context: 컨텍스트 객체
346
+
347
+ Returns:
348
+ 응답 딕셔너리
349
+ """
350
+ # 모델 초기화 (최초 1회만)
351
+ global pose_landmarker, image_classifier
352
+ if pose_landmarker is None and image_classifier is None:
353
+ init_models()
354
+
355
+ # 엔드포인트 경로 확인
356
+ endpoint = data.get("endpoint", "")
357
+
358
+ try:
359
+ if endpoint == "/extract_landmarks":
360
+ return extract_landmarks_handler(data)
361
+ elif endpoint == "/classify_image":
362
+ return classify_image_handler(data)
363
+ elif endpoint == "/is_person":
364
+ return is_person_handler(data)
365
+ else:
366
+ return {
367
+ "success": False,
368
+ "error": f"알 수 없는 엔드포인트: {endpoint}. 지원되는 엔드포인트: /extract_landmarks, /classify_image, /is_person"
369
+ }
370
+ except Exception as e:
371
+ import traceback
372
+ traceback.print_exc()
373
+ return {
374
+ "success": False,
375
+ "error": str(e)
376
+ }
377
+
mediapipe-endpoint/model/efficientnet_lite0.tflite ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6c7ab0a6e5dcbf38a8c33b960996a55a3b4300b36a018c4545801de3a3c8bde0
3
+ size 18582189
mediapipe-endpoint/model/pose_landmarker_lite.task ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:59929e1d1ee95287735ddd833b19cf4ac46d29bc7afddbbf6753c459690d574a
3
+ size 5777746
mediapipe-endpoint/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # MediaPipe 모델 실행을 위한 필수 의존성
2
+ mediapipe>=0.10.0
3
+ pillow>=10.0.0
4
+ numpy>=1.24.0
5
+