{ "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "objc[67754]: Class CaptureDelegate is implemented in both /Users/fuixlabsdev1/Programming/PP/graduation-thesis/env/lib/python3.8/site-packages/mediapipe/.dylibs/libopencv_videoio.3.4.16.dylib (0x10a8c8860) and /Users/fuixlabsdev1/Programming/PP/graduation-thesis/env/lib/python3.8/site-packages/cv2/cv2.abi3.so (0x161476480). One of the two will be used. Which one is undefined.\n", "objc[67754]: Class CVWindow is implemented in both /Users/fuixlabsdev1/Programming/PP/graduation-thesis/env/lib/python3.8/site-packages/mediapipe/.dylibs/libopencv_highgui.3.4.16.dylib (0x10567ca68) and /Users/fuixlabsdev1/Programming/PP/graduation-thesis/env/lib/python3.8/site-packages/cv2/cv2.abi3.so (0x1614764d0). One of the two will be used. Which one is undefined.\n", "objc[67754]: Class CVView is implemented in both /Users/fuixlabsdev1/Programming/PP/graduation-thesis/env/lib/python3.8/site-packages/mediapipe/.dylibs/libopencv_highgui.3.4.16.dylib (0x10567ca90) and /Users/fuixlabsdev1/Programming/PP/graduation-thesis/env/lib/python3.8/site-packages/cv2/cv2.abi3.so (0x1614764f8). One of the two will be used. Which one is undefined.\n", "objc[67754]: Class CVSlider is implemented in both /Users/fuixlabsdev1/Programming/PP/graduation-thesis/env/lib/python3.8/site-packages/mediapipe/.dylibs/libopencv_highgui.3.4.16.dylib (0x10567cab8) and /Users/fuixlabsdev1/Programming/PP/graduation-thesis/env/lib/python3.8/site-packages/cv2/cv2.abi3.so (0x161476520). One of the two will be used. Which one is undefined.\n" ] } ], "source": [ "import mediapipe as mp\n", "import cv2\n", "import numpy as np\n", "import pandas as pd\n", "import os, csv\n", "import seaborn as sns\n", "\n", "import warnings\n", "warnings.filterwarnings('ignore')\n", "\n", "# Drawing helpers\n", "mp_drawing = mp.solutions.drawing_utils\n", "mp_pose = mp.solutions.pose" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 1. Describe the data gathering process and build dataset from Video\n", "\n", "The purpose is to gather data to determine the correct standing posture for Bicep Curl exercise\n", "There are 2 stages:\n", "- Correct: \"C\"\n", "- Lean-back-error: \"L\"" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "# Determine important landmarks for plank\n", "IMPORTANT_LMS = [\n", " \"NOSE\",\n", " \"LEFT_SHOULDER\",\n", " \"RIGHT_SHOULDER\",\n", " \"RIGHT_ELBOW\",\n", " \"LEFT_ELBOW\",\n", " \"RIGHT_WRIST\",\n", " \"LEFT_WRIST\",\n", " \"LEFT_HIP\",\n", " \"RIGHT_HIP\",\n", "]\n", "\n", "# Generate all columns of the data frame\n", "\n", "HEADERS = [\"label\"] # Label column\n", "\n", "for lm in IMPORTANT_LMS:\n", " HEADERS += [f\"{lm.lower()}_x\", f\"{lm.lower()}_y\", f\"{lm.lower()}_z\", f\"{lm.lower()}_v\"]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 1.2. Set up important functions" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "def rescale_frame(frame, percent=50):\n", " '''\n", " Rescale a frame to a certain percentage compare to its original frame\n", " '''\n", " width = int(frame.shape[1] * percent/ 100)\n", " height = int(frame.shape[0] * percent/ 100)\n", " dim = (width, height)\n", " return cv2.resize(frame, dim, interpolation = cv2.INTER_AREA)\n", " \n", "\n", "def init_csv(dataset_path: str):\n", " '''\n", " Create a blank csv file with just columns\n", " '''\n", "\n", " # Ignore if file is already exist\n", " if os.path.exists(dataset_path):\n", " return\n", "\n", " # Write all the columns to a empty file\n", " with open(dataset_path, mode=\"w\", newline=\"\") as f:\n", " csv_writer = csv.writer(f, delimiter=\",\", quotechar='\"', quoting=csv.QUOTE_MINIMAL)\n", " csv_writer.writerow(HEADERS)\n", "\n", "\n", "def export_landmark_to_csv(dataset_path: str, results, action: str) -> None:\n", " '''\n", " Export Labeled Data from detected landmark to csv\n", " '''\n", " landmarks = results.pose_landmarks.landmark\n", " keypoints = []\n", "\n", " try:\n", " # Extract coordinate of important landmarks\n", " for lm in IMPORTANT_LMS:\n", " keypoint = landmarks[mp_pose.PoseLandmark[lm].value]\n", " keypoints.append([keypoint.x, keypoint.y, keypoint.z, keypoint.visibility])\n", " \n", " keypoints = list(np.array(keypoints).flatten())\n", "\n", " # Insert action as the label (first column)\n", " keypoints.insert(0, action)\n", "\n", " # Append new row to .csv file\n", " with open(dataset_path, mode=\"a\", newline=\"\") as f:\n", " csv_writer = csv.writer(f, delimiter=\",\", quotechar='\"', quoting=csv.QUOTE_MINIMAL)\n", " csv_writer.writerow(keypoints)\n", " \n", "\n", " except Exception as e:\n", " print(e)\n", " pass\n", "\n", "\n", "def describe_dataset(dataset_path: str):\n", " '''\n", " Describe dataset\n", " '''\n", "\n", " data = pd.read_csv(dataset_path)\n", " print(f\"Headers: {list(data.columns.values)}\")\n", " print(f'Number of rows: {data.shape[0]} \\nNumber of columns: {data.shape[1]}\\n')\n", " print(f\"Labels: \\n{data['label'].value_counts()}\\n\")\n", " print(f\"Missing values: {data.isnull().values.any()}\\n\")\n", " \n", " duplicate = data[data.duplicated()]\n", " print(f\"Duplicate Rows : {len(duplicate.sum(axis=1))}\")\n", "\n", " return data\n", "\n", "\n", "def remove_duplicate_rows(dataset_path: str):\n", " '''\n", " Remove duplicated data from the dataset then save it to another files\n", " '''\n", " \n", " df = pd.read_csv(dataset_path)\n", " df.drop_duplicates(keep=\"first\", inplace=True)\n", " df.to_csv(f\"cleaned_train.csv\", sep=',', encoding='utf-8', index=False)\n", " \n", "\n", "def concat_csv_files_with_same_headers(file_paths: list, saved_path: str):\n", " '''\n", " Concat different csv files\n", " '''\n", " all_df = []\n", " for path in file_paths:\n", " df = pd.read_csv(path, index_col=None, header=0)\n", " all_df.append(df)\n", " \n", " results = pd.concat(all_df, axis=0, ignore_index=True)\n", " results.to_csv(saved_path, sep=',', encoding='utf-8', index=False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 2. Extract data from video" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "ename": "", "evalue": "", "output_type": "error", "traceback": [ "\u001b[1;31mThe Kernel crashed while executing code in the the current cell or a previous cell. Please review the code in the cell(s) to identify a possible cause of the failure. Click here for more info. View Jupyter log for further details." ] } ], "source": [ "DATASET_PATH = \"train.csv\"\n", "\n", "cap = cv2.VideoCapture(\"../data/db_curl/stand_posture_11.mp4\")\n", "save_counts = 0\n", "\n", "# init_csv(DATASET_PATH)\n", "\n", "with mp_pose.Pose(min_detection_confidence=0.5, min_tracking_confidence=0.5) as pose:\n", " while cap.isOpened():\n", " ret, image = cap.read()\n", "\n", " if not ret:\n", " break\n", "\n", " # Reduce size of a frame\n", " image = rescale_frame(image, 60)\n", " image = cv2.flip(image, 1)\n", "\n", " # Recolor image from BGR to RGB for mediapipe\n", " image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n", " image.flags.writeable = False\n", "\n", " results = pose.process(image)\n", "\n", " if not results.pose_landmarks:\n", " print(\"Cannot detect pose - No human found\")\n", " continue\n", "\n", " # Recolor image from BGR to RGB for mediapipe\n", " image.flags.writeable = True\n", " image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)\n", "\n", " # Draw landmarks and connections\n", " mp_drawing.draw_landmarks(image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS, mp_drawing.DrawingSpec(color=(244, 117, 66), thickness=2, circle_radius=4), mp_drawing.DrawingSpec(color=(245, 66, 230), thickness=2, circle_radius=2))\n", "\n", " # Display the saved count\n", " cv2.putText(image, f\"Saved: {save_counts}\", (50, 50), cv2.FONT_HERSHEY_COMPLEX, 2, (0, 0, 0), 2, cv2.LINE_AA)\n", "\n", " cv2.imshow(\"CV2\", image)\n", "\n", " # Pressed key for action\n", " k = cv2.waitKey(1) & 0xFF\n", "\n", " # Press C to save as correct form\n", " if k == ord('c'): \n", " export_landmark_to_csv(DATASET_PATH, results, \"C\")\n", " save_counts += 1\n", " # Press L to save as low back\n", " elif k == ord(\"l\"):\n", " export_landmark_to_csv(DATASET_PATH, results, \"L\")\n", " save_counts += 1\n", "\n", " # Press q to stop\n", " elif k == ord(\"q\"):\n", " break\n", " else: continue\n", "\n", " cap.release()\n", " cv2.destroyAllWindows()\n", "\n", " # (Optional)Fix bugs cannot close windows in MacOS (https://stackoverflow.com/questions/6116564/destroywindow-does-not-close-window-on-mac-using-python-and-opencv)\n", " for i in range (1, 5):\n", " cv2.waitKey(1)\n", " " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# csv_files = [os.path.join(\"./\", f) for f in os.listdir(\"./\") if \"csv\" in f]\n", "\n", "# concat_csv_files_with_same_headers(csv_files, \"train.csv\")\n", "\n", "df = describe_dataset(\"./train.csv\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 3. Clean Data and Visualize data" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "remove_duplicate_rows(\"./train.csv\")" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Headers: ['label', 'nose_x', 'nose_y', 'nose_z', 'nose_v', 'left_shoulder_x', 'left_shoulder_y', 'left_shoulder_z', 'left_shoulder_v', 'right_shoulder_x', 'right_shoulder_y', 'right_shoulder_z', 'right_shoulder_v', 'right_elbow_x', 'right_elbow_y', 'right_elbow_z', 'right_elbow_v', 'left_elbow_x', 'left_elbow_y', 'left_elbow_z', 'left_elbow_v', 'right_wrist_x', 'right_wrist_y', 'right_wrist_z', 'right_wrist_v', 'left_wrist_x', 'left_wrist_y', 'left_wrist_z', 'left_wrist_v', 'left_hip_x', 'left_hip_y', 'left_hip_z', 'left_hip_v', 'right_hip_x', 'right_hip_y', 'right_hip_z', 'right_hip_v']\n", "Number of rows: 15372 \n", "Number of columns: 37\n", "\n", "Labels: \n", "C 8238\n", "L 7134\n", "Name: label, dtype: int64\n", "\n", "Missing values: False\n", "\n", "Duplicate Rows : 0\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/png": "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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "df = describe_dataset(\"./train.csv\")\n", "sns.countplot(x='label', data=df, palette=\"Set1\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 4. Gather Test Dataset" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "TEST_DATASET_PATH = \"test.csv\"\n", "\n", "cap = cv2.VideoCapture(\"../data/db_curl/bc_test_2.mp4\")\n", "save_counts = 0\n", "\n", "init_csv(TEST_DATASET_PATH)\n", "\n", "with mp_pose.Pose(min_detection_confidence=0.5, min_tracking_confidence=0.5) as pose:\n", " while cap.isOpened():\n", " ret, image = cap.read()\n", "\n", " if not ret:\n", " break\n", "\n", " # Reduce size of a frame\n", " image = rescale_frame(image, 60)\n", " image = cv2.flip(image, 1)\n", "\n", " # Recolor image from BGR to RGB for mediapipe\n", " image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n", " image.flags.writeable = False\n", "\n", " results = pose.process(image)\n", "\n", " if not results.pose_landmarks:\n", " print(\"Cannot detect pose - No human found\")\n", " continue\n", "\n", " # Recolor image from BGR to RGB for mediapipe\n", " image.flags.writeable = True\n", " image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)\n", "\n", " # Draw landmarks and connections\n", " mp_drawing.draw_landmarks(image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS, mp_drawing.DrawingSpec(color=(244, 117, 66), thickness=2, circle_radius=4), mp_drawing.DrawingSpec(color=(245, 66, 230), thickness=2, circle_radius=2))\n", "\n", " # Display the saved count\n", " cv2.putText(image, f\"Saved: {save_counts}\", (50, 50), cv2.FONT_HERSHEY_COMPLEX, 2, (0, 0, 0), 2, cv2.LINE_AA)\n", "\n", " cv2.imshow(\"CV2\", image)\n", "\n", " # Pressed key for action\n", " k = cv2.waitKey(10) & 0xFF\n", "\n", " # Press C to save as correct form\n", " if k == ord('c'): \n", " export_landmark_to_csv(TEST_DATASET_PATH, results, \"C\")\n", " save_counts += 1\n", " # Press L to save as low back\n", " elif k == ord(\"l\"):\n", " export_landmark_to_csv(TEST_DATASET_PATH, results, \"L\")\n", " save_counts += 1\n", "\n", " # Press q to stop\n", " elif k == ord(\"q\"):\n", " break\n", " else: continue\n", "\n", " cap.release()\n", " cv2.destroyAllWindows()\n", "\n", " # (Optional)Fix bugs cannot close windows in MacOS (https://stackoverflow.com/questions/6116564/destroywindow-does-not-close-window-on-mac-using-python-and-opencv)\n", " for i in range (1, 5):\n", " cv2.waitKey(1)\n", " " ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Headers: ['label', 'nose_x', 'nose_y', 'nose_z', 'nose_v', 'left_shoulder_x', 'left_shoulder_y', 'left_shoulder_z', 'left_shoulder_v', 'right_shoulder_x', 'right_shoulder_y', 'right_shoulder_z', 'right_shoulder_v', 'right_elbow_x', 'right_elbow_y', 'right_elbow_z', 'right_elbow_v', 'left_elbow_x', 'left_elbow_y', 'left_elbow_z', 'left_elbow_v', 'right_wrist_x', 'right_wrist_y', 'right_wrist_z', 'right_wrist_v', 'left_wrist_x', 'left_wrist_y', 'left_wrist_z', 'left_wrist_v', 'left_hip_x', 'left_hip_y', 'left_hip_z', 'left_hip_v', 'right_hip_x', 'right_hip_y', 'right_hip_z', 'right_hip_v']\n", "Number of rows: 604 \n", "Number of columns: 37\n", "\n", "Labels: \n", "C 339\n", "L 265\n", "Name: label, dtype: int64\n", "\n", "Missing values: False\n", "\n", "Duplicate Rows : 0\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/png": "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sltLnpOUf8zx3//+d+zcuTN69uxZ5/KePXvGhg0byjRVy/va174Wd999dxx11FHx9ttvx29/+9s4+eSTY9myZaU91LejN954oxzjlkVD9rBhw4Zo3759dO3adY/bHKjn6Zxzzonvfe970b9//1i9enVcddVVMXLkyHjxxRejsrLygN9JURRxxRVXxKmnnhrHHXdcRDgrEfXvJaL1npclS5bEiBEj4qOPPoovfOELMWvWrBg4cGDph21rPCt720lEy5+TlAGzW0VFRZ3Pi6LY47ID2TnnnFP69+DBg2PEiBFx+OGHx1133VV64lRr39FuTdnDgbyrCy+8sPTv4447Lk444YTo379/zJ49Oy644IK93u9A2cn48ePj5ZdfjqeeemqP61rzWdnbXlrreTn66KNj8eLFsWnTprjvvvtizJgxMX/+/NL1rfGs7G0nAwcObPFzkvJXSIcddlgcdNBBexTbxo0b9yji1qRTp04xePDgWLVqVenVSK19Rw3ZQ69evWL79u3x/vvv7/U2B7revXtH//79Y9WqVRFxYO9kwoQJ8eCDD8bcuXOjT58+pctb+1nZ217q01rOS/v27eOII46IE044IaZNmxZDhw6NG264oVWflb3tpD77+pykDJj27dvHsGHD4tFHH61z+aOPPhonn3xymaYqv9ra2li+fHn07t07BgwYEL169aqzo+3bt8f8+fNb1Y4asodhw4ZFu3bt6txm/fr1sXTp0lazq3fffTfWrl0bvXv3jogDcydFUcT48ePj/vvvjyeeeCIGDBhQ5/rWelY+ay/1aQ3npT5FUURtbW2rPSv12b2T+uzzc9Lop/3uJ2bOnFm0a9euuOOOO4pXXnmlmDhxYtGpU6fi9ddfL/doLWbSpEnFvHnzitdee61YsGBBce655xadO3cu7WD69OlFVVVVcf/99xdLliwpfvCDHxS9e/cuampqyjx589qyZUuxaNGiYtGiRUVEFNdff32xaNGi4o033iiKomF7uOSSS4o+ffoUjz32WLFw4cJi5MiRxdChQ4sdO3aU69v6XD5tJ1u2bCkmTZpUPPPMM8Xq1auLuXPnFiNGjCi++MUvHtA7ufTSS4uqqqpi3rx5xfr160sf27ZtK92mNZ6Vz9pLaz0vkydPLp588sli9erVxcsvv1z86le/Ktq0aVM88sgjRVG0zrPyaTspxzlJGzBFURQ33XRT0b9//6J9+/bFV7/61Tov+2sNLrzwwqJ3795Fu3btiurq6uKCCy4oli1bVrp+165dxZQpU4pevXoVlZWVxf/8z/8US5YsKePE+8bcuXOLiNjjY8yYMUVRNGwPH374YTF+/PiiW7duRceOHYtzzz23WLNmTRm+m+bxaTvZtm1bMWrUqKJ79+5Fu3btin79+hVjxozZ4/s90HZS3z4iovjTn/5Uuk1rPCuftZfWel7Gjh1b+vnSvXv34owzzijFS1G0zrPyaTspxzmpKIqiaPzjNgAA5ZPyOTAAQOsmYACAdAQMAJCOgAEA0hEwAEA6AgYASEfAAADpCBgAIB0BAwCkI2CAA9Lrr78eFRUVsXjx4nKPAuwDAgYASEfAAPvErl274tprr40jjjgiKisro1+/fjF16tSIiFiyZEmMHDkyOnbsGIceemhcfPHF8cEHH5Tue/rpp8fEiRPrfL3zzz8/LrrootLnX/rSl+Kaa66JsWPHRufOnaNfv37xhz/8oXT9gAEDIiLi+OOPj4qKijj99NP32fcKtDwBA+wTkydPjmuvvTauuuqqeOWVV+Ivf/lL9OzZM7Zt2xZnn312dO3aNZ5//vn461//Go899liMHz++0f/H7373uzjhhBNi0aJFMW7cuLj00ktjxYoVERHx3HPPRUTEY489FuvXr4/777+/Wb8/oLzalnsA4MCzZcuWuOGGG2LGjBkxZsyYiIg4/PDD49RTT43bb789Pvzww7j77rujU6dOERExY8aMOO+88+Laa6+Nnj17Nvj/+da3vhXjxo2LiIgrr7wyfv/738e8efPimGOOie7du0dExKGHHhq9evVq5u8QKDePwADNbvny5VFbWxtnnHFGvdcNHTq0FC8REaecckrs2rUrVq5c2aj/Z8iQIaV/V1RURK9evWLjxo1NHxxIQ8AAza5jx457va4oiqioqKj3ut2Xt2nTJoqiqHPdxx9/vMft27Vrt8f9d+3a1dhxgYQEDNDsjjzyyOjYsWM8/vjje1w3cODAWLx4cWzdurV02dNPPx1t2rSJo446KiIiunfvHuvXry9dv3Pnzli6dGmjZmjfvn3pvsCBR8AAza5Dhw5x5ZVXxi9+8Yu4++6749VXX40FCxbEHXfcET/84Q+jQ4cOMWbMmFi6dGnMnTs3JkyYED/60Y9Kz38ZOXJkzJ49O2bPnh0rVqyIcePGxaZNmxo1Q48ePaJjx44xZ86cePvtt2Pz5s374DsFykXAAPvEVVddFZMmTYrf/OY3ceyxx8aFF14YGzdujIMPPjgefvjheO+992L48OHx3e9+N84444yYMWNG6b5jx46NMWPGxI9//OM47bTTYsCAAfGNb3yjUf9/27Zt48Ybb4zbbrstqqurY/To0c39LQJlVFF88hfNAAD7OY/AAADpCBgAIB0BAwCkI2AAgHQEDACQjoABANIRMABAOgIGAEhHwAAA6QgYACAdAQMApPP/AOfQIr5Q2mUaAAAAAElFTkSuQmCC", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "test_df = describe_dataset(TEST_DATASET_PATH)\n", "sns.countplot(y='label', data=test_df, palette=\"Set1\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3.8.13 (conda)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.13" }, "orig_nbformat": 4, "vscode": { "interpreter": { "hash": "9260f401923fb5c4108c543a7d176de9733d378b3752e49535ad7c43c2271b65" } } }, "nbformat": 4, "nbformat_minor": 2 }