Spaces:
Sleeping
Sleeping
Upload 9 files
Browse files- .gitattributes +1 -0
- .gitignore +171 -0
- Dockerfile +17 -0
- FinalModel.ipynb +332 -0
- LICENSE +21 -0
- README.md +124 -10
- best_model.keras +3 -0
- main.py +67 -0
- requirements.txt +10 -0
- single_person_processor.py +284 -0
.gitattributes
CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
+
best_model.keras filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Byte-compiled / optimized / DLL files
|
2 |
+
__pycache__/
|
3 |
+
*.py[cod]
|
4 |
+
*$py.class
|
5 |
+
|
6 |
+
# C extensions
|
7 |
+
*.so
|
8 |
+
|
9 |
+
# Distribution / packaging
|
10 |
+
.Python
|
11 |
+
build/
|
12 |
+
develop-eggs/
|
13 |
+
dist/
|
14 |
+
downloads/
|
15 |
+
eggs/
|
16 |
+
.eggs/
|
17 |
+
lib/
|
18 |
+
lib64/
|
19 |
+
parts/
|
20 |
+
sdist/
|
21 |
+
var/
|
22 |
+
wheels/
|
23 |
+
share/python-wheels/
|
24 |
+
*.egg-info/
|
25 |
+
.installed.cfg
|
26 |
+
*.egg
|
27 |
+
MANIFEST
|
28 |
+
|
29 |
+
# PyInstaller
|
30 |
+
# Usually these files are written by a python script from a template
|
31 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
32 |
+
*.manifest
|
33 |
+
*.spec
|
34 |
+
|
35 |
+
# Installer logs
|
36 |
+
pip-log.txt
|
37 |
+
pip-delete-this-directory.txt
|
38 |
+
|
39 |
+
# Unit test / coverage reports
|
40 |
+
htmlcov/
|
41 |
+
.tox/
|
42 |
+
.nox/
|
43 |
+
.coverage
|
44 |
+
.coverage.*
|
45 |
+
.cache
|
46 |
+
nosetests.xml
|
47 |
+
coverage.xml
|
48 |
+
*.cover
|
49 |
+
*.py,cover
|
50 |
+
.hypothesis/
|
51 |
+
.pytest_cache/
|
52 |
+
cover/
|
53 |
+
|
54 |
+
# Translations
|
55 |
+
*.mo
|
56 |
+
*.pot
|
57 |
+
|
58 |
+
# Django stuff:
|
59 |
+
*.log
|
60 |
+
local_settings.py
|
61 |
+
db.sqlite3
|
62 |
+
db.sqlite3-journal
|
63 |
+
|
64 |
+
# Flask stuff:
|
65 |
+
instance/
|
66 |
+
.webassets-cache
|
67 |
+
|
68 |
+
# Scrapy stuff:
|
69 |
+
.scrapy
|
70 |
+
|
71 |
+
# Sphinx documentation
|
72 |
+
docs/_build/
|
73 |
+
|
74 |
+
# PyBuilder
|
75 |
+
.pybuilder/
|
76 |
+
target/
|
77 |
+
|
78 |
+
# Jupyter Notebook
|
79 |
+
.ipynb_checkpoints
|
80 |
+
|
81 |
+
# IPython
|
82 |
+
profile_default/
|
83 |
+
ipython_config.py
|
84 |
+
|
85 |
+
# pyenv
|
86 |
+
# For a library or package, you might want to ignore these files since the code is
|
87 |
+
# intended to run in multiple environments; otherwise, check them in:
|
88 |
+
# .python-version
|
89 |
+
|
90 |
+
# pipenv
|
91 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
92 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
93 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
94 |
+
# install all needed dependencies.
|
95 |
+
#Pipfile.lock
|
96 |
+
|
97 |
+
# UV
|
98 |
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
99 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
100 |
+
# commonly ignored for libraries.
|
101 |
+
#uv.lock
|
102 |
+
|
103 |
+
# poetry
|
104 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
105 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
106 |
+
# commonly ignored for libraries.
|
107 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
108 |
+
#poetry.lock
|
109 |
+
|
110 |
+
# pdm
|
111 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
112 |
+
#pdm.lock
|
113 |
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
114 |
+
# in version control.
|
115 |
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
116 |
+
.pdm.toml
|
117 |
+
.pdm-python
|
118 |
+
.pdm-build/
|
119 |
+
|
120 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
121 |
+
__pypackages__/
|
122 |
+
|
123 |
+
# Celery stuff
|
124 |
+
celerybeat-schedule
|
125 |
+
celerybeat.pid
|
126 |
+
|
127 |
+
# SageMath parsed files
|
128 |
+
*.sage.py
|
129 |
+
|
130 |
+
# Environments
|
131 |
+
.env
|
132 |
+
.venv
|
133 |
+
env/
|
134 |
+
venv/
|
135 |
+
ENV/
|
136 |
+
env.bak/
|
137 |
+
venv.bak/
|
138 |
+
|
139 |
+
# Spyder project settings
|
140 |
+
.spyderproject
|
141 |
+
.spyproject
|
142 |
+
|
143 |
+
# Rope project settings
|
144 |
+
.ropeproject
|
145 |
+
|
146 |
+
# mkdocs documentation
|
147 |
+
/site
|
148 |
+
|
149 |
+
# mypy
|
150 |
+
.mypy_cache/
|
151 |
+
.dmypy.json
|
152 |
+
dmypy.json
|
153 |
+
|
154 |
+
# Pyre type checker
|
155 |
+
.pyre/
|
156 |
+
|
157 |
+
# pytype static type analyzer
|
158 |
+
.pytype/
|
159 |
+
|
160 |
+
# Cython debug symbols
|
161 |
+
cython_debug/
|
162 |
+
|
163 |
+
# PyCharm
|
164 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
165 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
166 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
167 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
168 |
+
#.idea/
|
169 |
+
|
170 |
+
# PyPI configuration file
|
171 |
+
.pypirc
|
Dockerfile
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Use an official Python image
|
2 |
+
FROM python:3.9
|
3 |
+
|
4 |
+
# Set the working directory inside the container
|
5 |
+
WORKDIR /app
|
6 |
+
|
7 |
+
# Copy all files to the container
|
8 |
+
COPY . /app/
|
9 |
+
|
10 |
+
# Install dependencies
|
11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
12 |
+
|
13 |
+
# Expose port 7860 (Hugging Face default)
|
14 |
+
EXPOSE 7860
|
15 |
+
|
16 |
+
# Run FastAPI using Uvicorn
|
17 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
FinalModel.ipynb
ADDED
@@ -0,0 +1,332 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"cells": [
|
3 |
+
{
|
4 |
+
"cell_type": "code",
|
5 |
+
"execution_count": 1,
|
6 |
+
"id": "6b841a73",
|
7 |
+
"metadata": {},
|
8 |
+
"outputs": [],
|
9 |
+
"source": [
|
10 |
+
"import os\n",
|
11 |
+
"import pandas as pd\n",
|
12 |
+
"import numpy as np\n",
|
13 |
+
"import tensorflow as tf\n",
|
14 |
+
"from tensorflow.keras.layers import Input,Rescaling,Conv2D, MaxPooling2D,Activation,GlobalAveragePooling2D,SpatialDropout2D,Flatten, Dense, Concatenate, BatchNormalization, Dropout\n",
|
15 |
+
"from tensorflow.keras.models import Model\n",
|
16 |
+
"from tensorflow.keras.regularizers import l2\n",
|
17 |
+
"from tensorflow.keras.preprocessing.image import load_img, img_to_array\n",
|
18 |
+
"from sklearn.model_selection import train_test_split\n",
|
19 |
+
"from tensorflow.keras.callbacks import EarlyStopping\n",
|
20 |
+
"import matplotlib.pyplot as plt"
|
21 |
+
]
|
22 |
+
},
|
23 |
+
{
|
24 |
+
"cell_type": "code",
|
25 |
+
"execution_count": 2,
|
26 |
+
"id": "c1a753ad",
|
27 |
+
"metadata": {},
|
28 |
+
"outputs": [],
|
29 |
+
"source": [
|
30 |
+
"\n",
|
31 |
+
"IMG_SIZE = (128, 128)\n",
|
32 |
+
"BATCH_SIZE = 8\n",
|
33 |
+
"EPOCHS = 60\n",
|
34 |
+
"VALIDATION_SPLIT = 0.1\n",
|
35 |
+
"BASE_PATH = 'dataset'\n",
|
36 |
+
"MEASUREMENT_COLS = [\n",
|
37 |
+
" 'ankle', 'arm-length', 'bicep', 'calf', 'chest', 'forearm', \n",
|
38 |
+
" 'height', 'hip', 'leg-length', 'shoulder-breadth',\n",
|
39 |
+
" 'shoulder-to-crotch', 'thigh', 'waist', 'wrist'\n",
|
40 |
+
"]\n"
|
41 |
+
]
|
42 |
+
},
|
43 |
+
{
|
44 |
+
"cell_type": "code",
|
45 |
+
"execution_count": 3,
|
46 |
+
"id": "1ed89592",
|
47 |
+
"metadata": {},
|
48 |
+
"outputs": [],
|
49 |
+
"source": [
|
50 |
+
"def load_and_clean_data():\n",
|
51 |
+
" metadata = pd.read_csv(os.path.join(BASE_PATH, 'train', 'hwg_metadata.csv'))\n",
|
52 |
+
" measurements = pd.read_csv(os.path.join(BASE_PATH, 'train', 'measurements.csv'))\n",
|
53 |
+
" photo_map = pd.read_csv(os.path.join(BASE_PATH, 'train', 'subject_to_photo_map.csv'))\n",
|
54 |
+
"\n",
|
55 |
+
" linked_data = photo_map.merge(\n",
|
56 |
+
" metadata[['subject_id', 'gender', 'height_cm', 'weight_kg']],\n",
|
57 |
+
" on='subject_id',\n",
|
58 |
+
" how='inner'\n",
|
59 |
+
" ).merge(\n",
|
60 |
+
" measurements,\n",
|
61 |
+
" on='subject_id',\n",
|
62 |
+
" how='inner'\n",
|
63 |
+
" )\n",
|
64 |
+
" \n",
|
65 |
+
" linked_data = linked_data.drop_duplicates(subset=['subject_id'])\n",
|
66 |
+
" linked_data = linked_data.dropna(subset=['photo_id', 'subject_id'])\n",
|
67 |
+
" linked_data = linked_data[linked_data.photo_id.str.match(r'^[a-f0-9]{32}$')]\n",
|
68 |
+
" linked_data['gender'] = linked_data['gender'].map({'male': 0, 'female': 1})\n",
|
69 |
+
" MEASUREMENT_COLS = measurements.columns.drop('subject_id').tolist()\n",
|
70 |
+
" linked_data['measurements'] = linked_data[MEASUREMENT_COLS].values.tolist()\n",
|
71 |
+
" final_df = linked_data[['photo_id', 'gender', 'height_cm', 'weight_kg', 'measurements']]\n",
|
72 |
+
" return final_df"
|
73 |
+
]
|
74 |
+
},
|
75 |
+
{
|
76 |
+
"cell_type": "code",
|
77 |
+
"execution_count": 4,
|
78 |
+
"id": "e22acac3",
|
79 |
+
"metadata": {},
|
80 |
+
"outputs": [],
|
81 |
+
"source": [
|
82 |
+
"def load_image_data(data_df):\n",
|
83 |
+
" front_images = []\n",
|
84 |
+
" side_images = []\n",
|
85 |
+
" meta_data = []\n",
|
86 |
+
" measurements = []\n",
|
87 |
+
"\n",
|
88 |
+
" for _, row in data_df.iterrows():\n",
|
89 |
+
" try:\n",
|
90 |
+
" front_img = load_img(os.path.join(BASE_PATH, 'train', 'mask', f\"{row['photo_id']}.png\"),\n",
|
91 |
+
" color_mode='grayscale', target_size=IMG_SIZE)\n",
|
92 |
+
" side_img = load_img(os.path.join(BASE_PATH, 'train', 'mask_left', f\"{row['photo_id']}.png\"),\n",
|
93 |
+
" color_mode='grayscale', target_size=IMG_SIZE)\n",
|
94 |
+
"\n",
|
95 |
+
" front_array = img_to_array(front_img) / 255.0\n",
|
96 |
+
" side_array = img_to_array(side_img) / 255.0\n",
|
97 |
+
"\n",
|
98 |
+
" front_images.append(front_array)\n",
|
99 |
+
" side_images.append(side_array)\n",
|
100 |
+
" meta_data.append([row['gender'], row['height_cm'], row['weight_kg']])\n",
|
101 |
+
" measurements.append(row['measurements'])\n",
|
102 |
+
" except Exception as e:\n",
|
103 |
+
" continue\n",
|
104 |
+
"\n",
|
105 |
+
" return (\n",
|
106 |
+
" np.array(front_images, dtype=np.float32),\n",
|
107 |
+
" np.array(side_images, dtype=np.float32),\n",
|
108 |
+
" np.array(meta_data, dtype=np.float32),\n",
|
109 |
+
" np.array(measurements, dtype=np.float32)\n",
|
110 |
+
" )"
|
111 |
+
]
|
112 |
+
},
|
113 |
+
{
|
114 |
+
"cell_type": "code",
|
115 |
+
"execution_count": 11,
|
116 |
+
"id": "6d6b6bfc",
|
117 |
+
"metadata": {},
|
118 |
+
"outputs": [],
|
119 |
+
"source": [
|
120 |
+
"def create_dual_input_model(measurement_cols):\n",
|
121 |
+
" front_input = Input(shape=(*IMG_SIZE, 1), name='front_image')\n",
|
122 |
+
" side_input = Input(shape=(*IMG_SIZE, 1), name='side_image')\n",
|
123 |
+
" meta_input = Input(shape=(3,), name='metadata')\n",
|
124 |
+
"\n",
|
125 |
+
" def create_image_branch(input_layer):\n",
|
126 |
+
" x = Rescaling(1./255)(input_layer)\n",
|
127 |
+
" x = Conv2D(32, (3,3), padding='same')(x)\n",
|
128 |
+
" x = BatchNormalization()(x)\n",
|
129 |
+
" x = Activation('relu')(x)\n",
|
130 |
+
" x = SpatialDropout2D(0.2)(x) \n",
|
131 |
+
" x = MaxPooling2D(2,2)(x)\n",
|
132 |
+
"\n",
|
133 |
+
" \n",
|
134 |
+
" x = Conv2D(64, (3,3), padding='same', kernel_regularizer='l2')(x)\n",
|
135 |
+
" x = BatchNormalization()(x)\n",
|
136 |
+
" x = Activation('relu')(x)\n",
|
137 |
+
" x = SpatialDropout2D(0.3)(x)\n",
|
138 |
+
" x = MaxPooling2D(2,2)(x)\n",
|
139 |
+
"\n",
|
140 |
+
" return GlobalAveragePooling2D()(x)\n",
|
141 |
+
"\n",
|
142 |
+
" front_features = create_image_branch(front_input)\n",
|
143 |
+
" side_features = create_image_branch(side_input)\n",
|
144 |
+
"\n",
|
145 |
+
" merged_images = Concatenate()([front_features, side_features])\n",
|
146 |
+
" combined = Concatenate()([merged_images, meta_input])\n",
|
147 |
+
"\n",
|
148 |
+
" x = Dense(512, activation='relu', \n",
|
149 |
+
" kernel_regularizer=l2(0.01), \n",
|
150 |
+
" bias_regularizer=l2(0.01))(combined)\n",
|
151 |
+
" x = Dropout(0.6)(x) \n",
|
152 |
+
" x = BatchNormalization()(x)\n",
|
153 |
+
" x = Dense(512, activation='relu', \n",
|
154 |
+
" kernel_regularizer=l2(0.01), \n",
|
155 |
+
" bias_regularizer=l2(0.01))(x)\n",
|
156 |
+
" x = Dropout(0.6)(x) \n",
|
157 |
+
"\n",
|
158 |
+
" measurements_output = Dense(len(measurement_cols),\n",
|
159 |
+
" activation='linear',\n",
|
160 |
+
" name='measurements')(x)\n",
|
161 |
+
"\n",
|
162 |
+
" model = Model(\n",
|
163 |
+
" inputs=[front_input, side_input, meta_input],\n",
|
164 |
+
" outputs=measurements_output\n",
|
165 |
+
" )\n",
|
166 |
+
" \n",
|
167 |
+
" model.compile(\n",
|
168 |
+
" optimizer=tf.keras.optimizers.Adam(\n",
|
169 |
+
" learning_rate=0.00005, \n",
|
170 |
+
" clipvalue=0.5 \n",
|
171 |
+
" ),\n",
|
172 |
+
" loss='mse',\n",
|
173 |
+
" metrics=['mae']\n",
|
174 |
+
" )\n",
|
175 |
+
" \n",
|
176 |
+
" return model"
|
177 |
+
]
|
178 |
+
},
|
179 |
+
{
|
180 |
+
"cell_type": "code",
|
181 |
+
"execution_count": 6,
|
182 |
+
"id": "12fa9884",
|
183 |
+
"metadata": {},
|
184 |
+
"outputs": [],
|
185 |
+
"source": [
|
186 |
+
"def get_training_callbacks():\n",
|
187 |
+
" return [\n",
|
188 |
+
" tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=3, verbose=1),\n",
|
189 |
+
" tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True, verbose=1),\n",
|
190 |
+
" tf.keras.callbacks.ModelCheckpoint('best_model.keras', save_best_only=True, monitor='val_loss')\n",
|
191 |
+
" ]"
|
192 |
+
]
|
193 |
+
},
|
194 |
+
{
|
195 |
+
"cell_type": "code",
|
196 |
+
"execution_count": 12,
|
197 |
+
"id": "03c6efc2",
|
198 |
+
"metadata": {},
|
199 |
+
"outputs": [
|
200 |
+
{
|
201 |
+
"name": "stdout",
|
202 |
+
"output_type": "stream",
|
203 |
+
"text": [
|
204 |
+
"Epoch 1/60\n",
|
205 |
+
"\u001b[1m205/205\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m41s\u001b[0m 182ms/step - loss: 5674.9922 - mae: 62.7988 - val_loss: 5683.9194 - val_mae: 62.8206 - learning_rate: 5.0000e-05\n",
|
206 |
+
"Epoch 2/60\n",
|
207 |
+
"\u001b[1m205/205\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m37s\u001b[0m 182ms/step - loss: 5483.9150 - mae: 61.2921 - val_loss: 5513.6626 - val_mae: 61.6205 - learning_rate: 5.0000e-05\n",
|
208 |
+
"Epoch 3/60\n",
|
209 |
+
"\u001b[1m205/205\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m37s\u001b[0m 182ms/step - loss: 5132.9355 - mae: 58.6033 - val_loss: 4916.4282 - val_mae: 57.0114 - learning_rate: 5.0000e-05\n",
|
210 |
+
"Epoch 4/60\n",
|
211 |
+
"\u001b[1m205/205\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m37s\u001b[0m 182ms/step - loss: 4634.2549 - mae: 54.4180 - val_loss: 4393.1675 - val_mae: 52.4100 - learning_rate: 5.0000e-05\n",
|
212 |
+
"Epoch 5/60\n",
|
213 |
+
"\u001b[1m205/205\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m37s\u001b[0m 181ms/step - loss: 3850.5964 - mae: 47.1574 - val_loss: 3314.1902 - val_mae: 41.7666 - learning_rate: 5.0000e-05\n",
|
214 |
+
"Epoch 6/60\n",
|
215 |
+
"\u001b[1m205/205\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m37s\u001b[0m 182ms/step - loss: 2824.2920 - mae: 36.8301 - val_loss: 2080.4927 - val_mae: 28.9957 - learning_rate: 5.0000e-05\n",
|
216 |
+
"Epoch 7/60\n",
|
217 |
+
"\u001b[1m205/205\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m38s\u001b[0m 183ms/step - loss: 1813.3002 - mae: 26.8589 - val_loss: 1138.2924 - val_mae: 19.4825 - learning_rate: 5.0000e-05\n",
|
218 |
+
"Epoch 8/60\n",
|
219 |
+
"\u001b[1m205/205\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m37s\u001b[0m 182ms/step - loss: 971.0135 - mae: 18.2215 - val_loss: 480.0345 - val_mae: 11.6101 - learning_rate: 5.0000e-05\n",
|
220 |
+
"Epoch 9/60\n",
|
221 |
+
"\u001b[1m205/205\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m37s\u001b[0m 181ms/step - loss: 469.0742 - mae: 12.1349 - val_loss: 202.6879 - val_mae: 7.5811 - learning_rate: 5.0000e-05\n",
|
222 |
+
"Epoch 10/60\n",
|
223 |
+
"\u001b[1m205/205\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m37s\u001b[0m 181ms/step - loss: 229.5633 - mae: 9.1803 - val_loss: 93.9693 - val_mae: 4.9147 - learning_rate: 5.0000e-05\n",
|
224 |
+
"Epoch 11/60\n",
|
225 |
+
"\u001b[1m205/205\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m37s\u001b[0m 182ms/step - loss: 131.9381 - mae: 7.3022 - val_loss: 64.7270 - val_mae: 3.9374 - learning_rate: 5.0000e-05\n",
|
226 |
+
"Epoch 12/60\n",
|
227 |
+
"\u001b[1m205/205\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m37s\u001b[0m 182ms/step - loss: 111.0290 - mae: 6.9099 - val_loss: 45.5564 - val_mae: 3.2909 - learning_rate: 5.0000e-05\n",
|
228 |
+
"Epoch 13/60\n",
|
229 |
+
"\u001b[1m205/205\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m37s\u001b[0m 182ms/step - loss: 104.3956 - mae: 6.6508 - val_loss: 40.8269 - val_mae: 3.0271 - learning_rate: 5.0000e-05\n",
|
230 |
+
"Epoch 14/60\n",
|
231 |
+
"\u001b[1m205/205\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m37s\u001b[0m 182ms/step - loss: 100.3586 - mae: 6.5860 - val_loss: 44.4664 - val_mae: 3.1478 - learning_rate: 5.0000e-05\n",
|
232 |
+
"Epoch 15/60\n",
|
233 |
+
"\u001b[1m205/205\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m37s\u001b[0m 182ms/step - loss: 96.6314 - mae: 6.4145 - val_loss: 44.0387 - val_mae: 3.0308 - learning_rate: 5.0000e-05\n",
|
234 |
+
"Epoch 16/60\n",
|
235 |
+
"\u001b[1m204/205\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m━\u001b[0m \u001b[1m0s\u001b[0m 181ms/step - loss: 94.7115 - mae: 6.3199 \n",
|
236 |
+
"Epoch 16: ReduceLROnPlateau reducing learning rate to 2.499999936844688e-05.\n",
|
237 |
+
"\u001b[1m205/205\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m38s\u001b[0m 183ms/step - loss: 94.7210 - mae: 6.3201 - val_loss: 54.8999 - val_mae: 2.9513 - learning_rate: 5.0000e-05\n",
|
238 |
+
"Epoch 17/60\n",
|
239 |
+
"\u001b[1m205/205\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m37s\u001b[0m 182ms/step - loss: 91.5854 - mae: 6.1751 - val_loss: 40.7038 - val_mae: 2.9271 - learning_rate: 2.5000e-05\n",
|
240 |
+
"Epoch 18/60\n",
|
241 |
+
"\u001b[1m205/205\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m37s\u001b[0m 182ms/step - loss: 92.5108 - mae: 6.1862 - val_loss: 49.3668 - val_mae: 2.9976 - learning_rate: 2.5000e-05\n",
|
242 |
+
"Epoch 19/60\n",
|
243 |
+
"\u001b[1m205/205\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m38s\u001b[0m 187ms/step - loss: 87.6244 - mae: 6.0041 - val_loss: 65.3090 - val_mae: 3.0989 - learning_rate: 2.5000e-05\n",
|
244 |
+
"Epoch 20/60\n",
|
245 |
+
"\u001b[1m204/205\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m━\u001b[0m \u001b[1m0s\u001b[0m 180ms/step - loss: 90.8720 - mae: 6.1247 \n",
|
246 |
+
"Epoch 20: ReduceLROnPlateau reducing learning rate to 1.249999968422344e-05.\n",
|
247 |
+
"\u001b[1m205/205\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m37s\u001b[0m 182ms/step - loss: 90.8620 - mae: 6.1244 - val_loss: 53.7187 - val_mae: 3.0438 - learning_rate: 2.5000e-05\n",
|
248 |
+
"Epoch 21/60\n",
|
249 |
+
"\u001b[1m205/205\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m37s\u001b[0m 182ms/step - loss: 91.5362 - mae: 6.1713 - val_loss: 43.8898 - val_mae: 2.9120 - learning_rate: 1.2500e-05\n",
|
250 |
+
"Epoch 22/60\n",
|
251 |
+
"\u001b[1m205/205\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m38s\u001b[0m 186ms/step - loss: 87.7424 - mae: 6.0323 - val_loss: 44.6432 - val_mae: 2.8598 - learning_rate: 1.2500e-05\n",
|
252 |
+
"Epoch 22: early stopping\n",
|
253 |
+
"Restoring model weights from the end of the best epoch: 17.\n"
|
254 |
+
]
|
255 |
+
},
|
256 |
+
{
|
257 |
+
"data": {
|
258 |
+
"image/png": "iVBORw0KGgoAAAANSUhEUgAAA/YAAAIjCAYAAACpnIB8AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAiYtJREFUeJzt3Qd0FFXDxvFnNx1SqKH33pEmqCAIUgQbYFds6KdiwQq89t4FG2AFK9gQFQVBiiAdpPcqvUMCgdTNd+5dNiQQICSbbLL5/86Zs7Mzs3fvJoHkmdscqampqQIAAAAAAAWS09cVAAAAAAAA2UewBwAAAACgACPYAwAAAABQgBHsAQAAAAAowAj2AAAAAAAUYAR7AAAAAAAKMII9AAAAAAAFGMEeAAAAAIACjGAPAAAAAEABRrAHACCXOBwOPffcc+f8us2bN9vXjhw5Ur5m6m/qkh2m/ua15vMAAIDcQ7AHAPg1T7g02z///HPK+dTUVFWqVMme79GjhwqKqlWrpn2uM2354eaAL29IeLYiRYqofv36euqppxQbG+vr6gEA4FWB3i0OAID8KTQ0VN9++60uuuiiDMf//vtvbdu2TSEhISpIhgwZoiNHjqQ9/+OPPzRq1CgNHjxYpUqVSjt+wQUX5Oh9TBAeOHBgtl57yy236Prrr/fp13bYsGEKDw+3X6uJEyfq5Zdf1pQpUzRz5sxs90QAACC/IdgDAAqFyy67TD/88IPee+89BQae+PVnwn7z5s21b98+FSRXXXVVhue7du2ywd4cN635pxMXF6eiRYtm+X3M1yr91+tcBAQE2M2XevfunXaj45577lGvXr00ZswYzZkzR23atMn0NUePHrUt/HnhXL8fAABkhq74AIBC4YYbbtD+/fs1adKktGOJiYn68ccfdeONN542dD366KO2q75pda5Tp47eeust230/vYSEBD388MMqXbq0IiIidMUVV9heAJnZvn277rjjDpUpU8aW2aBBA33++efKDbfddpttrd6wYYO9sWHqdtNNN9lzM2bM0DXXXKPKlSvbepjPaD7DsWPHzjrG3jy///77NXbsWDVs2DDtc0yYMOGsY+zNTQcz5MEMi2jVqpXtSVG9enV9+eWXp9R/6dKluvjiixUWFqaKFSvqpZde0ogRI3I0bv+SSy6xj5s2bbKP7du3t59h4cKFateunQ30//vf/+y5PXv26M4777TfK1PPJk2a6IsvvjilTPNzZXonREZGqlixYrr11lu1ZMmSU4ZCnOn74XK5bC8M83U072Xe8//+7/908ODBDO+1YMECdenSxd6sMF+XatWq2Z+n9EaPHm1vVpnyTZ0aNWqkd999N1tfLwBAwUCLPQCgUDCB0rTQmlbtbt262WPjx49XTEyM7S5uWvLTM+HdBPSpU6facNe0aVP9+eefevzxx204N13ePfr27auvv/7a3iAwXd9NV+/u3bufUofdu3erdevWacHY3AgwdTDlm3Hf/fv39/rnTk5OtkHQDEEwNyU8LdGm94Jpmb733ntVsmRJzZs3T++//769IWHOnY0J5qbl+7777rMB0nz9TGv4li1bbHlnsn79etuSbj63CcHmxoYJvSaMmmBrmK9xhw4d7Ndq0KBBtlX7008/zXG3fhOqjfR1NMHc/EyYn4Obb77Zhmpzg8OEflNX870yAdp8XUw9Dx06pIceeigtkF9++eX262e+lnXr1tUvv/xiP9e5fD9MiDc3AW6//XY9+OCD9sbDBx98oEWLFtlhA0FBQfZGQ+fOne3PjRkeYW4imBsc5vvgYW5cmZtYHTt21Ouvv26PrVq1ypbhqTMAwA+lAgDgx0aMGGGa11Pnz5+f+sEHH6RGRESkHj161J675pprUjt06GD3q1Spktq9e/e0140dO9a+7qWXXspQXu/evVMdDkfq+vXr7fPFixfb6+67774M19144432+LPPPpt27M4770wtV65c6r59+zJce/3116dGRUWl1WvTpk32tabuWfXmm2/a15jXetx666322MCBA0+53vNe6b366qv2s/33339px0z9T/5zwTwPDg5O+xoYS5Ysscfff//9U7726etkvs7m2PTp09OO7dmzJzUkJCT10UcfTTv2wAMP2LosWrQo7dj+/ftTS5QocUqZmfHUe82aNal79+6113/00Uf2fcqUKZMaFxdnr7v44ovtdcOHD8/w+iFDhtjjX3/9ddqxxMTE1DZt2qSGh4enxsbG2mM//fSTvc5c75GSkpJ6ySWXnPI9PN33Y8aMGfb4N998k+H4hAkTMhz/+eef036WT+ehhx5KjYyMTE1OTj7j1wcA4F/oig8AKDSuvfZa2xI7btw4HT582D6erhu+mYzOjA83rafpma75JtualnbPdcbJ153c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AABAAUawBwAAAACgAGOMPQAAAACcZdmx5ORkpaSk+Loq8DNBQUEKCAjIcTkEewAAAAA4jcTERO3cuVNHjx71dVXgpxPjVaxYUeHh4Tkqh2APAAAAAJlwuVzatGmTbVEtX768goODszRDOZDVniB79+7Vtm3bVKtWrRy13BPsAQAAAOA0rfUm3Ju1xIsUKeLr6sAPlS5dWps3b1ZSUlKOgj2T5wEAAADAGTidxCbkDm/1AOEnFAAAAACAAoxgDwAAAABAAUawBwAAAACcUdWqVTVkyBBfVwOnQbAHAAAAAD8as32m7bnnnstWufPnz9fdd9+do7q1b99e/fv3z1EZyByz4gMAAACAn9i5c2fa/nfffadnnnlGa9asSTuWfr10s9xaSkqKAgMDszR7O/IvWuwBAAAAIItMGD6amJznm3nfrChbtmzaFhUVZVvpPc9Xr16tiIgIjR8/Xs2bN1dISIj++ecfbdiwQVdeeaXKlCljg3/Lli31119/nbErvin3008/1dVXX22XAjTrsP/66685+tr+9NNPatCgga2Xeb+33347w/mhQ4fa9wkNDbV17d27d9q5H3/8UY0aNVJYWJhKliypTp06KS4uToUFLfYAAAAAkEXHklJU/5k/8/x9V77QRUWCvRPfBg4cqLfeekvVq1dX8eLFtXXrVl122WV6+eWXbaj+8ssvdfnll9uW/sqVK5+2nOeff15vvPGG3nzzTb3//vu66aab9N9//6lEiRLnXKeFCxfq2muvtUMFrrvuOs2aNUv33XefDem33XabFixYoAcffFBfffWVLrjgAh04cEAzZsxI66Vwww032LqYGw2HDx+257J6M8QfEOwBAAAAoBB54YUXdOmll6Y9N0G8SZMmac9ffPFF/fzzz7YF/v777z9tOSZwm0BtvPLKK3rvvfc0b948de3a9Zzr9M4776hjx456+umn7fPatWtr5cqV9qaBeZ8tW7aoaNGi6tGjh+11UKVKFZ133nlpwT45OVk9e/a0xw3Tel+YEOz9SXyMtHiUVLe7VKySr2sDAAAA+J2woADbeu6L9/WWFi1aZHh+5MgR21L++++/p4XkY8eO2TB9Jo0bN07bN6E7MjJSe/bsyVadVq1aZYcDpHfhhRfa7v9mHgBzI8KEdtPLwNw4MJtnGECTJk3sTQET5rt06aLOnTvbbvqmN0JhwRh7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",
|
259 |
+
"text/plain": [
|
260 |
+
"<Figure size 1200x600 with 1 Axes>"
|
261 |
+
]
|
262 |
+
},
|
263 |
+
"metadata": {},
|
264 |
+
"output_type": "display_data"
|
265 |
+
}
|
266 |
+
],
|
267 |
+
"source": [
|
268 |
+
"if __name__ == \"__main__\":\n",
|
269 |
+
" cleaned_data = load_and_clean_data()\n",
|
270 |
+
" front_imgs, side_imgs, meta_data, measurements = load_image_data(cleaned_data)\n",
|
271 |
+
" \n",
|
272 |
+
" (X_front_train, X_front_test, \n",
|
273 |
+
" X_side_train, X_side_test,\n",
|
274 |
+
" X_meta_train, X_meta_test,\n",
|
275 |
+
" y_meas_train, y_meas_test) = train_test_split(\n",
|
276 |
+
" front_imgs, side_imgs, meta_data, measurements,\n",
|
277 |
+
" test_size=VALIDATION_SPLIT, random_state=15\n",
|
278 |
+
" )\n",
|
279 |
+
"\n",
|
280 |
+
" model = create_dual_input_model(MEASUREMENT_COLS)\n",
|
281 |
+
" \n",
|
282 |
+
" history = model.fit(\n",
|
283 |
+
" x=[X_front_train, X_side_train, X_meta_train],\n",
|
284 |
+
" y=y_meas_train,\n",
|
285 |
+
" epochs=EPOCHS,\n",
|
286 |
+
" batch_size=BATCH_SIZE,\n",
|
287 |
+
" validation_split=VALIDATION_SPLIT,\n",
|
288 |
+
" callbacks=get_training_callbacks()\n",
|
289 |
+
" )\n",
|
290 |
+
"\n",
|
291 |
+
" \n",
|
292 |
+
" plt.figure(figsize=(12, 6))\n",
|
293 |
+
" plt.plot(history.history['loss'], label='Train Loss')\n",
|
294 |
+
" plt.plot(history.history['val_loss'], label='Validation Loss')\n",
|
295 |
+
" plt.title('Model Training Progress')\n",
|
296 |
+
" plt.ylabel('Loss')\n",
|
297 |
+
" plt.xlabel('Epoch')\n",
|
298 |
+
" plt.legend()\n",
|
299 |
+
" plt.show()"
|
300 |
+
]
|
301 |
+
},
|
302 |
+
{
|
303 |
+
"cell_type": "code",
|
304 |
+
"execution_count": null,
|
305 |
+
"id": "814824c4",
|
306 |
+
"metadata": {},
|
307 |
+
"outputs": [],
|
308 |
+
"source": []
|
309 |
+
}
|
310 |
+
],
|
311 |
+
"metadata": {
|
312 |
+
"kernelspec": {
|
313 |
+
"display_name": "Python 3 (ipykernel)",
|
314 |
+
"language": "python",
|
315 |
+
"name": "python3"
|
316 |
+
},
|
317 |
+
"language_info": {
|
318 |
+
"codemirror_mode": {
|
319 |
+
"name": "ipython",
|
320 |
+
"version": 3
|
321 |
+
},
|
322 |
+
"file_extension": ".py",
|
323 |
+
"mimetype": "text/x-python",
|
324 |
+
"name": "python",
|
325 |
+
"nbconvert_exporter": "python",
|
326 |
+
"pygments_lexer": "ipython3",
|
327 |
+
"version": "3.11.11"
|
328 |
+
}
|
329 |
+
},
|
330 |
+
"nbformat": 4,
|
331 |
+
"nbformat_minor": 5
|
332 |
+
}
|
LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
MIT License
|
2 |
+
|
3 |
+
Copyright (c) 2025 Cloozy Brands "Muhammed Amar"
|
4 |
+
|
5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6 |
+
of this software and associated documentation files (the "Software"), to deal
|
7 |
+
in the Software without restriction, including without limitation the rights
|
8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9 |
+
copies of the Software, and to permit persons to whom the Software is
|
10 |
+
furnished to do so, subject to the following conditions:
|
11 |
+
|
12 |
+
The above copyright notice and this permission notice shall be included in all
|
13 |
+
copies or substantial portions of the Software.
|
14 |
+
|
15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21 |
+
SOFTWARE.
|
README.md
CHANGED
@@ -1,10 +1,124 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<<<<<<< HEAD
|
2 |
+
# AI-Powered Body Measurement & Apparel Sizing
|
3 |
+
|
4 |
+
This project is a **FastAPI-based AI system** that predicts **body measurements** and recommends **clothing sizes** using deep learning. It processes **front and side images**, removes backgrounds, and utilizes a trained **Keras model** for accurate predictions.
|
5 |
+
|
6 |
+
---
|
7 |
+
|
8 |
+
## 📌 Features
|
9 |
+
✅ AI-powered **body measurement estimation** using images.
|
10 |
+
✅ Automatic **background removal** for clean image processing.
|
11 |
+
✅ Deep learning-based **predictions** using a trained model.
|
12 |
+
✅ **FastAPI integration** for quick and efficient API responses.
|
13 |
+
|
14 |
+
---
|
15 |
+
|
16 |
+
|
17 |
+
## 🚀 Installation
|
18 |
+
|
19 |
+
To set up the project, follow these steps:
|
20 |
+
|
21 |
+
### **1️⃣ Clone the Repository**
|
22 |
+
```bash
|
23 |
+
git clone https://github.com/CloozyBrands/AI-BodyMeasurement.git
|
24 |
+
```
|
25 |
+
---
|
26 |
+
### **2️⃣ Install Dependencies**
|
27 |
+
```bash
|
28 |
+
pip install -r requirements.txt
|
29 |
+
```
|
30 |
+
|
31 |
+
### **3️⃣ Run the FastAPI Server**
|
32 |
+
```bash
|
33 |
+
uvicorn app.main:app --reload
|
34 |
+
```
|
35 |
+
Then open **http://127.0.0.1:8000/docs** to test the API.
|
36 |
+
|
37 |
+
---
|
38 |
+
|
39 |
+
## 🛠️ How It Works
|
40 |
+
|
41 |
+
### **1️⃣ Upload Images**
|
42 |
+
- The API accepts **two images**:
|
43 |
+
- **Front view**
|
44 |
+
- **Side view**
|
45 |
+
|
46 |
+
### **2️⃣ AI-Based Processing**
|
47 |
+
- The images are processed using **`single_person_processor.py`**, which:
|
48 |
+
✅ Removes the background using `rembg`.
|
49 |
+
✅ Converts the images into a **model-compatible format**.
|
50 |
+
✅ Predicts **body measurements** like chest, waist, and height.
|
51 |
+
|
52 |
+
### **3️⃣ Clothing Size Prediction**
|
53 |
+
- Based on the body measurements, the system suggests a **T-shirt and pants size** using predefined **size charts**.
|
54 |
+
|
55 |
+
---
|
56 |
+
|
57 |
+
## 🔍 API Endpoints
|
58 |
+
|
59 |
+
### **1️⃣ `/predict/` (POST) - Predict Body Measurements**
|
60 |
+
📌 **Example Request:**
|
61 |
+
```http
|
62 |
+
POST /predict/
|
63 |
+
```
|
64 |
+
|
65 |
+
📌 **Request Parameters:**
|
66 |
+
| Parameter | Type | Description |
|
67 |
+
|---------------|--------|-------------|
|
68 |
+
| `front_image` | File | Front view image (JPEG/PNG) |
|
69 |
+
| `side_image` | File | Side view image (JPEG/PNG) |
|
70 |
+
| `input_data` | JSON | User data (height, weight, gender) |
|
71 |
+
|
72 |
+
📌 **Example JSON Payload:**
|
73 |
+
```json
|
74 |
+
{
|
75 |
+
"gender": 0,
|
76 |
+
"height_cm": 175,
|
77 |
+
"weight_kg": 70,
|
78 |
+
"apparel_type": "all"
|
79 |
+
}
|
80 |
+
```
|
81 |
+
|
82 |
+
📌 **Example Response:**
|
83 |
+
```json
|
84 |
+
{
|
85 |
+
"results": {
|
86 |
+
"body_measurements": {
|
87 |
+
"chest": 100.5,
|
88 |
+
"waist": 80.2,
|
89 |
+
"hip": 97.3
|
90 |
+
},
|
91 |
+
"tshirt_size": "L",
|
92 |
+
"pants_size": 34
|
93 |
+
}
|
94 |
+
}
|
95 |
+
```
|
96 |
+
|
97 |
+
---
|
98 |
+
|
99 |
+
## 🎯 Model & AI Processing
|
100 |
+
|
101 |
+
📌 The model used in this project is a **TensorFlow/Keras** model stored as `best_model.keras`.
|
102 |
+
📌 The AI processing is handled inside **`single_person_processor.py`**, which:
|
103 |
+
- Loads the trained model using `tf.keras.models.load_model`.
|
104 |
+
- Extracts measurements based on input images.
|
105 |
+
- Maps the measurements to standard clothing sizes.
|
106 |
+
|
107 |
+
---
|
108 |
+
|
109 |
+
## 📜 License
|
110 |
+
|
111 |
+
This project is licensed under the **MIT License** – feel free to modify and use it.
|
112 |
+
|
113 |
+
---
|
114 |
+
|
115 |
+
## 🤝 Contributing
|
116 |
+
|
117 |
+
1. Fork the repository
|
118 |
+
2. Create a new branch
|
119 |
+
3. Make your changes
|
120 |
+
4. Submit a pull request
|
121 |
+
|
122 |
+
We welcome contributions and improvements! 🚀
|
123 |
+
```
|
124 |
+
>>>>>>> 4e80050 (FastAPI model for size recommendation)
|
best_model.keras
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:4650fbe54d37d645dd40e2e10f8256e692d0593ebba89ea49a92efe490c4f5da
|
3 |
+
size 4657156
|
main.py
ADDED
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI, File, UploadFile, Form
|
2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
3 |
+
import json
|
4 |
+
import tempfile
|
5 |
+
import shutil
|
6 |
+
import os
|
7 |
+
from single_person_processor import SinglePersonPredictor
|
8 |
+
|
9 |
+
app = FastAPI()
|
10 |
+
|
11 |
+
app.add_middleware(
|
12 |
+
CORSMiddleware,
|
13 |
+
allow_origins=["http://localhost:3000"], # Change this to match your frontend URL
|
14 |
+
allow_credentials=True,
|
15 |
+
allow_methods=["*"], # Allow all methods (POST, GET, etc.)
|
16 |
+
allow_headers=["*"], # Allow all headers
|
17 |
+
)
|
18 |
+
|
19 |
+
|
20 |
+
# Initialize the predictor
|
21 |
+
predictor = SinglePersonPredictor(model_path="best_model.keras")
|
22 |
+
|
23 |
+
@app.post("/predict/")
|
24 |
+
async def predict(
|
25 |
+
front_image: UploadFile = File(...),
|
26 |
+
side_image: UploadFile = File(...),
|
27 |
+
input_data: str = Form(...)
|
28 |
+
):
|
29 |
+
try:
|
30 |
+
try:
|
31 |
+
input_dict = json.loads(input_data)
|
32 |
+
except json.JSONDecodeError:
|
33 |
+
return {"error": "Invalid JSON format in input_data"}
|
34 |
+
|
35 |
+
# Validate file types
|
36 |
+
if front_image.content_type not in ["image/jpeg", "image/png"]:
|
37 |
+
return {"error": "Front image must be a JPEG or PNG file"}
|
38 |
+
if side_image.content_type not in ["image/jpeg", "image/png"]:
|
39 |
+
return {"error": "Side image must be a JPEG or PNG file"}
|
40 |
+
|
41 |
+
# Create temporary files for images
|
42 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as front_temp:
|
43 |
+
shutil.copyfileobj(front_image.file, front_temp)
|
44 |
+
front_temp_path = front_temp.name
|
45 |
+
|
46 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as side_temp:
|
47 |
+
shutil.copyfileobj(side_image.file, side_temp)
|
48 |
+
side_temp_path = side_temp.name
|
49 |
+
|
50 |
+
# Perform predictions
|
51 |
+
results = predictor.predict_measurements(
|
52 |
+
front_img_path=front_temp_path,
|
53 |
+
side_img_path=side_temp_path,
|
54 |
+
gender=input_dict.get("gender"),
|
55 |
+
height_cm=input_dict.get("height_cm"),
|
56 |
+
weight_kg=input_dict.get("weight_kg"),
|
57 |
+
apparel_type=input_dict.get("apparel_type")
|
58 |
+
)
|
59 |
+
|
60 |
+
# Clean up temporary files
|
61 |
+
os.remove(front_temp_path)
|
62 |
+
os.remove(side_temp_path)
|
63 |
+
|
64 |
+
return {"results": results}
|
65 |
+
|
66 |
+
except Exception as e:
|
67 |
+
return {"error": str(e)}
|
requirements.txt
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
fastapi
|
2 |
+
tensorflow
|
3 |
+
numpy
|
4 |
+
rembg
|
5 |
+
pillow
|
6 |
+
pandas
|
7 |
+
scikit-learn
|
8 |
+
matplotlib
|
9 |
+
uvicorn
|
10 |
+
python-multipart
|
single_person_processor.py
ADDED
@@ -0,0 +1,284 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# single_person_processor.py
|
2 |
+
|
3 |
+
import os
|
4 |
+
|
5 |
+
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' # Suppress TensorFlow warnings
|
6 |
+
import io
|
7 |
+
import logging
|
8 |
+
import numpy as np
|
9 |
+
import tensorflow as tf
|
10 |
+
from rembg import new_session, remove
|
11 |
+
from PIL import Image
|
12 |
+
from typing import Tuple, Dict
|
13 |
+
from tensorflow.keras.preprocessing.image import img_to_array
|
14 |
+
|
15 |
+
# Configure logging
|
16 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
17 |
+
|
18 |
+
|
19 |
+
class ImagePreprocessor:
|
20 |
+
"""Class to preprocess images by removing background."""
|
21 |
+
|
22 |
+
def __init__(self, img_size: Tuple[int, int] = (128, 128)):
|
23 |
+
"""
|
24 |
+
Initialize the image preprocessor.
|
25 |
+
|
26 |
+
Parameters:
|
27 |
+
- img_size: Target size for images (default is (128, 128)).
|
28 |
+
"""
|
29 |
+
self.img_size = img_size
|
30 |
+
self.session = new_session() # Create a new session for rembg
|
31 |
+
logging.info("Image processor initialized.")
|
32 |
+
|
33 |
+
def process_single_person(self, front_path: str, side_path: str) -> Tuple[np.ndarray, np.ndarray]:
|
34 |
+
"""
|
35 |
+
Process two images (front and side views).
|
36 |
+
|
37 |
+
Parameters:
|
38 |
+
- front_path: Path to the front image.
|
39 |
+
- side_path: Path to the side image.
|
40 |
+
|
41 |
+
Returns:
|
42 |
+
- A tuple containing processed front and side images as NumPy arrays.
|
43 |
+
"""
|
44 |
+
return (
|
45 |
+
self._process_image(front_path),
|
46 |
+
self._process_image(side_path)
|
47 |
+
)
|
48 |
+
|
49 |
+
def _process_image(self, image_path: str) -> np.ndarray:
|
50 |
+
"""
|
51 |
+
Remove background and process a single image.
|
52 |
+
|
53 |
+
Parameters:
|
54 |
+
- image_path: Path to the image file.
|
55 |
+
|
56 |
+
Returns:
|
57 |
+
- Processed image as a NumPy array.
|
58 |
+
"""
|
59 |
+
try:
|
60 |
+
with open(image_path, "rb") as f:
|
61 |
+
img_bytes = f.read()
|
62 |
+
|
63 |
+
# Remove background using rembg
|
64 |
+
bg_removed = Image.open(io.BytesIO(remove(img_bytes, session=self.session)))
|
65 |
+
|
66 |
+
# Convert person to white and background to black
|
67 |
+
white_img = Image.new("RGBA", bg_removed.size, (255, 255, 255, 255))
|
68 |
+
white_img.putalpha(bg_removed.getchannel('A'))
|
69 |
+
final_img = Image.alpha_composite(
|
70 |
+
Image.new("RGBA", white_img.size, (0, 0, 0, 255)),
|
71 |
+
white_img
|
72 |
+
)
|
73 |
+
|
74 |
+
# Prepare the image for the model
|
75 |
+
return self._prepare_image(final_img)
|
76 |
+
|
77 |
+
except Exception as e:
|
78 |
+
logging.error(f"Failed to process {image_path}: {str(e)}")
|
79 |
+
raise
|
80 |
+
|
81 |
+
def _prepare_image(self, image: Image.Image) -> np.ndarray:
|
82 |
+
"""
|
83 |
+
Convert the image to the model's input format.
|
84 |
+
|
85 |
+
Parameters:
|
86 |
+
- image: PIL Image object.
|
87 |
+
|
88 |
+
Returns:
|
89 |
+
- Image as a normalized NumPy array.
|
90 |
+
"""
|
91 |
+
return img_to_array(
|
92 |
+
image.convert('L').resize(self.img_size)
|
93 |
+
).astype(np.float32) / 255.0
|
94 |
+
|
95 |
+
|
96 |
+
class SinglePersonPredictor:
|
97 |
+
"""Class to predict body measurements and clothing sizes."""
|
98 |
+
|
99 |
+
MEASUREMENT_INDICES = {
|
100 |
+
'ankle': 0, 'arm-length': 1, 'bicep': 2, 'calf': 3,
|
101 |
+
'chest': 4, 'forearm': 5, 'height': 6, 'hip': 7,
|
102 |
+
'leg-length': 8, 'shoulder-breadth': 9,
|
103 |
+
'shoulder-to-crotch': 10, 'thigh': 11, 'waist': 12, 'wrist': 13
|
104 |
+
}
|
105 |
+
|
106 |
+
SIZE_CHARTS = {
|
107 |
+
'male': {
|
108 |
+
'tshirt': [
|
109 |
+
(97, 42, 'S'), (104, 45, 'M'), (112, 48, 'L'),
|
110 |
+
(120, 51, 'XL'), (128, 54, 'XXL'), (136, 57, 'XXXL')
|
111 |
+
],
|
112 |
+
'pants': [
|
113 |
+
(76, 102, 30), (81, 107, 32), (86, 112, 34),
|
114 |
+
(91, 117, 36), (97, 122, 38), (102, 127, 40), (107, 132, 42)
|
115 |
+
]
|
116 |
+
},
|
117 |
+
'female': {
|
118 |
+
'tshirt': [
|
119 |
+
(89, 38, 'S'), (96, 41, 'M'), (104, 44, 'L'),
|
120 |
+
(112, 47, 'XL'), (120, 50, 'XXL'), (128, 53, 'XXXL')
|
121 |
+
],
|
122 |
+
'pants': [
|
123 |
+
(66, 92, 26), (71, 97, 28), (76, 102, 30),
|
124 |
+
(81, 107, 32), (86, 112, 34), (91, 117, 36), (97, 122, 38)
|
125 |
+
]
|
126 |
+
}
|
127 |
+
}
|
128 |
+
|
129 |
+
def __init__(self, model_path: str = 'best_model.keras'):
|
130 |
+
"""
|
131 |
+
Initialize the predictor.
|
132 |
+
|
133 |
+
Parameters:
|
134 |
+
- model_path: Path to the trained model.
|
135 |
+
"""
|
136 |
+
self.model = tf.keras.models.load_model(model_path)
|
137 |
+
self.preprocessor = ImagePreprocessor()
|
138 |
+
logging.info("Model loaded successfully.")
|
139 |
+
|
140 |
+
def predict_measurements(self,
|
141 |
+
front_img_path: str,
|
142 |
+
side_img_path: str,
|
143 |
+
gender: int,
|
144 |
+
height_cm: float,
|
145 |
+
weight_kg: float,
|
146 |
+
apparel_type: str = "all") -> Dict:
|
147 |
+
"""
|
148 |
+
Perform predictions for a single person and calculate clothing sizes.
|
149 |
+
|
150 |
+
Parameters:
|
151 |
+
- front_img_path: Path to the front image.
|
152 |
+
- side_img_path: Path to the side image.
|
153 |
+
- gender: 0 for male, 1 for female.
|
154 |
+
- height_cm: Height in centimeters.
|
155 |
+
- weight_kg: Weight in kilograms.
|
156 |
+
- apparel_type: Specify "tshirt", "pants", or "all".
|
157 |
+
|
158 |
+
Returns:
|
159 |
+
- Dictionary containing predicted measurements and clothing sizes.
|
160 |
+
"""
|
161 |
+
try:
|
162 |
+
# Validate inputs
|
163 |
+
if gender not in (0, 1):
|
164 |
+
raise ValueError("Gender must be 0 (male) or 1 (female).")
|
165 |
+
if not 100 <= height_cm <= 250:
|
166 |
+
raise ValueError("Height must be between 100-250 cm.")
|
167 |
+
if not 30 <= weight_kg <= 300:
|
168 |
+
raise ValueError("Weight must be between 30-300 kg.")
|
169 |
+
if apparel_type not in ["tshirt", "pants", "all"]:
|
170 |
+
raise ValueError("Apparel type must be 'tshirt', 'pants', or 'all'.")
|
171 |
+
|
172 |
+
# Process images
|
173 |
+
front_arr, side_arr = self.preprocessor.process_single_person(front_img_path, side_img_path)
|
174 |
+
meta_arr = np.array([[gender, height_cm, weight_kg]], dtype=np.float32)
|
175 |
+
|
176 |
+
# Make predictions
|
177 |
+
prediction = self.model.predict([
|
178 |
+
np.expand_dims(front_arr, axis=0),
|
179 |
+
np.expand_dims(side_arr, axis=0),
|
180 |
+
meta_arr
|
181 |
+
])
|
182 |
+
|
183 |
+
# Convert predictions to dictionary
|
184 |
+
measurements = {
|
185 |
+
name: round(float(prediction[0][idx]), 2)
|
186 |
+
for name, idx in self.MEASUREMENT_INDICES.items()
|
187 |
+
}
|
188 |
+
|
189 |
+
result = {}
|
190 |
+
|
191 |
+
if apparel_type == "tshirt":
|
192 |
+
result["tshirt_size"] = self.calculate_tshirt_size(gender, measurements, weight_kg)
|
193 |
+
|
194 |
+
elif apparel_type == "pants":
|
195 |
+
result["pants_size"] = self.calculate_pants_size(gender, measurements, weight_kg)
|
196 |
+
|
197 |
+
elif apparel_type == "all":
|
198 |
+
result["body_measurements"] = measurements
|
199 |
+
tshirt_size, pants_size = self.calculate_apparel_size(gender, measurements, weight_kg)
|
200 |
+
result["tshirt_size"] = tshirt_size
|
201 |
+
result["pants_size"] = pants_size
|
202 |
+
|
203 |
+
return result
|
204 |
+
|
205 |
+
except Exception as e:
|
206 |
+
logging.error(f"Prediction failed: {str(e)}")
|
207 |
+
raise
|
208 |
+
|
209 |
+
def calculate_tshirt_size(self, gender: int, measurements: Dict, weight: float) -> str:
|
210 |
+
"""
|
211 |
+
Calculate t-shirt size based on chest and shoulder-breadth measurements.
|
212 |
+
|
213 |
+
Parameters:
|
214 |
+
- gender: 0 for male, 1 for female.
|
215 |
+
- measurements: Dictionary of body measurements.
|
216 |
+
- weight: Weight in kilograms.
|
217 |
+
|
218 |
+
Returns:
|
219 |
+
- T-Shirt size as a string.
|
220 |
+
"""
|
221 |
+
gender_str = 'male' if gender == 0 else 'female'
|
222 |
+
chart = self.SIZE_CHARTS[gender_str]['tshirt']
|
223 |
+
|
224 |
+
chest = measurements['chest']
|
225 |
+
shoulder_breadth = measurements['shoulder-breadth']
|
226 |
+
|
227 |
+
base_size = next(
|
228 |
+
(size for max_chest, max_shoulder, size in chart
|
229 |
+
if chest <= max_chest and shoulder_breadth <= max_shoulder),
|
230 |
+
'XXXL'
|
231 |
+
)
|
232 |
+
|
233 |
+
if weight > 95 and gender == 0 or weight > 80 and gender == 1:
|
234 |
+
base_size = 'XXL'
|
235 |
+
|
236 |
+
if measurements['height'] > 180:
|
237 |
+
base_size = f"Tall {base_size}"
|
238 |
+
|
239 |
+
return base_size
|
240 |
+
|
241 |
+
def calculate_pants_size(self, gender: int, measurements: Dict, weight: float) -> int:
|
242 |
+
"""
|
243 |
+
Calculate pants size based on waist and hip measurements.
|
244 |
+
|
245 |
+
Parameters:
|
246 |
+
- gender: 0 for male, 1 for female.
|
247 |
+
- measurements: Dictionary of body measurements.
|
248 |
+
- weight: Weight in kilograms.
|
249 |
+
|
250 |
+
Returns:
|
251 |
+
- Pants size as an integer.
|
252 |
+
"""
|
253 |
+
gender_str = 'male' if gender == 0 else 'female'
|
254 |
+
chart = self.SIZE_CHARTS[gender_str]['pants']
|
255 |
+
|
256 |
+
waist = measurements['waist']
|
257 |
+
hip = measurements['hip']
|
258 |
+
|
259 |
+
base_size = next(
|
260 |
+
(size for max_waist, max_hip, size in chart
|
261 |
+
if waist <= max_waist and hip <= max_hip),
|
262 |
+
chart[-1][2]
|
263 |
+
)
|
264 |
+
|
265 |
+
if measurements['height'] > 180 and gender == 0:
|
266 |
+
base_size += 2 if base_size < 40 else 0
|
267 |
+
|
268 |
+
return base_size
|
269 |
+
|
270 |
+
def calculate_apparel_size(self, gender: int, measurements: Dict, weight: float) -> Tuple[str, int]:
|
271 |
+
"""
|
272 |
+
Calculate both t-shirt and pants sizes.
|
273 |
+
|
274 |
+
Parameters:
|
275 |
+
- gender: 0 for male, 1 for female.
|
276 |
+
- measurements: Dictionary of body measurements.
|
277 |
+
- weight: Weight in kilograms.
|
278 |
+
|
279 |
+
Returns:
|
280 |
+
- A tuple containing t-shirt size and pants size.
|
281 |
+
"""
|
282 |
+
tshirt_size = self.calculate_tshirt_size(gender, measurements, weight)
|
283 |
+
pants_size = self.calculate_pants_size(gender, measurements, weight)
|
284 |
+
return tshirt_size, pants_size
|