steveyehtw commited on
Commit
50e1d54
·
1 Parent(s): 5e7cff3

Deploy optimized Arcade Hub with shared models and LFS binaries

Browse files

Concurrency limits, session timeout, shared MediaPipe models, static
frontend split, health/metrics endpoints, and Git LFS for image/apk assets.

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +5 -0
  2. Dockerfile +14 -12
  3. models/README.md +20 -0
  4. {projects/2026Spring專題班/胡惟媗/臉部辨識 → models}/face_landmarker.task +0 -0
  5. {projects/2026Spring專題班/胡惟媗/手部辨識 → models}/hand_landmarker.task +0 -0
  6. projects/2026_AI營1/劉宗穎/DataFrame指令练习/d1_1_pandas_intro.py +37 -0
  7. projects/2026_AI營1/劉宗穎/DataFrame指令练习/taipei_weather_2026.csv +16 -0
  8. projects/2026_AI營1/劉宗穎/Day1/taipei_weather_2026.csv +16 -0
  9. projects/{DEMO/Student1/手勢雷射發射/hand_landmarker.task → 2026_AI營1/劉宗穎/Day1/taipei_weather_2026.png} +2 -2
  10. projects/2026_AI營1/劉宗穎/ai_api串接练习/d2_1_gemini.py +41 -0
  11. projects/2026_AI營1/劉宗穎/mock&AI_api双模式/d2_3_chatbot_memory.py +95 -0
  12. projects/2026_AI營1/劉宗穎/mock&AI_api双模式/d2_4_chatbot_memory.py +129 -0
  13. projects/2026_AI營1/劉宗穎/mock_关键词抓取/d2_3_chatbot_memory_mock.py +84 -0
  14. projects/2026_AI營1/劉宗穎/mock_密室逃脱游戏/d2_3_chatbot_memory_game_template.py +118 -0
  15. projects/2026_AI營1/劉宗穎/requirements.txt +6 -0
  16. projects/2026_AI營1/劉宗穎/三指捏游戏/d5_pinch_challenge_solution.py +217 -0
  17. projects/2026_AI營1/劉宗穎/人脸辨识/d3_2_face_detection.py +128 -0
  18. projects/2026_AI營1/劉宗穎/人脸辨识/d3_2_face_detection2.py +139 -0
  19. projects/{DEMO/Student1/視力護眼衛士/face_landmarker.task → 2026_AI營1/劉宗穎/咖啡店分析图表/coffee_sales_analysis.png} +2 -2
  20. projects/2026_AI營1/劉宗穎/咖啡店分析图表/coffee_sales_data.csv +15 -0
  21. projects/2026_AI營1/劉宗穎/咖啡店分析图表/d1_4_visualization_coffee.py +115 -0
  22. projects/2026_AI營1/劉宗穎/嘴巴侦测/step3_mouth_ratio.py +113 -0
  23. projects/{DEMO/Student3/手勢特徵分類/hand_landmarker.task → 2026_AI營1/劉宗穎/处理过的照片/output_contours.jpg} +2 -2
  24. projects/2026_AI營1/劉宗穎/处理过的照片/output_edges.jpg +3 -0
  25. projects/2026_AI營1/劉宗穎/处理过的照片/output_gray.jpg +3 -0
  26. projects/2026_AI營1/劉宗穎/天气资料图表/d1_3_visualization.py +87 -0
  27. projects/2026_AI營1/劉宗穎/天气资料图表/taipei_weather_2026.csv +16 -0
  28. projects/2026_AI營1/劉宗穎/影像处理/d3_1_opencv_basic.py +70 -0
  29. projects/2026_AI營1/劉宗穎/手指画画游戏/draw_game_student.py +167 -0
  30. projects/2026_AI營1/劉宗穎/照片/onepiece(2).jpeg +3 -0
  31. projects/2026_AI營1/劉宗穎/照片/onepiece.jpeg +3 -0
  32. projects/2026_AI營1/劉宗穎/爬虫分析/d4_1_ex1_books.py +94 -0
  33. projects/2026_AI營1/劉宗穎/爬虫分析/d4_1_ex2_ptt.py +81 -0
  34. projects/2026_AI營1/劉宗穎/爬虫分析/d4_1_ex3_ithelp.py +98 -0
  35. projects/2026_AI營1/劉宗穎/爬虫分析与AI报告/d4_1_web_scraping.py +68 -0
  36. projects/2026_AI營1/劉宗穎/爬虫分析与AI报告/d4_2_agent_task_chain.py +141 -0
  37. projects/2026_AI營1/劉宗穎/爬虫分析与AI报告/d4_2_ex2_crawler_ai.py +231 -0
  38. projects/2026_AI營1/劉宗穎/眼睛距离侦测/step0_eyeguard_template.py +156 -0
  39. projects/2026_AI營1/劉宗穎/脸部侦测/face_mesh_connections.py +500 -0
  40. projects/2026_AI營1/劉宗穎/脸部侦测/step1_facemesh.py +111 -0
  41. projects/2026_AI營1/劉宗穎/脸部侦测/step2_facemesh_complete.py +362 -0
  42. projects/2026_AI營1/劉宗穎/设计AI角色&场景/d2_2_prompt_engineering.py +56 -0
  43. projects/2026_AI營1/劉宗穎/资料异常值清洗/d1_2_data_cleaning.py +38 -0
  44. projects/2026_AI營1/劉宗穎/资料异常值清洗/taipei_weather_2026.csv +16 -0
  45. projects/2026_AI營1/劉柔佑/DataFrame指令練習/d1_1_pandas_intro.py +34 -0
  46. projects/2026_AI營1/劉柔佑/DataFrame指令練習/taipei_weather_2026.csv +16 -0
  47. projects/2026_AI營1/劉柔佑/ai_api串接練習/d2_1_gemini.py +41 -0
  48. projects/2026_AI營1/劉柔佑/draw_game/draw_game_student.py +147 -0
  49. projects/2026_AI營1/劉柔佑/image/Boa.jpeg +3 -0
  50. projects/2026_AI營1/劉柔佑/image/Fandom.jpg +3 -0
.gitattributes CHANGED
@@ -38,3 +38,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
38
  *.png filter=lfs diff=lfs merge=lfs -text
39
  *.wav filter=lfs diff=lfs merge=lfs -text
40
  *.mp3 filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
38
  *.png filter=lfs diff=lfs merge=lfs -text
39
  *.wav filter=lfs diff=lfs merge=lfs -text
40
  *.mp3 filter=lfs diff=lfs merge=lfs -text
41
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
42
+ *.webp filter=lfs diff=lfs merge=lfs -text
43
+ *.avif filter=lfs diff=lfs merge=lfs -text
44
+ *.apk filter=lfs diff=lfs merge=lfs -text
45
+ *.JPEG filter=lfs diff=lfs merge=lfs -text
Dockerfile CHANGED
@@ -1,6 +1,6 @@
1
  FROM python:3.11-slim
2
 
3
- # Install system dependencies required by OpenCV headless, Pygame, and MediaPipe
4
  RUN apt-get update && apt-get install -y --no-install-recommends \
5
  libglib2.0-0 \
6
  libgl1 \
@@ -8,31 +8,33 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
8
  libgles2 \
9
  libegl1 \
10
  fonts-wqy-zenhei \
11
- fontconfig \
 
