natexcvi commited on
Commit
0edd049
1 Parent(s): 0e92099

Add fecnet

Browse files
.gitignore CHANGED
@@ -2,4 +2,8 @@
2
  .env
3
  __pycache__
4
  .vscode
5
- .cache
 
 
 
 
 
2
  .env
3
  __pycache__
4
  .vscode
5
+ .cache
6
+ .DS_Store
7
+ model/images
8
+ model/data
9
+ model/training_dataset
Dockerfile CHANGED
@@ -3,7 +3,7 @@ FROM python:3.7
3
  WORKDIR /code
4
 
5
  COPY ./requirements.txt /code/requirements.txt
6
- RUN apt-get update && apt-get install ffmpeg libsm6 libxext6 -y
7
  RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
8
 
9
  RUN useradd -m -u 1000 user
 
3
  WORKDIR /code
4
 
5
  COPY ./requirements.txt /code/requirements.txt
6
+ RUN apt-get update && apt-get install ffmpeg libsm6 libxext6 docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y
7
  RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
8
 
9
  RUN useradd -m -u 1000 user
app.py CHANGED
@@ -1,20 +1,18 @@
1
- import hmac
2
  import os
3
- from typing import Union
4
 
5
  import numpy as np
6
- from fastapi import Depends, FastAPI, File, HTTPException, Response, UploadFile, status
7
- from fastapi.security import APIKeyQuery
8
 
9
- from model import Model
 
 
10
  from schema import EmbeddingResponse, SimilarityResponse
11
 
12
  app = FastAPI(
13
  title="Facial Expression Embedding Service",
14
  )
15
 
16
- api_key = APIKeyQuery(name="token", auto_error=False)
17
- client_token: str = os.getenv("CLIENT_TOKEN", "")
18
 
