chemistrymath commited on
Commit
01b0eeb
·
verified ·
1 Parent(s): 4f83400

Upload 9 files

Browse files
Files changed (10) hide show
  1. .gitattributes +1 -0
  2. .gitignore +171 -0
  3. Dockerfile +17 -0
  4. FinalModel.ipynb +332 -0
  5. LICENSE +21 -0
  6. README.md +124 -10
  7. best_model.keras +3 -0
  8. main.py +67 -0
  9. requirements.txt +10 -0
  10. 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
- title: Fastapi Model
3
- emoji: 🦀
4
- colorFrom: gray
5
- colorTo: pink
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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