12
  && rm -rf /var/lib/apt/lists/*
13
 
14
- # Set up environment variables
15
  ENV PYTHONUNBUFFERED=1 \
16
  PYTHONDONTWRITEBYTECODE=1 \
17
  SDL_VIDEODRIVER=dummy \
18
  SDL_AUDIODRIVER=dummy \
19
- PORT=7860
 
 
 
20
 
21
- # Create working directory and copy dependencies
22
  WORKDIR /app
23
  COPY requirements.txt .
24
 
25
- # Install dependencies
 
26
  RUN pip install --no-cache-dir -r requirements.txt
27
 
28
- # Copy application files
29
  COPY . .
30
 
31
- # Create directory for projects if it doesn't exist
32
- RUN mkdir -p /app/projects
33
 
34
- # Set permissions for Hugging Face Spaces compatibility (runs as UID 1000)
35
- RUN chmod -R 777 /app
 
36
 
37
- # Run server.py on port specified by environment
38
  CMD uvicorn server:app --host 0.0.0.0 --port $PORT
 
1
  FROM python:3.11-slim
2
 
3
+ # System deps for OpenCV headless, Pygame, MediaPipe + CJK fonts
4
  RUN apt-get update && apt-get install -y --no-install-recommends \
5
  libglib2.0-0 \
6
  libgl1 \
 
8
  libgles2 \
9
  libegl1 \
10
  fonts-wqy-zenhei \
11
+ fonts-wqy-microhei \
12
+ fonts-noto-cjk \
13
  && rm -rf /var/lib/apt/lists/*
14
 
 
15
  ENV PYTHONUNBUFFERED=1 \
16
  PYTHONDONTWRITEBYTECODE=1 \
17
  SDL_VIDEODRIVER=dummy \
18
  SDL_AUDIODRIVER=dummy \
19
+ PORT=7860 \
20
+ MAX_CONCURRENT_RUNNERS=3 \
21
+ SESSION_TIMEOUT_SEC=900 \
22
+ PROJECT_CACHE_TTL_SEC=10
23
 
 
24
  WORKDIR /app
25
  COPY requirements.txt .
26
 
27
+ # Base image without TensorFlow (smaller / faster cold start).
28
+ # To include Keras demos: COPY requirements-ml.txt . && pip install -r requirements-ml.txt
29
  RUN pip install --no-cache-dir -r requirements.txt
30
 
 
31
  COPY . .
32
 
33
+ RUN mkdir -p /app/projects /app/models /app/static \
34
+ && chmod -R 777 /app
35
 
36
+ # Healthcheck for orchestrators
37
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
38
+ CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:%s/health' % __import__('os').environ.get('PORT','7860'))" || exit 1
39
 
 
40
  CMD uvicorn server:app --host 0.0.0.0 --port $PORT
models/README.md ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Shared MediaPipe models
2
+
3
+ Large `.task` model files live here once and are linked into student project
4
+ directories at runtime by `runner.py` when missing.
5
+
6
+ | File | Used by |
7
+ |------|---------|
8
+ | `hand_landmarker.task` | Hand / pinch / gesture projects |
9
+ | `face_landmarker.task` | Face mesh / eye-distance projects |
10
+
11
+ Student code may still use:
12
+
13
+ ```python
14
+ os.path.join(os.path.dirname(__file__), "hand_landmarker.task")
15
+ ```
16
+
17
+ The runner creates a symlink (or hard link) from the project folder to this
18
+ directory before executing `main.py`.
19
+
20
+ Do not commit duplicate copies under `projects/**`.
{projects/2026Spring專題班/胡惟媗/臉部辨識 → models}/face_landmarker.task RENAMED
File without changes
{projects/2026Spring專題班/胡惟媗/手部辨識 → models}/hand_landmarker.task RENAMED
File without changes
projects/2026_AI營1/劉宗穎/DataFrame指令练习/d1_1_pandas_intro.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # d1_1_pandas_intro.py
2
+ # Day 1 學員填空:Pandas 基礎(完成 TODO 後執行)
3
+ # cd examples/Day1/student && python3 d1_1_pandas_intro.py
4
+ # 參考答案:../Teacher/d1_1_pandas_intro.py
5
+
6
+ import pandas as pd
7
+
8
+ df = pd.read_csv("taipei_weather_2026.csv")
9
+ print("=== 數據載入成功! ===")
10
+
11
+ print("\n--- 1. head ---")
12
+ print(df.head())
13
+
14
+ print("\n--- 2. info ---")
15
+ df.info()
16
+
17
+ print("\n--- 3. describe ---")
18
+ print(df.describe())
19
+
20
+ print("\n--- 4. Temperature 前 5 筆 ---")
21
+ print(df['Temperature'].head())
22
+
23
+ print("\n--- 5. Date 與 Rainfall ---")
24
+ print(df[['Rainfall', 'Date']].head())
25
+
26
+ print("\n--- 挑戰:最高溫與平均濕度 ---")
27
+ max_temp = df['Temperature'].max()
28
+ avg_humidity = df['Humidity'].mean()
29
+ min_rainfall = df['Rainfall'].min()
30
+ print(f"最高溫度:{max_temp}°C")
31
+ print(f"平均濕度:{avg_humidity:.2f}%")
32
+ print(f"最低降雨:{min_rainfall:.2f}%")
33
+
34
+ print("\n--- whether it is good for egyptians")
35
+
36
+ egy = df[(df['Temperature'] > 27) & (df['Rainfall'] == 0)]
37
+ print(egy)
projects/2026_AI營1/劉宗穎/DataFrame指令练习/taipei_weather_2026.csv ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Date,Temperature,Humidity,Rainfall
2
+ 2026-07-01,28.5,82,5.2
3
+ 2026-07-02,29.0,80,0.0
4
+ 2026-07-03,30.2,75,0.0
5
+ 2026-07-04,31.5,70,0.0
6
+ 2026-07-05,27.8,90,25.4
7
+ 2026-07-06,26.5,95,42.0
8
+ 2026-07-07,28.0,85,12.5
9
+ 2026-07-08,29.5,78,0.0
10
+ 2026-07-09,30.8,72,0.0
11
+ 2026-07-10,32.0,68,0.0
12
+ 2026-07-11,28.2,88,18.0
13
+ 2026-07-12,27.0,92,30.5
14
+ 2026-07-13,29.2,80,2.1
15
+ 2026-07-14,30.0,76,0.0
16
+ 2026-07-15,31.2,71,0.0
projects/2026_AI營1/劉宗穎/Day1/taipei_weather_2026.csv ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Date,Temperature,Humidity,Rainfall
2
+ 2026-07-01,28.5,82,5.2
3
+ 2026-07-02,29.0,80,0.0
4
+ 2026-07-03,30.2,75,0.0
5
+ 2026-07-04,31.5,70,0.0
6
+ 2026-07-05,27.8,90,25.4
7
+ 2026-07-06,26.5,95,42.0
8
+ 2026-07-07,28.0,85,12.5
9
+ 2026-07-08,29.5,78,0.0
10
+ 2026-07-09,30.8,72,0.0
11
+ 2026-07-10,32.0,68,0.0
12
+ 2026-07-11,28.2,88,18.0
13
+ 2026-07-12,27.0,92,30.5
14
+ 2026-07-13,29.2,80,2.1
15
+ 2026-07-14,30.0,76,0.0
16
+ 2026-07-15,31.2,71,0.0
projects/{DEMO/Student1/手勢雷射發射/hand_landmarker.task → 2026_AI營1/劉宗穎/Day1/taipei_weather_2026.png} RENAMED
File without changes
projects/2026_AI營1/劉宗穎/ai_api串接练习/d2_1_gemini.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # d2_1_gemini.py — Day2 學員填空
2
+ # cd examples/Day2/Student && python3 d2_1_gemini.py
3
+ # 參考答案:../Teacher/d2_1_gemini.py
4
+
5
+ import os
6
+ from google import genai
7
+
8
+ # === 【學員填空練習點 1:設置與載入 API Key】 ===
9
+ # TODO: 嘗試從環境變數 GEMINI_API_KEY 取得 API Key。如果未設定,則使用 input() 提示手動輸入。
10
+ api_key = os.environ.get("GEMINI_API_KEY", "AQ.Ab8RN6LgriztRWiwQOdkK1wXmmfs4AzlZKSQsP2X0cnjzaMCCg")
11
+
12
+ if not api_key:
13
+ print("[提示] 未檢測到環境變數 GEMINI_API_KEY。")
14
+ print(" 請先設定:export GEMINI_API_KEY=\"your_key\"")
15
+ print(" 或於下方手動輸入(僅供本機測試,勿寫死在檔案中上傳):")
16
+ api_key = input("API Key: ").strip()
17
+ if not api_key:
18
+ print("[結束] 未提供 API Key。可改練習 d2_1_ollama_local.py。")
19
+ exit(0)
20
+
21
+ # === 【學員填空練習點 2:設定模型名稱】 ===
22
+ # TODO: 設定使用的 Gemini 模型名稱(例如 'gemini-3.1-flash-lite')
23
+ model_name = "gemini-3.1-flash-lite"
24
+
25
+ print(f"正在連線至 Google Cloud Model ({model_name})...")
26
+
27
+ # === 【學員填空練習點 3:初始化 Gemini Client】 ===
28
+ # TODO: 呼叫 genai.Client 並傳入 api_key 參數來初始化 Client 物件,指派給 client 變數
29
+ client = genai.Client(api_key=api_key)
30
+
31
+ # === 【學員填空練習點 4:發送內容生成請求】 ===
32
+ # TODO: 使用 client.models.generate_content 呼叫模型,傳入 model 參數與 contents 提示詞
33
+ # 提示詞內容:請模型以繁體中文解釋什麼是『API 串接』,並用餐廳點餐作為比喻,字數約 100 字。
34
+ response = client.models.generate_content(model=model_name, contents="请用中文解释为什么泡面很好吃,字数一百字以内")
35
+ # === 【學員填空練習點 5:讀取並印出回應】 ===
36
+ # TODO: 從 response 物件中取得 text 欄位內容,並列印出來。
37
+ print("\n=== AI 回應結果 ===")
38
+ # 請在下方實作列印邏輯:
39
+ print(response.text)
40
+ # for i in range(10):
41
+ # print(response.text)
projects/2026_AI營1/劉宗穎/mock&AI_api双模式/d2_3_chatbot_memory.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # d2_3_chatbot_memory.py
2
+ # Day 2: 生成式 AI 串接實戰 - 具對話記憶的智慧客服機器人 (Gemini / Local Mock 雙模)
3
+
4
+ import os
5
+ from google import genai
6
+
7
+ # 優先讀取環境變數 GEMINI_MODEL,預設為 gemini-3.1-flash-lite
8
+ GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite")
9
+ api_key = os.environ.get(
10
+ "GEMINI_API_KEY", "")
11
+
12
+
13
+ def local_mock_response(user_input, history):
14
+ user_input_lower = user_input.lower()
15
+
16
+ # TODO: 處理 營業時間 / 你好 / 再見,其餘回兜底句
17
+ if "營業時間" in user_input_lower or "時間" in user_input_lower:
18
+ return "我們觀測站的營業時間為週一至週五的上午 9:00 至下午 6:00,週末休息。"
19
+ # elif "XX" in user_input_lower or "XX" in user_input_lower:
20
+ # return "XXXXXXXXXXXXXXXXX"
21
+
22
+
23
+ else:
24
+ turns = len(history) // 2 + 1
25
+ return (
26
+ f"(模擬模式)收到您的訊息:『{user_input}』。"
27
+ f"這是我與您的第 {turns} 次對話,請問還有其他需要協助的嗎?"
28
+ )
29
+
30
+
31
+ def main():
32
+ print("=" * 50)
33
+ print(" 得能 AI 智慧客服對話系統 (支援對話上下文記憶)")
34
+ print("=" * 50)
35
+
36
+ use_mock = False
37
+ chat_session = None
38
+
39
+ if not api_key:
40
+ print("[提示] 未檢測到環境變數 GEMINI_API_KEY,將自動啟用「離線模擬模式 (Local Mock Mode)」。")
41
+ print(" (如需連線雲端模型,請設定 GEMINI_API_KEY 環境變數)")
42
+ use_mock = True
43
+ else:
44
+ try:
45
+ print(f"正在連線 Google Gemini 雲端客服伺服器 ({GEMINI_MODEL})...")
46
+ client = genai.Client(api_key=api_key)
47
+ # TODO: 設定系統指令 (System Instruction / System Prompt)
48
+ # system_instruction = (
49
+ # "你是一位專業的得能教育客服人員,語氣溫和客氣,"
50
+ # "XXXXXXXXXXXXXXXXXXXXXXX"
51
+ # )
52
+
53
+ chat_session = client.chats.create(
54
+ model=GEMINI_MODEL,
55
+ config={'system_instruction': system_instruction}
56
+ )
57
+ print("雲端客服系統已就緒!")
58
+ except Exception as e:
59
+ print(f"連線雲端客服失敗 ({e}),切換至離線模擬模式...")
60
+ use_mock = True
61
+
62
+ local_history = []
63
+
64
+ print("\n系統啟動完成!請輸入您要諮詢的問題(輸入 'quit' 或 'exit' 結束對話):")
65
+ print("-" * 50)
66
+
67
+ while True:
68
+ try:
69
+ user_input = input("\n[您]: ").strip()
70
+ if not user_input:
71
+ continue
72
+
73
+ if user_input.lower() in ["quit", "exit"]:
74
+ print("對話結束,感謝您的使用!")
75
+ break
76
+
77
+ if use_mock:
78
+ ai_reply = local_mock_response(user_input, local_history)
79
+ local_history.append({"role": "user", "text": user_input})
80
+ local_history.append({"role": "model", "text": ai_reply})
81
+ else:
82
+ response = chat_session.send_message(user_input)
83
+ ai_reply = response.text
84
+
85
+ print(f"[AI 客服]: {ai_reply}")
86
+
87
+ except KeyboardInterrupt:
88
+ print("\n對話強行終止。再見!")
89
+ break
90
+ except Exception as e:
91
+ print(f"\n[錯誤] 發生異常:{e}")
92
+
93
+
94
+ if __name__ == "__main__":
95
+ main()
projects/2026_AI營1/劉宗穎/mock&AI_api双模式/d2_4_chatbot_memory.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # d2_3_chatbot_memory.py
2
+ # Day 2: 生成式 AI 串接實戰 - 具對話記憶的智慧客服機器人 (Gemini / Local Mock 雙模)
3
+
4
+ import os
5
+ from google import genai
6
+
7
+ # 優先讀取環境變數 GEMINI_MODEL,預設為 gemini-3.1-flash-lite
8
+ GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite")
9
+ api_key = os.environ.get(
10
+ "GEMINI_API_KEY", "AQ.Ab8RN6JvbUqLW-wcddeMducmta0aGE2QtcrmF4Kfd8nnpZ9mzg")
11
+
12
+
13
+ def local_mock_response(user_input, history):
14
+ user_input_lower = user_input.lower()
15
+
16
+ if "營業時間" in user_input_lower or "時間" in user_input_lower:
17
+ return "我们冰淇淋店的營業時間為週一至週五的上午 9:00 至下午 6:00,週末休息。"
18
+ elif "地址" in user_input_lower or "地点" in user_input_lower or "地方" in user_input_lower:
19
+ return "我们的店在求毛星球毛笔城市QQ镇喜喜街167号一楼。"
20
+ elif "冰淇淋的口味" in user_input_lower or "口味" in user_input_lower:
21
+ return "我们的冰淇淋的口味有草莓,芒果,巧克力,咖啡,蓝莓,汽水和芭乐。"
22
+ elif "冰淇淋的价格" in user_input_lower or "价格" in user_input_lower or "冰淇淋怎么卖" in user_input_lower:
23
+ return "一球100元,两球170元,三球250元,五球400元,十球750元。"
24
+ elif "有打折吗" in user_input_lower or "有优惠吗" in user_input_lower or "有进行促销活动吗" in user_input_lower or "有活动吗" in user_input_lower or "折扣" in user_input_lower:
25
+ return "我们有和吃鸡炸鸡店合作,去那里消费满1000元即可换一球冰淇淋。"
26
+ elif "help" in user_input_lower or "帮助" in user_input_lower:
27
+ return "\n輸入「營業時間、時間」即可知道營業時間\n輸入「地址、地點、地方」即可知道地址\n輸入「冰淇淋的口味、口味」即可知道口味\n輸入「冰淇淋的價格、價格、冰淇淋怎麼賣」即可知道價格\n輸入「有打折嗎、有優惠嗎、有進行促銷活動嗎、有活動嗎、折扣」即可知道優惠資訊\n輸入「停車場、停車、附近有沒有停車場」即可知道停車資訊\n輸入「可以客製化嗎、客製化的服務、客製化」即可知道客製化服務\n輸入「客製化的價錢、客製化要多少錢」即可知道客製化價格\n輸入「推薦、招牌」即可知道推薦口味\n輸入「外帶、內用」即可知道用餐方式\n輸入「付款、支付、可以刷卡」即可知道付款方式。"
28
+ elif "你好" in user_input_lower or "hello" in user_input_lower or "hi" in user_input_lower or "Hi" in user_input_lower:
29
+ return "你好!请问您需要什么帮助吗?"
30
+ elif "停车场" in user_input_lower or "停车" in user_input_lower or "附近有没有停车场" in user_input_lower:
31
+ return "我们的店附近有三个停车场,步行三分钟即可到达我们的店。"
32
+ elif "可以克制化吗" in user_input_lower or "客制化的服务" in user_input_lower or "客制化" in user_input_lower:
33
+ return "我们的店可以克制化冰淇淋,可以选择彩糖、珍珠、草莓酱、巧克力酱、布丁、奶油和炼乳。"
34
+ elif "客制化的价钱" in user_input_lower or "客制化要多少钱" in user_input_lower or "客制化要多少钱?" in user_input_lower:
35
+ return "客制化每一种材料都要再加15元。"
36
+ elif "推薦" in user_input_lower or "招牌" in user_input_lower:
37
+ return "推薦您試試草莓、巧克力和芭樂口味,都是熱門選擇。"
38
+ elif "外帶" in user_input_lower or "內用" in user_input_lower:
39
+ return "我們提供內用及外帶服務。"
40
+ elif "付款" in user_input_lower or "支付" in user_input_lower or "可以刷卡" in user_input_lower:
41
+ return "我們接受現金、信用卡及行動支付。"
42
+ elif "谢谢" in user_input_lower or "Thank you" in user_input_lower:
43
+ return "不客气!还需要什么帮助吗?"
44
+
45
+ else:
46
+ # 計算目前是第幾輪對話(一輪包含使用者輸入與客服回覆,共 2 個元素)
47
+ # Why: 藉由傳入的 history 串列長度除以 2,即可動態得知目前互動的輪數,模擬記憶狀態。
48
+ turns = len(history) // 2 + 1
49
+ return (
50
+ f"(模擬模式)收到您的訊息:『{user_input}』。"
51
+ f"這是我與您的第 {turns} 次對話,請問還有其他需要協助的嗎?"
52
+ )
53
+
54
+
55
+ def main():
56
+ print("=" * 50)
57
+ print(" 吃吃吃冰淇淋店客服對話系統 (支援對話上下文記憶)")
58
+ print("=" * 50)
59
+
60
+ use_mock = False
61
+ chat_session = None
62
+
63
+ if not api_key:
64
+ print("[提示] 未檢測到環境變數 GEMINI_API_KEY,將自動啟用「離線模擬模式 (Local Mock Mode)」。")
65
+ print(" (如需連線雲端模型,請設定 GEMINI_API_KEY 環境變數)")
66
+ use_mock = True
67
+ else:
68
+ try:
69
+ print(f"正在連線 Google Gemini 雲端客服伺服器 ({GEMINI_MODEL})...")
70
+ client = genai.Client(api_key=api_key)
71
+ # TODO: 設定系統指令 (System Instruction / System Prompt)
72
+ system_instruction = (
73
+ "你是一位冰淇淋店的客服,负责提供资讯"
74
+ "我们冰淇淋店的營業時間為週一至週五的上午 9:00 至下午 6:00,週末休息。"
75
+ "我们的店在求毛星球毛笔城市QQ镇喜喜街167号一楼。"
76
+ "冰淇淋的价格一球100元,两球170元,三球250元,五球400元,十球750元。"
77
+ "我们的店有进行优惠,有和吃鸡炸鸡店合作,去那里消费满1000元即可换一球冰淇淋。"
78
+ "这是我们的资讯:輸入「營業時間、時間」即可知道營業時間,輸入「地址、地點、地方」即可知道地址,輸入「冰淇淋的口味、口味」即可知道口味,輸入「冰淇淋的價格、價格、冰淇淋怎麼賣」即可知道價格,輸入「有打折嗎、有優惠嗎、有進行促銷活動嗎、有活動嗎、折扣」即可知道優惠資訊,輸入「停車場、停車、附近有沒有停車場」即可知道停車資訊,輸入「可以客製化嗎、客製化的服務、客製化」即可知道客製化服務,輸入「客製化的價錢、客製化要多少錢」即可知道客製化價格,輸入「推薦、招牌」即可知道推薦口味,輸入「外帶、內用」即可知道用餐方式,輸入「付款、支付、可以刷卡」即可知道付款方式。"
79
+ "你应该亲切的向客户问好。"
80
+ "我们的店附近有三个停车场,步行三分钟即可到达我们的店。"
81
+ "我们的店可以克制化冰淇淋,可以选择彩糖、珍珠、草莓酱、巧克力酱、布丁、奶油和炼乳。"
82
+ "客制化每一种材料都要再加15元。"
83
+ "我們提供內用及外帶服務。"
84
+ "我們接受現金、信用卡及行動支付。"
85
+ )
86
+
87
+ chat_session = client.chats.create(
88
+ model=GEMINI_MODEL,
89
+ config={'system_instruction': system_instruction}
90
+ )
91
+ print("雲端客服系統已就緒!")
92
+ except Exception as e:
93
+ print(f"連線雲端客服失敗 ({e}),切換至離線模擬模式...")
94
+ use_mock = True
95
+
96
+ local_history = []
97
+
98
+ print("\n系統啟動完成!請輸入您要諮詢的問題(輸入 'quit' 或 'exit' 結束對話):")
99
+ print("-" * 50)
100
+
101
+ while True:
102
+ try:
103
+ user_input = input("\n[您]: ").strip()
104
+ if not user_input:
105
+ continue
106
+
107
+ if user_input.lower() in ["quit", "exit"]:
108
+ print("對話結束,感謝您的使用!")
109
+ break
110
+
111
+ if use_mock:
112
+ ai_reply = local_mock_response(user_input, local_history)
113
+ local_history.append({"role": "user", "text": user_input})
114
+ local_history.append({"role": "model", "text": ai_reply})
115
+ else:
116
+ response = chat_session.send_message(user_input)
117
+ ai_reply = response.text
118
+
119
+ print(f"[AI 客服]: {ai_reply}")
120
+
121
+ except KeyboardInterrupt:
122
+ print("\n對話強行終止。再見!")
123
+ break
124
+ except Exception as e:
125
+ print(f"\n[錯誤] 發生異常:{e}")
126
+
127
+
128
+ if __name__ == "__main__":
129
+ main()
projects/2026_AI營1/劉宗穎/mock_关键词抓取/d2_3_chatbot_memory_mock.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # d2_3_chatbot_memory_mock.py
2
+ # Day 2: 生成式 AI 串接實戰 - 具對話記憶的智慧客服機器人 (純 Local Mock 教師版)
3
+
4
+ def local_mock_response(user_input: str, history: list) -> str:
5
+ """
6
+ _ 根據使用者輸入與歷史對話紀錄,模擬客服助理的自動回覆。
7
+ 在沒有網路或 API 金鑰時,如何透過關鍵字比對
8
+ 與對話歷史長度(計算對話輪數)來模擬一個具有「記憶」能力的對話流程。
9
+ """
10
+ user_input_lower = user_input.lower()
11
+
12
+ # 關鍵字比對:模擬常見的客服問答
13
+ # TODO: 處理 營業時間 / 你好 / 再見,其餘回兜底句
14
+ if "營業時間" in user_input_lower or "時間" in user_input_lower:
15
+ return "我们冰淇淋店的營業時間為週一至週五的上午 9:00 至下午 6:00,週末休息。"
16
+ elif "地址" in user_input_lower or "地点" in user_input_lower or "地方" in user_input_lower:
17
+ return "我们的店在求毛星球毛笔城市QQ镇喜喜街167号一楼。"
18
+ elif "冰淇淋的口味" in user_input_lower or "口味" in user_input_lower:
19
+ return "我们的冰淇淋的口味有草莓,芒果,巧克力,咖啡,蓝莓,汽水和芭乐。"
20
+ elif "冰淇淋的价格" in user_input_lower or "价格" in user_input_lower or "冰淇淋怎么卖" in user_input_lower:
21
+ return "一球100元,两球170元,三球250元,五球400元,十球750元。"
22
+ elif "有打折吗" in user_input_lower or "有优惠吗" in user_input_lower or "有进行促销活动吗" in user_input_lower or "有活动吗" in user_input_lower or "折扣" in user_input_lower:
23
+ return "我们有和吃鸡炸鸡店合作,去那里消费满1000元即可换一球冰淇淋。"
24
+ elif "help" in user_input_lower or "帮助" in user_input_lower:
25
+ return "\n輸入「營業時間、時間」即可知道營業時間\n輸入「地址、地點、地方」即可知道地址\n輸入「冰淇淋的口味、口味」即可知道口味\n輸入「價格、冰淇淋怎麼卖、冰淇淋的价格」即可知道價格\n輸入「折扣、優惠、活動」即可知道優惠資訊。"
26
+ elif "你好" in user_input_lower or "hello" in user_input_lower or "hi" in user_input_lower or "Hi" in user_input_lower:
27
+ return "你好!请问您需要什么帮助吗?"
28
+
29
+ else:
30
+ # 計算目前是第幾輪對話(一輪包含使用者輸入與客服回覆,共 2 個元素)
31
+ # Why: 藉由傳入的 history 串列長度除以 2,即可動態得知目前互動的輪數,模擬記憶狀態。
32
+ turns = len(history) // 2 + 1
33
+ return (
34
+ f"(模擬模式)收到您的訊息:『{user_input}』。"
35
+ f"這是我與您的第 {turns} 次對話,請問還有其他需要協助的嗎?"
36
+ )
37
+
38
+
39
+ def main():
40
+ print("=" * 50)
41
+ print(" 吃吃吃冰淇淋店客服對話系統 (純離線模擬模式 - 支援對話記憶)")
42
+ print("=" * 50)
43
+ print("[提示] 目前為純 Local Mock 模式,不需設定環境變數與連線雲端 API。")
44
+ print("-" * 50)
45
+
46
+ # 用於儲存對話歷史的串列
47
+ local_history = []
48
+
49
+ print("\n系統啟動完成!請輸入您要諮詢的問題(輸入 'quit' 或 'exit' 結束對話):")
50
+ print("-" * 50)
51
+ print("如果你不知道要输入什么,输入help或者帮助。")
52
+ print("-" * 50)
53
+
54
+ while True:
55
+ try:
56
+ user_input = input("\n[您]: ").strip()
57
+ if not user_input:
58
+ continue
59
+
60
+ # 結束條件判斷
61
+ if user_input.lower() in ["quit", "exit"]:
62
+ print("對話結束,感謝您的使用!")
63
+ break
64
+
65
+ # 呼叫模擬回覆函式
66
+ ai_reply = local_mock_response(user_input, local_history)
67
+
68
+ # 將本次對話記錄寫入歷史紀錄中,維持記憶狀態
69
+ # Why: 每次成功的問答都必須 append 進 history,以便下一輪對話時能正確累計輪數。
70
+ local_history.append({"role": "user", "text": user_input})
71
+ local_history.append({"role": "model", "text": ai_reply})
72
+
73
+ print(f"[AI 客服]: {ai_reply}")
74
+
75
+ except KeyboardInterrupt:
76
+ # 處理使用者按下 Ctrl+C 強行結束的情境,避免程式崩潰
77
+ print("\n對話強行終止。再見!")
78
+ break
79
+ except Exception as e:
80
+ print(f"\n[錯誤] 發生異常:{e}")
81
+
82
+
83
+ if __name__ == "__main__":
84
+ main()
projects/2026_AI營1/劉宗穎/mock_密室逃脱游戏/d2_3_chatbot_memory_game_template.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # d2_3_chatbot_memory_game_template.py
2
+ # Day 2: 生成式 AI 串接實戰 - 離線 Mock 遊戲設計模板 (學生填空版)
3
+
4
+ def local_mock_response(user_input: str, history: list) -> str:
5
+ """
6
+ 根據使用者輸入與歷史對話紀錄,模擬 NPC 或是系統的對話反應。
7
+
8
+ Why:
9
+ 此檔案為困難版對話記憶解謎的設計模板。你將學習到:
10
+ 1. 前置狀態限制:必須先問過「線索一」,才能查到「線索二」...
11
+ 2. 動態 Flag:Flag 答案中的某個線索會隨提問次數(turns)改變。
12
+ """
13
+ user_input_lower = user_input.strip().lower()
14
+
15
+ # 計算目前是第幾輪對話
16
+ turns = len(history) // 2 + 1
17
+
18
+ # TODO: 設計你的動態變數(例如 turns * 5,或是 current_weather 等)
19
+ # 並設定最終預期的正確 Flag 答案(必須包含此動態變數)
20
+ expected_flag = "flag}.{...add_8_sprinkles_ice_cream_Chocolate_flavored...}.{,"
21
+
22
+ # 從歷史中判斷先前是否已取得前置線索(在 history 中,AI model 是否曾回覆過特定字眼)
23
+ # TODO: 依據你設計的線索關鍵字,修改以下 history 的 any 比對條件
24
+ asked_clue1 = any("Chocolate_flavored" in h["text"] for h in history if h["role"] == "model")
25
+ asked_clue2 = any("_ice_cream_" in h["text"] for h in history if h["role"] == "model")
26
+
27
+ # 1. 驗證 Flag 邏輯
28
+ if user_input_lower == expected_flag:
29
+ return f"🎉 恭喜你!成功破解案件並獲得了神秘的冰淇淋配方!你在第 {turns} 回合成功通關!"
30
+
31
+ elif "flag{" in user_input_lower:
32
+ return (
33
+ f"❌ Flag 驗證失敗!\n"
34
+ f"提示:最後一個線索會隨著提問次數動態改變。\n"
35
+ f"你現在是在第 {turns} 次提問輸入,因此當前的動態線索應該是 turns{turns} 喔!\n"
36
+ f"請重新計算並拼湊最新的 Flag!"
37
+ )
38
+
39
+ # 2. 劇情關鍵字問答分支(打造你的 NPC 對話樹!)
40
+ # TODO: 依據你的故事背景,修改並設計以下 if-elif-else 結構
41
+ if "flag" in user_input_lower or "答案" in user_input_lower:
42
+ return "NPC:『想要解開這起謎題,你必須向我打聽案子的「原因」、「重要物品」與「凶器」!』"
43
+
44
+ elif "原因" in user_input_lower or "谁做的" in user_input_lower:
45
+ # 線索一:靜態線索
46
+ return "【線索一】\n这个案件是暖暖火锅店做的,\n因为他说每次顾客都说吃完了火锅很冷,\n可是他们吃完就去吃冰淇淋,所以才会感觉很冷。\n有一天,他就去了吃吃吃冰淇淋店复仇,\n让他们的老板踩到掉到地上的巧克力冰淇淋,并把他绑起来了。\nChocolate_flavored,记住这个口味!"
47
+
48
+ elif "重要物品" in user_input_lower or "物品" in user_input_lower or "重要的物品" in user_input_lower:
49
+ # 線索二:有前置限制,必須先取得線索一
50
+ if not asked_clue1:
51
+ return "NPC:『你連发生了什么事都不知道,我才不告訴你重要物品是什么呢!』"
52
+ return "【線索二】\n他在绑架了老板以后,他就把神秘的冰淇淋配方藏了起来。\n_ice_cream_, 记住这个线索!"
53
+
54
+ elif "凶器" in user_input_lower or "武器" in user_input_lower or "線索三" in user_input_lower:
55
+ # 線索三:動態線索,包含 turns 變數,且必須先取得線索二
56
+ if not asked_clue1:
57
+ return "NPC:『你連发生了什么事都不知道,我才不告訴你凶器是什么呢!』"
58
+ if not asked_clue2:
59
+ return "NPC:『你必須先查出「重要的物品」,我才會告訴你凶器是什么。』"
60
+ return f"【線索三】犯人用的凶器是一种配料,在里面加了安眠药,所以最后一个线索是add_8_sprinkles。"
61
+ elif "hint" in user_input_lower:
62
+ return "321"
63
+
64
+ elif "拼湊" in user_input_lower or "結合" in user_input_lower or "怎麼組合" in user_input_lower:
65
+ return "提示:將【線索一】【線索二】【當下提問的線索三】依序結合,並用小寫的 flag}.{... ...}.{,包起來。"
66
+
67
+ # 3. 兜底回覆(當玩家輸入沒有匹配到任何關鍵字時)
68
+ else:
69
+ return (
70
+ f"NPC:『你在說什麼?我聽不懂。收到您的訊息:『{user_input}』。"
71
+ f"這是我與您的第 {turns} 次對話,請問還有其他需要協助的嗎?』"
72
+ )
73
+
74
+
75
+
76
+ def main():
77
+ # TODO: 修改為你的遊戲標題與故事背景
78
+ print("=" * 60)
79
+ print(" 🕵️ 我設計的第一個具解謎遊戲 - 冰淇淋店店长失踪案 🕵️")
80
+ print("=" * 60)
81
+ print("本次密室逃脱体验与吃吃吃冰淇淋店合作")
82
+ print("=" * 60)
83
+ print("[故事簡介] 你是一位偵探,必須利用歷史對話記憶拼湊線索。")
84
+ print("傳說中,有一家聞名世界的冰淇淋店,憑藉一份神秘配方製作出無人能複���的美味冰淇淋。\n然而,某天店長突然失蹤,珍貴的冰淇淋秘方也隨之消失,只留下一間被上鎖的店鋪和一連串神秘線索。\n你們是一群受邀前來協助調查的人,當踏入店內的瞬間,大門突然自動鎖上,並響起一段錄音:\n「只有找到真正的秘方繼承者,才能打開大門離開這裡。」\n店內藏有許多與冰淇淋、口味、價格、優惠活動及客製化相關的謎題。\n你們必須仔細觀察每個角落、破解一道道機關,蒐集所有線索,\n找到失落的冰淇淋秘方,才能成功逃離這間神秘的冰淇淋店。")
85
+ print("依序追查主角、地點與動態蹤跡。祝你逃脱成功!")
86
+ print("-" * 60)
87
+
88
+ local_history = []
89
+
90
+ print("\n遊戲已啟動!請輸入你想問 NPC 的話(輸入 'quit' 結束遊戲):")
91
+ print("-" * 60)
92
+
93
+ while True:
94
+ try:
95
+ user_input = input("\n[您]: ").strip()
96
+ if not user_input:
97
+ continue
98
+
99
+ if user_input.lower() in ["quit", "exit"]:
100
+ print("遊戲結束,感謝遊玩!")
101
+ break
102
+
103
+ ai_reply = local_mock_response(user_input, local_history)
104
+
105
+ local_history.append({"role": "user", "text": user_input})
106
+ local_history.append({"role": "model", "text": ai_reply})
107
+
108
+ print(f"[NPC]: {ai_reply}")
109
+
110
+ except KeyboardInterrupt:
111
+ print("\n遊戲強行終止!")
112
+ break
113
+ except Exception as e:
114
+ print(f"\n[錯誤] 發生異常:{e}")
115
+
116
+
117
+ if __name__ == "__main__":
118
+ main()
projects/2026_AI營1/劉宗穎/requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ pandas>=2.0.0
2
+ matplotlib>=3.7.0
3
+ requests>=2.31.0
4
+ beautifulsoup4>=4.12.0
5
+ opencv-python>=4.8.0
6
+ google-generativeai>=0.8.0
projects/2026_AI營1/劉宗穎/三指捏游戏/d5_pinch_challenge_solution.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Week 10 - 魔法手勢:Pinch 偵測與手勢按鈕 (三指捏合挑戰 Template 填空版)
3
+ ==================================================================
4
+ 本週挑戰目標:
5
+ 1. 實作「三指捏合 (Three-Finger Pinch)」:
6
+ 必須同時滿足「大拇指 (4) 與食指 (8) 的距離 < Threshold」
7
+ 以及「大拇指 (4) 與中指 (12) 的距離 < Threshold」才判定為 Pinch
8
+ 2. 用三指碰觸 Pygame 畫面上的按鈕 + 捏合 = 魔法點擊
9
+ 3. 使用邊緣觸發 (Edge Detection) 防止重複觸發
10
+ 4. 新增 30 秒限時機制,時間到顯示最終得分
11
+ """
12
+ import pygame
13
+ import cv2
14
+ import mediapipe as mp
15
+ import math
16
+ import sys
17
+ import os
18
+ import random
19
+
20
+ # ============================================
21
+ # 1. 初始化
22
+ # ============================================
23
+ pygame.init()
24
+ WIDTH, HEIGHT = 800, 600
25
+ screen = pygame.display.set_mode((WIDTH, HEIGHT))
26
+ pygame.display.set_caption("Week 10 - 三指魔法手勢按鈕 (填空版) ")
27
+ clock = pygame.time.Clock()
28
+ from mediapipe.tasks import python
29
+ from mediapipe.tasks.python import vision
30
+
31
+ # 攝影機
32
+ cap = cv2.VideoCapture(0)
33
+
34
+ # 【TODO 1】初始化 MediaPipe 手勢偵測器 (Hand Landmarker)
35
+ \
36
+
37
+ try:
38
+ from mediapipe.tasks import python
39
+ from mediapipe.tasks.python import vision
40
+ base_options = python.BaseOptions(model_asset_path=os.path.join('hand_landmarker.task'))
41
+ options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1)
42
+ detector = vision.HandLandmarker.create_from_options(options)
43
+ except Exception as e:
44
+ print(f"Error loading model: {e}")
45
+ pygame.quit()
46
+ sys.exit()
47
+
48
+ # ============================================
49
+ # 2. 遊戲變數
50
+ # ============================================
51
+ # 手指座標
52
+ finger_x, finger_y = 400, 300 # 食指
53
+ thumb_x, thumb_y = 400, 300 # 大拇指
54
+ middle_x, middle_y = 400, 300 # 中指
55
+
56
+ # 距離變數
57
+ dist_thumb_index = 0.0
58
+ dist_thumb_middle = 0.0
59
+
60
+ # Pinch 相關變數
61
+ is_pinching = False
62
+ was_pinching = False # 邊緣觸發
63
+ PINCH_THRESHOLD = 50 # 距離臨界值
64
+
65
+ # 按鈕設定
66
+ button_rect = pygame.Rect(300, 250, 200, 100)
67
+ button_colors = [(100, 100, 220), (220, 100, 100),
68
+ (100, 220, 100), (220, 180, 50)]
69
+ current_color_index = 0
70
+
71
+ # 點擊計數器
72
+ click_count = 0
73
+
74
+ # --- 限時機制變數 ---
75
+ START_TIME = pygame.time.get_ticks()
76
+ GAME_DURATION = 30 # 遊戲限制時間(秒)
77
+ game_state = "PLAYING" # 狀態機
78
+
79
+ # ============================================
80
+ # 3. 主迴圈
81
+ # ============================================
82
+ running = True
83
+ while running:
84
+ for event in pygame.event.get():
85
+ if event.type == pygame.QUIT:
86
+ running = False
87
+
88
+ # --- 讀取攝影機與手勢偵測 ---
89
+ success, img = cap.read()
90
+ is_pinching = False # 每幀重設狀態,防止手離屏時卡死
91
+
92
+ if success:
93
+ img = cv2.flip(img, 1) # 鏡像翻轉
94
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
95
+
96
+ mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb)
97
+ results = detector.detect(mp_image)
98
+
99
+ # === 【TODO 2:挑戰關卡三之手部安全偵測】 ===
100
+ # 1. 判斷手部是否在畫面中 (results.hand_landmarks 是否有資料)
101
+ # 2. 如果在畫面中,取得食指 (8)、大拇指 (4)、中指 (12) 的像素座標
102
+ # 3. 如果手不在畫面中,將 is_pinching 設為 False
103
+ #
104
+ hand_in_frame = False
105
+ if results.hand_landmarks:
106
+ hand_in_frame = True
107
+ for hand_lms in results.hand_landmarks:
108
+ # 食指 (8)
109
+ lm8 = hand_lms[8]
110
+ finger_x = int(lm8.x * WIDTH)
111
+ finger_y = int(lm8.y * HEIGHT)
112
+
113
+ # 大拇指 (4)
114
+ lm4 = hand_lms[4]
115
+ thumb_x = int(lm4.x * WIDTH)
116
+ thumb_y = int(lm4.y * HEIGHT)
117
+
118
+ # 中指 (12)
119
+ lm12 = hand_lms[12]
120
+ middle_x = int(lm12.x * WIDTH)
121
+ middle_y = int(lm12.y * HEIGHT)
122
+
123
+ # === 【TODO 3:挑戰關卡一之三指捏合判定】 ===
124
+ # 提示:計算食指與大拇指距離,以及中指與大拇指距離
125
+ # 兩組距離皆小於 PINCH_THRESHOLD 時,is_pinching 才為 True
126
+ dist_thumb_index = math.hypot(finger_x - thumb_x, finger_y - thumb_y)
127
+ dist_thumb_middle = math.hypot(middle_x - thumb_x, middle_y - thumb_y)
128
+ is_pinching = (dist_thumb_index < PINCH_THRESHOLD) and (dist_thumb_middle < PINCH_THRESHOLD)
129
+
130
+ dist_thumb_index = math.hypot(finger_x - thumb_x, finger_y - thumb_y)
131
+ dist_thumb_middle = math.hypot(middle_x - thumb_x, middle_y - thumb_y)
132
+ is_pinching = (dist_thumb_index < PINCH_THRESHOLD) and (dist_thumb_middle < PINCH_THRESHOLD)
133
+
134
+ # --- 狀態機邏輯處理 ---
135
+ if game_state == "PLAYING":
136
+ # 1. 計算剩餘時間
137
+ current_ticks = pygame.time.get_ticks()
138
+ elapsed_seconds = (current_ticks - START_TIME) / 1000
139
+ remaining_time = max(0, GAME_DURATION - elapsed_seconds)
140
+
141
+ # 2. 檢查時間是否到了
142
+ if remaining_time <= 0:
143
+ game_state = "GAME_OVER"
144
+
145
+ # 3. 邊緣觸發點擊判斷
146
+ if is_pinching and not was_pinching:
147
+ finger_rect = pygame.Rect(finger_x - 5, finger_y - 5, 10, 10)
148
+ if finger_rect.colliderect(button_rect):
149
+ click_count += 1
150
+ current_color_index = (current_color_index + 1) % len(button_colors)
151
+
152
+ # === 【TODO 4:挑戰關卡二之按鈕隨機移動】 ===
153
+ # 提示:當點擊發生時,讓按鈕的 x 和 y 座標隨機分配在畫面內
154
+
155
+ button_rect.x = random.randint(0, WIDTH - button_rect.width)
156
+ button_rect.y = random.randint(0, HEIGHT - button_rect.height)
157
+ print(f" 魔法三指點擊!第 {click_count} 次")
158
+
159
+ # 更新上一幀狀態
160
+ was_pinching = is_pinching
161
+
162
+ # --- 繪製畫面 ---
163
+ screen.fill((20, 20, 35))
164
+ font = pygame.font.SysFont(None, 32)
165
+ large_font = pygame.font.SysFont(None, 64)
166
+
167
+ if game_state == "PLAYING":
168
+ # 繪製按鈕
169
+ btn_color = button_colors[current_color_index]
170
+ pygame.draw.rect(screen, btn_color, button_rect, border_radius=16)
171
+
172
+ btn_text = font.render(f"Clicks: {click_count}", True, (255, 255, 255))
173
+ screen.blit(btn_text, (button_rect.centerx - btn_text.get_width() // 2,
174
+ button_rect.centery - btn_text.get_height() // 2))
175
+
176
+ # 顯示倒數計時
177
+ time_text = font.render(f"Time Left: {remaining_time:.1f}s", True, (255, 220, 100))
178
+ screen.blit(time_text, (WIDTH - time_text.get_width() - 20, 10))
179
+
180
+ # 畫手指游標
181
+ cursor_color = (50, 255, 50) if is_pinching else (255, 50, 50)
182
+ pygame.draw.circle(screen, cursor_color, (finger_x, finger_y), 12) # 食指
183
+ pygame.draw.circle(screen, (255, 255, 255), (thumb_x, thumb_y), 12) # 大拇指
184
+ pygame.draw.circle(screen, (100, 150, 255), (middle_x, middle_y), 12) # 中指
185
+
186
+ # === 【TODO 5:挑戰關卡三之畫面提示】 ===
187
+ # 提示:當手不在畫面中時,在畫面上方顯示警告文字提醒學生將手部放回
188
+
189
+ if not hand_in_frame:
190
+ warning_text = font.render("OUT OF FRAME - PLEASE PUT HANDS BACK", True, (255, 100, 100))
191
+ screen.blit(warning_text, (WIDTH // 2 - warning_text.get_width() // 2, 100))
192
+
193
+ # 顯示調試數值
194
+ small_font = pygame.font.SysFont(None, 24)
195
+ status = small_font.render(f"Finger: ({finger_x}, {finger_y})", True, (150, 150, 150))
196
+ screen.blit(status, (10, 10))
197
+
198
+ dist_text = small_font.render(f"Dist Thumb-Index: {dist_thumb_index:.0f} px", True, (150, 150, 200))
199
+ screen.blit(dist_text, (10, 35))
200
+
201
+ dist_text2 = small_font.render(f"Dist Thumb-Middle: {dist_thumb_middle:.0f} px", True, (150, 150, 200))
202
+ screen.blit(dist_text2, (10, 60))
203
+
204
+ elif game_state == "GAME_OVER":
205
+ # 繪製遊戲結束畫面
206
+ over_text = large_font.render("GAME OVER", True, (255, 50, 50))
207
+ score_text = font.render(f"Total Clicks: {click_count} times", True, (255, 255, 255))
208
+
209
+ screen.blit(over_text, (WIDTH // 2 - over_text.get_width() // 2, HEIGHT // 2 - 50))
210
+ screen.blit(score_text, (WIDTH // 2 - score_text.get_width() // 2, HEIGHT // 2 + 20))
211
+
212
+ pygame.display.update()
213
+ clock.tick(30)
214
+
215
+ cap.release()
216
+ pygame.quit()
217
+ sys.exit()
projects/2026_AI營1/劉宗穎/人脸辨识/d3_2_face_detection.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # d3_2_face_detection.py
2
+ # Day 3: 電腦視覺與影像辨識 - Haar Cascade 即時人臉偵測與相機串流 (學員練習版)
3
+ import cv2
4
+ import os
5
+ import urllib.request
6
+
7
+ # 1. 取得 Haar Cascade 臉部特徵分類器模型檔案
8
+ XML_FILE = "haarcascade_frontalface_default.xml"
9
+ XML_URL = (
10
+ "https://raw.githubusercontent.com/opencv/opencv/master/"
11
+ "data/haarcascades/haarcascade_frontalface_default.xml"
12
+ )
13
+
14
+ # 優先使用 OpenCV 內建路徑,其次本機檔案,最後才下載
15
+
16
+
17
+ def load_face_cascade():
18
+ bundled = os.path.join(cv2.data.haarcascades,
19
+ "haarcascade_frontalface_default.xml")
20
+ if os.path.exists(bundled):
21
+ print(f"使用 OpenCV 內建分類器:{bundled}")
22
+ return cv2.CascadeClassifier(bundled)
23
+
24
+ if not os.path.exists(XML_FILE):
25
+ print("未檢測到臉部特徵分類模型檔,正在從 OpenCV 官方來源下載...")
26
+ try:
27
+ urllib.request.urlretrieve(XML_URL, XML_FILE)
28
+ print("分類器下載完成!")
29
+ except Exception as e:
30
+ print(f"[錯誤] 下載模型檔失敗:{e}")
31
+ print("請手動下載 haarcascade_frontalface_default.xml 並置於同目錄下。")
32
+ exit(1)
33
+
34
+ cascade = cv2.CascadeClassifier(XML_FILE)
35
+ if cascade.empty():
36
+ print("[錯誤] 分類器載入失敗,XML 可能損毀。")
37
+ exit(1)
38
+ return cascade
39
+
40
+
41
+ face_cascade = load_face_cascade()
42
+
43
+ # 2. 啟動視訊鏡頭 (0 代表預設鏡頭)
44
+ # 無鏡頭時可改為影片:cv2.VideoCapture("test_video.mp4")
45
+ print("正在開啟視訊鏡頭,請稍候...")
46
+ cap = cv2.VideoCapture(0)
47
+
48
+ if not cap.isOpened():
49
+ print("[錯誤] 無法開啟視訊鏡頭!請確認鏡頭權限已開啟且未被其他程式佔用。")
50
+ print("備案:將 VideoCapture(0) 改為 VideoCapture(\"test_video.mp4\") 使用影片測試。")
51
+ exit(1)
52
+
53
+ print("\n視訊串流已啟動!")
54
+ print("提示:點擊視窗後,按下鍵盤上的 'q' 鍵即可退出程式。")
55
+ print("-" * 50)
56
+
57
+ while True:
58
+ ret, frame = cap.read()
59
+ if not ret:
60
+ print("[警告] 無法讀取鏡頭畫面,正在重新嘗試...")
61
+ continue
62
+
63
+ # === 【重要觀念 TODO 1:水平翻轉鏡頭畫面】 ===
64
+ # 觀念重點:預設讀取的鏡頭畫面是左右相反的(像看著別人)。
65
+ # 使用 cv2.flip(frame, 1) 可以將畫面水平翻轉,使其符合我們照鏡子的視覺習慣。
66
+ # 請解開下方註釋 (刪除行首的 #) 並執行,觀察鏡頭畫面的翻轉變化。
67
+ frame = cv2.flip(frame, 1)
68
+
69
+ # 預設使用原始畫面,避免未解開註釋時程式出錯
70
+ gray = frame
71
+
72
+ # === 【重要觀念 TODO 2:灰階轉換 (色彩空間轉換)】 ===
73
+ # 觀念重點:人臉特徵偵測 (如 Haar Cascade) 主要依賴亮度與對比資訊,不需要色彩資訊。
74
+ # 將影像轉換為灰階 (BGR to GRAY) 可以減少 2/3 的資料量,大幅提升偵測速度並減少干擾。
75
+ # 請解開下方註釋 (刪除行首的 #),將畫面轉換為灰階。
76
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
77
+
78
+ # 預設無偵測到人臉,避免未解開註釋時程式出錯
79
+ faces = []
80
+
81
+ # === 【重要觀念 TODO 3:使用 Haar Cascade 分類器進行人臉偵測】 ===
82
+ # 觀念重點:detectMultiScale 會在影像中尋找臉部,回傳每張臉的 (x, y, w, h) 座標。
83
+ # - scaleFactor=1.1: 每次搜尋時將影像縮小 10%,用來偵測不同大小(距離)的人臉。
84
+ # - minNeighbors=5: 判定同一區域為人臉的鄰近候選框數量,數值愈高愈嚴格,能減少誤判。
85
+ # - minSize=(60, 60): 忽略小於此尺寸的偵測框,避免遠處背景雜訊干擾。
86
+ # 請解開下方所有註釋 (刪除行首的 #) 來啟用人臉偵測功能。
87
+ faces = face_cascade.detectMultiScale(
88
+ gray,
89
+ scaleFactor=1.1,
90
+ minNeighbors=1,
91
+ minSize=(60, 60),
92
+ )
93
+
94
+ # === 【重要觀念 TODO 4:繪製人臉偵測外框與文字標記】 ===
95
+ # 觀念重點:當模型找出人臉 (x, y, w, h) 之後,我們可以在畫面上把結果畫出来。
96
+ # - cv2.rectangle: 畫矩形,需要指定左上角 (x, y) 與右下角 (x+w, y+h)、BGR 顏色與框線粗細。
97
+ # - cv2.putText: 寫文字,常用於分類結果或標記名稱。
98
+ # 請解開下方所有註釋 (刪除行首的 #),將偵測到的結果畫在畫面上。
99
+ for (x, y, w, h) in faces:
100
+ cv2.circle(frame, (x + w // 2, y + h // 2), 15, (0, 0, 255), -1)
101
+ cv2.rectangle(frame, (x, y), (x + w, y + h), (255, 100, 0), 3)
102
+ cv2.putText(
103
+ frame, "a big idiot", (x, y - 10),
104
+ cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 100, 0), 2,
105
+ )
106
+
107
+ # 畫面左上角顯示目前狀態:有抓到臉就顯示數量,否則顯示搜尋中
108
+ if len(faces) > 0:
109
+ cv2.putText(
110
+ frame, f"Faces Detected: {len(faces)}", (20, 40),
111
+ cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2,
112
+ )
113
+ else:
114
+ cv2.putText(
115
+ frame, "Searching Face...", (20, 40),
116
+ cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2,
117
+ )
118
+
119
+ # 把本回合處理後的影格送到視窗,形成即時串流效果
120
+ cv2.imshow("Smart Face Detection Camera", frame)
121
+
122
+ # waitKey(1) 既檢測按鍵,也驅動視窗重繪
123
+ if cv2.waitKey(1) & 0xFF == ord('q'):
124
+ break
125
+
126
+ cap.release()
127
+ cv2.destroyAllWindows()
128
+ print("視訊鏡頭已關閉,資源已安全釋放。")
projects/2026_AI營1/劉宗穎/人脸辨识/d3_2_face_detection2.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # d3_2_face_detection.py
2
+ # Day 3: 電腦視覺與影像辨識 - Haar Cascade 即時人臉偵測與相機串流 (學員練習版)
3
+ import cv2
4
+ import os
5
+ import urllib.request
6
+
7
+ # 1. 取得 Haar Cascade 臉部特徵分類器模型檔案
8
+ XML_FILE = "haarcascade_frontalface_default.xml"
9
+ XML_URL = (
10
+ "https://raw.githubusercontent.com/opencv/opencv/master/"
11
+ "data/haarcascades/haarcascade_frontalface_default.xml"
12
+ )
13
+
14
+ # 優先使用 OpenCV 內建路徑,其次本機檔案,最後才下載
15
+
16
+
17
+ def load_face_cascade():
18
+ bundled = os.path.join(cv2.data.haarcascades,
19
+ "haarcascade_frontalface_default.xml")
20
+ if os.path.exists(bundled):
21
+ print(f"使用 OpenCV 內建分類器:{bundled}")
22
+ return cv2.CascadeClassifier(bundled)
23
+
24
+ if not os.path.exists(XML_FILE):
25
+ print("未檢測到臉部特徵分類模型檔,正在從 OpenCV 官方來源下載...")
26
+ try:
27
+ urllib.request.urlretrieve(XML_URL, XML_FILE)
28
+ print("分類器下載完成!")
29
+ except Exception as e:
30
+ print(f"[錯誤] 下載模型檔失敗:{e}")
31
+ print("請手動下載 haarcascade_frontalface_default.xml 並置於同目錄下。")
32
+ exit(1)
33
+
34
+ cascade = cv2.CascadeClassifier(XML_FILE)
35
+ if cascade.empty():
36
+ print("[錯誤] 分類器載入失敗,XML 可能損毀。")
37
+ exit(1)
38
+ return cascade
39
+
40
+
41
+ face_cascade = load_face_cascade()
42
+
43
+ # 2. 啟動視訊鏡頭 (0 代表預設鏡頭)
44
+ # 無鏡頭時可改為影片:cv2.VideoCapture("test_video.mp4")
45
+ print("正在開啟視訊鏡頭,請稍候...")
46
+ cap = cv2.VideoCapture(0)
47
+
48
+ if not cap.isOpened():
49
+ print("[錯誤] 無法開啟視訊鏡頭!請確認鏡頭權限已開啟且未被其他程式佔用。")
50
+ print("備案:將 VideoCapture(0) 改為 VideoCapture(\"test_video.mp4\") 使用影片測試。")
51
+ exit(1)
52
+
53
+ print("\n視訊串流已啟動!")
54
+ print("提示:點擊視窗後,按下鍵盤上的 'q' 鍵即可退出程式。")
55
+ print("-" * 50)
56
+
57
+ while True:
58
+ ret, frame = cap.read()
59
+ if not ret:
60
+ print("[警告] 無法讀取鏡頭畫面,正在重新嘗試...")
61
+ continue
62
+
63
+ # === 【重要觀念 TODO 1:水平翻轉鏡頭畫面】 ===
64
+ # 觀念重點:預設讀取的鏡頭畫面是左右相反的(像看著別人)。
65
+ # 使用 cv2.flip(frame, 1) 可以將畫面水平翻轉,使其符合我們照鏡子的視覺習慣。
66
+ # 請解開下方註釋 (刪除行首的 #) 並執行,觀察鏡頭畫面的翻轉變化。
67
+ frame = cv2.flip(frame, 1)
68
+
69
+ # 預設使用原始畫面,避免未解開註釋時程式出錯
70
+ gray = frame
71
+
72
+ # === 【重要觀念 TODO 2:灰階轉換 (色彩空間轉換)】 ===
73
+ # 觀念重點:人臉特徵偵測 (如 Haar Cascade) 主要依賴亮度與對比資訊,不需要色彩資訊。
74
+ # 將影像轉換為灰階 (BGR to GRAY) 可以減少 2/3 的資料量,大幅提升偵測速度並減少干擾。
75
+ # 請解開下方註釋 (刪除行首的 #),將畫面轉換為灰階。
76
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
77
+
78
+ # 預設無偵測到人臉,避免未解開註釋時程式出錯
79
+ faces = []
80
+
81
+ # === 【重要觀念 TODO 3:使用 Haar Cascade 分類器進行人臉偵測】 ===
82
+ # 觀念重點:detectMultiScale 會在影像中尋找臉部,回傳每張臉的 (x, y, w, h) 座標。
83
+ # - scaleFactor=1.1: 每次搜尋時將影像縮小 10%,用來偵測不同大小(距離)的人臉。
84
+ # - minNeighbors=5: 判定同一區域為人臉的鄰近候選框數量,數值愈高愈嚴格,能減少誤判。
85
+ # - minSize=(60, 60): 忽略小於此尺寸的偵測框,避免遠處背景雜訊干擾。
86
+ # 請解開下方所有註釋 (刪除行首的 #) 來啟用人臉偵測功能。
87
+ faces = face_cascade.detectMultiScale(
88
+ gray,
89
+ scaleFactor=1.1,
90
+ minNeighbors=5,
91
+ minSize=(60, 60),
92
+ )
93
+
94
+ # === 【重要觀念 TODO 4:繪製人臉偵測外框與文字標記】 ===
95
+ # 觀念重點:當模型找出人臉 (x, y, w, h) 之後,我們可以在畫面上把結果畫出来。
96
+ # - cv2.rectangle: 畫矩形,需要指定左上角 (x, y) 與右下角 (x+w, y+h)、BGR 顏色與框線粗細。
97
+ # - cv2.putText: 寫文字,常用於分類結果或標記名稱。
98
+ # 請解開下方所有註釋 (刪除行首的 #),將偵測到的結果畫在畫面上。
99
+ for (x, y, w, h) in faces:
100
+ #1. 擷取人臉區域 (Slice)
101
+ face_roi = frame[y:y+h, x:x+w]
102
+
103
+ #2. 縮小人臉圖像 (例如縮小到 15x15 像素
104
+ small = cv2.resize(face_roi, (15, 15), interpolation=cv2.INTER_LINEAR)
105
+
106
+ #3. 放大回原本的大小 (使用 NEAREST 內插法保留顆粒感)
107
+ mosaic = cv2.resize(small, (w, h), interpolation=cv2.INTER_NEAREST)
108
+
109
+ #4. 放回原畫面中
110
+ frame[y:y+h, x:x+w] = mosaic
111
+
112
+ cv2.rectangle(frame, (x, y), (x + w, y + h), (255, 100, 0), 3)
113
+ cv2.putText(
114
+ frame, "dumboo dumboo", (x, y - 10),
115
+ cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 100, 0), 2,
116
+ )
117
+
118
+ # 畫面左上角顯示目前狀態:有抓到臉就顯示數量,否則顯示搜尋中
119
+ if len(faces) > 0:
120
+ cv2.putText(
121
+ frame, f"Faces Detected: {len(faces)}", (20, 40),
122
+ cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2,
123
+ )
124
+ else:
125
+ cv2.putText(
126
+ frame, "Searching Face...", (20, 40),
127
+ cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2,
128
+ )
129
+
130
+ # 把本回合處理後的影格送到視窗,形成即時串流效果
131
+ cv2.imshow("Smart Face Detection Camera", frame)
132
+
133
+ # waitKey(1) 既檢測按鍵,也驅動視窗重繪
134
+ if cv2.waitKey(1) & 0xFF == ord('q'):
135
+ break
136
+
137
+ cap.release()
138
+ cv2.destroyAllWindows()
139
+ print("視訊鏡頭已關閉,資源已安全釋放。")
projects/{DEMO/Student1/視力護眼衛士/face_landmarker.task → 2026_AI營1/劉宗穎/咖啡店分析图表/coffee_sales_analysis.png} RENAMED
File without changes
projects/2026_AI營1/劉宗穎/咖啡店分析图表/coffee_sales_data.csv ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Date,Sales_Amount,Customer_Count,Coffee_Cups_Sold,Dessert_Sold,Promotion_Active
2
+ 2026-07-01,15200,120,85,30,No
3
+ 2026-07-02,16800,135,95,35,No
4
+ 2026-07-03,24500,190,140,55,Yes
5
+ 2026-07-04,28000,210,165,68,Yes
6
+ 2026-07-05,18500,145,110,40,No
7
+ 2026-07-06,14200,110,80,25,No
8
+ 2026-07-07,15900,125,90,28,No
9
+ 2026-07-08,17200,138,100,32,No
10
+ 2026-07-09,26000,205,150,60,Yes
11
+ 2026-07-10,29500,225,175,75,Yes
12
+ 2026-07-11,21000,160,120,45,No
13
+ 2026-07-12,19800,150,115,42,No
14
+ 2026-07-13,15000,118,85,26,No
15
+ 2026-07-14,16500,130,92,30,No
projects/2026_AI營1/劉宗穎/咖啡店分析图表/d1_4_visualization_coffee.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # d1_4_visualization_coffee.py
2
+ # Day 1: 資料科學與數據視覺化 - 咖啡廳營運數據進階分析(學員填空)
3
+ # cd examples/Day1/student && python3 d1_4_visualization_coffee.py
4
+ # 參考答案:../Teacher/d1_4_visualization_coffee.py
5
+
6
+ import os
7
+ import pandas as pd
8
+ import matplotlib.pyplot as plt
9
+
10
+
11
+ def load_csv(filename="coffee_sales_data.csv"):
12
+ """安全載入 CSV 的輔助函式,用以防範在不同執行路徑下相對路徑讀檔失敗的問題"""
13
+ # === 【學員填空練習點 1:CSV 檔案安全讀取】 ===
14
+ # TODO: 1. 使用 os.path.join 將當前腳本所在的目錄與檔案名稱拼接成絕對路徑
15
+ # 2. 使用 os.path.exists 判斷路徑是否存在,若存在則用 pd.read_csv 讀取並回傳
16
+ path = os.path.join(os.path.dirname(__file__), filename)
17
+ if os.path.exists(path):
18
+ return pd.read_csv(path)
19
+ else:
20
+ print("❌ 找不到 CSV 檔案!請確認 coffee_sales_data.csv 與本腳本在同一目錄。")
21
+ exit(1)
22
+
23
+
24
+ df = load_csv()
25
+
26
+ # 設定中文字型與負號顯示防禦,避免圖表出現亂碼方塊
27
+ plt.rcParams['font.sans-serif'] = [
28
+ 'PingFang TC', 'Heiti TC', 'Arial Unicode MS',
29
+ 'Microsoft JhengHei', 'Noto Sans CJK TC', 'SimHei',
30
+ ]
31
+ plt.rcParams['axes.unicode_minus'] = False
32
+
33
+ # === 【學員填空練習點 2:建立子圖畫布 (Subplots)】 ===
34
+ # TODO: 使用 plt.subplots 建立一個 1 列 2 行 (1x2) 的子圖畫布,尺寸大小為寬 15 英吋、高 6 英吋
35
+ # 並指派給 fig 與 (ax1, ax2)
36
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
37
+
38
+
39
+ # ==========================================
40
+ # 1. 子圖 1 (ax1):營業額與咖啡銷量趨勢對比 (雙 Y 軸)
41
+ # ==========================================
42
+ # === 【學員填空練習點 3:繪製雙 Y 軸折線圖】 ===
43
+ # TODO:
44
+ # 1. 在 ax1 (左軸) 繪製「營業額」折線圖:X軸為 df['Date'], Y軸為 df['Sales_Amount']
45
+ # 設定參數:點形 marker='o', 顏色 color='#b87e6b' (鐵鏽紅), 圖例 label='營業額 (NTD)', 線寬 linewidth=2
46
+ # 2. 呼叫 ax1.twinx() 建立共用 X 軸的右側 Y 軸物件,指派給 ax1_twin
47
+ # 3. 在 ax1_twin (右軸) 繪製「咖啡銷量」折線圖:X軸為 df['Date'], Y軸為 df['Coffee_Cups_Sold']
48
+ # 設定參數:點形 marker='s' (正方形), 顏色 color='#5f7186' (石板藍), 虛線 linestyle='--', 圖例 label='咖啡銷量 (杯)'
49
+ # 4. 分別設定左軸與右軸的 Y 軸標籤與顏色以示區隔 (提示:使用 set_ylabel 與 tick_params)
50
+
51
+ # 請在下方完成 ax1 雙 Y 軸折線圖的繪製:
52
+ line1 = ax1.plot(df['Date'], df['Sales_Amount'], marker='o', color="#640f91", label='营业额 (NTD)', linewidth=2 )
53
+ ax1_twin = ax1.twinx()
54
+ line2 = ax1_twin.plot(df['Date'], df['Coffee_Cups_Sold'], marker='s', color= "#19077D", linestyle='--', label='咖啡销量 (杯)', linewidth=2)
55
+
56
+ ax1.set_ylabel("营业额 (NTD)", color='#640f91', fontsize=12)
57
+ ax1_twin.set_ylabel("咖啡销量 (杯)", color='#19077D', fontsize=12)
58
+ ax1.tick_params(axis='y', labelcolor='#640f91')
59
+ ax1_twin.tick_params(axis='y', labelcolor='#19077D')
60
+
61
+ # === 【學員填空練習點 4:雙 Y 軸圖例合併】 ===
62
+ # TODO:
63
+ # 1. 由於雙 Y 軸屬於不同的 Axes 物件,若直接呼叫 ax1.legend() 只會顯示左軸的圖例。
64
+ # 請獲取兩軸的 lines 與 labels,合併後統一顯示在 ax1 的左上角 (loc='upper left')
65
+ # 提示:
66
+ # lines1, labels1 = ax1.get_legend_handles_labels()
67
+ # lines2, labels2 = ax1_twin.get_legend_handles_labels()
68
+ # ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left')
69
+
70
+ # 請在下方完成圖例合併:
71
+ lines = line1 + line2
72
+ labels = [l.get_label() for l in lines]
73
+ ax1.legend(lines, labels, loc='upper left')
74
+
75
+ # 設定 ax1 基礎格式
76
+ ax1.set_title("咖啡廳每日營業額與咖啡銷量趨勢", fontsize=14, fontweight='bold')
77
+ ax1.set_xlabel("日期")
78
+ ax1.grid(True, linestyle='--', alpha=0.5)
79
+ ax1.tick_params(axis='x', rotation=45)
80
+
81
+
82
+ # ==========================================
83
+ # 2. 子圖 2 (ax2):促銷活動對客流量的影響 (數據聚合長條圖)
84
+ # ==========================================
85
+ # === 【學員填空練習點 5:數據聚合與長條圖繪製】 ===
86
+ # TODO:
87
+ # 1. 使用 groupby 依據 'Promotion_Active' 欄位分組,並計算 'Customer_Count' 的平均值 (mean)
88
+ # 提示:promo_avg = df.groupby('Promotion_Active')['Customer_Count'].mean()
89
+ # 2. 在 ax2 上繪製長條圖,以 promo_avg.index 作為 X 軸,promo_avg.values 作為 Y 軸
90
+ # 設定參數:顏色 color=['#e6e2d8', '#b87e6b'],外框顏色 edgecolor='#3d3a36',寬度 width=0.5
91
+ # 3. 在長條圖上方顯示具體的平均數值 (提示:使用 ax2.bar_label 或是跑迴圈加文字)
92
+
93
+ # 請在下方完成資料聚合與長條圖繪製:
94
+ promo_avg = df.groupby('Promotion_Active')['Customer_Count'].mean()
95
+ bars = ax2.bar(promo_avg.index, promo_avg.values, color=["#15c81e", "#1a54f3"], edgecolor="#bd10cd", width=0.5)
96
+ ax2.bar_label(bars,fmt='%.1f 人', label_type='edge', padding=3, fontweight='bold')
97
+
98
+ # 設定 ax2 基礎格式
99
+ ax2.set_title("促銷活動對每日平均客流量的影響", fontsize=14, fontweight='bold')
100
+ ax2.set_xlabel("是否舉辦促銷活動")
101
+ ax2.set_ylabel("平均來客數 (人)")
102
+ ax2.grid(True, axis='y', linestyle='--', alpha=0.5)
103
+
104
+
105
+ plt.tight_layout() # 自動調整間距,避免文字重疊
106
+
107
+ # === 【學員填空練習點 6:圖表儲存與顯示】 ===
108
+ # TODO: 1. 呼叫 savefig 將畫布儲存為檔名 "coffee_sales_analysis.png",並設定 dpi=300
109
+ # 2. 呼叫 show 顯示圖表
110
+ # (⚠️ 注意:不要在 print 或 savefig 內使用不支援 cp950 的 emoji)
111
+
112
+ output_image = "coffee_sales_analysis.png"
113
+ # 請在下方完成圖表存檔與顯示:
114
+ plt.savefig(output_image, dpi=300)
115
+ plt.show()
projects/2026_AI營1/劉宗穎/嘴巴侦测/step3_mouth_ratio.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Week 11 - Step 2: Mouth Opening Ratio Detection (Template 填空版)
3
+ =============================================================
4
+ 本步驟目標:
5
+ 1. 找出特定的臉部特徵點:眼角(33, 263)與嘴唇(13, 14)
6
+ 2. 計算兩眼距離,當作臉寬基準比例尺,防止人靠近或遠離鏡頭造成的誤判 (歸一化)
7
+ 3. 計算嘴巴張開比例:嘴巴距離 / 兩眼距離
8
+ 4. 在 OpenCV 畫面上即時顯示比例與嘴巴開合狀態 (OPEN / CLOSED)
9
+ """
10
+ import cv2
11
+ import mediapipe as mp
12
+ import math
13
+ import sys
14
+ import os
15
+
16
+ # 1. 初始化攝影機
17
+ cap = cv2.VideoCapture(0)
18
+ if not cap.isOpened():
19
+ print("Error: Could not open camera.")
20
+ sys.exit()
21
+
22
+ # 2. 載入 MediaPipe 臉部偵測器模型 (Face Landmarker)
23
+ model_path = os.path.join('face_landmarker.task')
24
+ if not os.path.exists(model_path):
25
+ print(f"Error: Model file not found at {model_path}")
26
+ sys.exit()
27
+
28
+ try:
29
+ from mediapipe.tasks import python
30
+ from mediapipe.tasks.python import vision
31
+ base_options = python.BaseOptions(model_asset_path=model_path)
32
+ options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1)
33
+ detector = vision.FaceLandmarker.create_from_options(options)
34
+ print("Face Landmarker initialized successfully.")
35
+ except Exception as e:
36
+ print(f"Error: Failed to initialize detector: {e}")
37
+ sys.exit()
38
+
39
+ # 特徵點索引定義
40
+ LEFT_EYE_OUTER = 33
41
+ RIGHT_EYE_OUTER = 263
42
+ LIP_TOP = 13
43
+ LIP_BOTTOM = 14
44
+
45
+ # 嘴巴張開的比例閾值 (超過代表張開)
46
+ THRESHOLD = 0.25
47
+
48
+ print("Press 'q' in the window to exit.")
49
+
50
+ # 3. 主迴圈
51
+ while True:
52
+ success, img = cap.read()
53
+ if not success:
54
+ break
55
+
56
+ img = cv2.flip(img, 1)
57
+ img_h, img_w, _ = img.shape
58
+
59
+ # 影像處理
60
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
61
+ mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb)
62
+ results = detector.detect(mp_image)
63
+
64
+ if results.face_landmarks:
65
+ face_lms = results.face_landmarks[0]
66
+
67
+ # 【TODO 1】取得這 4 個特徵點在畫面中的 (x, y) 像素座標,並存在 pts 字典中
68
+ pts = {}
69
+ for idx in [LEFT_EYE_OUTER, RIGHT_EYE_OUTER, LIP_TOP, LIP_BOTTOM]:
70
+ pt = face_lms[idx]
71
+ pts[idx] = (int(pt.x * img_w), int(pt.y * img_h))
72
+
73
+
74
+ # 【TODO 2】計算兩眼外角距離 (做為基準比例尺)
75
+ p_lx, p_ly = pts[LEFT_EYE_OUTER]
76
+ p_rx, p_ry = pts[RIGHT_EYE_OUTER]
77
+ base_dist = math.hypot(p_rx - p_lx, p_ry - p_ly)
78
+
79
+
80
+ if base_dist > 0:
81
+ # 【TODO 3】計算嘴巴張開的實際像素距離,並除以眼距算出「嘴巴張開比例 (mouth_ratio)」
82
+ p_top_x, p_top_y = pts[LIP_TOP]
83
+ p_bot_x, p_bot_y = pts[LIP_BOTTOM]
84
+ mouth_dist = math.hypot(p_bot_x - p_top_x, p_bot_y - p_top_y)
85
+ mouth_ratio = mouth_dist / base_dist
86
+
87
+
88
+ # 【TODO 4】判斷嘴巴狀態是否大於 THRESHOLD,並在畫面上繪製量測輔助線
89
+ # 提示:用 cv2.line 畫出眼部基準線 (藍色) 和嘴巴測量線 (橘色)
90
+ is_open = mouth_ratio > THRESHOLD
91
+ cv2.line(img, (p_lx, p_ly), (p_rx, p_ry), (255, 0, 0), 2) # 藍色眼線
92
+ cv2.line(img, (p_top_x, p_top_y), (p_bot_x, p_bot_y), (0, 165, 255), 2) # 橘色嘴線
93
+
94
+
95
+ # 畫出特徵點小黃圓圈
96
+ for idx, pos in pts.items():
97
+ cv2.circle(img, pos, 5, (0, 255, 255), -1)
98
+
99
+ # 顯示比例與開合狀態文字
100
+ cv2.putText(img, f"Mouth Ratio: {mouth_ratio:.2f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
101
+ status_text = "OPEN" if is_open else "CLOSED"
102
+ status_color = (0, 255, 0) if is_open else (0, 0, 255)
103
+ cv2.putText(img, f"State: {status_text}", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, status_color, 2)
104
+ else:
105
+ cv2.putText(img, "No Face Detected", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
106
+
107
+ cv2.imshow("Mouth Ratio Detector - Press 'q' to quit", img)
108
+
109
+ if cv2.waitKey(1) & 0xFF == ord('q'):
110
+ break
111
+
112
+ cap.release()
113
+ cv2.destroyAllWindows()
projects/{DEMO/Student3/手勢特徵分類/hand_landmarker.task → 2026_AI營1/劉宗穎/处理过的照片/output_contours.jpg} RENAMED
File without changes
projects/2026_AI營1/劉宗穎/处理过的照片/output_edges.jpg ADDED

Git LFS Details

  • SHA256: 8625e1b5fb07bd6acd2165c794e6bfac0367111413a546acdd331cef340b2640
  • Pointer size: 131 Bytes
  • Size of remote file: 200 kB
projects/2026_AI營1/劉宗穎/处理过的照片/output_gray.jpg ADDED

Git LFS Details

  • SHA256: 85375871beed2bf92c34132701aa4677a849d43214d4d8292c8e3f78298e5165
  • Pointer size: 131 Bytes
  • Size of remote file: 135 kB
projects/2026_AI營1/劉宗穎/天气资料图表/d1_3_visualization.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # d1_3_visualization.py
2
+ # Day 1: 資料科學與數據視覺化 - Matplotlib 繪圖基礎(學員填空)
3
+ # cd examples/Day1/student && python3 d1_3_visualization.py
4
+ # 參考答案:../Teacher/d1_3_visualization.py
5
+
6
+ import os
7
+ import pandas as pd
8
+ import matplotlib.pyplot as plt
9
+
10
+ def load_csv(filename="taipei_weather_2026.csv"):
11
+ """安全載入 CSV 的輔助函式,用以防範在不同執行路徑下相對路徑讀檔失敗的問題"""
12
+ # === 【學員填空練習點 1:CSV 檔案安全讀取】 ===
13
+ # TODO: 1. 使用 os.path.join 將當前腳本所在的目錄與檔案名稱拼接成絕對路徑
14
+ # 提示: 當前腳本目錄為 os.path.dirname(__file__)
15
+ # 2. 使用 os.path.exists 判斷路徑是否存在,若存在則用 pd.read_csv 讀取並回傳
16
+ path = os.path.join(os.path.dirname(__file__), filename)
17
+
18
+ if os.path.exists(path):
19
+ return pd.read_csv(path)
20
+ else:
21
+ print("error: no files found")
22
+
23
+ df = load_csv()
24
+
25
+ # 設定中文字型與負號顯示防禦,避免圖表出現亂碼方塊
26
+ plt.rcParams['font.sans-serif'] = [
27
+ 'PingFang TC', 'Heiti TC', 'Arial Unicode MS',
28
+ 'Microsoft JhengHei', 'Noto Sans CJK TC', 'SimHei',
29
+ ]
30
+ plt.rcParams['axes.unicode_minus'] = False
31
+
32
+ # === 【學員填空練習點 2:建立子圖畫布 (Subplots)】 ===
33
+ # TODO: 使用 plt.subplots 建立一個 2 列 1 行 (2x1) 的子圖畫布,尺寸大小為寬 10 英吋、高 8 英吋
34
+ # 並指派給 fig 與 (ax1, ax2)
35
+ fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))
36
+
37
+ # 1. 繪製折線圖 (Line Plot) - 每日溫度
38
+ # === 【學員填空練習點 3:繪製每日溫度折線圖】 ===
39
+ # TODO: 1. 在 ax1 畫布上以 ax1.plot() 繪製折線圖,X軸傳入 df['Date'],Y軸傳入 df['Temperature']
40
+ # 設定參數:帶有圓點 marker='o',顏色 color='crimson',圖例 label='溫度 (°C)',線寬 linewidth=2
41
+ # 2. 使用 ax1.set_title 設定標題 "台北觀測站每日溫度趨勢圖"
42
+ # 3. 設定 ax1.set_xlabel("日期") 與 ax1.set_ylabel("數值")
43
+ # 4. 設定網格 ax1.grid(True, linestyle='--', alpha=0.5)
44
+ # 5. 呼叫 ax1.legend() 顯示圖例
45
+ # 6. 設定 X軸刻度旋轉 ax1.tick_params(axis='x', rotation=45)
46
+ #
47
+ # 請在下方完成 ax1 的溫度折線圖繪製:
48
+ ax1.plot(df['Date'], df['Temperature'], marker='o', color= 'crimson',label='温度 (°C)', linewidth=2)
49
+ ax1.set_title("台北观测站每日温度趋势图", fontsize=14, fontweight='bold')
50
+ ax1.set_xlabel("日期")
51
+
52
+ ax1.set_ylabel("数值")
53
+ ax1.grid(True,linestyle="--", alpha=0.5)
54
+ ax1.legend()
55
+ ax1.tick_params(axis='x', rotation=45)
56
+ ax1.plot(df['Date'], df['Humidity'], marker='o', color='lime', label='湿度 (%)', linewidth=2)
57
+ ax1.legend()
58
+
59
+ # 2. 繪製條形圖 (Bar Plot) - 每日降雨量
60
+ # === 【學員填空練習點 4:繪製每日降雨量長條圖】 ===
61
+ # TODO: 1. 在 ax2 畫布上以 ax2.bar() 繪製長條圖,X軸傳入 df['Date'],Y軸傳入 df['Rainfall']
62
+ # 設定參數:顏色 color='skyblue',外框顏色 edgecolor='royalblue',圖例 label='降雨量 (mm)'
63
+ # 2. 使用 ax2.set_title 設定標題 "台北觀測站每日降雨量分佈"
64
+ # 3. 設定 ax2.set_xlabel("日期") 與 ax2.set_ylabel("降雨量 (mm)")
65
+ # 4. 設定 Y 軸網格 ax2.grid(True, axis='y', linestyle='--', alpha=0.5)
66
+ # 5. 呼叫 ax2.legend() 顯示圖例
67
+ # 6. 設定 X軸刻度旋轉 ax2.tick_params(axis='x', rotation=45)
68
+ #
69
+ # 請在下方完成 ax2 的降雨量長條圖繪製:
70
+ ax2.bar(df['Date'], df['Rainfall'], color='turquoise',edgecolor = 'turquoise',label='降雨量 (mm)')
71
+ ax2.set_title("台北每天降雨量长条图", fontsize=14, fontweight='bold')
72
+ ax2.set_xlabel("日期")
73
+ ax2.set_ylabel("降雨量")
74
+ ax2.grid(True, axis='y', linestyle='--', alpha=0.5)
75
+ ax2.legend()
76
+ ax2.tick_params(axis='x', rotation=45)
77
+
78
+ plt.tight_layout() # 自動調整間距,避免文字重疊
79
+
80
+ # === 【學員填空練習點 5:圖表自動儲存與顯示】 ===
81
+ # TODO: 1. 呼叫 plt.savefig 將畫布儲存為檔名 "taipei_weather_2026.png",並設定高畫質 dpi=300
82
+ # (⚠️ 注意:savefig 必須在 show() 之前呼叫)
83
+ # 2. 呼叫 plt.show() 將圖表視窗顯示在畫面上
84
+ #
85
+ # 請在下方完成圖表存檔與顯示:
86
+ plt.savefig("taipei_weather_2026.png", dpi=300)
87
+ plt.show()
projects/2026_AI營1/劉宗穎/天气资料图表/taipei_weather_2026.csv ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Date,Temperature,Humidity,Rainfall
2
+ 2026-07-01,28.5,82,5.2
3
+ 2026-07-02,29.0,80,0.0
4
+ 2026-07-03,30.2,75,0.0
5
+ 2026-07-04,31.5,70,0.0
6
+ 2026-07-05,27.8,90,25.4
7
+ 2026-07-06,26.5,95,42.0
8
+ 2026-07-07,28.0,85,12.5
9
+ 2026-07-08,29.5,78,0.0
10
+ 2026-07-09,30.8,72,0.0
11
+ 2026-07-10,32.0,68,0.0
12
+ 2026-07-11,28.2,88,18.0
13
+ 2026-07-12,27.0,92,30.5
14
+ 2026-07-13,29.2,80,2.1
15
+ 2026-07-14,30.0,76,0.0
16
+ 2026-07-15,31.2,71,0.0
projects/2026_AI營1/劉宗穎/影像处理/d3_1_opencv_basic.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # d3_1_opencv_basic.py
2
+ # Day 3: 電腦視覺與影像辨識 - OpenCV 影像處理基礎 (學員練習版)
3
+ # 練習指引:依序找到程式碼中的 TODO 1 到 TODO 5,將其下方的程式碼「解除註釋(刪除行首的 # )」並執行,觀察影像處理管線中每一步的變化。
4
+
5
+ import cv2
6
+ import numpy as np
7
+
8
+ # 1. 影像讀取 (Read)
9
+ img = cv2.imread("onepiece(2).jpeg")
10
+ if img is None:
11
+ print("[錯誤] 無法讀取 jpg")
12
+ exit(1)
13
+
14
+ # 預設灰階影像為原始彩色影像,避免未解開註釋時程式出錯
15
+ gray = img
16
+
17
+ # === 【重要觀念 TODO 1:灰階轉換 (Grayscale Conversion)】 ===
18
+ # 觀念重點:大多數電腦視覺演算法在處理單通道(黑白)影像時速度更快。
19
+ # 將 BGR 彩色圖像轉換為單通道灰階圖像,可以減少 2/3 的資料量,且能過濾掉無用的顏色雜訊。
20
+ # 請解開下方註釋 (刪除行首的 #) 並執行,觀察變為灰階後的影像。
21
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
22
+
23
+ # 預設模糊影像為灰階影像,避免未解開註釋時程式出錯
24
+ blurred = gray
25
+
26
+ # === 【重要觀念 TODO 2:高斯模糊平均來平 (Gaussian Blur)】 ===
27
+ # 觀念重點:高斯模糊是透過像素加權滑影像,主要目的是「消除噪點和細微反光」。
28
+ # 這可以避免後續邊緣偵測時把細微的噪輪廓,讓偵測出來的邊緣更加平滑。
29
+ # 請解開下方註釋 (刪除行首的 #) 並執行,觀察影像邊緣的柔焦效果。
30
+ blurred = cv2.GaussianBlur(gray, (5, 5), 0)
31
+
32
+ # 預設邊緣影像為全黑畫布,避免未解開註釋時點誤判為程式出錯
33
+ edges = np.zeros(img.shape[:2], dtype=np.uint8)
34
+
35
+ # === 【重要觀念 TODO 3:Canny 邊緣偵測 (Canny Edge Detection)】 ===
36
+ # 觀念重點:Canny 演算法會找出影像中明暗變化劇烈的分界線。它使用雙閥值法:
37
+ # 高於高閥值(150)的 100% 判定為邊緣;介於高低閥值(50~150)之間的若連接強邊緣則保留;其餘捨棄。
38
+ # 請解開下方註釋 (刪除行首的 #) 並執行,觀察提取出的白色線條輪廓。
39
+ edges = cv2.Canny(blurred, 10, 80)
40
+
41
+ # 預設無偵測到輪廓,且繪製圖像為原始圖像,避免未解開註釋時程式出錯
42
+ contours = []
43
+ contour_img = img.copy()
44
+
45
+ # === 【重要觀念 TODO 4:尋找與繪製輪廓 (Find & Draw Contours)】 ===
46
+ # 觀念重點:`findContours` 會在二值化(邊緣)影像中尋找相連的邊界,並建立幾何結構。
47
+ # `drawContours` 則會將這些幾何結構繪製到指定的影像上(此處使用綠色框繪製於原始彩色影像)。
48
+ # 請解開下方所有註釋 (刪除行首的 #) 並執行,觀察圖形外框被綠線框起來的結果。
49
+ contours, hierarchy = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
50
+ print(f"在影像中偵測到 {len(contours)} 個獨立輪廓特徵。")
51
+ contour_img = img.copy()
52
+ cv2.drawContours(contour_img, contours, -1, (173,216,203), 1)
53
+ alpha=1
54
+
55
+ # === 【重要觀念 TODO 5:視窗顯示與釋放 (Window Display)】 ===
56
+ # 觀念重點:使用 cv2.imshow 顯示各步驟處理結果,並用 waitKey(0) 讓程式停下等待按鍵,
57
+ # 最後使用 destroyAllWindows 釋放視窗資源。
58
+ # 請解開下方所有註釋 (刪除行首的 #),以觀看影像處理的成果!
59
+ print("\n正在顯示影像處理結果(請點擊影像視窗並按下任意鍵關閉)...")
60
+ cv2.imshow("Original Shapes", img)
61
+ cv2.imshow("Grayscale", gray)
62
+ cv2.imshow("Canny Edges", edges)
63
+ cv2.imshow("Contours Output", contour_img)
64
+
65
+ success_gray = cv2.imwrite("output_gray.jpg", gray)
66
+ success_edges = cv2.imwrite("output_edges.jpg", edges)
67
+ success_contour = cv2.imwrite("output_contours.jpg", contour_img)
68
+
69
+ cv2.waitKey(0)
70
+ cv2.destroyAllWindows()
projects/2026_AI營1/劉宗穎/手指画画游戏/draw_game_student.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ import mediapipe as mp
4
+ from mediapipe.tasks import python
5
+ from mediapipe.tasks.python import vision
6
+
7
+ # ============================================
8
+ # Week 9 - 簡單學生版:支援多色切換的 AR 空中寫字板 (純黑畫布版)
9
+ # ============================================
10
+
11
+ # === 【重要觀念 TODO 1:MediaPipe 手部偵測模型初始化】 ===
12
+ # 觀念重點:
13
+ # 1. BaseOptions 用於設定模型檔案的路徑,這裡使用下載好的 'hand_landmarker.task'。
14
+ # 2. HandLandmarkerOptions 用於設定手部偵測的細項設定,例如 num_hands=1 代表最多偵測一隻手。
15
+ # 3. 最後透過 vision.HandLandmarker.create_from_options 建立手部偵測器物件。
16
+ # 請解開下方三行程式碼的註釋:
17
+ base_options = python.BaseOptions(model_asset_path='hand_landmarker.task')
18
+ options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1)
19
+ detector = vision.HandLandmarker.create_from_options(options)
20
+
21
+ cap = cv2.VideoCapture(0)
22
+ canvas = None
23
+ px, py = 0, 0
24
+
25
+ # 定義幾種高對比的顏色 (格式為 B, G, R)
26
+ COLORS = {
27
+ 'red': (0, 0, 255),
28
+ 'orange': (0, 165, 255),
29
+ 'yellow': (0, 255, 255),
30
+ 'green': (0, 255, 0),
31
+ 'cyan': (255, 255, 0),
32
+ 'dark_blue': (255, 0, 0),
33
+ 'light_purple': (164, 97, 237),
34
+ 'purple': (63, 0, 127),
35
+ 'magenta': (255, 0, 255),
36
+ }
37
+
38
+ # 預設使用螢光綠
39
+ current_color = COLORS['green']
40
+
41
+ print("系統啟動中...")
42
+ print("【操作指南】")
43
+ print("☝️ 單伸食指:下筆寫字")
44
+ print("✌️ 食指+中指:懸停移動 (不寫字)")
45
+ print("⌨️ 按下 '1~9':切換畫筆顏色")
46
+ print("⌨️ 按下 'e':清空畫面")
47
+ print("⌨️ 按下 'q':結束程式")
48
+
49
+ while True:
50
+ success, img = cap.read()
51
+ if not success:
52
+ break
53
+
54
+ # === 【重要觀念 TODO 2:攝影機影像水平翻轉】 ===
55
+ # 觀念重點:
56
+ # 1. 攝影機預設取得的影像與我們的真實動作是相反的(非鏡像)。
57
+ # 2. 在互動式 AR 應用中,為了讓使用者的左手移動時,畫面中的手也往左移動,
58
+ # 必須使用 cv2.flip(img, 1) 將影像水平翻轉,以提供符合直覺的「鏡像體驗」。
59
+ # 請解開下方一行程式碼的註釋:
60
+ img = cv2.flip(img, 1)
61
+
62
+ h, w, c = img.shape
63
+
64
+ # 初始化畫布
65
+ if canvas is None:
66
+ canvas = np.zeros((h, w, c), dtype=np.uint8)
67
+
68
+ # 為了維持畫筆提示點(或懸停圓圈)的動態顯示,且不破壞原本的 canvas 紀錄,
69
+ # 我們在每一訊框都建立一個純黑的暫時畫布來呈現最終畫面。
70
+ display_img = np.zeros((h, w, c), dtype=np.uint8)
71
+
72
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
73
+ mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb)
74
+ results = detector.detect(mp_image)
75
+
76
+ if results.hand_landmarks:
77
+ hand_lms = results.hand_landmarks[0]
78
+
79
+ index_tip = hand_lms[8]
80
+ index_pip = hand_lms[6]
81
+ middle_tip = hand_lms[12]
82
+ middle_pip = hand_lms[10]
83
+
84
+ cx, cy = int(index_tip.x * w), int(index_tip.y * h)
85
+
86
+ index_up = index_tip.y < index_pip.y
87
+ middle_up = middle_tip.y < middle_pip.y
88
+
89
+ if index_up and not middle_up:
90
+ # 狀態:【繪圖模式】
91
+ if px == 0 and py == 0:
92
+ px, py = cx, cy
93
+
94
+ # === 【重要觀念 TODO 3:在 OpenCV Canvas 上繪製軌跡】 ===
95
+ # 觀念重點:
96
+ # 1. cv2.line 用於在影像上繪製直線。
97
+ # 2. 參數說明:畫布(canvas)、起點(px, py)、終點(cx, cy)、顏色(current_color)、線寬(8)。
98
+ # 3. 藉由在每一訊框連續繪製前一個位置到當前位置的線段,即可形成平滑的寫字軌跡。
99
+ # 請解開下方一行程式碼的註釋:
100
+ cv2.line(canvas, (px, py), (cx, cy), current_color, 8)
101
+
102
+ px, py = cx, cy
103
+
104
+ # 筆頭提示點直接畫在暫時的顯示畫布上,避免在黑畫布上找不到筆尖
105
+ cv2.circle(display_img, (cx, cy), 10, current_color, cv2.FILLED)
106
+
107
+ elif index_up and middle_up:
108
+ # 狀態:【懸停模式】
109
+ px, py = 0, 0
110
+ # 懸停提示圈畫在暫時的顯示畫布上,移動時才不會在黑背景中迷路
111
+ cv2.circle(display_img, (cx, cy), 12, (255, 255, 255), 2)
112
+
113
+ else:
114
+ px, py = 0, 0
115
+ else:
116
+ px, py = 0, 0
117
+
118
+ # 為什麼改用 canvas 與 display_img 疊加?
119
+ # 原本是與相機影像 (img) 疊加,現在改與純黑的 display_img 疊加,
120
+ # 這樣畫面上就只會留下寫字的軌跡以及當前的筆頭/懸停提示,完全遮蔽人像背景。
121
+
122
+ # === 【重要觀念 TODO 4:AR 畫面疊加與顯示】 ===
123
+ # 觀念重點:
124
+ # 1. cv2.add 用於將兩個相同大小與類型的影像矩陣進行相加。
125
+ # 2. 這裡將寫字痕跡畫布 (canvas) 與當前的筆頭/懸停提示畫布 (display_img) 疊加。
126
+ # 3. 疊加後使用 cv2.imshow 在視窗中顯示最終成果。
127
+ # 請解開下方兩行程式碼的註釋:
128
+ final_output = cv2.add(canvas, display_img)
129
+ cv2.imshow("Hand Tracking - AR Canvas", final_output)
130
+
131
+ # 鍵盤監聽與控制邏輯
132
+ key = cv2.waitKey(1) & 0xFF
133
+ if key == ord('q'):
134
+ break
135
+ elif key == ord('e'):
136
+ canvas = np.zeros((h, w, c), dtype=np.uint8)
137
+
138
+ elif key == ord('1'):
139
+ current_color = COLORS['red']
140
+ print("已切換為:红色")
141
+ elif key == ord('2'):
142
+ current_color = COLORS['orange']
143
+ print("已切換為:橘色")
144
+ elif key == ord('3'):
145
+ current_color = COLORS['yellow']
146
+ print("已切換為:黃色")
147
+ elif key == ord('4'):
148
+ current_color = COLORS['green']
149
+ print("已切換為:綠色")
150
+ elif key == ord('5'):
151
+ current_color = COLORS['cyan']
152
+ print("已切換為:青色")
153
+ elif key == ord('6'):
154
+ current_color = COLORS['dark_blue']
155
+ print("已切換為:深蓝色")
156
+ elif key == ord('7'):
157
+ current_color = COLORS['light_purple']
158
+ print("已切換為:浅紫色")
159
+ elif key == ord('8'):
160
+ current_color = COLORS['purple']
161
+ print("已切換為:紫色")
162
+ elif key == ord('9'):
163
+ current_color = COLORS['magenta']
164
+ print("已切換為:粉色")
165
+
166
+ cap.release()
167
+ cv2.destroyAllWindows()
projects/2026_AI營1/劉宗穎/照片/onepiece(2).jpeg ADDED

Git LFS Details

  • SHA256: 16063edf453095349df30308f927b018b8b6eaf10cdcff3fd64dbe3e5c6c0cf6
  • Pointer size: 130 Bytes
  • Size of remote file: 80.2 kB
projects/2026_AI營1/劉宗穎/照片/onepiece.jpeg ADDED

Git LFS Details

  • SHA256: 737a93e3e54652d08eca4b148bf126686bfe5b1647c314d33d2eee42919d7a38
  • Pointer size: 131 Bytes
  • Size of remote file: 168 kB
projects/2026_AI營1/劉宗穎/爬虫分析/d4_1_ex1_books.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # d4_1_ex1_books.py — Day 4 學員填空
2
+ # cd examples/Day4/student && python d4_1_ex1_books.py
3
+ # 參考答案:../d4_1_ex1_books.py
4
+
5
+ import requests
6
+ from bs4 import BeautifulSoup
7
+ from urllib.parse import urljoin
8
+
9
+ # 目標網站:專供爬蟲練習的圖書商城沙盒
10
+ URL = "http://books.toscrape.com/"
11
+
12
+ # === 【學員填空練習點 1:模擬瀏覽器 Header】 ===
13
+ # TODO: 建立 headers 字典,填入 "User-Agent" 來模擬真實瀏覽器(降低被簡易防爬機制拒絕的機率)
14
+ HEADERS = {
15
+ "User-Agent": (
16
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
17
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
18
+ "Chrome/120.0.0.0 Safari/537.36"
19
+ )
20
+ }
21
+
22
+ print(f"正在連線至圖書沙盒網站: {URL} ...")
23
+
24
+ try:
25
+ # 發送 GET 請求取得網頁 HTML
26
+ response = requests.get(URL, headers=HEADERS, timeout=10)
27
+
28
+ # 檢查 HTTP 狀態碼是否為 200 (OK),非 200 時引發 HTTPError 異常
29
+ response.raise_for_status()
30
+
31
+ # 解決編碼問題:強制將解碼設定為 utf-8 (避免 requests 預設 ISO-8859-1 解碼導致英鎊符號亂碼且在 Windows 印出時崩潰)
32
+ response.encoding = response.apparent_encoding
33
+
34
+ print("連線成功!開始解析書籍資料...\n")
35
+
36
+ # 初始化 BeautifulSoup 解析器
37
+ soup = BeautifulSoup(response.text, "html.parser")
38
+
39
+ # === 【學員填空練習點 2:定位 HTML 標籤區塊】 ===
40
+ # TODO: 找出網頁中所有書籍的容器。每本書都被包在一個 <article> 標籤中,且該標籤的 class 固定為 "product_pod"
41
+ books = soup.find_all("article", class_="product_pod")
42
+
43
+ print(f"成功在本頁找到 {len(books)} 本書籍:")
44
+ print("=" * 70)
45
+
46
+ for idx, book in enumerate(books, 1):
47
+ # === 【學員填空練習點 3:提取書籍名稱與連結】 ===
48
+ # TODO:
49
+ # 1. 尋找 <h3> 底下的 <a> 標籤
50
+ # 2. 取得 <a> 的 "title" 屬性(因為畫面上長度限制,.get_text() 可能會被 ellipsis 截斷)
51
+ # 3. 取得 <a> 的 "href" 屬性並使用 urljoin 拼接成完整網址
52
+ title_link = book.find("h3").find("a")
53
+ title = title_link.get("title") # 取得屬性 'title' 的值,避免拿到被截斷的文字
54
+
55
+ relative_url = title_link.get("href")
56
+ detail_url = urljoin(URL, relative_url)
57
+
58
+ # === 【學員填空練習點 4:提取書籍價格】 ===
59
+ # 提示:為了避免 Windows (CP950) 終端機不支援英鎊符號 '£' 而編碼崩潰,請加上 .replace("£", "GBP") 將其替換
60
+ # 觀察:價格存放在 class 為 "price_color" 的 <p> 標籤中,格式如 "£51.77"
61
+ # 技巧:將英鎊符號 '£' 替換為 'GBP',避免在 Windows (CP950) 終端機印出時因不支援該字元而崩潰
62
+ price_text = book.find("p", class_="price_color").get_text().replace("£", "GBP")
63
+
64
+ # === 【學員填空練習點 5:提取評價星等 (Rating)】 ===
65
+ # TODO:
66
+ # 1. 尋找 class 為 "star-rating" 的 <p> 標籤
67
+ # 2. 取得該標籤的 "class" 屬性值(這會是一個串列,如 ['star-rating', 'Three'])
68
+ # 3. 提取該串列的第二個元素作為星等字串 (若存在)
69
+
70
+ rating_tag = book.find("p", class_="star-rating")
71
+ rating_classes = rating_tag.get("class")
72
+ rating = "無"
73
+ if len(rating_classes) > 1:
74
+ rating = rating_classes[1] # 提取例如 'Three' (代表三顆星)
75
+
76
+ # === 【學員填空練習點 6:提取庫存狀態】 ===
77
+ # TODO: 尋找 class 為 "instock availability" 的 <p> 標籤,取得文字後去除首尾空白 (.strip())
78
+ stock_text = book.find("p", class_="instock availability").get_text().strip()
79
+
80
+ # 印出解析結果
81
+ print(f"【編號 {idx}】")
82
+ print(f"[書名] {title}")
83
+ print(f"[價格] {price_text}")
84
+ print(f"[評價] {rating} 星")
85
+ print(f"[狀態] {stock_text}")
86
+ print(f"[連結] {detail_url}")
87
+ print("-" * 70)
88
+
89
+ except requests.exceptions.HTTPError as http_err:
90
+ print(f"[錯誤] HTTP 連線異常:{http_err}")
91
+ except requests.exceptions.RequestException as e:
92
+ print(f"[錯誤] 網路請求失敗:{e}")
93
+ except Exception as ex:
94
+ print(f"[錯誤] 解析資料時發生非預期錯誤:{ex}")
projects/2026_AI營1/劉宗穎/爬虫分析/d4_1_ex2_ptt.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # d4_1_ex2_ptt.py — Day 4 學員填空
2
+ # cd examples/Day4/student && python d4_1_ex2_ptt.py
3
+ # 參考答案:../d4_1_ex2_ptt.py
4
+
5
+ import requests
6
+ from bs4 import BeautifulSoup
7
+ from urllib.parse import urljoin
8
+
9
+ # 目標網站:PTT Python 討論板首頁
10
+ URL = "https://www.ptt.cc/bbs/Python/index.html"
11
+
12
+ # === 【學員填空練習點 1:模擬瀏覽器 Header】 ===
13
+ # TODO: 填入瀏覽器的 User-Agent,否則 PTT 會拒絕請求並回傳 403 錯誤
14
+ HEADERS = {
15
+ "User-Agent": (
16
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
17
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
18
+ "Chrome/120.0.0.0 Safari/537.36"
19
+ )
20
+ }
21
+
22
+ print(f"正在抓取 PTT Python 板最新文章: {URL} ...")
23
+
24
+ try:
25
+ response = requests.get(URL, headers=HEADERS, timeout=10)
26
+ response.raise_for_status()
27
+
28
+ soup = BeautifulSoup(response.text, "html.parser")
29
+
30
+ # === 【學員填空練習點 2:定位文章區塊】 ===
31
+ # TODO: 觀察網頁 HTML 結構,找出所有包裹文章資訊的 <div> 標籤(其 class 名稱為何?)
32
+ posts = soup.find_all("div", class_="r-ent")
33
+
34
+ print(f"\n成功取得 {len(posts)} 筆文章項目(含已刪除與置底公告):")
35
+ print("=" * 80)
36
+
37
+ for idx, post in enumerate(posts, 1):
38
+ # === 【學員填空練習點 3:提取文章標題與連結並防錯 (Null Check)】 ===
39
+ # TODO:
40
+ # 1. 尋找 class 為 "title" 的 <div> 標籤
41
+ # 2. 在該 <div> 底下尋找 <a> 標籤 (即文章連結)
42
+ # 3. ⚠️ 重要思考:若文章已刪除,將不會有 <a> 標籤。請撰寫條件判斷:
43
+ # - 若 <a> 標籤存在:提取其文字,並使用 urljoin 拼接 href 屬性得到完整網址。
44
+ # - 若 <a> 標籤不存在:直接將 title_div 的文字作為標題,並將網址設為 "無 (文章已刪除)"。
45
+ title_div = post.find("div", class_="title")
46
+ title_link = title_div.find("a") if title_div else None
47
+
48
+ if title_link:
49
+ title = title_link.get("title")
50
+ relative_url = title_link.get("href")
51
+ post_url = urljoin(URL, relative_url)
52
+ else:
53
+ # 文章已刪除
54
+ title_div = post.find("div", class_="title")
55
+ title = title_div.get_text().strip() if title_div else "(已刪除)"
56
+ post_url = "無 (文章已刪除)"
57
+
58
+
59
+ # === 【學員填空練習點 4:提取推文數】 ===
60
+ # TODO: 尋找 class 為 "nrec" 的 <div> 標籤。
61
+ # 技巧:使用 .get_text().strip() 取得文字。若無文字(0 推),請將變數預設為 "0"。
62
+ nrec_div = post.find("div", class_="nrec")
63
+ nrec = nrec_div.get_text().strip() if nrec_div else "0"
64
+
65
+ # === 【學員填空練習點 5:提取作者與發文日期】 ===
66
+ # TODO: 分別尋找 class 為 "author" 與 "date" 的 <div> 標籤,並提取純文字(記得使用 .strip() 去除空白)
67
+ author = post.find("div", class_="author").get_text().strip()
68
+ date = post.find("div", class_="date").get_text().strip()
69
+
70
+ # 輸出單篇文章資訊
71
+ print(f"[{idx}] 推文數: {nrec:3s} | 發文日期: {date} | 作者: {author}")
72
+ print(f" 標題: {title}")
73
+ print(f" 網址: {post_url}")
74
+ print("-" * 80)
75
+
76
+ except requests.exceptions.HTTPError as http_err:
77
+ print(f"[錯誤] PTT 伺服器拒絕或找不到網頁:{http_err}")
78
+ except requests.exceptions.RequestException as e:
79
+ print(f"[錯誤] 網路請求失敗:{e}")
80
+ except Exception as ex:
81
+ print(f"[錯誤] 解析資料時發生非預期錯誤:{ex}")
projects/2026_AI營1/劉宗穎/爬虫分析/d4_1_ex3_ithelp.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # d4_1_ex3_ithelp.py — Day 4 學員填空
2
+ # cd examples/Day4/student && python d4_1_ex3_ithelp.py
3
+ # 參考答案:../d4_1_ex3_ithelp.py
4
+
5
+ import requests
6
+ from bs4 import BeautifulSoup
7
+
8
+ # 目標網站:iT 邦幫忙技術文章首頁
9
+ URL = "https://ithelp.ithome.com.tw/articles?tab=tech"
10
+
11
+ # === 【學員填空練習點 1:模擬瀏覽器 Header】 ===
12
+ # TODO: 填入瀏覽器的 User-Agent,否則可能被伺服器拒絕連線
13
+ HEADERS = {
14
+ "User-Agent": (
15
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
16
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
17
+ "Chrome/120.0.0.0 Safari/537.36"
18
+ )
19
+ }
20
+
21
+ print(f"正在連線至 iT 邦幫忙並抓取技術文章: {URL} ...")
22
+
23
+ try:
24
+ response = requests.get(URL, headers=HEADERS, timeout=10)
25
+ response.raise_for_status()
26
+
27
+ print("連線成功!解析文章列表中...\n")
28
+ soup = BeautifulSoup(response.text, "html.parser")
29
+
30
+ # === 【學員填空練習點 2:定位文章卡片區塊】 ===
31
+ # TODO: 尋找所有包裹文章資訊的 <div> 標籤(其 class 為 "qa-list")
32
+ articles = soup.find_all("div", class_="qa-list")
33
+
34
+ print(f"成功取得前 {len(articles)} 篇熱門技術文章:")
35
+ print("=" * 80)
36
+
37
+ for idx, art in enumerate(articles, 1):
38
+ # === 【學員填空練習點 3:提取標題與超連結】 ===
39
+ # TODO:
40
+ # 1. 在 art 中尋找 class 為 "qa-list__title-link" 的 <a> 標籤
41
+ # 2. 提取其純文字內容並去除首尾空白 (.strip()),作為標題
42
+ # 3. 提取其 "href" 屬性,作為文章連結
43
+ title_link = art.find("a", class_="qa-list__title-link")
44
+ if not title_link:
45
+ continue
46
+ title = title_link.get_text().strip()
47
+ post_url = title_link.get("href").strip()
48
+
49
+ # === 【學員填空練習點 4:提取作者名稱】 ===
50
+ # TODO:
51
+ # 1. 尋找 class 為 "qa-list__info" 的 <div> 標籤
52
+ # 2. 在該 <div> 下尋找 class 為 "qa-list__info-link" 的 <a> 標籤
53
+ # 3. 提取其文字內容作為作者名稱
54
+ info_div = art.find("div", class_="qa-list__info")
55
+ author = "未知作者"
56
+ if info_div:
57
+ author_link = info_div.find("a", class_="qa-list__info-link")
58
+ if author_link:
59
+ author = author_link.get_text().strip()
60
+
61
+ # === 【學員填空練習點 5:提取發文時間】 ===
62
+ # TODO: 尋找 class 為 "qa-list__info-time" 的 <a> 標籤,並提取其 "title" 屬性值(因 title 屬性中包含完整時間秒數)
63
+ time_link = art.find("div", class_="qa-list__info-time")
64
+ post_time = time_link.get("title") if time_link else "未知時間"
65
+
66
+ # === 【學員填空練習點 6:利用 List 索引提取多項數據】 ===
67
+ # TODO:
68
+ # 1. 使用 find_all 找出卡片中所有 class 為 "qa-condition" 的 <a> 標籤
69
+ # 2. 當抓取到的數量大於等於 3 時,利用 List 索引取得對應的資訊:
70
+ # - 索引 0 的項目底下的 "qa-condition__count" <span>:Like 數
71
+ # - 索引 1 的項目底下的 "qa-condition__count" <span>:回應數
72
+ # - 索引 2 的項目底下的 "qa-condition__count" <span>:瀏覽數
73
+ conditions = art.find_all("a", class_="qa-condition")
74
+
75
+ likes = "0"
76
+ comments = "0"
77
+ views = "0"
78
+
79
+ if len(conditions) >= 3:
80
+ # 請在此處填入利用 conditions[0]、conditions[1]、conditions[2] 尋找 span 的代碼
81
+ likes = conditions[0].find("span", class_="qa-condition__count").get_text().strip()
82
+ comments = conditions[1].find("span", class_="qa-condition__count").get_text().strip()
83
+ views = conditions[2].find("span", class_="qa-condition__count").get_text().strip()
84
+
85
+ # 格式化輸出
86
+ print(f"【排名 {idx}】")
87
+ print(f"[標題] {title}")
88
+ print(f"[作者] {author} | [時間] {post_time}")
89
+ print(f"[數據] Like: {likes} | 回應: {comments} | 瀏覽/訂閱: {views}")
90
+ print(f"[連結] {post_url}")
91
+ print("-" * 80)
92
+
93
+ except requests.exceptions.HTTPError as http_err:
94
+ print(f"[錯誤] 伺服器回應異常:{http_err}")
95
+ except requests.exceptions.RequestException as e:
96
+ print(f"[錯誤] 網路請求失敗:{e}")
97
+ except Exception as ex:
98
+ print(f"[錯誤] 解析資料時發生非預期錯誤:{ex}")
projects/2026_AI營1/劉宗穎/爬虫分析与AI报告/d4_1_web_scraping.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # d4_1_web_scraping.py — Day4 學員填空
2
+ # cd examples/Day4/student && python3 d4_1_web_scraping.py
3
+ # 參考答案:../d4_1_web_scraping.py
4
+
5
+ import requests
6
+ from bs4 import BeautifulSoup
7
+
8
+ # 本範例抓取專供網頁爬蟲教學使用的模擬沙盒網站:Quotes to Scrape
9
+ URL = "http://quotes.toscrape.com/"
10
+
11
+ # === 【學員填空練習點 1:模擬瀏覽器 Header】 ===
12
+ # TODO: 建立 headers 字典,填入 "User-Agent" 來模擬真實瀏覽器(降低被簡易防爬機制拒絕的機率)
13
+ HEADERS = {
14
+ "User-Agent": (
15
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
16
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
17
+ "Chrome/120.0.0.0 Safari/537.36"
18
+ )
19
+ }
20
+
21
+ print(f"正在連線並抓取網站內容: {URL} ...")
22
+
23
+ try:
24
+ # === 【學員填空練習點 2:連線請求與下載網頁】 ===
25
+ # TODO: 1. 使用 requests.get 發送 GET 請求,帶入 headers=HEADERS 與設定 timeout=5
26
+ # TODO: 2. 判斷 response.status_code 是否為 200 (下載成功)
27
+ response = requests.get(URL, headers=HEADERS, timeout=5)
28
+
29
+ if response and response.status_code == 200:
30
+ print("網頁下載成功!開始解析 HTML 結構...")
31
+
32
+ # === 【學員填空練習點 3:初始化 BeautifulSoup 解析器】 ===
33
+ # TODO: 使用 BeautifulSoup 解析 response.text,初始化 html.parser
34
+ soup = BeautifulSoup(response.text, "html.parser")
35
+
36
+ # === 【學員填空練習點 4:定位 HTML 標籤區塊】 ===
37
+ # TODO: 找出網頁中所有名言與作者的 div 標籤(其 class 為 'quote')
38
+ quote_divs = soup.find_all("div", class_="quote")
39
+
40
+ print(f"\n成功解析!本日精選名言如下(共 {len(quote_divs)} 筆):")
41
+ print("=" * 60)
42
+
43
+ # === 【學員填空練習點 5:迴圈遍歷提取內容與防錯】 ===
44
+ # TODO:
45
+ # 1. 遍歷 quote_divs,並使用 enumerate 取得索引 idx(從 1 開始)
46
+ # 2. 用 .find() 尋找 span 且 class 為 'text' 的名言元素
47
+ # 3. 用 .find() 尋找 small 且 class 為 'author' 的作者元素
48
+ # 4. 進行 None 防錯處理,確保兩者皆非空值後,使用 .get_text() 提取純文字並印出
49
+ for idx, quote_div in enumerate(quote_divs, 1):
50
+ text_el = quote_div.find("span", class_="text")
51
+ author_el = quote_div.find("small", class_="author")
52
+ if text_el is None or author_el is None:
53
+ continue
54
+ text = text_el.get_text()
55
+ author = author_el.get_text()
56
+
57
+ print(f"[{idx}] \"{text}\"")
58
+ print(f" -- by {author}\n")
59
+
60
+ print("=" * 60)
61
+ print("提醒:請尊重 robots.txt 與網站使用條款,勿對目標網站發送高頻請求。")
62
+ else:
63
+ status = response.status_code if response else "未知"
64
+ print(f"[錯誤] 無法抓取網頁,伺服器回應狀態碼:{status}")
65
+
66
+ except requests.exceptions.RequestException as e:
67
+ print(f"[錯誤] 連線失敗:{e}")
68
+ print("請確認電腦的網路連線是否通暢。")
projects/2026_AI營1/劉宗穎/爬虫分析与AI报告/d4_2_agent_task_chain.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # d4_2_agent_task_chain.py
2
+ # Day 4: 自動化任務鏈 (Task Chain / 簡化版 Agent 工作流)
3
+ # 說明:本範例是「固定管線」——步驟由我們寫死,不是模型自主 function calling。
4
+ # 目的是讓學生先理解:工具 + LLM + 記憶狀態 如何串成可交付的自動化流程。
5
+
6
+ import os
7
+ import requests
8
+ from bs4 import BeautifulSoup
9
+
10
+ GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite")
11
+ api_key = os.environ.get("GEMINI_API_KEY", "AQ.Ab8RN6JvbUqLW-wcddeMducmta0aGE2QtcrmF4Kfd8nnpZ9mzg")
12
+
13
+ HEADERS = {
14
+ "User-Agent": (
15
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
16
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
17
+ "Chrome/120.0.0.0 Safari/537.36"
18
+ )
19
+ }
20
+
21
+
22
+ def plan_steps(topic):
23
+ """規劃階段:對學生展示任務拆解(固定計畫,非 LLM 自主規劃)。"""
24
+ steps = [
25
+ f"1. 使用爬蟲工具擷取主題「{topic}」相關名言",
26
+ "2. 將原始文字作為 context 送入 LLM 摘要與翻譯",
27
+ "3. 將 Markdown 報告寫入本機檔案",
28
+ ]
29
+ print("[Planning] 任務計畫:")
30
+ for s in steps:
31
+ print(" ", s)
32
+ return steps
33
+
34
+
35
+ def tool_scrape_topic_quotes(topic):
36
+ """
37
+ 工具 A:抓取 Quotes to Scrape 的標籤頁面內容。
38
+ 例如 topic = 'love', 'life', 'inspirational' 等。
39
+ """
40
+ url = f"http://quotes.toscrape.com/tag/{topic}/"
41
+ print(f"[Tool] 執行網頁資料檢索。目標網址: {url}")
42
+ try:
43
+ response = requests.get(url, headers=HEADERS, timeout=5)
44
+ if response.status_code == 200:
45
+ soup = BeautifulSoup(response.text, "html.parser")
46
+ quote_divs = soup.find_all("div", class_="quote")
47
+ results = []
48
+ for q in quote_divs:
49
+ text_el = q.find("span", class_="text")
50
+ author_el = q.find("small", class_="author")
51
+ if text_el is None or author_el is None:
52
+ continue
53
+ text = text_el.get_text()
54
+ author = author_el.get_text()
55
+ results.append(f"名言: {text} | 作者: {author}")
56
+ return "\n".join(results) if results else "找不到與此主題相關的內容。"
57
+ return f"網頁讀取錯誤,代碼: {response.status_code}"
58
+ except Exception as e:
59
+ return f"工具執行異常: {e}"
60
+
61
+
62
+ def tool_save_report(filename, content):
63
+ """工具 B:將報告寫入本機檔案。"""
64
+ with open(filename, "w", encoding="utf-8") as f:
65
+ f.write(content)
66
+ return filename
67
+
68
+
69
+ def run_task_chain(topic):
70
+ print("=" * 60)
71
+ print(f"啟動自動化任務鏈。目標:整理與分析關於『{topic}』的文獻報告")
72
+ print("(模式:固定 Pipeline,非自主 Tool-Calling Agent)")
73
+ print("=" * 60)
74
+
75
+ # 步驟 0: 規劃(教學展示)
76
+ plan_steps(topic)
77
+
78
+ # 步驟 1: 呼叫檢索工具
79
+ raw_data = tool_scrape_topic_quotes(topic)
80
+ print(f"\n[步驟 1] 檢索完成!取得原始數據長度: {len(raw_data)} 字元。")
81
+ print("--- 原始數據預覽 ---")
82
+ preview = raw_data[:200] + ("..." if len(raw_data) > 200 else "")
83
+ print(preview)
84
+ print("--------------------")
85
+
86
+ # 工作記憶:保存中間狀態
87
+ memory = {"topic": topic, "raw_data": raw_data, "report": ""}
88
+
89
+ # 步驟 2: LLM 分析(或 Mock 降級)
90
+ if not api_key:
91
+ print("\n[步驟 2] 未偵測到 API Key。啟動模擬生成中...")
92
+ report_content = (
93
+ f"# 模擬關於《{topic}》的名人名言分析報告\n\n"
94
+ f"## 一、 數據檢索摘要\n"
95
+ f"本次任務共檢索到數筆關於 {topic} 的語錄。以下為代表性文獻:\n"
96
+ f"- 模擬數據:\"Love is all, love is new.\" -- John Lennon\n\n"
97
+ f"## 二、 核心思想分析\n"
98
+ f"本機模擬分析指出,此主題的語錄主要著眼於人與人之間的連結與心靈啟發。\n"
99
+ f"*(注意:此為 Mock 離線生成報告,設定 GEMINI_API_KEY 以啟用 Gemini 真實生成。)*\n"
100
+ )
101
+ else:
102
+ print(f"\n[步驟 2] 正在調用 Gemini ({GEMINI_MODEL}) 進行深度分析與排版...")
103
+ try:
104
+ import google.generativeai as genai # 僅雲端模式需要
105
+
106
+ genai.configure(api_key=api_key)
107
+ model = genai.GenerativeModel(GEMINI_MODEL)
108
+
109
+ prompt = (
110
+ f"請幫我分析以下關於『{topic}』的原始英文名言數據,並用繁體中文撰寫一份 Markdown 報告。\n"
111
+ f"報告必須包含三個部分:\n"
112
+ f"1. 導言:說明這個主題的核心意義。\n"
113
+ f"2. 名言分析:挑選原始資料中 2-3 句具代表性的句子進行翻譯與深度賞析。\n"
114
+ f"3. 結論:對學生的啟發與建議。\n\n"
115
+ f"=== 原始數據 ===\n"
116
+ f"{raw_data}"
117
+ )
118
+
119
+ response = model.generate_content(prompt)
120
+ report_content = response.text
121
+ print("[步驟 2] 雲端分析完成!")
122
+ except Exception as e:
123
+ print(f"[錯誤] AI 分析失敗: {e}。轉為模擬輸出。")
124
+ report_content = f"# 分析失敗\n\n錯誤詳情:{e}\n"
125
+
126
+ memory["report"] = report_content
127
+
128
+ # 步驟 3: 自動存檔
129
+ output_filename = f"agent_report_{topic}.md"
130
+ try:
131
+ saved = tool_save_report(output_filename, report_content)
132
+ print(f"\n[步驟 3] 任務報告生成成功!已儲存為檔案: {saved}")
133
+ print("=" * 60)
134
+ except Exception as e:
135
+ print(f"[錯誤] 儲存報告失敗: {e}")
136
+
137
+
138
+ if __name__ == "__main__":
139
+ # 可更換為: 'love', 'life', 'inspirational', 'humor' 等
140
+ test_topic = "s"
141
+ run_task_chain(test_topic)
projects/2026_AI營1/劉宗穎/爬虫分析与AI报告/d4_2_ex2_crawler_ai.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # d4_2_ex2_crawler_ai.py
2
+ # Day 4: 爬蟲與 AI 分析任務鏈 (學生填空版)
3
+ # 執行方式:cd examples/Day4/student && python d4_2_ex2_crawler_ai.py
4
+
5
+ import os
6
+ import requests
7
+ from bs4 import BeautifulSoup
8
+ from urllib.parse import urljoin
9
+ from google import genai
10
+
11
+ # 設定預設使用的 Gemini 模型
12
+ GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite")
13
+ api_key = os.environ.get("GEMINI_API_KEY", "AQ.Ab8RN6JvbUqLW-wcddeMducmta0aGE2QtcrmF4Kfd8nnpZ9mzg")
14
+
15
+ HEADERS = {
16
+ "User-Agent": (
17
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
18
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
19
+ "Chrome/120.0.0.0 Safari/537.36"
20
+ )
21
+ }
22
+
23
+ def plan_steps(source_type):
24
+ """規劃階段:展示任務拆解流程。"""
25
+ steps = [
26
+ f"步驟 1: 啟動爬蟲工具擷取『{source_type.upper()}』熱門文章與輿情數據",
27
+ "步驟 2: 將爬取資料作為 Context,呼叫 Gemini 進行輿情趨勢與熱門話題分析",
28
+ "步驟 3: 自動將 AI 生成的繁體中文分析報告以 UTF-8 編碼存為 Markdown 檔案"
29
+ ]
30
+ print("\n[Planning] 任務計畫已啟動:")
31
+ for s in steps:
32
+ print(f" -> {s}")
33
+ return steps
34
+
35
+ def tool_scrape_ptt():
36
+ """工具 A:抓取 PTT Python 板最新文章資訊。"""
37
+ url = "https://www.ptt.cc/bbs/Tennis/index.html"
38
+ print(f"\n[Tool] 正在爬取 PTT Python 板文章: {url} ...")
39
+ try:
40
+ response = requests.get(url, headers=HEADERS, timeout=10)
41
+ response.raise_for_status()
42
+
43
+ soup = BeautifulSoup(response.text, "html.parser")
44
+ posts = soup.find_all("div", class_="r-ent")
45
+
46
+ results = []
47
+ for post in posts:
48
+
49
+ # === 【學員填空練習點 1:PTT 文章標題與連結提取及防錯 (Null Check)】 ===
50
+ # TODO 1: 若文章已被作者或板主刪除,title_div 底下將會「沒有 <a> 標籤」
51
+ # 1. 請在 title_div 底下尋找 <a> 標籤 (指派給 title_link 變數)
52
+ # 2. 撰寫條件判斷:若 title_link 存在,提取其純文字並用 urljoin 拼接成完整網址。
53
+ # 若不存在,直接將 title_div 的純文字作為 title,且將連結 post_url 設為 "無 (文章已刪除)"。
54
+
55
+ # 請在此處實作 TODO 1 的邏輯...
56
+ title_div = post.find("div", class_="title")
57
+ title_link = title_div.find("a") if title_div else None
58
+
59
+ if title_link:
60
+ title = title_link.get_text().strip()
61
+ relative_url = title_link.get("href")
62
+ post_url = urljoin(url, relative_url)
63
+ else:
64
+ # 文章已刪除
65
+ title_div = post.find("div", class_="title")
66
+ title = title_div.get_text().strip() if title_div else "(已刪除)"
67
+ post_url = "無 (文章已刪除)"
68
+
69
+ nrec_div = post.find("div", class_="nrec")
70
+ nrec = nrec_div.get_text().strip() if nrec_div else "0"
71
+ if not nrec:
72
+ nrec = "0"
73
+
74
+ author = post.find("div", class_="author").get_text().strip() if post.find("div", class_="author") else "未知作者"
75
+ date = post.find("div", class_="date").get_text().strip() if post.find("div", class_="date") else "未知日期"
76
+
77
+ results.append(f"標題: {title} | 作者: {author} | 日期: {date} | 推文數: {nrec} | 網址: {post_url}")
78
+
79
+ return "\n".join(results) if results else "找不到任何文章項目。"
80
+ except Exception as e:
81
+ return f"[PTT 爬蟲錯誤] 執行異常: {e}"
82
+
83
+ def tool_scrape_ithelp():
84
+ """工具 B:抓取 iT 邦幫忙熱門技術文章。"""
85
+ url = "https://ithelp.ithome.com.tw/articles?tab=tech"
86
+ print(f"\n[Tool] 正在爬取 iT 邦幫忙技術文章: {url} ...")
87
+ try:
88
+ response = requests.get(url, headers=HEADERS, timeout=10)
89
+ response.raise_for_status()
90
+
91
+ soup = BeautifulSoup(response.text, "html.parser")
92
+ articles = soup.find_all("div", class_="qa-list")
93
+
94
+ results = []
95
+ for art in articles:
96
+ title_link = art.find("a", class_="qa-list__title-link")
97
+ if not title_link:
98
+ continue
99
+ title = title_link.get_text().strip()
100
+ post_url = title_link.get("href").strip()
101
+
102
+ info_div = art.find("div", class_="qa-list__info")
103
+ author = "未知作者"
104
+ if info_div:
105
+ author_link = info_div.find("a", class_="qa-list__info-link")
106
+ if author_link:
107
+ author = author_link.get_text().strip()
108
+
109
+ time_link = art.find("a", class_="qa-list__info-time")
110
+ post_time = time_link.get("title") if time_link else "未知時間"
111
+
112
+ conditions = art.find_all("a", class_="qa-condition")
113
+ likes, comments, views = "0", "0", "0"
114
+
115
+ # === 【學員填空練習點 2:利用 List 索引提取多項數據】 ===
116
+ # TODO 2: 當抓取到的 conditions 數量大於等於 3 時,利用 List 索引取得對應的資訊:
117
+ # - 索引 0 的項目底下的 "qa-condition__count" <span>:Like 數
118
+ # - 索引 1 的項目底下的 "qa-condition__count" <span>:回應數
119
+ # - 索引 2 的項目底下的 "qa-condition__count" <span>:瀏覽數
120
+ if len(conditions) >= 3:
121
+ # 請在下方填入提取 Like、回應與瀏覽數的代碼:
122
+ likes = conditions[0].find("span", class_="qa-condition__count").get_text().strip()
123
+ comments = conditions[1].find("span", class_="qa-condition__count").get_text().strip()
124
+ views = conditions[2].find("span", class_="qa-condition__count").get_text().strip()
125
+
126
+ results.append(
127
+ f"標題: {title} | 作者: {author} | 時間: {post_time} | "
128
+ f"Like: {likes} | 回應: {comments} | 瀏覽: {views} | 網址: {post_url}"
129
+ )
130
+
131
+ return "\n".join(results) if results else "找不到任何技術文章。"
132
+ except Exception as e:
133
+ return f"[iT邦幫忙爬蟲錯誤] 執行異常: {e}"
134
+
135
+ def tool_save_report(filename, content):
136
+ """工具 C:將報告以 UTF-8 編碼寫入本機檔案。"""
137
+ print(f"\n[Tool] 正在將分析報告儲存至檔案: {filename} ...")
138
+ # === 【學員填空練習點 6:寫入檔案編碼處理】 ===
139
+ # TODO 6: 請使用 with open 開啟檔案,以 "utf-8" 編碼寫入 content,並回傳 filename。
140
+ # 提示:必須傳入 encoding="utf-8" 以避免 Windows 系統的中文亂碼
141
+ with open(filename, "w", encoding="utf-8") as f:
142
+ f.write(content)
143
+ # 請在此處實作 TODO 6 ...
144
+ return filename
145
+
146
+ def run_analysis_chain(source_type):
147
+ print("=" * 70)
148
+ print(f"啟動自動化輿情任務鏈。目標來源:『{source_type.upper()}』數據分析")
149
+ print("=" * 70)
150
+
151
+ # 步驟 0: 展示規劃
152
+ plan_steps(source_type)
153
+
154
+ # 步驟 1: 執行對應的爬蟲工具
155
+ if source_type.lower() == "ptt":
156
+ raw_data = tool_scrape_ptt()
157
+ elif source_type.lower() == "ithelp":
158
+ raw_data = tool_scrape_ithelp()
159
+ else:
160
+ print("[錯誤] 未支援的數據來源。僅支援 'ptt' 或 'ithelp'")
161
+ return
162
+
163
+ print(f"\n[步驟 1] 資料檢索完成!取得原始數據長度: {len(raw_data)} 字元。")
164
+ print("--- 數據預覽 (前 200 字) ---")
165
+ print(raw_data[:200] + ("..." if len(raw_data) > 200 else ""))
166
+ print("----------------------------")
167
+
168
+ # === 【學員填空練習點 3:任務鏈工作記憶與態狀維護】 ===
169
+ # TODO 3: 請建立名為 memory 的字典 (dict),包含三個鍵值:
170
+ # "source" 指向來源 source_type、"raw_data" 指向原始爬取資料 raw_data、"report" 設為空字串 "" 用以儲存後續 AI 報告。
171
+ memory = {
172
+ "source": source_type,
173
+ "raw_data": raw_data,
174
+ "report": ""
175
+ }
176
+
177
+ # 步驟 2: AI 分析 (或 Mock 降級防禦)
178
+ if not api_key:
179
+ print("\n[步驟 2] 未偵測到環境變數 GEMINI_API_KEY,自動啟動 Mock 離線降級機制...")
180
+ # === 【學員填空練習點 5:Mock 降級防衛機制】 ===
181
+ # TODO 5: 請設計一段模擬的 Markdown 格式分析報告字串,指派給 report_content。
182
+ # 報告內容至少需要包含標題與數據摘要,避免程式在無 Key 時報錯中斷。
183
+ report_content = (
184
+ f"#模拟与情分析报告 - 数据来源:{source_type.upper()}\n\n"
185
+ )
186
+ print("[步驟 2] Mock 報告模擬生成完畢。")
187
+ else:
188
+ print(f"\n[步驟 2] 正在調用 Gemini ({GEMINI_MODEL}) 進行數據分析與排版...")
189
+ try:
190
+ # === 【學員填空練習點 4:新版 google-genai SDK 初始化與調用】 ===
191
+ # TODO 4:
192
+ # 1. 呼叫 genai.Client 並傳入 api_key 來初始化 client 物件。
193
+ # 2. 使用 client.models.generate_content 發送生成請求。
194
+ # 傳入參數 model=GEMINI_MODEL,contents 則為準備好的 Prompt。
195
+ client = genai.Client(api_key=api_key)
196
+ prompt = (
197
+ f"請幫我分析以下來自 {source_type.upper()} 的最新網頁抓取文章列表,並用繁體中文撰寫一份 Markdown 网球分析報告。\n"
198
+ f"報告必須包含以下部分:\n"
199
+ f"1. 说网球界的名人。\n"
200
+ f"2. 说网球在哪时候被发明的。\n"
201
+ f"3. 说网球对身体有什么帮助。\n"
202
+ f"4. 说一些最精彩的比赛。\n\n"
203
+ f"=== 原始爬蟲數據 ===\n"
204
+ f"{raw_data}"
205
+ )
206
+
207
+ # 請在此處實作 TODO 4 ...
208
+ resopnse = client.models.generate_content(
209
+ model=GEMINI_MODEL,
210
+ contents=prompt
211
+ )
212
+ report_content = ""
213
+ print("[步驟 2] 雲端 Gemini 分析完成!")
214
+ except Exception as e:
215
+ print(f"[錯誤] AI 分析失敗: {e},自動轉為錯誤備份輸出。")
216
+ report_content = f"# 輿情分析失敗\n\n錯誤詳情:{e}\n\n=== 原始數據 ===\n{raw_data}"
217
+
218
+ memory["report"] = report_content
219
+
220
+ # 步驟 3: 自動存檔
221
+ output_filename = f"ai_analysis_report_{source_type}.md"
222
+ try:
223
+ saved_file = tool_save_report(output_filename, memory["report"])
224
+ print(f"\n[步驟 3] 任務報告生成成功!已儲存至: {saved_file}")
225
+ print("=" * 70)
226
+ except Exception as e:
227
+ print(f"[錯誤] 儲存報告失敗: {e}")
228
+
229
+ if __name__ == "__main__":
230
+ # 可以切換為 "ptt" 或 "ithelp" 進行測試
231
+ run_analysis_chain("ptt")
projects/2026_AI營1/劉宗穎/眼睛距离侦测/step0_eyeguard_template.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Week 12 - AI 生活工具箱:護眼大師 (Template 填空版)
3
+ =====================================================
4
+ 本週目標:
5
+ 1. 將 AI 視覺技術整合進一個「解決生活痛點」的完整專案
6
+ 2. 使用 MediaPipe 臉部偵測兩眼距離,判斷是否離螢幕太近
7
+ 3. 在 Pygame 畫面上顯示即時狀態與警報
8
+ 4. 加入防呆機制(try-except、None 檢查)
9
+
10
+ 本範例:護眼大師 EyeGuard
11
+ - 偵測臉部兩眼之間的距離(兩眼距離越大 = 離鏡頭越近)
12
+ - 超過安全閾值時發出警報文字 + 閃爍效果
13
+ - 適用場景:打電動、寫作業時防止坐姿太差靠太近
14
+
15
+ 注意:
16
+ - 確保 face_landmarker.task 檔案和這個腳本放在同一個資料夾。
17
+ """
18
+ import pygame
19
+ import cv2
20
+ import mediapipe as mp
21
+ from mediapipe.tasks import python
22
+ from mediapipe.tasks.python import vision
23
+ import math
24
+ import sys
25
+ import os
26
+
27
+ # ============================================
28
+ # 1. 初始化
29
+ # ============================================
30
+ pygame.init()
31
+ pygame.mixer.init()
32
+ WIDTH, HEIGHT = 800, 600
33
+ screen = pygame.display.set_mode((WIDTH, HEIGHT))
34
+ pygame.display.set_caption("Week 12 - 護眼大師 EyeGuard 👁️🛡️")
35
+ clock = pygame.time.Clock()
36
+
37
+ font = pygame.font.SysFont(None, 48)
38
+ small_font = pygame.font.SysFont(None, 28)
39
+ big_font = pygame.font.SysFont(None, 72)
40
+
41
+ # ============================================
42
+ # 2. 攝影機與 AI 模組
43
+ # ============================================
44
+ cap = cv2.VideoCapture(0)
45
+ # 沒有攝影機就改成影片:cap = cv2.VideoCapture("backup_face_video.mp4")
46
+
47
+ # 【TODO 1】初始化 MediaPipe 臉部偵測 (Face Landmarker)
48
+ base_options = python.BaseOptions(model_asset_path=os.path.join('face_landmarker.task'))
49
+ options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1)
50
+ detector = vision.FaceLandmarker.create_from_options(options)
51
+
52
+ # 提示:左眼的 Landmark 索引為 33,右眼為 263
53
+
54
+ # ============================================
55
+ # 3. 遊戲變數
56
+ # ============================================
57
+ eye_distance = 0
58
+ DANGER_THRESHOLD = 120 # 兩眼像素距離超過此值 = 太近了!
59
+
60
+ # 【TODO 2】警報狀態變數
61
+ is_too_close = False
62
+ was_too_close = False # 邊緣觸發
63
+ warning_timer = 0 # 警告閃爍計時器
64
+
65
+ # 統計數據
66
+ total_warnings = 0
67
+ safe_seconds = 0
68
+ frame_counter = 0
69
+
70
+ # ============================================
71
+ # 4. 主迴圈
72
+ # ============================================
73
+ running = True
74
+ while running:
75
+ for event in pygame.event.get():
76
+ if event.type == pygame.QUIT:
77
+ running = False
78
+
79
+ # --- 讀取攝影機 ---
80
+ success, img = cap.read()
81
+ if not success:
82
+ cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
83
+ continue
84
+
85
+ img = cv2.flip(img, 1)
86
+ frame_counter += 1
87
+
88
+ # 【TODO 3】臉部偵測與兩眼距離計算
89
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
90
+ mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb)
91
+ results = detector.detect(mp_image)
92
+
93
+ if results.face_landmarks:
94
+ face_lms = results.face_landmarks[0]
95
+ #
96
+ # # 取得左眼 (33) 和右眼 (263) 座標
97
+ left_eye = face_lms[33]
98
+ right_eye = face_lms[263]
99
+ #
100
+ lx, ly = int(left_eye.x * WIDTH), int(left_eye.y * HEIGHT)
101
+ rx, ry = int(right_eye.x * WIDTH), int(right_eye.y * HEIGHT)
102
+
103
+ # 計算兩眼之間的像素距離
104
+ eye_distance = math.hypot(rx - lx, ry - ly)
105
+
106
+ # 【TODO 4】判斷是否太近
107
+ is_too_close = eye_distance > DANGER_THRESHOLD
108
+
109
+ # 【TODO 5】邊緣觸發:剛進入危險區才播放警報
110
+ if is_too_close and not was_too_close:
111
+ total_warnings += 1
112
+ warning_timer = 60 # 閃爍 60 幀
113
+ print(f"⚠️ 第 {total_warnings} 次警告:你離螢幕太近了!")
114
+
115
+ was_too_close = is_too_close
116
+
117
+ # --- 繪製畫面 ---
118
+ # 【TODO 6】根據狀態切換背景顏色
119
+ if is_too_close:
120
+ if warning_timer > 0 and warning_timer % 4 < 2:
121
+ screen.fill((80, 20, 20)) # 閃爍紅色
122
+ else:
123
+ screen.fill((50, 15, 15)) # 暗紅色
124
+ warning_timer = max(0, warning_timer - 1)
125
+ else:
126
+ screen.fill((15, 30, 20)) # 安全綠色
127
+ screen.fill((20, 20, 35)) # 預設背景
128
+
129
+ # 【TODO 7】繪製距離量表
130
+ # 提示:用 pygame.draw.rect 畫一個長條,長度 = eye_distance 的比例
131
+ # 安全時綠色,危險時紅色
132
+ if is_too_close:
133
+ pygame.draw.rect(screen, (255, 80, 80), (50, HEIGHT - 50, min(eye_distance * 2, WIDTH - 100), 20))
134
+ else:
135
+ pygame.draw.rect(screen, (80, 200, 100), (50, HEIGHT - 50, min(eye_distance * 2, WIDTH - 100), 20))
136
+ # 【TODO 8】如果太近,顯示大字警告
137
+ if is_too_close:
138
+ warn_text = big_font.render("⚠️ TOO CLOSE!", True, (255, 80, 80))
139
+ screen.blit(warn_text, (WIDTH//2 - warn_text.get_width()//2, HEIGHT//2 - 40))
140
+ else:
141
+ safe_text = font.render("✅ Safe Distance", True, (80, 200, 100))
142
+ screen.blit(safe_text, (WIDTH//2 - safe_text.get_width()//2, HEIGHT//2 - 20))
143
+
144
+ # 顯示數值
145
+ info = small_font.render(f"Eye Distance: {eye_distance:.0f} px | Threshold: {DANGER_THRESHOLD}", True, (150, 150, 180))
146
+ screen.blit(info, (10, 10))
147
+
148
+ stats = small_font.render(f"Warnings: {total_warnings}", True, (150, 150, 180))
149
+ screen.blit(stats, (10, 40))
150
+
151
+ pygame.display.update()
152
+ clock.tick(30)
153
+
154
+ cap.release()
155
+ pygame.quit()
156
+ sys.exit()
projects/2026_AI營1/劉宗穎/脸部侦测/face_mesh_connections.py ADDED
@@ -0,0 +1,500 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2021 The MediaPipe Authors.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """MediaPipe FaceMesh connections."""
15
+
16
+ FACEMESH_LIPS = frozenset([(61, 146), (146, 91), (91, 181), (181, 84), (84, 17),
17
+ (17, 314), (314, 405), (405, 321), (321, 375),
18
+ (375, 291), (61, 185), (185, 40), (40, 39), (39, 37),
19
+ (37, 0), (0, 267),
20
+ (267, 269), (269, 270), (270, 409), (409, 291),
21
+ (78, 95), (95, 88), (88, 178), (178, 87), (87, 14),
22
+ (14, 317), (317, 402), (402, 318), (318, 324),
23
+ (324, 308), (78, 191), (191, 80), (80, 81), (81, 82),
24
+ (82, 13), (13, 312), (312, 311), (311, 310),
25
+ (310, 415), (415, 308)])
26
+
27
+ FACEMESH_LEFT_EYE = frozenset([(263, 249), (249, 390), (390, 373), (373, 374),
28
+ (374, 380), (380, 381), (381, 382), (382, 362),
29
+ (263, 466), (466, 388), (388, 387), (387, 386),
30
+ (386, 385), (385, 384), (384, 398), (398, 362)])
31
+
32
+ FACEMESH_LEFT_IRIS = frozenset([(474, 475), (475, 476), (476, 477),
33
+ (477, 474)])
34
+
35
+ FACEMESH_LEFT_EYEBROW = frozenset([(276, 283), (283, 282), (282, 295),
36
+ (295, 285), (300, 293), (293, 334),
37
+ (334, 296), (296, 336)])
38
+
39
+ FACEMESH_RIGHT_EYE = frozenset([(33, 7), (7, 163), (163, 144), (144, 145),
40
+ (145, 153), (153, 154), (154, 155), (155, 133),
41
+ (33, 246), (246, 161), (161, 160), (160, 159),
42
+ (159, 158), (158, 157), (157, 173), (173, 133)])
43
+
44
+ FACEMESH_RIGHT_EYEBROW = frozenset([(46, 53), (53, 52), (52, 65), (65, 55),
45
+ (70, 63), (63, 105), (105, 66), (66, 107)])
46
+
47
+ FACEMESH_RIGHT_IRIS = frozenset([(469, 470), (470, 471), (471, 472),
48
+ (472, 469)])
49
+
50
+ FACEMESH_FACE_OVAL = frozenset([(10, 338), (338, 297), (297, 332), (332, 284),
51
+ (284, 251), (251, 389), (389, 356), (356, 454),
52
+ (454, 323), (323, 361), (361, 288), (288, 397),
53
+ (397, 365), (365, 379), (379, 378), (378, 400),
54
+ (400, 377), (377, 152), (152, 148), (148, 176),
55
+ (176, 149), (149, 150), (150, 136), (136, 172),
56
+ (172, 58), (58, 132), (132, 93), (93, 234),
57
+ (234, 127), (127, 162), (162, 21), (21, 54),
58
+ (54, 103), (103, 67), (67, 109), (109, 10)])
59
+
60
+ FACEMESH_NOSE = frozenset([(168, 6), (6, 197), (197, 195), (195, 5),
61
+ (5, 4), (4, 1), (1, 19), (19, 94), (94, 2), (98, 97),
62
+ (97, 2), (2, 326), (326, 327), (327, 294),
63
+ (294, 278), (278, 344), (344, 440), (440, 275),
64
+ (275, 4), (4, 45), (45, 220), (220, 115), (115, 48),
65
+ (48, 64), (64, 98)])
66
+
67
+ FACEMESH_CONTOURS = frozenset().union(*[
68
+ FACEMESH_LIPS, FACEMESH_LEFT_EYE, FACEMESH_LEFT_EYEBROW, FACEMESH_RIGHT_EYE,
69
+ FACEMESH_RIGHT_EYEBROW, FACEMESH_FACE_OVAL
70
+ ])
71
+
72
+ FACEMESH_IRISES = frozenset().union(*[FACEMESH_LEFT_IRIS, FACEMESH_RIGHT_IRIS])
73
+
74
+ FACEMESH_TESSELATION = frozenset([
75
+ (127, 34), (34, 139), (139, 127), (11, 0), (0, 37), (37, 11),
76
+ (232, 231), (231, 120), (120, 232), (72, 37), (37, 39), (39, 72),
77
+ (128, 121), (121, 47), (47, 128), (232, 121), (121, 128), (128, 232),
78
+ (104, 69), (69, 67), (67, 104), (175, 171), (171, 148), (148, 175),
79
+ (118, 50), (50, 101), (101, 118), (73, 39), (39, 40), (40, 73),
80
+ (9, 151), (151, 108), (108, 9), (48, 115), (115, 131), (131, 48),
81
+ (194, 204), (204, 211), (211, 194), (74, 40), (40, 185), (185, 74),
82
+ (80, 42), (42, 183), (183, 80), (40, 92), (92, 186), (186, 40),
83
+ (230, 229), (229, 118), (118, 230), (202, 212), (212, 214), (214, 202),
84
+ (83, 18), (18, 17), (17, 83), (76, 61), (61, 146), (146, 76),
85
+ (160, 29), (29, 30), (30, 160), (56, 157), (157, 173), (173, 56),
86
+ (106, 204), (204, 194), (194, 106), (135, 214), (214, 192), (192, 135),
87
+ (203, 165), (165, 98), (98, 203), (21, 71), (71, 68), (68, 21),
88
+ (51, 45), (45, 4), (4, 51), (144, 24), (24, 23), (23, 144),
89
+ (77, 146), (146, 91), (91, 77), (205, 50), (50, 187), (187, 205),
90
+ (201, 200), (200, 18), (18, 201), (91, 106), (106, 182), (182, 91),
91
+ (90, 91), (91, 181), (181, 90), (85, 84), (84, 17), (17, 85),
92
+ (206, 203), (203, 36), (36, 206), (148, 171), (171, 140), (140, 148),
93
+ (92, 40), (40, 39), (39, 92), (193, 189), (189, 244), (244, 193),
94
+ (159, 158), (158, 28), (28, 159), (247, 246), (246, 161), (161, 247),
95
+ (236, 3), (3, 196), (196, 236), (54, 68), (68, 104), (104, 54),
96
+ (193, 168), (168, 8), (8, 193), (117, 228), (228, 31), (31, 117),
97
+ (189, 193), (193, 55), (55, 189), (98, 97), (97, 99), (99, 98),
98
+ (126, 47), (47, 100), (100, 126), (166, 79), (79, 218), (218, 166),
99
+ (155, 154), (154, 26), (26, 155), (209, 49), (49, 131), (131, 209),
100
+ (135, 136), (136, 150), (150, 135), (47, 126), (126, 217), (217, 47),
101
+ (223, 52), (52, 53), (53, 223), (45, 51), (51, 134), (134, 45),
102
+ (211, 170), (170, 140), (140, 211), (67, 69), (69, 108), (108, 67),
103
+ (43, 106), (106, 91), (91, 43), (230, 119), (119, 120), (120, 230),
104
+ (226, 130), (130, 247), (247, 226), (63, 53), (53, 52), (52, 63),
105
+ (238, 20), (20, 242), (242, 238), (46, 70), (70, 156), (156, 46),
106
+ (78, 62), (62, 96), (96, 78), (46, 53), (53, 63), (63, 46),
107
+ (143, 34), (34, 227), (227, 143), (123, 117), (117, 111), (111, 123),
108
+ (44, 125), (125, 19), (19, 44), (236, 134), (134, 51), (51, 236),
109
+ (216, 206), (206, 205), (205, 216), (154, 153), (153, 22), (22, 154),
110
+ (39, 37), (37, 167), (167, 39), (200, 201), (201, 208), (208, 200),
111
+ (36, 142), (142, 100), (100, 36), (57, 212), (212, 202), (202, 57),
112
+ (20, 60), (60, 99), (99, 20), (28, 158), (158, 157), (157, 28),
113
+ (35, 226), (226, 113), (113, 35), (160, 159), (159, 27), (27, 160),
114
+ (204, 202), (202, 210), (210, 204), (113, 225), (225, 46), (46, 113),
115
+ (43, 202), (202, 204), (204, 43), (62, 76), (76, 77), (77, 62),
116
+ (137, 123), (123, 116), (116, 137), (41, 38), (38, 72), (72, 41),
117
+ (203, 129), (129, 142), (142, 203), (64, 98), (98, 240), (240, 64),
118
+ (49, 102), (102, 64), (64, 49), (41, 73), (73, 74), (74, 41),
119
+ (212, 216), (216, 207), (207, 212), (42, 74), (74, 184), (184, 42),
120
+ (169, 170), (170, 211), (211, 169), (170, 149), (149, 176), (176, 170),
121
+ (105, 66), (66, 69), (69, 105), (122, 6), (6, 168), (168, 122),
122
+ (123, 147), (147, 187), (187, 123), (96, 77), (77, 90), (90, 96),
123
+ (65, 55), (55, 107), (107, 65), (89, 90), (90, 180), (180, 89),
124
+ (101, 100), (100, 120), (120, 101), (63, 105), (105, 104), (104, 63),
125
+ (93, 137), (137, 227), (227, 93), (15, 86), (86, 85), (85, 15),
126
+ (129, 102), (102, 49), (49, 129), (14, 87), (87, 86), (86, 14),
127
+ (55, 8), (8, 9), (9, 55), (100, 47), (47, 121), (121, 100),
128
+ (145, 23), (23, 22), (22, 145), (88, 89), (89, 179), (179, 88),
129
+ (6, 122), (122, 196), (196, 6), (88, 95), (95, 96), (96, 88),
130
+ (138, 172), (172, 136), (136, 138), (215, 58), (58, 172), (172, 215),
131
+ (115, 48), (48, 219), (219, 115), (42, 80), (80, 81), (81, 42),
132
+ (195, 3), (3, 51), (51, 195), (43, 146), (146, 61), (61, 43),
133
+ (171, 175), (175, 199), (199, 171), (81, 82), (82, 38), (38, 81),
134
+ (53, 46), (46, 225), (225, 53), (144, 163), (163, 110), (110, 144),
135
+ (52, 65), (65, 66), (66, 52), (229, 228), (228, 117), (117, 229),
136
+ (34, 127), (127, 234), (234, 34), (107, 108), (108, 69), (69, 107),
137
+ (109, 108), (108, 151), (151, 109), (48, 64), (64, 235), (235, 48),
138
+ (62, 78), (78, 191), (191, 62), (129, 209), (209, 126), (126, 129),
139
+ (111, 35), (35, 143), (143, 111), (117, 123), (123, 50), (50, 117),
140
+ (222, 65), (65, 52), (52, 222), (19, 125), (125, 141), (141, 19),
141
+ (221, 55), (55, 65), (65, 221), (3, 195), (195, 197), (197, 3),
142
+ (25, 7), (7, 33), (33, 25), (220, 237), (237, 44), (44, 220),
143
+ (70, 71), (71, 139), (139, 70), (122, 193), (193, 245), (245, 122),
144
+ (247, 130), (130, 33), (33, 247), (71, 21), (21, 162), (162, 71),
145
+ (170, 169), (169, 150), (150, 170), (188, 174), (174, 196), (196, 188),
146
+ (216, 186), (186, 92), (92, 216), (2, 97), (97, 167), (167, 2),
147
+ (141, 125), (125, 241), (241, 141), (164, 167), (167, 37), (37, 164),
148
+ (72, 38), (38, 12), (12, 72), (38, 82), (82, 13), (13, 38),
149
+ (63, 68), (68, 71), (71, 63), (226, 35), (35, 111), (111, 226),
150
+ (101, 50), (50, 205), (205, 101), (206, 92), (92, 165), (165, 206),
151
+ (209, 198), (198, 217), (217, 209), (165, 167), (167, 97), (97, 165),
152
+ (220, 115), (115, 218), (218, 220), (133, 112), (112, 243), (243, 133),
153
+ (239, 238), (238, 241), (241, 239), (214, 135), (135, 169), (169, 214),
154
+ (190, 173), (173, 133), (133, 190), (171, 208), (208, 32), (32, 171),
155
+ (125, 44), (44, 237), (237, 125), (86, 87), (87, 178), (178, 86),
156
+ (85, 86), (86, 179), (179, 85), (84, 85), (85, 180), (180, 84),
157
+ (83, 84), (84, 181), (181, 83), (201, 83), (83, 182), (182, 201),
158
+ (137, 93), (93, 132), (132, 137), (76, 62), (62, 183), (183, 76),
159
+ (61, 76), (76, 184), (184, 61), (57, 61), (61, 185), (185, 57),
160
+ (212, 57), (57, 186), (186, 212), (214, 207), (207, 187), (187, 214),
161
+ (34, 143), (143, 156), (156, 34), (79, 239), (239, 237), (237, 79),
162
+ (123, 137), (137, 177), (177, 123), (44, 1), (1, 4), (4, 44),
163
+ (201, 194), (194, 32), (32, 201), (64, 102), (102, 129), (129, 64),
164
+ (213, 215), (215, 138), (138, 213), (59, 166), (166, 219), (219, 59),
165
+ (242, 99), (99, 97), (97, 242), (2, 94), (94, 141), (141, 2),
166
+ (75, 59), (59, 235), (235, 75), (24, 110), (110, 228), (228, 24),
167
+ (25, 130), (130, 226), (226, 25), (23, 24), (24, 229), (229, 23),
168
+ (22, 23), (23, 230), (230, 22), (26, 22), (22, 231), (231, 26),
169
+ (112, 26), (26, 232), (232, 112), (189, 190), (190, 243), (243, 189),
170
+ (221, 56), (56, 190), (190, 221), (28, 56), (56, 221), (221, 28),
171
+ (27, 28), (28, 222), (222, 27), (29, 27), (27, 223), (223, 29),
172
+ (30, 29), (29, 224), (224, 30), (247, 30), (30, 225), (225, 247),
173
+ (238, 79), (79, 20), (20, 238), (166, 59), (59, 75), (75, 166),
174
+ (60, 75), (75, 240), (240, 60), (147, 177), (177, 215), (215, 147),
175
+ (20, 79), (79, 166), (166, 20), (187, 147), (147, 213), (213, 187),
176
+ (112, 233), (233, 244), (244, 112), (233, 128), (128, 245), (245, 233),
177
+ (128, 114), (114, 188), (188, 128), (114, 217), (217, 174), (174, 114),
178
+ (131, 115), (115, 220), (220, 131), (217, 198), (198, 236), (236, 217),
179
+ (198, 131), (131, 134), (134, 198), (177, 132), (132, 58), (58, 177),
180
+ (143, 35), (35, 124), (124, 143), (110, 163), (163, 7), (7, 110),
181
+ (228, 110), (110, 25), (25, 228), (356, 389), (389, 368), (368, 356),
182
+ (11, 302), (302, 267), (267, 11), (452, 350), (350, 349), (349, 452),
183
+ (302, 303), (303, 269), (269, 302), (357, 343), (343, 277), (277, 357),
184
+ (452, 453), (453, 357), (357, 452), (333, 332), (332, 297), (297, 333),
185
+ (175, 152), (152, 377), (377, 175), (347, 348), (348, 330), (330, 347),
186
+ (303, 304), (304, 270), (270, 303), (9, 336), (336, 337), (337, 9),
187
+ (278, 279), (279, 360), (360, 278), (418, 262), (262, 431), (431, 418),
188
+ (304, 408), (408, 409), (409, 304), (310, 415), (415, 407), (407, 310),
189
+ (270, 409), (409, 410), (410, 270), (450, 348), (348, 347), (347, 450),
190
+ (422, 430), (430, 434), (434, 422), (313, 314), (314, 17), (17, 313),
191
+ (306, 307), (307, 375), (375, 306), (387, 388), (388, 260), (260, 387),
192
+ (286, 414), (414, 398), (398, 286), (335, 406), (406, 418), (418, 335),
193
+ (364, 367), (367, 416), (416, 364), (423, 358), (358, 327), (327, 423),
194
+ (251, 284), (284, 298), (298, 251), (281, 5), (5, 4), (4, 281),
195
+ (373, 374), (374, 253), (253, 373), (307, 320), (320, 321), (321, 307),
196
+ (425, 427), (427, 411), (411, 425), (421, 313), (313, 18), (18, 421),
197
+ (321, 405), (405, 406), (406, 321), (320, 404), (404, 405), (405, 320),
198
+ (315, 16), (16, 17), (17, 315), (426, 425), (425, 266), (266, 426),
199
+ (377, 400), (400, 369), (369, 377), (322, 391), (391, 269), (269, 322),
200
+ (417, 465), (465, 464), (464, 417), (386, 257), (257, 258), (258, 386),
201
+ (466, 260), (260, 388), (388, 466), (456, 399), (399, 419), (419, 456),
202
+ (284, 332), (332, 333), (333, 284), (417, 285), (285, 8), (8, 417),
203
+ (346, 340), (340, 261), (261, 346), (413, 441), (441, 285), (285, 413),
204
+ (327, 460), (460, 328), (328, 327), (355, 371), (371, 329), (329, 355),
205
+ (392, 439), (439, 438), (438, 392), (382, 341), (341, 256), (256, 382),
206
+ (429, 420), (420, 360), (360, 429), (364, 394), (394, 379), (379, 364),
207
+ (277, 343), (343, 437), (437, 277), (443, 444), (444, 283), (283, 443),
208
+ (275, 440), (440, 363), (363, 275), (431, 262), (262, 369), (369, 431),
209
+ (297, 338), (338, 337), (337, 297), (273, 375), (375, 321), (321, 273),
210
+ (450, 451), (451, 349), (349, 450), (446, 342), (342, 467), (467, 446),
211
+ (293, 334), (334, 282), (282, 293), (458, 461), (461, 462), (462, 458),
212
+ (276, 353), (353, 383), (383, 276), (308, 324), (324, 325), (325, 308),
213
+ (276, 300), (300, 293), (293, 276), (372, 345), (345, 447), (447, 372),
214
+ (352, 345), (345, 340), (340, 352), (274, 1), (1, 19), (19, 274),
215
+ (456, 248), (248, 281), (281, 456), (436, 427), (427, 425), (425, 436),
216
+ (381, 256), (256, 252), (252, 381), (269, 391), (391, 393), (393, 269),
217
+ (200, 199), (199, 428), (428, 200), (266, 330), (330, 329), (329, 266),
218
+ (287, 273), (273, 422), (422, 287), (250, 462), (462, 328), (328, 250),
219
+ (258, 286), (286, 384), (384, 258), (265, 353), (353, 342), (342, 265),
220
+ (387, 259), (259, 257), (257, 387), (424, 431), (431, 430), (430, 424),
221
+ (342, 353), (353, 276), (276, 342), (273, 335), (335, 424), (424, 273),
222
+ (292, 325), (325, 307), (307, 292), (366, 447), (447, 345), (345, 366),
223
+ (271, 303), (303, 302), (302, 271), (423, 266), (266, 371), (371, 423),
224
+ (294, 455), (455, 460), (460, 294), (279, 278), (278, 294), (294, 279),
225
+ (271, 272), (272, 304), (304, 271), (432, 434), (434, 427), (427, 432),
226
+ (272, 407), (407, 408), (408, 272), (394, 430), (430, 431), (431, 394),
227
+ (395, 369), (369, 400), (400, 395), (334, 333), (333, 299), (299, 334),
228
+ (351, 417), (417, 168), (168, 351), (352, 280), (280, 411), (411, 352),
229
+ (325, 319), (319, 320), (320, 325), (295, 296), (296, 336), (336, 295),
230
+ (319, 403), (403, 404), (404, 319), (330, 348), (348, 349), (349, 330),
231
+ (293, 298), (298, 333), (333, 293), (323, 454), (454, 447), (447, 323),
232
+ (15, 16), (16, 315), (315, 15), (358, 429), (429, 279), (279, 358),
233
+ (14, 15), (15, 316), (316, 14), (285, 336), (336, 9), (9, 285),
234
+ (329, 349), (349, 350), (350, 329), (374, 380), (380, 252), (252, 374),
235
+ (318, 402), (402, 403), (403, 318), (6, 197), (197, 419), (419, 6),
236
+ (318, 319), (319, 325), (325, 318), (367, 364), (364, 365), (365, 367),
237
+ (435, 367), (367, 397), (397, 435), (344, 438), (438, 439), (439, 344),
238
+ (272, 271), (271, 311), (311, 272), (195, 5), (5, 281), (281, 195),
239
+ (273, 287), (287, 291), (291, 273), (396, 428), (428, 199), (199, 396),
240
+ (311, 271), (271, 268), (268, 311), (283, 444), (444, 445), (445, 283),
241
+ (373, 254), (254, 339), (339, 373), (282, 334), (334, 296), (296, 282),
242
+ (449, 347), (347, 346), (346, 449), (264, 447), (447, 454), (454, 264),
243
+ (336, 296), (296, 299), (299, 336), (338, 10), (10, 151), (151, 338),
244
+ (278, 439), (439, 455), (455, 278), (292, 407), (407, 415), (415, 292),
245
+ (358, 371), (371, 355), (355, 358), (340, 345), (345, 372), (372, 340),
246
+ (346, 347), (347, 280), (280, 346), (442, 443), (443, 282), (282, 442),
247
+ (19, 94), (94, 370), (370, 19), (441, 442), (442, 295), (295, 441),
248
+ (248, 419), (419, 197), (197, 248), (263, 255), (255, 359), (359, 263),
249
+ (440, 275), (275, 274), (274, 440), (300, 383), (383, 368), (368, 300),
250
+ (351, 412), (412, 465), (465, 351), (263, 467), (467, 466), (466, 263),
251
+ (301, 368), (368, 389), (389, 301), (395, 378), (378, 379), (379, 395),
252
+ (412, 351), (351, 419), (419, 412), (436, 426), (426, 322), (322, 436),
253
+ (2, 164), (164, 393), (393, 2), (370, 462), (462, 461), (461, 370),
254
+ (164, 0), (0, 267), (267, 164), (302, 11), (11, 12), (12, 302),
255
+ (268, 12), (12, 13), (13, 268), (293, 300), (300, 301), (301, 293),
256
+ (446, 261), (261, 340), (340, 446), (330, 266), (266, 425), (425, 330),
257
+ (426, 423), (423, 391), (391, 426), (429, 355), (355, 437), (437, 429),
258
+ (391, 327), (327, 326), (326, 391), (440, 457), (457, 438), (438, 440),
259
+ (341, 382), (382, 362), (362, 341), (459, 457), (457, 461), (461, 459),
260
+ (434, 430), (430, 394), (394, 434), (414, 463), (463, 362), (362, 414),
261
+ (396, 369), (369, 262), (262, 396), (354, 461), (461, 457), (457, 354),
262
+ (316, 403), (403, 402), (402, 316), (315, 404), (404, 403), (403, 315),
263
+ (314, 405), (405, 404), (404, 314), (313, 406), (406, 405), (405, 313),
264
+ (421, 418), (418, 406), (406, 421), (366, 401), (401, 361), (361, 366),
265
+ (306, 408), (408, 407), (407, 306), (291, 409), (409, 408), (408, 291),
266
+ (287, 410), (410, 409), (409, 287), (432, 436), (436, 410), (410, 432),
267
+ (434, 416), (416, 411), (411, 434), (264, 368), (368, 383), (383, 264),
268
+ (309, 438), (438, 457), (457, 309), (352, 376), (376, 401), (401, 352),
269
+ (274, 275), (275, 4), (4, 274), (421, 428), (428, 262), (262, 421),
270
+ (294, 327), (327, 358), (358, 294), (433, 416), (416, 367), (367, 433),
271
+ (289, 455), (455, 439), (439, 289), (462, 370), (370, 326), (326, 462),
272
+ (2, 326), (326, 370), (370, 2), (305, 460), (460, 455), (455, 305),
273
+ (254, 449), (449, 448), (448, 254), (255, 261), (261, 446), (446, 255),
274
+ (253, 450), (450, 449), (449, 253), (252, 451), (451, 450), (450, 252),
275
+ (256, 452), (452, 451), (451, 256), (341, 453), (453, 452), (452, 341),
276
+ (413, 464), (464, 463), (463, 413), (441, 413), (413, 414), (414, 441),
277
+ (258, 442), (442, 441), (441, 258), (257, 443), (443, 442), (442, 257),
278
+ (259, 444), (444, 443), (443, 259), (260, 445), (445, 444), (444, 260),
279
+ (467, 342), (342, 445), (445, 467), (459, 458), (458, 250), (250, 459),
280
+ (289, 392), (392, 290), (290, 289), (290, 328), (328, 460), (460, 290),
281
+ (376, 433), (433, 435), (435, 376), (250, 290), (290, 392), (392, 250),
282
+ (411, 416), (416, 433), (433, 411), (341, 463), (463, 464), (464, 341),
283
+ (453, 464), (464, 465), (465, 453), (357, 465), (465, 412), (412, 357),
284
+ (343, 412), (412, 399), (399, 343), (360, 363), (363, 440), (440, 360),
285
+ (437, 399), (399, 456), (456, 437), (420, 456), (456, 363), (363, 420),
286
+ (401, 435), (435, 288), (288, 401), (372, 383), (383, 353), (353, 372),
287
+ (339, 255), (255, 249), (249, 339), (448, 261), (261, 255), (255, 448),
288
+ (133, 243), (243, 190), (190, 133), (133, 155), (155, 112), (112, 133),
289
+ (33, 246), (246, 247), (247, 33), (33, 130), (130, 25), (25, 33),
290
+ (398, 384), (384, 286), (286, 398), (362, 398), (398, 414), (414, 362),
291
+ (362, 463), (463, 341), (341, 362), (263, 359), (359, 467), (467, 263),
292
+ (263, 249), (249, 255), (255, 263), (466, 467), (467, 260), (260, 466),
293
+ (75, 60), (60, 166), (166, 75), (238, 239), (239, 79), (79, 238),
294
+ (162, 127), (127, 139), (139, 162), (72, 11), (11, 37), (37, 72),
295
+ (121, 232), (232, 120), (120, 121), (73, 72), (72, 39), (39, 73),
296
+ (114, 128), (128, 47), (47, 114), (233, 232), (232, 128), (128, 233),
297
+ (103, 104), (104, 67), (67, 103), (152, 175), (175, 148), (148, 152),
298
+ (119, 118), (118, 101), (101, 119), (74, 73), (73, 40), (40, 74),
299
+ (107, 9), (9, 108), (108, 107), (49, 48), (48, 131), (131, 49),
300
+ (32, 194), (194, 211), (211, 32), (184, 74), (74, 185), (185, 184),
301
+ (191, 80), (80, 183), (183, 191), (185, 40), (40, 186), (186, 185),
302
+ (119, 230), (230, 118), (118, 119), (210, 202), (202, 214), (214, 210),
303
+ (84, 83), (83, 17), (17, 84), (77, 76), (76, 146), (146, 77),
304
+ (161, 160), (160, 30), (30, 161), (190, 56), (56, 173), (173, 190),
305
+ (182, 106), (106, 194), (194, 182), (138, 135), (135, 192), (192, 138),
306
+ (129, 203), (203, 98), (98, 129), (54, 21), (21, 68), (68, 54),
307
+ (5, 51), (51, 4), (4, 5), (145, 144), (144, 23), (23, 145),
308
+ (90, 77), (77, 91), (91, 90), (207, 205), (205, 187), (187, 207),
309
+ (83, 201), (201, 18), (18, 83), (181, 91), (91, 182), (182, 181),
310
+ (180, 90), (90, 181), (181, 180), (16, 85), (85, 17), (17, 16),
311
+ (205, 206), (206, 36), (36, 205), (176, 148), (148, 140), (140, 176),
312
+ (165, 92), (92, 39), (39, 165), (245, 193), (193, 244), (244, 245),
313
+ (27, 159), (159, 28), (28, 27), (30, 247), (247, 161), (161, 30),
314
+ (174, 236), (236, 196), (196, 174), (103, 54), (54, 104), (104, 103),
315
+ (55, 193), (193, 8), (8, 55), (111, 117), (117, 31), (31, 111),
316
+ (221, 189), (189, 55), (55, 221), (240, 98), (98, 99), (99, 240),
317
+ (142, 126), (126, 100), (100, 142), (219, 166), (166, 218), (218, 219),
318
+ (112, 155), (155, 26), (26, 112), (198, 209), (209, 131), (131, 198),
319
+ (169, 135), (135, 150), (150, 169), (114, 47), (47, 217), (217, 114),
320
+ (224, 223), (223, 53), (53, 224), (220, 45), (45, 134), (134, 220),
321
+ (32, 211), (211, 140), (140, 32), (109, 67), (67, 108), (108, 109),
322
+ (146, 43), (43, 91), (91, 146), (231, 230), (230, 120), (120, 231),
323
+ (113, 226), (226, 247), (247, 113), (105, 63), (63, 52), (52, 105),
324
+ (241, 238), (238, 242), (242, 241), (124, 46), (46, 156), (156, 124),
325
+ (95, 78), (78, 96), (96, 95), (70, 46), (46, 63), (63, 70),
326
+ (116, 143), (143, 227), (227, 116), (116, 123), (123, 111), (111, 116),
327
+ (1, 44), (44, 19), (19, 1), (3, 236), (236, 51), (51, 3),
328
+ (207, 216), (216, 205), (205, 207), (26, 154), (154, 22), (22, 26),
329
+ (165, 39), (39, 167), (167, 165), (199, 200), (200, 208), (208, 199),
330
+ (101, 36), (36, 100), (100, 101), (43, 57), (57, 202), (202, 43),
331
+ (242, 20), (20, 99), (99, 242), (56, 28), (28, 157), (157, 56),
332
+ (124, 35), (35, 113), (113, 124), (29, 160), (160, 27), (27, 29),
333
+ (211, 204), (204, 210), (210, 211), (124, 113), (113, 46), (46, 124),
334
+ (106, 43), (43, 204), (204, 106), (96, 62), (62, 77), (77, 96),
335
+ (227, 137), (137, 116), (116, 227), (73, 41), (41, 72), (72, 73),
336
+ (36, 203), (203, 142), (142, 36), (235, 64), (64, 240), (240, 235),
337
+ (48, 49), (49, 64), (64, 48), (42, 41), (41, 74), (74, 42),
338
+ (214, 212), (212, 207), (207, 214), (183, 42), (42, 184), (184, 183),
339
+ (210, 169), (169, 211), (211, 210), (140, 170), (170, 176), (176, 140),
340
+ (104, 105), (105, 69), (69, 104), (193, 122), (122, 168), (168, 193),
341
+ (50, 123), (123, 187), (187, 50), (89, 96), (96, 90), (90, 89),
342
+ (66, 65), (65, 107), (107, 66), (179, 89), (89, 180), (180, 179),
343
+ (119, 101), (101, 120), (120, 119), (68, 63), (63, 104), (104, 68),
344
+ (234, 93), (93, 227), (227, 234), (16, 15), (15, 85), (85, 16),
345
+ (209, 129), (129, 49), (49, 209), (15, 14), (14, 86), (86, 15),
346
+ (107, 55), (55, 9), (9, 107), (120, 100), (100, 121), (121, 120),
347
+ (153, 145), (145, 22), (22, 153), (178, 88), (88, 179), (179, 178),
348
+ (197, 6), (6, 196), (196, 197), (89, 88), (88, 96), (96, 89),
349
+ (135, 138), (138, 136), (136, 135), (138, 215), (215, 172), (172, 138),
350
+ (218, 115), (115, 219), (219, 218), (41, 42), (42, 81), (81, 41),
351
+ (5, 195), (195, 51), (51, 5), (57, 43), (43, 61), (61, 57),
352
+ (208, 171), (171, 199), (199, 208), (41, 81), (81, 38), (38, 41),
353
+ (224, 53), (53, 225), (225, 224), (24, 144), (144, 110), (110, 24),
354
+ (105, 52), (52, 66), (66, 105), (118, 229), (229, 117), (117, 118),
355
+ (227, 34), (34, 234), (234, 227), (66, 107), (107, 69), (69, 66),
356
+ (10, 109), (109, 151), (151, 10), (219, 48), (48, 235), (235, 219),
357
+ (183, 62), (62, 191), (191, 183), (142, 129), (129, 126), (126, 142),
358
+ (116, 111), (111, 143), (143, 116), (118, 117), (117, 50), (50, 118),
359
+ (223, 222), (222, 52), (52, 223), (94, 19), (19, 141), (141, 94),
360
+ (222, 221), (221, 65), (65, 222), (196, 3), (3, 197), (197, 196),
361
+ (45, 220), (220, 44), (44, 45), (156, 70), (70, 139), (139, 156),
362
+ (188, 122), (122, 245), (245, 188), (139, 71), (71, 162), (162, 139),
363
+ (149, 170), (170, 150), (150, 149), (122, 188), (188, 196), (196, 122),
364
+ (206, 216), (216, 92), (92, 206), (164, 2), (2, 167), (167, 164),
365
+ (242, 141), (141, 241), (241, 242), (0, 164), (164, 37), (37, 0),
366
+ (11, 72), (72, 12), (12, 11), (12, 38), (38, 13), (13, 12),
367
+ (70, 63), (63, 71), (71, 70), (31, 226), (226, 111), (111, 31),
368
+ (36, 101), (101, 205), (205, 36), (203, 206), (206, 165), (165, 203),
369
+ (126, 209), (209, 217), (217, 126), (98, 165), (165, 97), (97, 98),
370
+ (237, 220), (220, 218), (218, 237), (237, 239), (239, 241), (241, 237),
371
+ (210, 214), (214, 169), (169, 210), (140, 171), (171, 32), (32, 140),
372
+ (241, 125), (125, 237), (237, 241), (179, 86), (86, 178), (178, 179),
373
+ (180, 85), (85, 179), (179, 180), (181, 84), (84, 180), (180, 181),
374
+ (182, 83), (83, 181), (181, 182), (194, 201), (201, 182), (182, 194),
375
+ (177, 137), (137, 132), (132, 177), (184, 76), (76, 183), (183, 184),
376
+ (185, 61), (61, 184), (184, 185), (186, 57), (57, 185), (185, 186),
377
+ (216, 212), (212, 186), (186, 216), (192, 214), (214, 187), (187, 192),
378
+ (139, 34), (34, 156), (156, 139), (218, 79), (79, 237), (237, 218),
379
+ (147, 123), (123, 177), (177, 147), (45, 44), (44, 4), (4, 45),
380
+ (208, 201), (201, 32), (32, 208), (98, 64), (64, 129), (129, 98),
381
+ (192, 213), (213, 138), (138, 192), (235, 59), (59, 219), (219, 235),
382
+ (141, 242), (242, 97), (97, 141), (97, 2), (2, 141), (141, 97),
383
+ (240, 75), (75, 235), (235, 240), (229, 24), (24, 228), (228, 229),
384
+ (31, 25), (25, 226), (226, 31), (230, 23), (23, 229), (229, 230),
385
+ (231, 22), (22, 230), (230, 231), (232, 26), (26, 231), (231, 232),
386
+ (233, 112), (112, 232), (232, 233), (244, 189), (189, 243), (243, 244),
387
+ (189, 221), (221, 190), (190, 189), (222, 28), (28, 221), (221, 222),
388
+ (223, 27), (27, 222), (222, 223), (224, 29), (29, 223), (223, 224),
389
+ (225, 30), (30, 224), (224, 225), (113, 247), (247, 225), (225, 113),
390
+ (99, 60), (60, 240), (240, 99), (213, 147), (147, 215), (215, 213),
391
+ (60, 20), (20, 166), (166, 60), (192, 187), (187, 213), (213, 192),
392
+ (243, 112), (112, 244), (244, 243), (244, 233), (233, 245), (245, 244),
393
+ (245, 128), (128, 188), (188, 245), (188, 114), (114, 174), (174, 188),
394
+ (134, 131), (131, 220), (220, 134), (174, 217), (217, 236), (236, 174),
395
+ (236, 198), (198, 134), (134, 236), (215, 177), (177, 58), (58, 215),
396
+ (156, 143), (143, 124), (124, 156), (25, 110), (110, 7), (7, 25),
397
+ (31, 228), (228, 25), (25, 31), (264, 356), (356, 368), (368, 264),
398
+ (0, 11), (11, 267), (267, 0), (451, 452), (452, 349), (349, 451),
399
+ (267, 302), (302, 269), (269, 267), (350, 357), (357, 277), (277, 350),
400
+ (350, 452), (452, 357), (357, 350), (299, 333), (333, 297), (297, 299),
401
+ (396, 175), (175, 377), (377, 396), (280, 347), (347, 330), (330, 280),
402
+ (269, 303), (303, 270), (270, 269), (151, 9), (9, 337), (337, 151),
403
+ (344, 278), (278, 360), (360, 344), (424, 418), (418, 431), (431, 424),
404
+ (270, 304), (304, 409), (409, 270), (272, 310), (310, 407), (407, 272),
405
+ (322, 270), (270, 410), (410, 322), (449, 450), (450, 347), (347, 449),
406
+ (432, 422), (422, 434), (434, 432), (18, 313), (313, 17), (17, 18),
407
+ (291, 306), (306, 375), (375, 291), (259, 387), (387, 260), (260, 259),
408
+ (424, 335), (335, 418), (418, 424), (434, 364), (364, 416), (416, 434),
409
+ (391, 423), (423, 327), (327, 391), (301, 251), (251, 298), (298, 301),
410
+ (275, 281), (281, 4), (4, 275), (254, 373), (373, 253), (253, 254),
411
+ (375, 307), (307, 321), (321, 375), (280, 425), (425, 411), (411, 280),
412
+ (200, 421), (421, 18), (18, 200), (335, 321), (321, 406), (406, 335),
413
+ (321, 320), (320, 405), (405, 321), (314, 315), (315, 17), (17, 314),
414
+ (423, 426), (426, 266), (266, 423), (396, 377), (377, 369), (369, 396),
415
+ (270, 322), (322, 269), (269, 270), (413, 417), (417, 464), (464, 413),
416
+ (385, 386), (386, 258), (258, 385), (248, 456), (456, 419), (419, 248),
417
+ (298, 284), (284, 333), (333, 298), (168, 417), (417, 8), (8, 168),
418
+ (448, 346), (346, 261), (261, 448), (417, 413), (413, 285), (285, 417),
419
+ (326, 327), (327, 328), (328, 326), (277, 355), (355, 329), (329, 277),
420
+ (309, 392), (392, 438), (438, 309), (381, 382), (382, 256), (256, 381),
421
+ (279, 429), (429, 360), (360, 279), (365, 364), (364, 379), (379, 365),
422
+ (355, 277), (277, 437), (437, 355), (282, 443), (443, 283), (283, 282),
423
+ (281, 275), (275, 363), (363, 281), (395, 431), (431, 369), (369, 395),
424
+ (299, 297), (297, 337), (337, 299), (335, 273), (273, 321), (321, 335),
425
+ (348, 450), (450, 349), (349, 348), (359, 446), (446, 467), (467, 359),
426
+ (283, 293), (293, 282), (282, 283), (250, 458), (458, 462), (462, 250),
427
+ (300, 276), (276, 383), (383, 300), (292, 308), (308, 325), (325, 292),
428
+ (283, 276), (276, 293), (293, 283), (264, 372), (372, 447), (447, 264),
429
+ (346, 352), (352, 340), (340, 346), (354, 274), (274, 19), (19, 354),
430
+ (363, 456), (456, 281), (281, 363), (426, 436), (436, 425), (425, 426),
431
+ (380, 381), (381, 252), (252, 380), (267, 269), (269, 393), (393, 267),
432
+ (421, 200), (200, 428), (428, 421), (371, 266), (266, 329), (329, 371),
433
+ (432, 287), (287, 422), (422, 432), (290, 250), (250, 328), (328, 290),
434
+ (385, 258), (258, 384), (384, 385), (446, 265), (265, 342), (342, 446),
435
+ (386, 387), (387, 257), (257, 386), (422, 424), (424, 430), (430, 422),
436
+ (445, 342), (342, 276), (276, 445), (422, 273), (273, 424), (424, 422),
437
+ (306, 292), (292, 307), (307, 306), (352, 366), (366, 345), (345, 352),
438
+ (268, 271), (271, 302), (302, 268), (358, 423), (423, 371), (371, 358),
439
+ (327, 294), (294, 460), (460, 327), (331, 279), (279, 294), (294, 331),
440
+ (303, 271), (271, 304), (304, 303), (436, 432), (432, 427), (427, 436),
441
+ (304, 272), (272, 408), (408, 304), (395, 394), (394, 431), (431, 395),
442
+ (378, 395), (395, 400), (400, 378), (296, 334), (334, 299), (299, 296),
443
+ (6, 351), (351, 168), (168, 6), (376, 352), (352, 411), (411, 376),
444
+ (307, 325), (325, 320), (320, 307), (285, 295), (295, 336), (336, 285),
445
+ (320, 319), (319, 404), (404, 320), (329, 330), (330, 349), (349, 329),
446
+ (334, 293), (293, 333), (333, 334), (366, 323), (323, 447), (447, 366),
447
+ (316, 15), (15, 315), (315, 316), (331, 358), (358, 279), (279, 331),
448
+ (317, 14), (14, 316), (316, 317), (8, 285), (285, 9), (9, 8),
449
+ (277, 329), (329, 350), (350, 277), (253, 374), (374, 252), (252, 253),
450
+ (319, 318), (318, 403), (403, 319), (351, 6), (6, 419), (419, 351),
451
+ (324, 318), (318, 325), (325, 324), (397, 367), (367, 365), (365, 397),
452
+ (288, 435), (435, 397), (397, 288), (278, 344), (344, 439), (439, 278),
453
+ (310, 272), (272, 311), (311, 310), (248, 195), (195, 281), (281, 248),
454
+ (375, 273), (273, 291), (291, 375), (175, 396), (396, 199), (199, 175),
455
+ (312, 311), (311, 268), (268, 312), (276, 283), (283, 445), (445, 276),
456
+ (390, 373), (373, 339), (339, 390), (295, 282), (282, 296), (296, 295),
457
+ (448, 449), (449, 346), (346, 448), (356, 264), (264, 454), (454, 356),
458
+ (337, 336), (336, 299), (299, 337), (337, 338), (338, 151), (151, 337),
459
+ (294, 278), (278, 455), (455, 294), (308, 292), (292, 415), (415, 308),
460
+ (429, 358), (358, 355), (355, 429), (265, 340), (340, 372), (372, 265),
461
+ (352, 346), (346, 280), (280, 352), (295, 442), (442, 282), (282, 295),
462
+ (354, 19), (19, 370), (370, 354), (285, 441), (441, 295), (295, 285),
463
+ (195, 248), (248, 197), (197, 195), (457, 440), (440, 274), (274, 457),
464
+ (301, 300), (300, 368), (368, 301), (417, 351), (351, 465), (465, 417),
465
+ (251, 301), (301, 389), (389, 251), (394, 395), (395, 379), (379, 394),
466
+ (399, 412), (412, 419), (419, 399), (410, 436), (436, 322), (322, 410),
467
+ (326, 2), (2, 393), (393, 326), (354, 370), (370, 461), (461, 354),
468
+ (393, 164), (164, 267), (267, 393), (268, 302), (302, 12), (12, 268),
469
+ (312, 268), (268, 13), (13, 312), (298, 293), (293, 301), (301, 298),
470
+ (265, 446), (446, 340), (340, 265), (280, 330), (330, 425), (425, 280),
471
+ (322, 426), (426, 391), (391, 322), (420, 429), (429, 437), (437, 420),
472
+ (393, 391), (391, 326), (326, 393), (344, 440), (440, 438), (438, 344),
473
+ (458, 459), (459, 461), (461, 458), (364, 434), (434, 394), (394, 364),
474
+ (428, 396), (396, 262), (262, 428), (274, 354), (354, 457), (457, 274),
475
+ (317, 316), (316, 402), (402, 317), (316, 315), (315, 403), (403, 316),
476
+ (315, 314), (314, 404), (404, 315), (314, 313), (313, 405), (405, 314),
477
+ (313, 421), (421, 406), (406, 313), (323, 366), (366, 361), (361, 323),
478
+ (292, 306), (306, 407), (407, 292), (306, 291), (291, 408), (408, 306),
479
+ (291, 287), (287, 409), (409, 291), (287, 432), (432, 410), (410, 287),
480
+ (427, 434), (434, 411), (411, 427), (372, 264), (264, 383), (383, 372),
481
+ (459, 309), (309, 457), (457, 459), (366, 352), (352, 401), (401, 366),
482
+ (1, 274), (274, 4), (4, 1), (418, 421), (421, 262), (262, 418),
483
+ (331, 294), (294, 358), (358, 331), (435, 433), (433, 367), (367, 435),
484
+ (392, 289), (289, 439), (439, 392), (328, 462), (462, 326), (326, 328),
485
+ (94, 2), (2, 370), (370, 94), (289, 305), (305, 455), (455, 289),
486
+ (339, 254), (254, 448), (448, 339), (359, 255), (255, 446), (446, 359),
487
+ (254, 253), (253, 449), (449, 254), (253, 252), (252, 450), (450, 253),
488
+ (252, 256), (256, 451), (451, 252), (256, 341), (341, 452), (452, 256),
489
+ (414, 413), (413, 463), (463, 414), (286, 441), (441, 414), (414, 286),
490
+ (286, 258), (258, 441), (441, 286), (258, 257), (257, 442), (442, 258),
491
+ (257, 259), (259, 443), (443, 257), (259, 260), (260, 444), (444, 259),
492
+ (260, 467), (467, 445), (445, 260), (309, 459), (459, 250), (250, 309),
493
+ (305, 289), (289, 290), (290, 305), (305, 290), (290, 460), (460, 305),
494
+ (401, 376), (376, 435), (435, 401), (309, 250), (250, 392), (392, 309),
495
+ (376, 411), (411, 433), (433, 376), (453, 341), (341, 464), (464, 453),
496
+ (357, 453), (453, 465), (465, 357), (343, 357), (357, 412), (412, 343),
497
+ (437, 343), (343, 399), (399, 437), (344, 360), (360, 440), (440, 344),
498
+ (420, 437), (437, 456), (456, 420), (360, 420), (420, 363), (363, 360),
499
+ (361, 401), (401, 288), (288, 361), (265, 372), (372, 353), (353, 265),
500
+ (390, 339), (339, 249), (249, 390), (339, 448), (448, 255), (255, 339)])
projects/2026_AI營1/劉宗穎/脸部侦测/step1_facemesh.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Week 11 - Step 1: Face Mesh Tracking with Connections (Template 填空版)
3
+ ===================================================================
4
+ 本步驟目標:
5
+ 1. 使用 OpenCV 開啟視訊鏡頭
6
+ 2. 載入 MediaPipe 臉部特徵偵測器 (face_landmarker.task)
7
+ 3. 偵測人臉,並在臉上畫出綠色的 3D 特效網格線與黃色特徵點
8
+ """
9
+ import cv2
10
+ import mediapipe as mp
11
+ import numpy as np
12
+ import sys
13
+ import os
14
+
15
+ # 1. 初始化攝影機
16
+ cap = cv2.VideoCapture(0)
17
+ if not cap.isOpened():
18
+ print("Error: Could not open camera.")
19
+ sys.exit()
20
+
21
+ # 2. 載入 MediaPipe 臉部偵測器模型 (Face Landmarker)
22
+ model_path = os.path.join('face_landmarker.task')
23
+ if not os.path.exists(model_path):
24
+ print(f"Error: Model file not found at {model_path}")
25
+ print("Please make sure face_landmarker.task is in the examples folder.")
26
+ sys.exit()
27
+
28
+ try:
29
+ from mediapipe.tasks import python
30
+ from mediapipe.tasks.python import vision
31
+ base_options = python.BaseOptions(model_asset_path=model_path)
32
+ options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1)
33
+ detector = vision.FaceLandmarker.create_from_options(options)
34
+ print("Face Landmarker Tasks API initialized successfully.")
35
+ except Exception as e:
36
+ print(f"Error initializing detector: {e}")
37
+ sys.exit()
38
+
39
+ # 匯入臉部網格連接點定義
40
+ try:
41
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
42
+ from face_mesh_connections import FACEMESH_TESSELATION
43
+ except ImportError:
44
+ FACEMESH_TESSELATION = []
45
+
46
+ # 繪圖模式:0 = 僅畫網格線, 1 = 僅畫點點, 2 = 點線都畫
47
+ draw_mode = 0
48
+
49
+ print("Press 'q' in the window to exit, or 'm' to toggle display modes.")
50
+
51
+ # 3. 主迴圈
52
+ while True:
53
+ success, img = cap.read()
54
+ if not success:
55
+ print("Error: Failed to read frame.")
56
+ break
57
+
58
+ # 【TODO 1】畫面水平翻轉,並將 BGR 轉成 MediaPipe 需要的 RGB 格式
59
+ img = cv2.flip(img, 1)
60
+ img_h, img_w, _ = img.shape
61
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
62
+ mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb)
63
+
64
+
65
+ # 執行臉部偵測
66
+ results = detector.detect(mp_image)
67
+
68
+ # 【TODO 2】如果偵測到臉部,畫出 3D 特效網格線
69
+ # 提示:取出 results.face_landmarks[0],並用迴圈巡覽 FACEMESH_TESSELATION
70
+ # 點的座標 (lm.x, lm.y) 是 0~1 的比例,需要乘上 img_w 和 img_h 換算成像素座標
71
+ if results.face_landmarks:
72
+ face_lms = results.face_landmarks[0]
73
+ num_lms = len(face_lms)
74
+
75
+ if draw_mode == 0 or draw_mode == 2:
76
+ for start_idx, end_idx in FACEMESH_TESSELATION:
77
+ if start_idx < num_lms and end_idx < num_lms:
78
+ lm1 = face_lms[start_idx]
79
+ lm2 = face_lms[end_idx]
80
+ pt1 = (int(lm1.x * img_w), int(lm1.y * img_h))
81
+ pt2 = (int(lm2.x * img_w), int(lm2.y * img_h))
82
+ cv2.line(img, pt1, pt2, (224, 224, 0), 1) # 青綠色線條
83
+
84
+
85
+ # 【TODO 3】畫出臉部 468 個黃色特徵點點
86
+ if draw_mode == 1 or draw_mode == 2:
87
+ for lm in face_lms:
88
+ cx = int(lm.x * img_w)
89
+ cy = int(lm.y * img_h)
90
+ cv2.circle(img, (cx, cy), 1, (0, 255, 255), -1) # 黃色小圓點
91
+
92
+
93
+
94
+ mode_str = "Mesh" if draw_mode == 0 else "Dots" if draw_mode == 1 else "Both"
95
+ cv2.putText(img, f"Face Detected | Mode: {mode_str}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
96
+ else:
97
+ cv2.putText(img, "No Face Detected", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
98
+
99
+ # 顯示操作提示文字
100
+ cv2.putText(img, "Press 'm' to toggle mode | 'q' to quit", (10, img_h - 20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1)
101
+
102
+ cv2.imshow("Face Mesh Connections", img)
103
+
104
+ key = cv2.waitKey(1) & 0xFF
105
+ if key == ord('q'):
106
+ break
107
+ elif key == ord('m'):
108
+ draw_mode = (draw_mode + 1) % 3
109
+
110
+ cap.release()
111
+ cv2.destroyAllWindows()
projects/2026_AI營1/劉宗穎/脸部侦测/step2_facemesh_complete.py ADDED
@@ -0,0 +1,362 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Week 11 - Face Mesh Expression Detection (Complete Template 填空版)
3
+ ===========================================================
4
+ 本步驟目標:
5
+ 1. 將 MediaPipe Face Landmarker 與 Pygame 完美結合,打造科幻儀表板
6
+ 2. 計算臉部指標:嘴巴張開比例與眼睛胖瘦比例 (EAR)
7
+ 3. 實作「雙門檻滯後防吵鎖」,避免警報器反覆爆音
8
+ 4. 實作「跳幀推理優化」,解決 AI 推理造成的畫面卡頓
9
+ 5. 整合音效播放功能
10
+ """
11
+ import pygame
12
+ import cv2
13
+ import mediapipe as mp
14
+ import math
15
+ import sys
16
+ import os
17
+ import time
18
+
19
+ # ============================================
20
+ # 1. 初始化 Pygame & 音效
21
+ # ============================================
22
+ pygame.init()
23
+ pygame.mixer.init()
24
+ WIDTH, HEIGHT = 800, 600
25
+ screen = pygame.display.set_mode((WIDTH, HEIGHT))
26
+ pygame.display.set_caption("Week 11 - Face Mesh Expression Detector ")
27
+ clock = pygame.time.Clock()
28
+
29
+ font = pygame.font.SysFont(None, 36)
30
+ small_font = pygame.font.SysFont(None, 24)
31
+ title_font = pygame.font.SysFont(None, 48)
32
+
33
+ # 載入警告音效 (shoot.wav)
34
+ try:
35
+ trigger_sound = pygame.mixer.Sound(os.path.join(os.path.dirname(__file__), "shoot.wav"))
36
+ print("Success: Loaded sound file shoot.wav")
37
+ except FileNotFoundError:
38
+ trigger_sound = None
39
+ print("Warning: Could not find sound file shoot.wav")
40
+
41
+ # ============================================
42
+ # 2. 初始化 MediaPipe Face Landmarker
43
+ # ============================================
44
+ model_path = os.path.join(os.path.dirname(__file__), 'face_landmarker.task')
45
+ if not os.path.exists(model_path):
46
+ # 若在當前資料夾找不到,就去上一層找
47
+ model_path = os.path.join(os.path.dirname(__file__), '..', 'face_landmarker.task')
48
+ if not os.path.exists(model_path):
49
+ print(f"Error: Model file not found at {model_path}")
50
+ print("Please make sure face_landmarker.task is in the directory.")
51
+ pygame.quit()
52
+ sys.exit()
53
+
54
+ # 【TODO 1】初始化 MediaPipe Face Landmarker 偵測器
55
+ # try:
56
+ # from mediapipe.tasks import python
57
+ # from mediapipe.tasks.python import vision
58
+ # base_options = python.BaseOptions(model_asset_path=model_path)
59
+ # options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1)
60
+ # detector = vision.FaceLandmarker.create_from_options(options)
61
+ # print("Success: Loaded MediaPipe detector model")
62
+ # except Exception as e:
63
+ # print(f"Error: Failed to load model: {e}")
64
+ # pygame.quit()
65
+ # sys.exit()
66
+
67
+ try:
68
+ from mediapipe.tasks import python
69
+ from mediapipe.tasks.python import vision
70
+ base_options = python.BaseOptions(model_asset_path=model_path)
71
+ options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1)
72
+ detector = vision.FaceLandmarker.create_from_options(options)
73
+ print("Success: Loaded MediaPipe detector model")
74
+ except Exception as e:
75
+ print(f"Error: Failed to load model: {e}")
76
+ pygame.quit()
77
+ sys.exit()
78
+
79
+ # ============================================
80
+ # 3. 臉部特徵點索引定義
81
+ # ============================================
82
+ LEFT_EYE_OUTER = 33
83
+ RIGHT_EYE_OUTER = 263
84
+ LIP_TOP = 13
85
+ LIP_BOTTOM = 14
86
+ LEFT_EYE_TOP = 159
87
+ LEFT_EYE_BOTTOM = 145
88
+ LEFT_EYE_RIGHT = 133
89
+
90
+ # ============================================
91
+ # 4. 門檻值與狀態變數
92
+ # ============================================
93
+ # 嘴巴雙門檻防吵鎖
94
+ MOUTH_OPEN_THRESHOLD = 0.35
95
+ MOUTH_CLOSE_THRESHOLD = 0.15
96
+
97
+ # 眼睛雙門檻防吵鎖
98
+ EAR_BLINK_THRESHOLD = 0.18
99
+ EAR_OPEN_THRESHOLD = 0.25
100
+
101
+ is_mouth_open = False
102
+ was_mouth_open = False
103
+
104
+ is_blinking = False
105
+ was_blinking = False
106
+
107
+ mouth_open_count = 0
108
+ blink_count = 0
109
+
110
+ # 跳幀優化變數:每 3 影格才做一次 AI 推理
111
+ frame_counter = 0
112
+ FRAME_SKIP = 3
113
+ detector_results = None
114
+
115
+ # 開啟攝影機
116
+ cap = cv2.VideoCapture(0)
117
+ if not cap.isOpened():
118
+ print("Error: Could not open camera.")
119
+ pygame.quit()
120
+ sys.exit()
121
+
122
+ # ============================================
123
+ # 5. 主迴圈
124
+ # ============================================
125
+ running = True
126
+ while running:
127
+ for event in pygame.event.get():
128
+ if event.type == pygame.QUIT:
129
+ running = False
130
+ elif event.type == pygame.KEYDOWN:
131
+ if event.key == pygame.K_ESCAPE:
132
+ running = False
133
+
134
+ success, img = cap.read()
135
+ if not success:
136
+ cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
137
+ continue
138
+
139
+ img = cv2.flip(img, 1)
140
+ img_h, img_w, _ = img.shape
141
+ frame_counter += 1
142
+
143
+ plot_points = {}
144
+
145
+ # 【TODO 2】實作跳幀推理優化 (每 FRAME_SKIP 影格才跑一次 AI)
146
+ # if frame_counter % FRAME_SKIP == 0 or detector_results is None:
147
+ # try:
148
+ # img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
149
+ # mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb)
150
+ # detector_results = detector.detect(mp_image)
151
+ # except Exception as e:
152
+ # print(f"Error: Inference exception: {e}")
153
+
154
+ if frame_counter % FRAME_SKIP == 0 or detector_results is None:
155
+ try:
156
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
157
+ mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb)
158
+ detector_results = detector.detect(mp_image)
159
+ except Exception as e:
160
+ print(f"Error: Inference exception: {e}")
161
+
162
+ # 【TODO 3】特徵點座標映射與比例計算
163
+ # 1. 取得對應點在 Pygame 視窗 (WIDTH, HEIGHT) 中的像素位置並存入 plot_points
164
+ # 2. 計算眼寬基準線 base_dist
165
+ # 3. 計算歸一化嘴巴比例 mouth_ratio
166
+ # 4. 計算眼睛胖瘦比 ear (提示:垂直高度 eye_h / 水平寬度 eye_w)
167
+ # if detector_results and detector_results.face_landmarks:
168
+ # face_lms = detector_results.face_landmarks[0]
169
+ #
170
+ # for idx in [LEFT_EYE_OUTER, RIGHT_EYE_OUTER, LIP_TOP, LIP_BOTTOM, LEFT_EYE_TOP, LEFT_EYE_BOTTOM, LEFT_EYE_RIGHT]:
171
+ # pt = face_lms[idx]
172
+ # plot_points[idx] = (int(pt.x * WIDTH), int(pt.y * HEIGHT))
173
+ #
174
+ # p_lx, p_ly = plot_points[LEFT_EYE_OUTER]
175
+ # p_rx, p_ry = plot_points[RIGHT_EYE_OUTER]
176
+ # base_dist = math.hypot(p_rx - p_lx, p_ry - p_ly)
177
+ #
178
+ # if base_dist > 0:
179
+ # p_m_top_x, p_m_top_y = plot_points[LIP_TOP]
180
+ # p_m_bot_x, p_m_bot_y = plot_points[LIP_BOTTOM]
181
+ # mouth_dist = math.hypot(p_m_bot_x - p_m_top_x, p_m_bot_y - p_m_top_y)
182
+ # mouth_ratio = mouth_dist / base_dist
183
+ #
184
+ # p_e_top_x, p_e_top_y = plot_points[LEFT_EYE_TOP]
185
+ # p_e_bot_x, p_e_bot_y = plot_points[LEFT_EYE_BOTTOM]
186
+ # p_e_right_x, p_e_right_y = plot_points[LEFT_EYE_RIGHT]
187
+ #
188
+ # eye_h = math.hypot(p_e_bot_x - p_e_top_x, p_e_bot_y - p_e_top_y)
189
+ # eye_w = math.hypot(p_e_right_x - p_lx, p_e_right_y - p_ly)
190
+ # ear = eye_h / eye_w if eye_w > 0 else 0
191
+
192
+ if detector_results and detector_results.face_landmarks:
193
+ face_lms = detector_results.face_landmarks[0]
194
+
195
+ for idx in [LEFT_EYE_OUTER, RIGHT_EYE_OUTER, LIP_TOP, LIP_BOTTOM, LEFT_EYE_TOP, LEFT_EYE_BOTTOM, LEFT_EYE_RIGHT]:
196
+ pt = face_lms[idx]
197
+ plot_points[idx] = (int(pt.x * WIDTH), int(pt.y * HEIGHT))
198
+
199
+ p_lx, p_ly = plot_points[LEFT_EYE_OUTER]
200
+ p_rx, p_ry = plot_points[RIGHT_EYE_OUTER]
201
+ base_dist = math.hypot(p_rx - p_lx, p_ry - p_ly)
202
+
203
+ if base_dist > 0:
204
+ p_m_top_x, p_m_top_y = plot_points[LIP_TOP]
205
+ p_m_bot_x, p_m_bot_y = plot_points[LIP_BOTTOM]
206
+ mouth_dist = math.hypot(p_m_bot_x - p_m_top_x, p_m_bot_y - p_m_top_y)
207
+ mouth_ratio = mouth_dist / base_dist
208
+
209
+ p_e_top_x, p_e_top_y = plot_points[LEFT_EYE_TOP]
210
+ p_e_bot_x, p_e_bot_y = plot_points[LEFT_EYE_BOTTOM]
211
+ p_e_right_x, p_e_right_y = plot_points[LEFT_EYE_RIGHT]
212
+
213
+ eye_h = math.hypot(p_e_bot_x - p_e_top_x, p_e_bot_y - p_e_top_y)
214
+ eye_w = math.hypot(p_e_right_x - p_lx, p_e_right_y - p_ly)
215
+ ear = eye_h / eye_w if eye_w > 0 else 0
216
+
217
+ # 【TODO 4】實作雙門檻防吵鎖 (滯後判斷邏輯)
218
+ # if mouth_ratio > MOUTH_OPEN_THRESHOLD:
219
+ # is_mouth_open = True
220
+ # elif mouth_ratio < MOUTH_CLOSE_THRESHOLD:
221
+ # is_mouth_open = False
222
+ #
223
+ # if ear < EAR_BLINK_THRESHOLD:
224
+ # is_blinking = True
225
+ # elif ear > EAR_OPEN_THRESHOLD:
226
+ # is_blinking = False
227
+
228
+ if mouth_ratio > MOUTH_OPEN_THRESHOLD:
229
+ is_mouth_open = True
230
+ elif mouth_ratio < MOUTH_CLOSE_THRESHOLD:
231
+ is_mouth_open = False
232
+
233
+ if ear < EAR_BLINK_THRESHOLD:
234
+ is_blinking = True
235
+ elif ear > EAR_OPEN_THRESHOLD:
236
+ is_blinking = False
237
+
238
+ # 【TODO 5】實作邊緣觸發判定 (只有在剛張嘴或剛閉眼的那一瞬間,播放一次警告音效)
239
+ # if is_mouth_open and not was_mouth_open:
240
+ # mouth_open_count += 1
241
+ # if trigger_sound:
242
+ # trigger_sound.play()
243
+ #
244
+ # if is_blinking and not was_blinking:
245
+ # blink_count += 1
246
+ #
247
+ # was_mouth_open = is_mouth_open
248
+ # was_blinking = is_blinking
249
+
250
+ if is_mouth_open and not was_mouth_open:
251
+ mouth_open_count += 1
252
+ if trigger_sound:
253
+ trigger_sound.play()
254
+
255
+ if is_blinking and not was_blinking:
256
+ blink_count += 1
257
+
258
+ was_mouth_open = is_mouth_open
259
+ was_blinking = is_blinking
260
+
261
+ else:
262
+ is_mouth_open = False
263
+ is_blinking = False
264
+ was_mouth_open = False
265
+ was_blinking = False
266
+ mouth_ratio = 0.0
267
+ ear = 0.30
268
+
269
+ # ============================================
270
+ # 6. 渲染 Pygame 畫面與 UI 元素
271
+ # ============================================
272
+ # 【TODO 6】根據狀態切換背景顏色 (安全時深色,張嘴時深綠色,眨眼時深藍色)
273
+ # if is_mouth_open:
274
+ # screen.fill((16, 44, 28))
275
+ # elif is_blinking:
276
+ # screen.fill((15, 30, 54))
277
+ # else:
278
+ # screen.fill((20, 20, 30))
279
+
280
+ if is_mouth_open:
281
+ screen.fill((16, 44, 28))
282
+ elif is_blinking:
283
+ screen.fill((15, 30, 54))
284
+ else:
285
+ screen.fill((20, 20, 30))
286
+
287
+ # 【TODO 7】畫出眼寬基準線、眼睛高度線與嘴唇高度線
288
+ # 提示:用 pygame.draw.line 在螢幕上將點位連線
289
+ # if plot_points:
290
+ # pygame.draw.line(screen, (100, 100, 150), plot_points[LEFT_EYE_OUTER], plot_points[RIGHT_EYE_OUTER], 2)
291
+ # pygame.draw.line(screen, (245, 158, 11), plot_points[LIP_TOP], plot_points[LIP_BOTTOM], 3)
292
+ # pygame.draw.line(screen, (59, 130, 246), plot_points[LEFT_EYE_TOP], plot_points[LEFT_EYE_BOTTOM], 3)
293
+
294
+ if plot_points:
295
+ pygame.draw.line(screen, (100, 100, 150), plot_points[LEFT_EYE_OUTER], plot_points[RIGHT_EYE_OUTER], 2)
296
+ pygame.draw.line(screen, (245, 158, 11), plot_points[LIP_TOP], plot_points[LIP_BOTTOM], 3)
297
+ pygame.draw.line(screen, (59, 130, 246), plot_points[LEFT_EYE_TOP], plot_points[LEFT_EYE_BOTTOM], 3)
298
+
299
+ # 畫特徵點小圓圈
300
+ for idx, pos in plot_points.items():
301
+ color = (255, 255, 255)
302
+ if idx in [LIP_TOP, LIP_BOTTOM]:
303
+ color = (245, 158, 11)
304
+ elif idx in [LEFT_EYE_TOP, LEFT_EYE_BOTTOM, LEFT_EYE_OUTER, LEFT_EYE_RIGHT]:
305
+ color = (59, 130, 246)
306
+ pygame.draw.circle(screen, color, pos, 6)
307
+
308
+ # 繪製儀表板 HUD 卡片
309
+ pygame.draw.rect(screen, (30, 30, 45), (30, 30, 340, 220), border_radius=12)
310
+ pygame.draw.rect(screen, (50, 50, 70), (30, 30, 340, 220), width=2, border_radius=12)
311
+
312
+ screen.blit(font.render("Face Mesh Dashboard", True, (255, 255, 255)), (50, 45))
313
+
314
+ m_color = (34, 197, 94) if is_mouth_open else (245, 158, 11)
315
+ screen.blit(small_font.render(f"Mouth Ratio: {mouth_ratio:.2f}", True, m_color), (50, 90))
316
+ pygame.draw.rect(screen, (50, 50, 65), (50, 110, 200, 12), border_radius=4)
317
+ pygame.draw.rect(screen, m_color, (50, 110, int(min(mouth_ratio, 1.0) * 200), 12), border_radius=4)
318
+
319
+ e_color = (59, 130, 246) if not is_blinking else (239, 68, 68)
320
+ screen.blit(small_font.render(f"Eye EAR: {ear:.2f}", True, e_color), (50, 140))
321
+ pygame.draw.rect(screen, (50, 50, 65), (50, 160, 200, 12), border_radius=4)
322
+ pygame.draw.rect(screen, e_color, (50, 160, int(min(ear, 0.5) * 400), 12), border_radius=4)
323
+
324
+ # 繪製閾值指示白線
325
+ pygame.draw.line(screen, (255, 255, 255), (50 + int(MOUTH_OPEN_THRESHOLD * 200), 108), (50 + int(MOUTH_OPEN_THRESHOLD * 200), 124), 2)
326
+ pygame.draw.line(screen, (255, 255, 255), (50 + int(EAR_BLINK_THRESHOLD * 400), 158), (50 + int(EAR_BLINK_THRESHOLD * 400), 174), 2)
327
+
328
+ # 繪製統計卡片
329
+ pygame.draw.rect(screen, (30, 30, 45), (WIDTH - 320, 30, 290, 110), border_radius=12)
330
+ pygame.draw.rect(screen, (50, 50, 70), (WIDTH - 320, 30, 290, 110), width=2, border_radius=12)
331
+ screen.blit(small_font.render(f"Mouth Open Count: {mouth_open_count}", True, (255, 255, 255)), (WIDTH - 300, 45))
332
+ screen.blit(small_font.render(f"Blink/Close Count: {blink_count}", True, (255, 255, 255)), (WIDTH - 300, 75))
333
+ screen.blit(small_font.render(f"Hysteresis Status: ACTIVE", True, (34, 197, 94)), (WIDTH - 300, 105))
334
+
335
+ # 顯示置中警告狀態大字
336
+ if is_mouth_open:
337
+ status_lbl = title_font.render("MOUTH OPENED", True, (34, 197, 94))
338
+ screen.blit(status_lbl, (WIDTH // 2 - status_lbl.get_width() // 2, HEIGHT // 2 + 100))
339
+ elif is_blinking:
340
+ status_lbl = title_font.render("BLINKING", True, (59, 130, 246))
341
+ screen.blit(status_lbl, (WIDTH // 2 - status_lbl.get_width() // 2, HEIGHT // 2 + 100))
342
+ else:
343
+ if not plot_points:
344
+ status_lbl = title_font.render("NO FACE DETECTED", True, (239, 68, 68))
345
+ screen.blit(status_lbl, (WIDTH // 2 - status_lbl.get_width() // 2, HEIGHT // 2 - 20))
346
+ else:
347
+ status_lbl = title_font.render("NORMAL", True, (200, 200, 200))
348
+ screen.blit(status_lbl, (WIDTH // 2 - status_lbl.get_width() // 2, HEIGHT // 2 + 100))
349
+
350
+ # 顯示底部效能狀態文字
351
+ status_text = small_font.render(
352
+ f"Frame: {frame_counter} | Skip: {FRAME_SKIP} frames | FPS: {clock.get_fps():.0f}",
353
+ True, (100, 100, 120)
354
+ )
355
+ screen.blit(status_text, (20, HEIGHT - 35))
356
+
357
+ pygame.display.update()
358
+ clock.tick(30)
359
+
360
+ cap.release()
361
+ pygame.quit()
362
+ sys.exit()
projects/2026_AI營1/劉宗穎/设计AI角色&场景/d2_2_prompt_engineering.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # d2_2_prompt_engineering.py
2
+ # Day 2: 生成式 AI 串接實戰 - Prompt 工程 (系統指令與結構化輸出)
3
+ import os
4
+ from google import genai
5
+
6
+ # 取得 API 金鑰
7
+ api_key = os.environ.get("GEMINI_API_KEY", "AQ.Ab8RN6JvbUqLW-wcddeMducmta0aGE2QtcrmF4Kfd8nnpZ9mzg")
8
+ if not api_key:
9
+ print("[提示] 請輸入您的 Google AI Studio API 金鑰:")
10
+ api_key = input("API Key: ").strip()
11
+
12
+ client = genai.Client(api_key=api_key)
13
+
14
+ # 1. 設置系統指令 (System Instruction / System Prompt)
15
+ # TODO 1:
16
+ system_prompt = "你是AI國中英語老師,请输入你想要翻译的中文单字"
17
+
18
+ print("正在初始化强制英語教學模型...")
19
+
20
+ # 2. Few-shot (少樣本) 引導與對話演練
21
+ # TODO 2:
22
+ print(f"\n[學生輸入]: ")
23
+ user_word = input().strip()
24
+ prompt = f"請幫我翻譯這個詞:'{user_word}'。"
25
+
26
+ response = client.models.generate_content(
27
+ model='gemini-3.1-flash-lite',
28
+ contents=prompt,
29
+ config={'system_instruction': system_prompt}
30
+ )
31
+
32
+ print("\n[AI 老師回應]:")
33
+ print(response.text)
34
+
35
+
36
+ # 3. 結構化 JSON 輸出 (Structured Output) 範例
37
+ print("\n" + "="*40)
38
+ print("進階挑戰:要求模型回傳 JSON 格式")
39
+ print("="*40)
40
+
41
+ # 我們可以在 prompt 中要求,並透過 configuration 設定 JSON 輸出格式
42
+ # TODO 3:
43
+ json_prompt = (
44
+ f"請將中文單字 '{user_word}' 的翻譯結果整理成 JSON 格式,包含以下三個欄位:\n"
45
+ "- 'english': 英文單字\n"
46
+ "- 'kk_phonetic': KK音標\n"
47
+ "- 'example_sentence': 適合國中生理解的英文例句與中文翻譯"
48
+ )
49
+
50
+ response = client.models.generate_content(
51
+ model='gemini-3.1-flash-lite',
52
+ contents=json_prompt,
53
+ config={"response_mime_type": "application/json"}
54
+ )
55
+ print("\n[AI 回傳的原始 JSON]:")
56
+ print(response.text)
projects/2026_AI營1/劉宗穎/资料异常值清洗/d1_2_data_cleaning.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # d1_2_data_cleaning.py
2
+ # Day 1 學員填空:資料清洗(完成 TODO)
3
+ # cd examples/Day1/student && python3 d1_2_data_cleaning.py
4
+ # 參考答案:../Teacher/d1_2_data_cleaning.py
5
+
6
+ import pandas as pd
7
+ import numpy as np
8
+
9
+ # 與 Teacher 一致:第 5 天高溫有雨,方便篩選示範
10
+ raw_data = {
11
+ "Date": ["2026-07-01", "2026-07-02", "2026-07-03", "2026-07-04", "2026-07-05"],
12
+ "Temperature": [28.5, np.nan, 30.2, 31.5, 30.8],
13
+ "Humidity": [82, 80, np.nan, 70, 90],
14
+ "Rainfall": [5.2, 0.0, 0.0, np.nan, 25.4],
15
+ }
16
+
17
+ df = pd.DataFrame(raw_data)
18
+ print("=== 原始含有缺失值的數據 ===")
19
+ print(df)
20
+
21
+ df_cleaned = df.copy()
22
+ mean_temp = df_cleaned["Temperature"].mean()
23
+ df_cleaned["Temperature"] = df_cleaned["Temperature"].fillna(mean_temp)
24
+ df_cleaned["Humidity"] = df_cleaned["Humidity"].ffill()
25
+ df_cleaned["Rainfall"] = df_cleaned["Rainfall"].fillna(0.0)
26
+
27
+ print("\n=== 清洗與填補後的數據 ===")
28
+ print(df_cleaned)
29
+
30
+ print("\n=== 篩選:溫度>29 且 降雨>0 ===")
31
+ high_temp_and_rain = df_cleaned[(df_cleaned['Temperature'] > 29) & (df_cleaned['Rainfall'] > 0)]
32
+ print(high_temp_and_rain)
33
+
34
+ print("\n=== 新增 Is_Hot(溫度 >= 30)===")
35
+ df_cleaned['Is_Hot'] = df_cleaned["Temperature"] >= 30
36
+ print(df_cleaned)
37
+ df_cleaned['rainy'] = df_cleaned["Rainfall"] > 0
38
+ print(df_cleaned)
projects/2026_AI營1/劉宗穎/资料异常值清洗/taipei_weather_2026.csv ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Date,Temperature,Humidity,Rainfall
2
+ 2026-07-01,28.5,82,5.2
3
+ 2026-07-02,29.0,80,0.0
4
+ 2026-07-03,30.2,75,0.0
5
+ 2026-07-04,31.5,70,0.0
6
+ 2026-07-05,27.8,90,25.4
7
+ 2026-07-06,26.5,95,42.0
8
+ 2026-07-07,28.0,85,12.5
9
+ 2026-07-08,29.5,78,0.0
10
+ 2026-07-09,30.8,72,0.0
11
+ 2026-07-10,32.0,68,0.0
12
+ 2026-07-11,28.2,88,18.0
13
+ 2026-07-12,27.0,92,30.5
14
+ 2026-07-13,29.2,80,2.1
15
+ 2026-07-14,30.0,76,0.0
16
+ 2026-07-15,31.2,71,0.0
projects/2026_AI營1/劉柔佑/DataFrame指令練習/d1_1_pandas_intro.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # d1_1_pandas_intro.py
2
+ # Day 1 學員填空:Pandas 基礎(完成 TODO 後執行)
3
+ # cd examples/Day1/student && python3 d1_1_pandas_intro.py
4
+ # 參考答案:../Teacher/d1_1_pandas_intro.py
5
+
6
+ import pandas as pd
7
+
8
+ df = pd.read_csv("taipei_weather_2026.csv")
9
+ print("=== 數據載入成功! ===")
10
+
11
+ print("\n--- 1. head ---")
12
+ print(df.head())
13
+
14
+ print("\n--- 2. info ---")
15
+ df.info()
16
+
17
+ print("\n--- 3. describe ---")
18
+ print(df.describe())
19
+
20
+ print("\n--- 4. Temperature 前 5 筆 ---")
21
+ print(df['Temperature'].head())
22
+
23
+ print("\n--- 5. Date 與 Rainfall ---")
24
+ print(df[['Date', 'Rainfall']].head())
25
+
26
+ print("\n--- 挑戰:最高溫與平均濕度 ---")
27
+ max_temp = df['Temperature'].max()
28
+ avg_humidity = df['Humidity'].mean()
29
+ print(f"最高溫度:{max_temp}°C")
30
+ print(f"平均濕度:{avg_humidity:.2f}%")
31
+ min_Rainfall = df['Rainfall'].min()
32
+ print(f"平均濕度:{min_Rainfall:.2f}%")
33
+ egyptian_good_weather =df[(df["Temperature"]>27) & (df["Rainfall"]==0)]
34
+ print(egyptian_good_weather)
projects/2026_AI營1/劉柔佑/DataFrame指令練習/taipei_weather_2026.csv ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Date,Temperature,Humidity,Rainfall
2
+ 2026-07-01,28.5,82,5.2
3
+ 2026-07-02,29.0,80,0.0
4
+ 2026-07-03,30.2,75,0.0
5
+ 2026-07-04,31.5,70,0.0
6
+ 2026-07-05,27.8,90,25.4
7
+ 2026-07-06,26.5,95,42.0
8
+ 2026-07-07,28.0,85,12.5
9
+ 2026-07-08,29.5,78,0.0
10
+ 2026-07-09,30.8,72,0.0
11
+ 2026-07-10,32.0,68,0.0
12
+ 2026-07-11,28.2,88,18.0
13
+ 2026-07-12,27.0,92,30.5
14
+ 2026-07-13,29.2,80,2.1
15
+ 2026-07-14,30.0,76,0.0
16
+ 2026-07-15,31.2,71,0.0
projects/2026_AI營1/劉柔佑/ai_api串接練習/d2_1_gemini.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # d2_1_gemini.py — Day2 學員填空
2
+ # cd examples/Day2/Student && python3 d2_1_gemini.py
3
+ # 參考答案:../Teacher/d2_1_gemini.py
4
+
5
+ import os
6
+ from google import genai
7
+
8
+ # === 【學員填空練習點 1:設置與載入 API Key】 ===
9
+ # TODO: 嘗試從環境變數 GEMINI_API_KEY 取得 API Key。如果未設定,則使用 input() 提示手動輸入。
10
+ api_key = os.environ.get("GEMINI_API_KEY", "AQ.Ab8RN6J-S5dtl4YCuomoO9V4L5YAgTzLh9E8vJUuScN0c4VyKg")
11
+
12
+ if not api_key:
13
+ print("[提示] 未檢測到環境變數 GEMINI_API_KEY。")
14
+ print(" 請先設定:export GEMINI_API_KEY=\"your_key\"")
15
+ print(" 或於下方手動輸入(僅供本機測試,勿寫死在檔案中上傳):")
16
+ api_key = input("API Key: ").strip()
17
+ if not api_key:
18
+ print("[結束] 未提供 API Key。可改練習 d2_1_ollama_local.py。")
19
+ exit(0)
20
+
21
+ # === 【學員填空練習點 2:設定模型名稱】 ===
22
+ # TODO: 設定使用的 Gemini 模型名稱(例如 'gemini-3.1-flash-lite')
23
+ model_name = "gemini-3.1-flash-lite"
24
+
25
+ print(f"正在連線至 Google Cloud Model ({model_name})...")
26
+
27
+ # === 【學員填空練習點 3:初始化 Gemini Client】 ===
28
+ # TODO: 呼叫 genai.Client 並傳入 api_key 參數來初始化 Client 物件,指派給 client 變數
29
+ client = genai.Client(api_key=api_key)
30
+
31
+ # === 【學員填空練習點 4:發送內容生成請求】 ===
32
+ # TODO: 使用 client.models.generate_content 呼叫模型,傳入 model 參數與 contents 提示詞
33
+ # 提示詞內容:請模型以繁體中文解釋什麼是『API 串接』,並用餐廳點餐作為比喻,字數約 100 字。
34
+ response = client.models.generate_content(model= model_name, contents="請以繁體中文解釋什麼是『AI』,字數約 100 字。")
35
+
36
+
37
+ # === 【學員填空練習點 5:讀取並印出回應】 ===
38
+ # TODO: 從 response 物件中取得 text 欄位內容,並列印出來。
39
+ print("\n=== AI 回應結果 ===")
40
+ # 請在下方實作列印邏輯:
41
+ print(response.text)
projects/2026_AI營1/劉柔佑/draw_game/draw_game_student.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ import mediapipe as mp
4
+ from mediapipe.tasks import python
5
+ from mediapipe.tasks.python import vision
6
+
7
+ # ============================================
8
+ # Week 9 - 簡單學生版:支援多色切換的 AR 空中寫字板 (純黑畫布版)
9
+ # ============================================
10
+
11
+ # === 【重要觀念 TODO 1:MediaPipe 手部偵測模型初始化】 ===
12
+ # 觀念重點:
13
+ # 1. BaseOptions 用於設定模型檔案的路徑,這裡使用下載好的 'hand_landmarker.task'。
14
+ # 2. HandLandmarkerOptions 用於設定手部偵測的細項設定,例如 num_hands=1 代表最多偵測一隻手。
15
+ # 3. 最後透過 vision.HandLandmarker.create_from_options 建立手部偵測器物件。
16
+ # 請解開下方三行程式碼的註釋:
17
+ base_options = python.BaseOptions(model_asset_path='hand_landmarker.task')
18
+ options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1)
19
+ detector = vision.HandLandmarker.create_from_options(options)
20
+
21
+ cap = cv2.VideoCapture(0)
22
+ canvas = None
23
+ px, py = 0, 0
24
+
25
+ # 定義幾種高對比的顏色 (格式為 B, G, R)
26
+ COLORS = {
27
+ 'purple': (191,161,196), # 紫色
28
+ 'turquoise': (128,217,255), # 青綠色
29
+ 'skyblue': (138,202,229), # 天蓝色
30
+ 'pink': (213,110,237) # 粉色
31
+ }
32
+
33
+ # 預設使用螢光綠
34
+ current_color = COLORS['purple']
35
+
36
+ print("系統啟動中...")
37
+ print("【操作指南】")
38
+ print("☝️ 單伸食指:下筆寫字")
39
+ print("✌️ 食指+中指:懸停移動 (不寫字)")
40
+ print("⌨️ 按下 '1~4':切換畫筆顏色")
41
+ print("⌨️ 按下 's':清空畫面")
42
+ print("⌨️ 按下 'f':結束程式")
43
+
44
+ while True:
45
+ success, img = cap.read()
46
+ if not success:
47
+ break
48
+
49
+ # === 【重要觀念 TODO 2:攝影機影像水平翻轉】 ===
50
+ # 觀念重點:
51
+ # 1. 攝影機預設取得的影像與我們的真實動作是相反的(非鏡像)。
52
+ # 2. 在互動式 AR 應用中,為了讓使用者的左手移動時,畫面中的手也往左移動,
53
+ # 必須使用 cv2.flip(img, 1) 將影像水平翻轉,以提供符合直覺的「鏡像體驗」。
54
+ # 請解開下方一行程式碼的註釋:
55
+ img = cv2.flip(img, 1)
56
+
57
+ h, w, c = img.shape
58
+
59
+ # 初始化畫布
60
+ if canvas is None:
61
+ canvas = np.zeros((h, w, c), dtype=np.uint8)
62
+
63
+ # 為了維持畫筆提示點(或懸停圓圈)的動態顯示,且不破壞原本的 canvas 紀錄,
64
+ # 我們在每一訊框都建立一個純黑的暫時畫布來呈現最終畫面。
65
+ display_img = np.zeros((h, w, c), dtype=np.uint8)
66
+
67
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
68
+ mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb)
69
+ results = detector.detect(mp_image)
70
+
71
+ if results.hand_landmarks:
72
+ hand_lms = results.hand_landmarks[0]
73
+
74
+ index_tip = hand_lms[8]
75
+ index_pip = hand_lms[6]
76
+ middle_tip = hand_lms[12]
77
+ middle_pip = hand_lms[10]
78
+
79
+ cx, cy = int(index_tip.x * w), int(index_tip.y * h)
80
+
81
+ index_up = index_tip.y < index_pip.y
82
+ middle_up = middle_tip.y < middle_pip.y
83
+
84
+ if index_up and not middle_up:
85
+ # 狀態:【繪圖模式】
86
+ if px == 0 and py == 0:
87
+ px, py = cx, cy
88
+
89
+ # === 【重要觀念 TODO 3:在 OpenCV Canvas 上繪製軌跡】 ===
90
+ # 觀念重點:
91
+ # 1. cv2.line 用於在影像上繪製直線。
92
+ # 2. 參數說明:畫布(canvas)、起點(px, py)、終點(cx, cy)、顏色(current_color)、線寬(8)。
93
+ # 3. 藉由在每一訊框連續繪製前一個位置到當前位置的線段,即可形成平滑的寫字軌跡。
94
+ # 請解開下方一行程式碼的註釋:
95
+ cv2.line(canvas, (px, py), (cx, cy), current_color, 8)
96
+
97
+ px, py = cx, cy
98
+
99
+ # 筆頭提示點直接畫在暫時的顯示畫布上,避免在黑畫布上找不到筆尖
100
+ cv2.circle(display_img, (cx, cy), 10, current_color, cv2.FILLED)
101
+
102
+ elif index_up and middle_up:
103
+ # 狀態:【懸停模式】
104
+ px, py = 0, 0
105
+ # 懸停提示圈畫在暫時的顯示畫布上,移動時才不會在黑背景中迷路
106
+ cv2.circle(display_img, (cx, cy), 12, (255, 255, 255), 2)
107
+
108
+ else:
109
+ px, py = 0, 0
110
+ else:
111
+ px, py = 0, 0
112
+
113
+ # 為什麼改用 canvas 與 display_img 疊加?
114
+ # 原本是與相機影像 (img) 疊加,現在改與純黑的 display_img 疊加,
115
+ # 這樣畫面上就只會留下寫字的軌跡以及當前的筆頭/懸停提示,完全遮蔽人像背景。
116
+
117
+ # === 【重要觀念 TODO 4:AR 畫面疊加與顯示】 ===
118
+ # 觀念重點:
119
+ # 1. cv2.add 用於將兩個相同大小與類型的影像矩陣進行相加。
120
+ # 2. 這裡將寫字痕跡畫布 (canvas) 與當前的筆頭/懸停提示畫布 (display_img) 疊加。
121
+ # 3. 疊加後使用 cv2.imshow 在視窗中顯示最終成果。
122
+ # 請解開下方兩行程式碼的註釋:
123
+ final_output = cv2.add(canvas, display_img)
124
+ cv2.imshow("Hand Tracking - AR Canvas", final_output)
125
+
126
+ # 鍵盤監聽與控制邏輯
127
+ key = cv2.waitKey(1) & 0xFF
128
+ if key == ord('q'):
129
+ break
130
+ elif key == ord('c'):
131
+ canvas = np.zeros((h, w, c), dtype=np.uint8)
132
+
133
+ elif key == ord('1'):
134
+ current_color = COLORS['purple']
135
+ print("已切換為:紫色")
136
+ elif key == ord('2'):
137
+ current_color = COLORS['turquoise']
138
+ print("已切換為:青綠色")
139
+ elif key == ord('3'):
140
+ current_color = COLORS['skyblue']
141
+ print("已切換為:蓝色")
142
+ elif key == ord('4'):
143
+ current_color = COLORS['pink']
144
+ print("已切換為:粉色")
145
+
146
+ cap.release()
147
+ cv2.destroyAllWindows()
projects/2026_AI營1/劉柔佑/image/Boa.jpeg ADDED

Git LFS Details

  • SHA256: 13395d211888bc201f3fda8362245cab13512f9096cdfdb06d16c92b63739eec
  • Pointer size: 131 Bytes
  • Size of remote file: 219 kB
projects/2026_AI營1/劉柔佑/image/Fandom.jpg ADDED

Git LFS Details

  • SHA256: 484e07ce68cc8245070e8a0395e8c33241f7e2e1909b6044d37977c9a48449f8
  • Pointer size: 130 Bytes
  • Size of remote file: 34.5 kB