19
  model = Model(
20
  os.getenv("MODEL_REPO_ID", ""),
@@ -23,16 +21,6 @@ model = Model(
23
  )
24
 
25
 
26
- async def validate_token(
27
- token: Union[str, None] = Depends(api_key),
28
- ):
29
- if token is None:
30
- raise HTTPException(status.HTTP_401_UNAUTHORIZED, "No token provided")
31
- if not hmac.compare_digest(token, client_token):
32
- raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid token")
33
- return token
34
-
35
-
36
  @app.post(
37
  "/embed",
38
  status_code=status.HTTP_200_OK,
 
 
1
  import os
 
2
 
3
  import numpy as np
4
+ from fastapi import Depends, FastAPI, File, Response, UploadFile, status
 
5
 
6
+ from auth import validate_token
7
+ from model.model import Model
8
+ from routers.fecnet_router import router as fecnet_router
9
  from schema import EmbeddingResponse, SimilarityResponse
10
 
11
  app = FastAPI(
12
  title="Facial Expression Embedding Service",
13
  )
14
 
15
+ app.include_router(fecnet_router)
 
16
 
17
  model = Model(
18
  os.getenv("MODEL_REPO_ID", ""),
 
21
  )
22
 
23
 
 
 
 
 
 
 
 
 
 
 
24
  @app.post(
25
  "/embed",
26
  status_code=status.HTTP_200_OK,
auth.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hmac
2
+ import os
3
+ from typing import Union
4
+
5
+ from fastapi import Depends, HTTPException, status
6
+ from fastapi.security import APIKeyQuery
7
+
8
+ api_key = APIKeyQuery(name="token", auto_error=False)
9
+ client_token: str = os.getenv("CLIENT_TOKEN", "")
10
+
11
+
12
+ async def validate_token(
13
+ token: Union[str, None] = Depends(api_key),
14
+ ):
15
+ if token is None:
16
+ raise HTTPException(status.HTTP_401_UNAUTHORIZED, "No token provided")
17
+ if not hmac.compare_digest(token, client_token):
18
+ raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid token")
19
+ return token
dev-requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ ipykernel
2
+ plotly
3
+ retrying
4
+ swifter
5
+ nbformat
model/facial_expression_embedding.ipynb ADDED
@@ -0,0 +1,808 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "attachments": {},
5
+ "cell_type": "markdown",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Facial Expression Embedding"
9
+ ]
10
+ },
11
+ {
12
+ "cell_type": "code",
13
+ "execution_count": 1,
14
+ "metadata": {},
15
+ "outputs": [
16
+ {
17
+ "name": "stderr",
18
+ "output_type": "stream",
19
+ "text": [
20
+ "2023-04-19 12:14:54.399884: I tensorflow/core/platform/cpu_feature_guard.cc:193] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations: AVX2 AVX512F AVX512_VNNI FMA\n",
21
+ "To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.\n"
22
+ ]
23
+ }
24
+ ],
25
+ "source": [
26
+ "import numpy as np\n",
27
+ "import os\n",
28
+ "import random\n",
29
+ "from typing import *\n",
30
+ "import tensorflow as tf\n",
31
+ "from pathlib import Path\n",
32
+ "from tensorflow.keras import applications\n",
33
+ "from tensorflow.keras import layers\n",
34
+ "from tensorflow.keras import losses\n",
35
+ "from tensorflow.keras import optimizers\n",
36
+ "from tensorflow.keras import metrics\n",
37
+ "from tensorflow.keras import Model\n",
38
+ "from tensorflow.keras.applications import resnet\n",
39
+ "import pandas as pd\n",
40
+ "import mediapipe as mp\n",
41
+ "import plotly.express as px\n",
42
+ "from plotly.subplots import make_subplots\n",
43
+ "import plotly.graph_objects as go\n",
44
+ "import requests\n",
45
+ "from tqdm import tqdm\n",
46
+ "import base64\n",
47
+ "from concurrent.futures import ThreadPoolExecutor, as_completed\n",
48
+ "from retrying import retry\n",
49
+ "import swifter\n"
50
+ ]
51
+ },
52
+ {
53
+ "attachments": {},
54
+ "cell_type": "markdown",
55
+ "metadata": {},
56
+ "source": [
57
+ "## Dataset Loading"
58
+ ]
59
+ },
60
+ {
61
+ "cell_type": "code",
62
+ "execution_count": 9,
63
+ "metadata": {},
64
+ "outputs": [],
65
+ "source": [
66
+ "IMAGE_DIR = \"images\"\n",
67
+ "TRAINING_DATASET = \"training_dataset\""
68
+ ]
69
+ },
70
+ {
71
+ "cell_type": "code",
72
+ "execution_count": 3,
73
+ "metadata": {},
74
+ "outputs": [],
75
+ "source": [
76
+ "@retry(stop_max_attempt_number=3)\n",
77
+ "def image_downloader(url: str):\n",
78
+ " get_name = lambda url: base64.urlsafe_b64encode(url.encode()).decode()\n",
79
+ " Path(IMAGE_DIR).mkdir(exist_ok=True)\n",
80
+ " filename = get_name(url)\n",
81
+ " if os.path.exists(os.path.join(IMAGE_DIR, filename)):\n",
82
+ " return filename\n",
83
+ " res = requests.get(url, timeout=10)\n",
84
+ " if not res.ok:\n",
85
+ " return None\n",
86
+ " with open(os.path.join(IMAGE_DIR, filename), \"wb\") as f:\n",
87
+ " f.write(res.content)\n",
88
+ " return filename\n"
89
+ ]
90
+ },
91
+ {
92
+ "cell_type": "code",
93
+ "execution_count": 4,
94
+ "metadata": {},
95
+ "outputs": [],
96
+ "source": [
97
+ "def get_column_names():\n",
98
+ " names = []\n",
99
+ " for i in range(1, 4):\n",
100
+ " names += [\n",
101
+ " f\"img{i}_url\",\n",
102
+ " f\"img{i}_tl_col\",\n",
103
+ " f\"img{i}_br_col\",\n",
104
+ " f\"img{i}_tl_row\",\n",
105
+ " f\"img{i}_br_row\",\n",
106
+ " ]\n",
107
+ " names += [\"triplet_type\"]\n",
108
+ " for i in range(6):\n",
109
+ " names += [f\"annotator{i+1}_id\", f\"annotation{i+1}\"]\n",
110
+ " return names\n",
111
+ "\n",
112
+ "\n",
113
+ "def get_local_storage_column_names():\n",
114
+ " names = []\n",
115
+ " for i in range(1, 4):\n",
116
+ " names += [\n",
117
+ " f\"img{i}_id\",\n",
118
+ " f\"img{i}_tl_col\",\n",
119
+ " f\"img{i}_br_col\",\n",
120
+ " f\"img{i}_tl_row\",\n",
121
+ " f\"img{i}_br_row\",\n",
122
+ " ]\n",
123
+ " names += [\"triplet_type\"]\n",
124
+ " names += [\"annotator1_id\"]\n",
125
+ " names += [\"annotation\"]\n",
126
+ " return names\n",
127
+ "\n",
128
+ "\n",
129
+ "def get_label(annotations: pd.Series):\n",
130
+ " def mode(x):\n",
131
+ " s = pd.Series(x)\n",
132
+ " if s.value_counts(normalize=True).max() < 0.5:\n",
133
+ " return np.nan\n",
134
+ " return s.mode().at[0]\n",
135
+ "\n",
136
+ " return annotations.swifter.apply(mode)\n",
137
+ "\n",
138
+ "\n",
139
+ "def fecnet_dataset_loader(dataset_csv: str):\n",
140
+ " if isinstance(dataset_csv, bytes):\n",
141
+ " dataset_csv = dataset_csv.decode()\n",
142
+ " df = pd.read_csv(\n",
143
+ " dataset_csv, header=None, names=get_column_names(), nrows=10000\n",
144
+ " ) # TODO: remove nrows\n",
145
+ "\n",
146
+ " # download images\n",
147
+ " df[\"img1_url\"] = df[\"img1_url\"].swifter.apply(image_downloader)\n",
148
+ " df[\"img2_url\"] = df[\"img2_url\"].swifter.apply(image_downloader)\n",
149
+ " df[\"img3_url\"] = df[\"img3_url\"].swifter.apply(image_downloader)\n",
150
+ " df.dropna(subset=[\"img1_url\", \"img2_url\", \"img3_url\"], inplace=True)\n",
151
+ "\n",
152
+ " # determine label\n",
153
+ " df[\"label\"] = get_label(\n",
154
+ " pd.Series(df[[f\"annotation{i}\" for i in range(1, 7)]].values.tolist())\n",
155
+ " )\n",
156
+ " df.dropna(subset=[\"label\"], inplace=True)\n",
157
+ "\n",
158
+ " samples = {\n",
159
+ " \"img1\": [],\n",
160
+ " \"img1_box\": [],\n",
161
+ " \"img2\": [],\n",
162
+ " \"img2_box\": [],\n",
163
+ " \"img3\": [],\n",
164
+ " \"img3_box\": [],\n",
165
+ " }\n",
166
+ "\n",
167
+ " for _, row in df.iterrows():\n",
168
+ " img1_idx, img2_idx, img3_idx = 1, 2, 3\n",
169
+ " if row.label == 1:\n",
170
+ " img1_idx, img3_idx = img3_idx, img1_idx\n",
171
+ " elif row.label == 2:\n",
172
+ " img2_idx, img3_idx = img3_idx, img2_idx\n",
173
+ " bounding_boxes = (\n",
174
+ " (\n",
175
+ " (row[f\"img{img1_idx}_tl_col\"], row[f\"img{img1_idx}_tl_row\"]),\n",
176
+ " (row[f\"img{img1_idx}_br_col\"], row[f\"img{img1_idx}_br_row\"]),\n",
177
+ " ),\n",
178
+ " (\n",
179
+ " (row[f\"img{img2_idx}_tl_col\"], row[f\"img{img2_idx}_tl_row\"]),\n",
180
+ " (row[f\"img{img2_idx}_br_col\"], row[f\"img{img2_idx}_br_row\"]),\n",
181
+ " ),\n",
182
+ " (\n",
183
+ " (row[f\"img{img3_idx}_tl_col\"], row[f\"img{img3_idx}_tl_row\"]),\n",
184
+ " (row[f\"img{img3_idx}_br_col\"], row[f\"img{img3_idx}_br_row\"]),\n",
185
+ " ),\n",
186
+ " )\n",
187
+ " samples[\"img1\"].append(row[f\"img{img1_idx}_url\"])\n",
188
+ " samples[\"img1_box\"].append(bounding_boxes[0])\n",
189
+ " samples[\"img2\"].append(row[f\"img{img2_idx}_url\"])\n",
190
+ " samples[\"img2_box\"].append(bounding_boxes[1])\n",
191
+ " samples[\"img3\"].append(row[f\"img{img3_idx}_url\"])\n",
192
+ " samples[\"img3_box\"].append(bounding_boxes[2])\n",
193
+ " return samples\n"
194
+ ]
195
+ },
196
+ {
197
+ "cell_type": "code",
198
+ "execution_count": 5,
199
+ "metadata": {},
200
+ "outputs": [],
201
+ "source": [
202
+ "def extract_landmarks(image):\n",
203
+ " with mp.solutions.face_mesh.FaceMesh(\n",
204
+ " static_image_mode=True,\n",
205
+ " max_num_faces=1,\n",
206
+ " refine_landmarks=True,\n",
207
+ " min_detection_confidence=0.5,\n",
208
+ " ) as face_mesh:\n",
209
+ " results = face_mesh.process(image.numpy())\n",
210
+ " if results.multi_face_landmarks:\n",
211
+ " landmarks = results.multi_face_landmarks[0]\n",
212
+ " landmarks = np.array(\n",
213
+ " [[lm.x, lm.y, lm.z] for lm in landmarks.landmark], dtype=np.float32\n",
214
+ " )\n",
215
+ " landmarks = landmarks.flatten()\n",
216
+ " else:\n",
217
+ " landmarks = np.zeros(478 * 3, dtype=np.float32)\n",
218
+ " return landmarks\n",
219
+ "\n",
220
+ "\n",
221
+ "def preprocess_image(filename: str, tl: Tuple[float, float], br: Tuple[float, float]):\n",
222
+ " image_string = tf.io.read_file(tf.strings.join([IMAGE_DIR, \"/\", filename]))\n",
223
+ " image = tf.image.decode_jpeg(image_string, channels=3)\n",
224
+ " image = tf.image.convert_image_dtype(image, tf.uint8)\n",
225
+ "\n",
226
+ " # crop image\n",
227
+ " tl = tf.cast(tf.multiply(tl, tf.cast(tf.shape(image)[:2][::-1], tf.float32)), tf.int32)\n",
228
+ " br = tf.cast(tf.multiply(br, tf.cast(tf.shape(image)[:2][::-1], tf.float32)), tf.int32)\n",
229
+ " image = tf.image.crop_to_bounding_box(\n",
230
+ " image, tl[1], tl[0], br[1] - tl[1], br[0] - tl[0]\n",
231
+ " )\n",
232
+ "\n",
233
+ " # extract landmarks using facemesh\n",
234
+ " return tf.py_function(extract_landmarks, [image], tf.float32)\n",
235
+ " # return image\n",
236
+ "\n",
237
+ "\n",
238
+ "ImgType = Tuple[str, Tuple[float, float], Tuple[float, float]]\n",
239
+ "\n",
240
+ "\n",
241
+ "def preprocess_triplets(triplet: dict):\n",
242
+ " anchor: ImgType = (triplet[\"img1\"], triplet[\"img1_box\"][0], triplet[\"img1_box\"][1])\n",
243
+ " positive: ImgType = (\n",
244
+ " triplet[\"img2\"],\n",
245
+ " triplet[\"img2_box\"][0],\n",
246
+ " triplet[\"img2_box\"][1],\n",
247
+ " )\n",
248
+ " negative: ImgType = (\n",
249
+ " triplet[\"img3\"],\n",
250
+ " triplet[\"img3_box\"][0],\n",
251
+ " triplet[\"img3_box\"][1],\n",
252
+ " )\n",
253
+ " return (\n",
254
+ " preprocess_image(*anchor),\n",
255
+ " preprocess_image(*positive),\n",
256
+ " preprocess_image(*negative),\n",
257
+ " )\n"
258
+ ]
259
+ },
260
+ {
261
+ "cell_type": "code",
262
+ "execution_count": 6,
263
+ "metadata": {},
264
+ "outputs": [
265
+ {
266
+ "data": {
267
+ "application/vnd.jupyter.widget-view+json": {
268
+ "model_id": "dc545e1717044afd9ddc0b32fecd72c3",
269
+ "version_major": 2,
270
+ "version_minor": 0
271
+ },
272
+ "text/plain": [
273
+ "Pandas Apply: 0%| | 0/10000 [00:00<?, ?it/s]"
274
+ ]
275
+ },
276
+ "metadata": {},
277
+ "output_type": "display_data"
278
+ },
279
+ {
280
+ "data": {
281
+ "application/vnd.jupyter.widget-view+json": {
282
+ "model_id": "0e653babd8104a72a0e4425167b44005",
283
+ "version_major": 2,
284
+ "version_minor": 0
285
+ },
286
+ "text/plain": [
287
+ "Pandas Apply: 0%| | 0/10000 [00:00<?, ?it/s]"
288
+ ]
289
+ },
290
+ "metadata": {},
291
+ "output_type": "display_data"
292
+ },
293
+ {
294
+ "data": {
295
+ "application/vnd.jupyter.widget-view+json": {
296
+ "model_id": "5fe7ddb8d18c41c599a69b9ca87cd8cd",
297
+ "version_major": 2,
298
+ "version_minor": 0
299
+ },
300
+ "text/plain": [
301
+ "Pandas Apply: 0%| | 0/10000 [00:00<?, ?it/s]"
302
+ ]
303
+ },
304
+ "metadata": {},
305
+ "output_type": "display_data"
306
+ },
307
+ {
308
+ "data": {
309
+ "application/vnd.jupyter.widget-view+json": {
310
+ "model_id": "38e2dc2d2517465f896d4fe4ca05da75",
311
+ "version_major": 2,
312
+ "version_minor": 0
313
+ },
314
+ "text/plain": [
315
+ "Pandas Apply: 0%| | 0/8143 [00:00<?, ?it/s]"
316
+ ]
317
+ },
318
+ "metadata": {},
319
+ "output_type": "display_data"
320
+ },
321
+ {
322
+ "name": "stderr",
323
+ "output_type": "stream",
324
+ "text": [
325
+ "2023-04-19 13:04:21.919175: I tensorflow/core/platform/cpu_feature_guard.cc:193] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations: AVX2 AVX512F AVX512_VNNI FMA\n",
326
+ "To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.\n"
327
+ ]
328
+ }
329
+ ],
330
+ "source": [
331
+ "if not TRAINING_DATASET:\n",
332
+ " df = fecnet_dataset_loader(\"data/faceexp-comparison-data-train-public.csv\")\n",
333
+ " image_count = len(df)\n",
334
+ "\n",
335
+ " dataset = tf.data.Dataset.from_tensor_slices(\n",
336
+ " df\n",
337
+ " )\n",
338
+ "\n",
339
+ " dataset = dataset.shuffle(buffer_size=1024)\n",
340
+ " dataset = dataset.map(preprocess_triplets)\n",
341
+ "else:\n",
342
+ " dataset = tf.data.Dataset.load(TRAINING_DATASET)\n",
343
+ "\n",
344
+ "train_dataset = dataset.take(round(image_count * 0.8))\n",
345
+ "val_dataset = dataset.skip(round(image_count * 0.8))\n",
346
+ "\n",
347
+ "train_dataset = train_dataset.batch(32, drop_remainder=False)\n",
348
+ "train_dataset = train_dataset.prefetch(8)\n",
349
+ "\n",
350
+ "val_dataset = val_dataset.batch(32, drop_remainder=False)\n",
351
+ "val_dataset = val_dataset.prefetch(8)"
352
+ ]
353
+ },
354
+ {
355
+ "attachments": {},
356
+ "cell_type": "markdown",
357
+ "metadata": {},
358
+ "source": [
359
+ "## Data Visualisation"
360
+ ]
361
+ },
362
+ {
363
+ "cell_type": "code",
364
+ "execution_count": 10,
365
+ "metadata": {},
366
+ "outputs": [],
367
+ "source": [
368
+ "def visualise_face_mesh(landmarks):\n",
369
+ " landmarks = landmarks.reshape(-1, 3)\n",
370
+ " fig = go.Figure(\n",
371
+ " go.Mesh3d(\n",
372
+ " x=landmarks[:, 0],\n",
373
+ " y=landmarks[:, 1],\n",
374
+ " z=landmarks[:, 2],\n",
375
+ " color=\"lightpink\",\n",
376
+ " opacity=0.50,\n",
377
+ " contour=dict( \n",
378
+ " color=\"grey\",\n",
379
+ " width=1,\n",
380
+ " ),\n",
381
+ " )\n",
382
+ " )\n",
383
+ " fig.show()\n",
384
+ "\n",
385
+ "def visualise_face_mesh_triplets(anchor, positive, negative):\n",
386
+ " fig = make_subplots(\n",
387
+ " rows=anchor.shape[0],\n",
388
+ " cols=3,\n",
389
+ " specs=[[{\"type\": \"surface\"}, {\"type\": \"surface\"}, {\"type\": \"surface\"}]],\n",
390
+ " )\n",
391
+ " for i in range(anchor.shape[0]):\n",
392
+ " fig.add_trace(\n",
393
+ " go.Mesh3d(\n",
394
+ " x=anchor[i, :, 0],\n",
395
+ " y=anchor[i, :, 1],\n",
396
+ " z=anchor[i, :, 2],\n",
397
+ " color=\"lightpink\",\n",
398
+ " opacity=0.50,\n",
399
+ " ),\n",
400
+ " row=i + 1,\n",
401
+ " col=1,\n",
402
+ " )\n",
403
+ " fig.add_trace(\n",
404
+ " go.Mesh3d(\n",
405
+ " x=positive[i, :, 0],\n",
406
+ " y=positive[i, :, 1],\n",
407
+ " z=positive[i, :, 2],\n",
408
+ " color=\"lightpink\",\n",
409
+ " opacity=0.50,\n",
410
+ " ),\n",
411
+ " row=i + 1,\n",
412
+ " col=2,\n",
413
+ " )\n",
414
+ " fig.add_trace(\n",
415
+ " go.Mesh3d(\n",
416
+ " x=negative[i, :, 0],\n",
417
+ " y=negative[i, :, 1],\n",
418
+ " z=negative[i, :, 2],\n",
419
+ " color=\"lightpink\",\n",
420
+ " opacity=0.50,\n",
421
+ " ),\n",
422
+ " row=i + 1,\n",
423
+ " col=3,\n",
424
+ " )\n",
425
+ " fig.show()\n",
426
+ "\n",
427
+ "def visualise_image(image):\n",
428
+ " fig = px.imshow(image)\n",
429
+ " fig.show()\n",
430
+ "\n",
431
+ "\n",
432
+ "def visualise_triplet(anchor, positive, negative):\n",
433
+ " pass\n"
434
+ ]
435
+ },
436
+ {
437
+ "cell_type": "code",
438
+ "execution_count": 160,
439
+ "metadata": {},
440
+ "outputs": [],
441
+ "source": [
442
+ "# ds_samples = list(dataset.as_numpy_iterator())"
443
+ ]
444
+ },
445
+ {
446
+ "cell_type": "code",
447
+ "execution_count": 161,
448
+ "metadata": {},
449
+ "outputs": [],
450
+ "source": [
451
+ "# visualise_face_mesh(ds_samples[30][0])"
452
+ ]
453
+ },
454
+ {
455
+ "attachments": {},
456
+ "cell_type": "markdown",
457
+ "metadata": {},
458
+ "source": [
459
+ "## Model Definition"
460
+ ]
461
+ },
462
+ {
463
+ "cell_type": "code",
464
+ "execution_count": 11,
465
+ "metadata": {},
466
+ "outputs": [],
467
+ "source": [
468
+ "def create_embedding_model():\n",
469
+ " input_layer = layers.Input(shape=(478, 3))\n",
470
+ " flatten = layers.Flatten()(input_layer)\n",
471
+ " dense1 = layers.Dense(512, activation=\"relu\")(flatten)\n",
472
+ " dense1 = layers.BatchNormalization()(dense1)\n",
473
+ " dense2 = layers.Dense(256, activation=\"relu\")(dense1)\n",
474
+ " dense2 = layers.BatchNormalization()(dense2)\n",
475
+ " output = layers.Dense(16)(dense2)\n",
476
+ "\n",
477
+ " embedding = Model(input_layer, output, name=\"Embedding\")\n",
478
+ " return embedding"
479
+ ]
480
+ },
481
+ {
482
+ "cell_type": "code",
483
+ "execution_count": 12,
484
+ "metadata": {},
485
+ "outputs": [
486
+ {
487
+ "name": "stdout",
488
+ "output_type": "stream",
489
+ "text": [
490
+ "Model: \"Embedding\"\n",
491
+ "_________________________________________________________________\n",
492
+ " Layer (type) Output Shape Param # \n",
493
+ "=================================================================\n",
494
+ " input_1 (InputLayer) [(None, 478, 3)] 0 \n",
495
+ " \n",
496
+ " flatten (Flatten) (None, 1434) 0 \n",
497
+ " \n",
498
+ " dense (Dense) (None, 512) 734720 \n",
499
+ " \n",
500
+ " batch_normalization (BatchN (None, 512) 2048 \n",
501
+ " ormalization) \n",
502
+ " \n",
503
+ " dense_1 (Dense) (None, 256) 131328 \n",
504
+ " \n",
505
+ " batch_normalization_1 (Batc (None, 256) 1024 \n",
506
+ " hNormalization) \n",
507
+ " \n",
508
+ " dense_2 (Dense) (None, 16) 4112 \n",
509
+ " \n",
510
+ "=================================================================\n",
511
+ "Total params: 873,232\n",
512
+ "Trainable params: 871,696\n",
513
+ "Non-trainable params: 1,536\n",
514
+ "_________________________________________________________________\n"
515
+ ]
516
+ }
517
+ ],
518
+ "source": [
519
+ "embedding = create_embedding_model()\n",
520
+ "embedding.summary()"
521
+ ]
522
+ },
523
+ {
524
+ "cell_type": "code",
525
+ "execution_count": 13,
526
+ "metadata": {},
527
+ "outputs": [],
528
+ "source": [
529
+ "class DistanceLayer(layers.Layer):\n",
530
+ " def __init__(self, **kwargs):\n",
531
+ " super().__init__(**kwargs)\n",
532
+ "\n",
533
+ " def call(self, anchor, positive, negative):\n",
534
+ " ap_distance = tf.reduce_sum(tf.square(anchor - positive), -1)\n",
535
+ " an_distance = tf.reduce_sum(tf.square(anchor - negative), -1)\n",
536
+ " return (ap_distance, an_distance)\n"
537
+ ]
538
+ },
539
+ {
540
+ "cell_type": "code",
541
+ "execution_count": 14,
542
+ "metadata": {},
543
+ "outputs": [],
544
+ "source": [
545
+ "anchor_input = layers.Input(name=\"anchor\", shape=(478,) + (3,))\n",
546
+ "positive_input = layers.Input(name=\"positive\", shape=(478,) + (3,))\n",
547
+ "negative_input = layers.Input(name=\"negative\", shape=(478,) + (3,))\n",
548
+ "\n",
549
+ "distances = DistanceLayer()(\n",
550
+ " embedding(anchor_input),\n",
551
+ " embedding(positive_input),\n",
552
+ " embedding(negative_input),\n",
553
+ ")\n",
554
+ "\n",
555
+ "siamese_network = Model(\n",
556
+ " inputs=[anchor_input, positive_input, negative_input], outputs=distances\n",
557
+ ")"
558
+ ]
559
+ },
560
+ {
561
+ "cell_type": "code",
562
+ "execution_count": 15,
563
+ "metadata": {},
564
+ "outputs": [],
565
+ "source": [
566
+ "class SiameseModel(Model):\n",
567
+ " \"\"\"The Siamese Network model with a custom training and testing loops.\n",
568
+ "\n",
569
+ " Computes the triplet loss using the three embeddings produced by the\n",
570
+ " Siamese Network.\n",
571
+ "\n",
572
+ " The triplet loss is defined as:\n",
573
+ " L(A, P, N) = max(‖f(A) - f(P)‖² - ‖f(A) - f(N)‖² + margin, 0)\n",
574
+ " \"\"\"\n",
575
+ "\n",
576
+ " def __init__(self, siamese_network, margin=0.2):\n",
577
+ " super().__init__()\n",
578
+ " self.siamese_network = siamese_network\n",
579
+ " self.margin = margin\n",
580
+ " self.loss_tracker = metrics.Mean(name=\"loss\")\n",
581
+ " self.acc_tracker = metrics.Mean(name=\"accuracy\")\n",
582
+ "\n",
583
+ " def call(self, inputs):\n",
584
+ " return self.siamese_network(inputs)\n",
585
+ "\n",
586
+ " def train_step(self, data):\n",
587
+ " # GradientTape is a context manager that records every operation that\n",
588
+ " # you do inside. We are using it here to compute the loss so we can get\n",
589
+ " # the gradients and apply them using the optimizer specified in\n",
590
+ " # `compile()`.\n",
591
+ " with tf.GradientTape() as tape:\n",
592
+ " loss = self._compute_loss(data)\n",
593
+ "\n",
594
+ " # Storing the gradients of the loss function with respect to the\n",
595
+ " # weights/parameters.\n",
596
+ " gradients = tape.gradient(loss, self.siamese_network.trainable_weights)\n",
597
+ "\n",
598
+ " # Applying the gradients on the model using the specified optimizer\n",
599
+ " self.optimizer.apply_gradients(\n",
600
+ " zip(gradients, self.siamese_network.trainable_weights)\n",
601
+ " )\n",
602
+ "\n",
603
+ " # Let's update and return the training loss metric.\n",
604
+ " self.loss_tracker.update_state(loss)\n",
605
+ " return {\"loss\": self.loss_tracker.result()}\n",
606
+ "\n",
607
+ " def test_step(self, data):\n",
608
+ " loss = self._compute_loss(data)\n",
609
+ "\n",
610
+ " # Let's update and return the loss metric.\n",
611
+ " self.loss_tracker.update_state(loss)\n",
612
+ " self.acc_tracker.update_state(\n",
613
+ " self._compute_accuracy(data)\n",
614
+ " )\n",
615
+ " return {\"loss\": self.loss_tracker.result(), \"accuracy\": self.acc_tracker.result()}\n",
616
+ "\n",
617
+ " def _compute_loss(self, data):\n",
618
+ " # The output of the network is a tuple containing the distances\n",
619
+ " # between the anchor and the positive example, and the anchor and\n",
620
+ " # the negative example.\n",
621
+ " ap_distance, an_distance = self.siamese_network(data)\n",
622
+ "\n",
623
+ " # Computing the Triplet Loss by subtracting both distances and\n",
624
+ " # making sure we don't get a negative value.\n",
625
+ " loss = ap_distance - an_distance\n",
626
+ " loss = tf.maximum(loss + self.margin, 0.0)\n",
627
+ " return loss\n",
628
+ " \n",
629
+ " def _compute_accuracy(self, data):\n",
630
+ " ap_distance, an_distance = self.siamese_network(data)\n",
631
+ " return tf.cast(ap_distance < an_distance, tf.float32)\n",
632
+ "\n",
633
+ " @property\n",
634
+ " def metrics(self):\n",
635
+ " # We need to list our metrics here so the `reset_states()` can be\n",
636
+ " # called automatically.\n",
637
+ " return [self.loss_tracker, self.acc_tracker]"
638
+ ]
639
+ },
640
+ {
641
+ "attachments": {},
642
+ "cell_type": "markdown",
643
+ "metadata": {},
644
+ "source": [
645
+ "## Model Fitting"
646
+ ]
647
+ },
648
+ {
649
+ "cell_type": "code",
650
+ "execution_count": 16,
651
+ "metadata": {},
652
+ "outputs": [
653
+ {
654
+ "name": "stdout",
655
+ "output_type": "stream",
656
+ "text": [
657
+ "Epoch 1/40\n",
658
+ "1/1 [==============================] - 1010s 1010s/step - loss: 0.6925 - val_loss: 0.4313 - val_accuracy: 0.5041\n",
659
+ "Epoch 2/40\n",
660
+ "1/1 [==============================] - ETA: 0s - loss: 0.3325"
661
+ ]
662
+ },
663
+ {
664
+ "ename": "KeyboardInterrupt",
665
+ "evalue": "",
666
+ "output_type": "error",
667
+ "traceback": [
668
+ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
669
+ "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)",
670
+ "\u001b[0;32m/var/folders/9f/9272h8p951zfstxdwzn0cj2h0000gn/T/ipykernel_61590/1150799400.py\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0msiamese_model\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mSiameseModel\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msiamese_network\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0msiamese_model\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcompile\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0moptimizer\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0moptimizers\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mAdam\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m0.0005\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mweighted_metrics\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m\"accuracy\"\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0msiamese_model\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfit\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtrain_dataset\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mepochs\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m40\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvalidation_data\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mval_dataset\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
671
+ "\u001b[0;32m~/Git/FacialExpressionSyncService/.venv/lib/python3.7/site-packages/keras/utils/traceback_utils.py\u001b[0m in \u001b[0;36merror_handler\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 63\u001b[0m \u001b[0mfiltered_tb\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 64\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 65\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mfn\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 66\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mException\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 67\u001b[0m \u001b[0mfiltered_tb\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_process_traceback_frames\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0me\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__traceback__\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
672
+ "\u001b[0;32m~/Git/FacialExpressionSyncService/.venv/lib/python3.7/site-packages/keras/engine/training.py\u001b[0m in \u001b[0;36mfit\u001b[0;34m(self, x, y, batch_size, epochs, verbose, callbacks, validation_split, validation_data, shuffle, class_weight, sample_weight, initial_epoch, steps_per_epoch, validation_steps, validation_batch_size, validation_freq, max_queue_size, workers, use_multiprocessing)\u001b[0m\n\u001b[1;32m 1703\u001b[0m \u001b[0muse_multiprocessing\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0muse_multiprocessing\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1704\u001b[0m \u001b[0mreturn_dict\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mTrue\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1705\u001b[0;31m \u001b[0m_use_cached_eval_dataset\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mTrue\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1706\u001b[0m )\n\u001b[1;32m 1707\u001b[0m val_logs = {\n",
673
+ "\u001b[0;32m~/Git/FacialExpressionSyncService/.venv/lib/python3.7/site-packages/keras/utils/traceback_utils.py\u001b[0m in \u001b[0;36merror_handler\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 63\u001b[0m \u001b[0mfiltered_tb\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 64\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 65\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mfn\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 66\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mException\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 67\u001b[0m \u001b[0mfiltered_tb\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_process_traceback_frames\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0me\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__traceback__\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
674
+ "\u001b[0;32m~/Git/FacialExpressionSyncService/.venv/lib/python3.7/site-packages/keras/engine/training.py\u001b[0m in \u001b[0;36mevaluate\u001b[0;34m(self, x, y, batch_size, verbose, sample_weight, steps, callbacks, max_queue_size, workers, use_multiprocessing, return_dict, **kwargs)\u001b[0m\n\u001b[1;32m 2038\u001b[0m ):\n\u001b[1;32m 2039\u001b[0m \u001b[0mcallbacks\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mon_test_batch_begin\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstep\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 2040\u001b[0;31m \u001b[0mtmp_logs\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtest_function\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0miterator\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2041\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mdata_handler\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mshould_sync\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2042\u001b[0m \u001b[0mcontext\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0masync_wait\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
675
+ "\u001b[0;32m~/Git/FacialExpressionSyncService/.venv/lib/python3.7/site-packages/tensorflow/python/util/traceback_utils.py\u001b[0m in \u001b[0;36merror_handler\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 148\u001b[0m \u001b[0mfiltered_tb\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 149\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 150\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mfn\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 151\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mException\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 152\u001b[0m \u001b[0mfiltered_tb\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_process_traceback_frames\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0me\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__traceback__\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
676
+ "\u001b[0;32m~/Git/FacialExpressionSyncService/.venv/lib/python3.7/site-packages/tensorflow/python/eager/polymorphic_function/polymorphic_function.py\u001b[0m in \u001b[0;36m__call__\u001b[0;34m(self, *args, **kwds)\u001b[0m\n\u001b[1;32m 878\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 879\u001b[0m \u001b[0;32mwith\u001b[0m \u001b[0mOptionalXlaContext\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_jit_compile\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 880\u001b[0;31m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_call\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwds\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 881\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 882\u001b[0m \u001b[0mnew_tracing_count\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexperimental_get_tracing_count\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
677
+ "\u001b[0;32m~/Git/FacialExpressionSyncService/.venv/lib/python3.7/site-packages/tensorflow/python/eager/polymorphic_function/polymorphic_function.py\u001b[0m in \u001b[0;36m_call\u001b[0;34m(self, *args, **kwds)\u001b[0m\n\u001b[1;32m 917\u001b[0m \u001b[0;31m# In this case we have not created variables on the first call. So we can\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 918\u001b[0m \u001b[0;31m# run the first trace but we should fail if variables are created.\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 919\u001b[0;31m \u001b[0mresults\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_variable_creation_fn\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwds\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 920\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_created_variables\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mALLOW_DYNAMIC_VARIABLE_CREATION\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 921\u001b[0m raise ValueError(\"Creating variables on a non-first call to a function\"\n",
678
+ "\u001b[0;32m~/Git/FacialExpressionSyncService/.venv/lib/python3.7/site-packages/tensorflow/python/eager/polymorphic_function/tracing_compiler.py\u001b[0m in \u001b[0;36m__call__\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 133\u001b[0m filtered_flat_args) = self._maybe_define_function(args, kwargs)\n\u001b[1;32m 134\u001b[0m return concrete_function._call_flat(\n\u001b[0;32m--> 135\u001b[0;31m filtered_flat_args, captured_inputs=concrete_function.captured_inputs) # pylint: disable=protected-access\n\u001b[0m\u001b[1;32m 136\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 137\u001b[0m \u001b[0;34m@\u001b[0m\u001b[0mproperty\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
679
+ "\u001b[0;32m~/Git/FacialExpressionSyncService/.venv/lib/python3.7/site-packages/tensorflow/python/eager/polymorphic_function/monomorphic_function.py\u001b[0m in \u001b[0;36m_call_flat\u001b[0;34m(self, args, captured_inputs, cancellation_manager)\u001b[0m\n\u001b[1;32m 1744\u001b[0m \u001b[0;31m# No tape is watching; skip to running the function.\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1745\u001b[0m return self._build_call_outputs(self._inference_function.call(\n\u001b[0;32m-> 1746\u001b[0;31m ctx, args, cancellation_manager=cancellation_manager))\n\u001b[0m\u001b[1;32m 1747\u001b[0m forward_backward = self._select_forward_and_backward_functions(\n\u001b[1;32m 1748\u001b[0m \u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
680
+ "\u001b[0;32m~/Git/FacialExpressionSyncService/.venv/lib/python3.7/site-packages/tensorflow/python/eager/polymorphic_function/monomorphic_function.py\u001b[0m in \u001b[0;36mcall\u001b[0;34m(self, ctx, args, cancellation_manager)\u001b[0m\n\u001b[1;32m 381\u001b[0m \u001b[0minputs\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 382\u001b[0m \u001b[0mattrs\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mattrs\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 383\u001b[0;31m ctx=ctx)\n\u001b[0m\u001b[1;32m 384\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 385\u001b[0m outputs = execute.execute_with_cancellation(\n",
681
+ "\u001b[0;32m~/Git/FacialExpressionSyncService/.venv/lib/python3.7/site-packages/tensorflow/python/eager/execute.py\u001b[0m in \u001b[0;36mquick_execute\u001b[0;34m(op_name, num_outputs, inputs, attrs, ctx, name)\u001b[0m\n\u001b[1;32m 51\u001b[0m \u001b[0mctx\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mensure_initialized\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 52\u001b[0m tensors = pywrap_tfe.TFE_Py_Execute(ctx._handle, device_name, op_name,\n\u001b[0;32m---> 53\u001b[0;31m inputs, attrs, num_outputs)\n\u001b[0m\u001b[1;32m 54\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mcore\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_NotOkStatusException\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 55\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mname\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
682
+ "\u001b[0;31mKeyboardInterrupt\u001b[0m: "
683
+ ]
684
+ }
685
+ ],
686
+ "source": [
687
+ "siamese_model = SiameseModel(siamese_network)\n",
688
+ "siamese_model.compile(optimizer=optimizers.Adam(0.0005), weighted_metrics=[\"accuracy\"])\n",
689
+ "siamese_model.fit(train_dataset, epochs=40, validation_data=val_dataset)"
690
+ ]
691
+ },
692
+ {
693
+ "attachments": {},
694
+ "cell_type": "markdown",
695
+ "metadata": {},
696
+ "source": [
697
+ "## Evaluate Model"
698
+ ]
699
+ },
700
+ {
701
+ "cell_type": "code",
702
+ "execution_count": null,
703
+ "metadata": {},
704
+ "outputs": [],
705
+ "source": [
706
+ "test_df = fecnet_dataset_loader(\"data/faceexp-comparison-data-test-public.csv\")\n",
707
+ "\n",
708
+ "test_dataset = tf.data.Dataset.from_tensor_slices(\n",
709
+ " test_df\n",
710
+ ")\n",
711
+ "\n",
712
+ "test_dataset = test_dataset.map(preprocess_triplets)"
713
+ ]
714
+ },
715
+ {
716
+ "cell_type": "code",
717
+ "execution_count": null,
718
+ "metadata": {},
719
+ "outputs": [],
720
+ "source": [
721
+ "siamese_model.evaluate(test_dataset)"
722
+ ]
723
+ },
724
+ {
725
+ "cell_type": "code",
726
+ "execution_count": 168,
727
+ "metadata": {},
728
+ "outputs": [
729
+ {
730
+ "name": "stdout",
731
+ "output_type": "stream",
732
+ "text": [
733
+ "Positive similarity: 0.96082336\n",
734
+ "Negative similarity 0.85784876\n",
735
+ "Positive-Negative similarity 0.8386482\n"
736
+ ]
737
+ }
738
+ ],
739
+ "source": [
740
+ "sample = next(iter(train_dataset))\n",
741
+ "# visualise_face_mesh_triplet(*sample)\n",
742
+ "\n",
743
+ "anchor, positive, negative = sample\n",
744
+ "anchor_embedding, positive_embedding, negative_embedding = (\n",
745
+ " embedding(tf.reshape(anchor, (-1, 478,3))),\n",
746
+ " embedding(tf.reshape(positive, (-1, 478,3))),\n",
747
+ " embedding(tf.reshape(negative, (-1, 478,3))),\n",
748
+ ")\n",
749
+ "cosine_similarity = metrics.CosineSimilarity()\n",
750
+ "\n",
751
+ "positive_similarity = cosine_similarity(anchor_embedding, positive_embedding)\n",
752
+ "print(\"Positive similarity:\", positive_similarity.numpy())\n",
753
+ "\n",
754
+ "negative_similarity = cosine_similarity(anchor_embedding, negative_embedding)\n",
755
+ "print(\"Negative similarity\", negative_similarity.numpy())\n",
756
+ "\n",
757
+ "positive_negative_similarity = cosine_similarity(positive_embedding, negative_embedding)\n",
758
+ "print(\"Positive-Negative similarity\", positive_negative_similarity.numpy())"
759
+ ]
760
+ },
761
+ {
762
+ "cell_type": "code",
763
+ "execution_count": 8,
764
+ "metadata": {},
765
+ "outputs": [
766
+ {
767
+ "name": "stderr",
768
+ "output_type": "stream",
769
+ "text": [
770
+ "INFO: Created TensorFlow Lite XNNPACK delegate for CPU.\n"
771
+ ]
772
+ }
773
+ ],
774
+ "source": [
775
+ "# tf.data.Dataset.save(dataset, \"training_dataset\")"
776
+ ]
777
+ },
778
+ {
779
+ "cell_type": "code",
780
+ "execution_count": null,
781
+ "metadata": {},
782
+ "outputs": [],
783
+ "source": []
784
+ }
785
+ ],
786
+ "metadata": {
787
+ "kernelspec": {
788
+ "display_name": ".venv",
789
+ "language": "python",
790
+ "name": "python3"
791
+ },
792
+ "language_info": {
793
+ "codemirror_mode": {
794
+ "name": "ipython",
795
+ "version": 3
796
+ },
797
+ "file_extension": ".py",
798
+ "mimetype": "text/x-python",
799
+ "name": "python",
800
+ "nbconvert_exporter": "python",
801
+ "pygments_lexer": "ipython3",
802
+ "version": "3.7.9"
803
+ },
804
+ "orig_nbformat": 4
805
+ },
806
+ "nbformat": 4,
807
+ "nbformat_minor": 2
808
+ }
model/fecnet.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ from importlib import import_module, invalidate_caches
4
+ from importlib.util import module_from_spec, spec_from_file_location
5
+ from tempfile import TemporaryDirectory
6
+
7
+ import cv2
8
+ import numpy as np
9
+ import plotly.express as px
10
+ import requests
11
+ import torch
12
+ from git import Repo
13
+ from huggingface_hub import hf_hub_download
14
+
15
+
16
+ class FECNetModel:
17
+ def __init__(self, hf_token: str) -> None:
18
+ self.hf_token = hf_token
19
+ repo_dir = TemporaryDirectory()
20
+ Repo.clone_from(
21
+ "https://github.com/AmirSh15/FECNet.git",
22
+ repo_dir.name,
23
+ )
24
+ invalidate_caches()
25
+ sys.path.append(repo_dir.name)
26
+ fecnet_module_path = os.path.join(repo_dir.name, "models", "FECNet.py")
27
+ with open(fecnet_module_path, "r") as f:
28
+ content = f.read()
29
+ content = content.replace(
30
+ "cuda",
31
+ "cpu",
32
+ )
33
+ with open(fecnet_module_path, "w") as f:
34
+ f.write(content)
35
+ spec = spec_from_file_location("FECNet", fecnet_module_path)
36
+ fecnet_module = module_from_spec(spec) # type: ignore
37
+ spec.loader.exec_module(fecnet_module) # type: ignore
38
+
39
+ self.model = self.__load_model(
40
+ self.__download_weights(repo_dir.name), fecnet_module.FECNet
41
+ )
42
+
43
+ def __download_weights(self, model_dir: str) -> str:
44
+ model_path = hf_hub_download(
45
+ "natexcvi/pretrained-fecnet",
46
+ "fecnet.pt",
47
+ token=self.hf_token,
48
+ )
49
+ return model_path
50
+
51
+ def __load_model(self, model_path: str, model_class):
52
+ model = model_class(pretrained=False)
53
+ model_weights = torch.load(model_path, map_location=torch.device("cpu"))
54
+ model.load_state_dict(model_weights)
55
+ model.eval()
56
+ return model.double()
57
+
58
+ def predict(self, image: np.ndarray):
59
+ pred = self.model.forward(image)
60
+ return pred
61
+
62
+ def distance(a, b):
63
+ return np.linalg.norm(a - b)
64
+
65
+ def embed_image(self, image) -> np.ndarray:
66
+ image = cv2.imdecode(image, cv2.IMREAD_COLOR)
67
+ image = cv2.resize(image, (224, 224))
68
+ image = np.transpose(image, (2, 0, 1))
69
+ image = np.expand_dims(image, axis=0)
70
+ image = torch.from_numpy(image).double()
71
+ pred = self.predict(image)
72
+ return pred.detach().numpy()
model/fecnet_test.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import numpy as np
4
+ import pytest
5
+ from dotenv import load_dotenv
6
+ from fecnet import FECNetModel
7
+
8
+ load_dotenv()
9
+
10
+
11
+ @pytest.fixture()
12
+ def model():
13
+ return FECNetModel(hf_token=os.getenv("HF_TOKEN"))
14
+
15
+
16
+ def test_embed(model: FECNetModel):
17
+ image = open("testdata/face_pic.jpeg", "rb").read()
18
+ image_arr = np.asarray(bytearray(image), dtype=np.uint8)
19
+ rep = model.embed_image(image_arr)
20
+ assert rep.shape == (1, 16)
model.py → model/model.py RENAMED
File without changes
model/openface_model.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ import openface
4
+ import docker
5
+
6
+
7
+ class OpenFaceModel:
8
+ def __init__(self) -> None:
9
+ self.client = docker.from_env()
10
+ self.client.images.pull("bamos/openface")
11
+
12
+ def preprocess(self, image: bytes) -> np.ndarray:
13
+ raise NotImplemented
14
+
15
+ def embed(self, aligned_face):
16
+ container = self.client.containers.run(
17
+ "bamos/openface",
18
+ "python /root/openface/demos/classifier.py infer /root/openface/models/openface/celeb-classifier.nn4.small2.v1.pkl -",
19
+ detach=True,
20
+ stdin_open=True,
21
+ tty=True,
22
+ )
23
+ raise NotImplemented
24
+ return rep
25
+
26
+ def similarity(self, rep1, rep2):
27
+ return np.linalg.norm(rep1 - rep2, ord=2)
model/openface_model_test.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from openface_model import OpenFaceModel
3
+
4
+
5
+ @pytest.fixture()
6
+ def model():
7
+ return OpenFaceModel()
8
+
9
+ @pytest.mark.skip(reason="Not implemented")
10
+ def test_embed(model):
11
+ image = open("../testdata/face_pic.jpeg", "rb").read()
12
+ aligned_face = model.preprocess(image)
13
+ rep = model.embed(aligned_face)
14
+ assert rep.shape == (128,)
requirements.txt CHANGED
@@ -9,4 +9,9 @@ mediapipe
9
  pandas
10
  pytest
11
  python-dotenv
12
- bcrypt
 
 
 
 
 
 
9
  pandas
10
  pytest
11
  python-dotenv
12
+ bcrypt
13
+ openface
14
+ dlib
15
+ docker
16
+ torch
17
+ gitpython
routers/fecnet_router.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, File, UploadFile, status
2
+
3
+ from auth import validate_token
4
+ from schema import EmbeddingResponse, SimilarityResponse
5
+
6
+ router = APIRouter(
7
+ prefix="/fecnet",
8
+ tags=["fecnet"],
9
+ dependencies=[Depends(validate_token)],
10
+ )
11
+
12
+
13
+ @router.get(
14
+ "/embed",
15
+ status_code=status.HTTP_200_OK,
16
+ response_model=EmbeddingResponse,
17
+ )
18
+ async def calculate_embedding(
19
+ image: UploadFile = File(...),
20
+ ):
21
+ return {"message": "Hello World"}
22
+
23
+
24
+ @router.get(
25
+ "/similarity",
26
+ status_code=status.HTTP_200_OK,
27
+ response_model=SimilarityResponse,
28
+ )
29
+ async def calculate_similarity_score(
30
+ image1: UploadFile = File(...),
31
+ image2: UploadFile = File(...),
32
+ ):
33
+ return {"message": "Hello World"}
routers/openface_router.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, File, UploadFile, status
2
+
3
+ from auth import validate_token
4
+ from schema import EmbeddingResponse, SimilarityResponse
5
+
6
+ router = APIRouter(
7
+ prefix="/openface",
8
+ tags=["openface"],
9
+ dependencies=[Depends(validate_token)],
10
+ )
11
+
12
+
13
+ @router.get(
14
+ "/embed",
15
+ status_code=status.HTTP_200_OK,
16
+ response_model=EmbeddingResponse,
17
+ )
18
+ async def calculate_embedding(
19
+ image: UploadFile = File(...),
20
+ ):
21
+ return {"message": "Hello World"}
22
+
23
+
24
+ @router.get(
25
+ "/similarity",
26
+ status_code=status.HTTP_200_OK,
27
+ response_model=SimilarityResponse,
28
+ )
29
+ async def calculate_similarity_score(
30
+ image1: UploadFile = File(...),
31
+ image2: UploadFile = File(...),
32
+ ):
33
+ return {"message": "Hello World"}