3v324v23 commited on
Commit
b6e5ef8
·
1 Parent(s): a9d095e

feat: implement initial facial expression recognition training pipeline with TTA support

Browse files
.gitignore ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
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
+ target/
76
+
77
+ # Jupyter Notebook
78
+ .ipynb_checkpoints
79
+
80
+ # IPython
81
+ profile_default/
82
+ ipython_config.py
83
+
84
+ # pyenv
85
+ .python-version
86
+
87
+ # pipenv
88
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
90
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
91
+ # install all needed dependencies.
92
+ #Pipfile.lock
93
+
94
+ # celery beat schedule file
95
+ celerybeat-schedule
96
+
97
+ # SageMath parsed files
98
+ *.sage.py
99
+
100
+ # Environments
101
+ .env
102
+ .venv
103
+ env/
104
+ venv/
105
+ ENV/
106
+ env.bak/
107
+ venv.bak/
108
+
109
+ # Spyder project settings
110
+ .spyderproject
111
+ .spyproject
112
+
113
+ # Rope project settings
114
+ .ropeproject
115
+
116
+ # mkdocs documentation
117
+ /site
118
+
119
+ # mypy
120
+ .mypy_cache/
121
+ .dmypy.json
122
+ dmypy.json
123
+
124
+ # Pyre type checker
125
+ .pyre/
126
+
127
+ ####
128
+ saved/checkpoints/*
129
+ saved/logs/*
130
+ debug
131
+ ssh.sh
132
+ vim-markdown-preview.html
133
+ deploy.prototxt.txt
134
+ res10_300x300_ssd_iter_140000.caffemodel
135
+ run.sh
136
+ saved
137
+ pretrained_ckpt
138
+ version.py
139
+ *.swp
140
+ .idea/
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Luan Pham
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.
app.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import cv2
3
+ import numpy as np
4
+ from rmn import RMN
5
+
6
+ # Khởi tạo model RMN.
7
+ # Quá trình này sẽ mất một chút thời gian ở lần chạy đầu tiên để tải weights.
8
+ m = RMN()
9
+
10
+ def detect_emotion(image):
11
+ """
12
+ Hàm xử lý ảnh đầu vào, gọi model RMN và trả về ảnh đã vẽ bounding box.
13
+ """
14
+ if image is None:
15
+ return None
16
+
17
+ # Chuyển đổi từ PIL / Gradio RGB sang OpenCV BGR để tương thích với RMN
18
+ image_bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
19
+
20
+ # Dự đoán cảm xúc
21
+ results = m.detect_emotion_for_single_frame(image_bgr)
22
+
23
+ # RMN cung cấp sẵn hàm draw() để vẽ bounding box và label lên ảnh
24
+ image_with_boxes = m.draw(image_bgr, results)
25
+
26
+ # Chuyển ngược lại từ BGR sang RGB để Gradio hiển thị đúng màu
27
+ output_image = cv2.cvtColor(image_with_boxes, cv2.COLOR_BGR2RGB)
28
+
29
+ # Có thể format lại text kết quả để hiển thị ra màn hình
30
+ results_text = "No face detected"
31
+ if results:
32
+ results_text = "\n".join([f"Face {i+1}: {res['emo_label']} (Prob: {res['emo_proba']:.2f})" for i, res in enumerate(results)])
33
+
34
+ return output_image, results_text
35
+
36
+ # Khởi tạo giao diện Gradio
37
+ with gr.Blocks(title="Facial Expression Recognition") as demo:
38
+ gr.Markdown("# 🎭 Facial Expression Recognition (RMN)")
39
+ gr.Markdown(
40
+ "Upload an image or use your webcam to detect facial expressions using the **Residual Masking Network (RMN)**. "
41
+ "This model achieves state-of-the-art results on the FER2013 dataset."
42
+ )
43
+
44
+ with gr.Row():
45
+ with gr.Column():
46
+ # Đầu vào ảnh
47
+ input_image = gr.Image(label="Input Image", type="numpy")
48
+ btn = gr.Button("Detect Emotion", variant="primary")
49
+
50
+ with gr.Column():
51
+ # Đầu ra ảnh và text
52
+ output_image = gr.Image(label="Detected Result", type="numpy")
53
+ output_text = gr.Textbox(label="Detection Details", lines=3)
54
+
55
+ # Kết nối nút bấm với hàm xử lý
56
+ btn.click(fn=detect_emotion, inputs=input_image, outputs=[output_image, output_text])
57
+
58
+ # Cho phép demo các ảnh có sẵn
59
+ gr.Examples(
60
+ examples=[
61
+ # Bạn có thể thêm đường dẫn các ảnh mẫu vào đây nếu có
62
+ # ["examples/happy.jpg"],
63
+ # ["examples/sad.jpg"]
64
+ ],
65
+ inputs=input_image
66
+ )
67
+
68
+ # Chạy ứng dụng
69
+ if __name__ == "__main__":
70
+ # share=True tạo ra một đường link public tạm thời (rất tiện để khoe nhanh)
71
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=True)
demo.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from rmn import RMN
2
+
3
+ if __name__ == "__main__":
4
+ rmn = RMN()
5
+ rmn.video_demo()
models/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .resmasking import resmasking_dropout1
2
+
3
+ __all__ = ["resmasking_dropout1"]
models/masking.py ADDED
@@ -0,0 +1,375 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import traceback
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+
6
+ from .resnet import BasicBlock, conv1x1
7
+
8
+
9
+ def up_pooling(in_channels, out_channels, kernel_size=2, stride=2):
10
+ return nn.Sequential(
11
+ nn.ConvTranspose2d(
12
+ in_channels, out_channels, kernel_size=kernel_size, stride=stride
13
+ ),
14
+ nn.BatchNorm2d(out_channels),
15
+ nn.ReLU(inplace=True),
16
+ )
17
+
18
+
19
+ class Masking4(nn.Module):
20
+ def __init__(self, in_channels, out_channels, block=BasicBlock):
21
+ assert in_channels == out_channels
22
+ super(Masking4, self).__init__()
23
+ filters = [
24
+ in_channels,
25
+ in_channels * 2,
26
+ in_channels * 4,
27
+ in_channels * 8,
28
+ in_channels * 16,
29
+ ]
30
+
31
+ self.downsample1 = nn.Sequential(
32
+ conv1x1(filters[0], filters[1], 1),
33
+ nn.BatchNorm2d(filters[1]),
34
+ )
35
+
36
+ self.downsample2 = nn.Sequential(
37
+ conv1x1(filters[1], filters[2], 1),
38
+ nn.BatchNorm2d(filters[2]),
39
+ )
40
+
41
+ self.downsample3 = nn.Sequential(
42
+ conv1x1(filters[2], filters[3], 1),
43
+ nn.BatchNorm2d(filters[3]),
44
+ )
45
+
46
+ self.downsample4 = nn.Sequential(
47
+ conv1x1(filters[3], filters[4], 1),
48
+ nn.BatchNorm2d(filters[4]),
49
+ )
50
+
51
+ """
52
+ self.conv1 = block(filters[0], filters[1], downsample=conv1x1(filters[0], filters[1], 1))
53
+ self.conv2 = block(filters[1], filters[2], downsample=conv1x1(filters[1], filters[2], 1))
54
+ self.conv3 = block(filters[2], filters[3], downsample=conv1x1(filters[2], filters[3], 1))
55
+ """
56
+
57
+ self.conv1 = block(filters[0], filters[1], downsample=self.downsample1)
58
+ self.conv2 = block(filters[1], filters[2], downsample=self.downsample2)
59
+ self.conv3 = block(filters[2], filters[3], downsample=self.downsample3)
60
+ self.conv4 = block(filters[3], filters[4], downsample=self.downsample4)
61
+
62
+ self.down_pooling = nn.MaxPool2d(kernel_size=2)
63
+
64
+ self.downsample5 = nn.Sequential(
65
+ conv1x1(filters[4], filters[3], 1),
66
+ nn.BatchNorm2d(filters[3]),
67
+ )
68
+
69
+ self.downsample6 = nn.Sequential(
70
+ conv1x1(filters[3], filters[2], 1),
71
+ nn.BatchNorm2d(filters[2]),
72
+ )
73
+
74
+ self.downsample7 = nn.Sequential(
75
+ conv1x1(filters[2], filters[1], 1),
76
+ nn.BatchNorm2d(filters[1]),
77
+ )
78
+
79
+ self.downsample8 = nn.Sequential(
80
+ conv1x1(filters[1], filters[0], 1),
81
+ nn.BatchNorm2d(filters[0]),
82
+ )
83
+
84
+ """
85
+ self.up_pool4 = up_pooling(filters[3], filters[2])
86
+ self.conv4 = block(filters[3], filters[2], downsample=conv1x1(filters[3], filters[2], 1))
87
+ self.up_pool5 = up_pooling(filters[2], filters[1])
88
+ self.conv5 = block(filters[2], filters[1], downsample=conv1x1(filters[2], filters[1], 1))
89
+
90
+ self.conv6 = block(filters[1], filters[0], downsample=conv1x1(filters[1], filters[0], 1))
91
+ """
92
+
93
+ self.up_pool5 = up_pooling(filters[4], filters[3])
94
+ self.conv5 = block(filters[4], filters[3], downsample=self.downsample5)
95
+ self.up_pool6 = up_pooling(filters[3], filters[2])
96
+ self.conv6 = block(filters[3], filters[2], downsample=self.downsample6)
97
+ self.up_pool7 = up_pooling(filters[2], filters[1])
98
+ self.conv7 = block(filters[2], filters[1], downsample=self.downsample7)
99
+ self.conv8 = block(filters[1], filters[0], downsample=self.downsample8)
100
+
101
+ # init weight
102
+ for m in self.modules():
103
+ if isinstance(m, nn.Conv2d):
104
+ nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu")
105
+ elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
106
+ nn.init.constant_(m.weight, 1)
107
+ nn.init.constant_(m.bias, 0)
108
+
109
+ # Zero-initialize the last BN in each residual branch,
110
+ # so that the residual branch starts with zeros, and each residual block behaves like an identity.
111
+ # This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677
112
+ for m in self.modules():
113
+ if isinstance(m, BasicBlock):
114
+ nn.init.constant_(m.bn2.weight, 0)
115
+
116
+ def forward(self, x):
117
+ x1 = self.conv1(x)
118
+ p1 = self.down_pooling(x1)
119
+ x2 = self.conv2(p1)
120
+ p2 = self.down_pooling(x2)
121
+ x3 = self.conv3(p2)
122
+ p3 = self.down_pooling(x3)
123
+ x4 = self.conv4(p3)
124
+
125
+ x5 = self.up_pool5(x4)
126
+ x5 = torch.cat([x5, x3], dim=1)
127
+ x5 = self.conv5(x5)
128
+
129
+ x6 = self.up_pool6(x5)
130
+ x6 = torch.cat([x6, x2], dim=1)
131
+ x6 = self.conv6(x6)
132
+
133
+ x7 = self.up_pool7(x6)
134
+ x7 = torch.cat([x7, x1], dim=1)
135
+ x7 = self.conv7(x7)
136
+
137
+ x8 = self.conv8(x7)
138
+
139
+ output = torch.softmax(x8, dim=1)
140
+ # output = torch.sigmoid(x8)
141
+ return output
142
+
143
+
144
+ class Masking3(nn.Module):
145
+ def __init__(self, in_channels, out_channels, block=BasicBlock):
146
+ assert in_channels == out_channels
147
+ super(Masking3, self).__init__()
148
+ filters = [in_channels, in_channels * 2, in_channels * 4, in_channels * 8]
149
+
150
+ self.downsample1 = nn.Sequential(
151
+ conv1x1(filters[0], filters[1], 1),
152
+ nn.BatchNorm2d(filters[1]),
153
+ )
154
+
155
+ self.downsample2 = nn.Sequential(
156
+ conv1x1(filters[1], filters[2], 1),
157
+ nn.BatchNorm2d(filters[2]),
158
+ )
159
+
160
+ self.downsample3 = nn.Sequential(
161
+ conv1x1(filters[2], filters[3], 1),
162
+ nn.BatchNorm2d(filters[3]),
163
+ )
164
+
165
+ """
166
+ self.conv1 = block(filters[0], filters[1], downsample=conv1x1(filters[0], filters[1], 1))
167
+ self.conv2 = block(filters[1], filters[2], downsample=conv1x1(filters[1], filters[2], 1))
168
+ self.conv3 = block(filters[2], filters[3], downsample=conv1x1(filters[2], filters[3], 1))
169
+ """
170
+
171
+ self.conv1 = block(filters[0], filters[1], downsample=self.downsample1)
172
+ self.conv2 = block(filters[1], filters[2], downsample=self.downsample2)
173
+ self.conv3 = block(filters[2], filters[3], downsample=self.downsample3)
174
+
175
+ self.down_pooling = nn.MaxPool2d(kernel_size=2)
176
+
177
+ self.downsample4 = nn.Sequential(
178
+ conv1x1(filters[3], filters[2], 1),
179
+ nn.BatchNorm2d(filters[2]),
180
+ )
181
+
182
+ self.downsample5 = nn.Sequential(
183
+ conv1x1(filters[2], filters[1], 1),
184
+ nn.BatchNorm2d(filters[1]),
185
+ )
186
+
187
+ self.downsample6 = nn.Sequential(
188
+ conv1x1(filters[1], filters[0], 1),
189
+ nn.BatchNorm2d(filters[0]),
190
+ )
191
+
192
+ """
193
+ self.up_pool4 = up_pooling(filters[3], filters[2])
194
+ self.conv4 = block(filters[3], filters[2], downsample=conv1x1(filters[3], filters[2], 1))
195
+ self.up_pool5 = up_pooling(filters[2], filters[1])
196
+ self.conv5 = block(filters[2], filters[1], downsample=conv1x1(filters[2], filters[1], 1))
197
+
198
+ self.conv6 = block(filters[1], filters[0], downsample=conv1x1(filters[1], filters[0], 1))
199
+ """
200
+
201
+ self.up_pool4 = up_pooling(filters[3], filters[2])
202
+ self.conv4 = block(filters[3], filters[2], downsample=self.downsample4)
203
+ self.up_pool5 = up_pooling(filters[2], filters[1])
204
+ self.conv5 = block(filters[2], filters[1], downsample=self.downsample5)
205
+
206
+ self.conv6 = block(filters[1], filters[0], downsample=self.downsample6)
207
+
208
+ # init weight
209
+ for m in self.modules():
210
+ if isinstance(m, nn.Conv2d):
211
+ nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu")
212
+ elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
213
+ nn.init.constant_(m.weight, 1)
214
+ nn.init.constant_(m.bias, 0)
215
+
216
+ # Zero-initialize the last BN in each residual branch,
217
+ # so that the residual branch starts with zeros, and each residual block behaves like an identity.
218
+ # This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677
219
+ for m in self.modules():
220
+ if isinstance(m, BasicBlock):
221
+ nn.init.constant_(m.bn2.weight, 0)
222
+
223
+ def forward(self, x):
224
+ x1 = self.conv1(x)
225
+ p1 = self.down_pooling(x1)
226
+ x2 = self.conv2(p1)
227
+ p2 = self.down_pooling(x2)
228
+ x3 = self.conv3(p2)
229
+
230
+ x4 = self.up_pool4(x3)
231
+ x4 = torch.cat([x4, x2], dim=1)
232
+
233
+ x4 = self.conv4(x4)
234
+
235
+ x5 = self.up_pool5(x4)
236
+ x5 = torch.cat([x5, x1], dim=1)
237
+ x5 = self.conv5(x5)
238
+
239
+ x6 = self.conv6(x5)
240
+
241
+ output = torch.softmax(x6, dim=1)
242
+ # output = torch.sigmoid(x6)
243
+ return output
244
+
245
+
246
+ class Masking2(nn.Module):
247
+ def __init__(self, in_channels, out_channels, block=BasicBlock):
248
+ assert in_channels == out_channels
249
+ super(Masking2, self).__init__()
250
+ filters = [in_channels, in_channels * 2, in_channels * 4, in_channels * 8]
251
+
252
+ self.downsample1 = nn.Sequential(
253
+ conv1x1(filters[0], filters[1], 1),
254
+ nn.BatchNorm2d(filters[1]),
255
+ )
256
+
257
+ self.downsample2 = nn.Sequential(
258
+ conv1x1(filters[1], filters[2], 1),
259
+ nn.BatchNorm2d(filters[2]),
260
+ )
261
+
262
+ """
263
+ self.conv1 = block(filters[0], filters[1], downsample=conv1x1(filters[0], filters[1], 1))
264
+ self.conv2 = block(filters[1], filters[2], downsample=conv1x1(filters[1], filters[2], 1))
265
+ """
266
+ self.conv1 = block(filters[0], filters[1], downsample=self.downsample1)
267
+ self.conv2 = block(filters[1], filters[2], downsample=self.downsample2)
268
+
269
+ self.down_pooling = nn.MaxPool2d(kernel_size=2)
270
+
271
+ self.downsample3 = nn.Sequential(
272
+ conv1x1(filters[2], filters[1], 1),
273
+ nn.BatchNorm2d(filters[1]),
274
+ )
275
+
276
+ self.downsample4 = nn.Sequential(
277
+ conv1x1(filters[1], filters[0], 1),
278
+ nn.BatchNorm2d(filters[0]),
279
+ )
280
+
281
+ """
282
+ self.up_pool3 = up_pooling(filters[2], filters[1])
283
+ self.conv3 = block(filters[2], filters[1], downsample=conv1x1(filters[2], filters[1], 1))
284
+ self.conv4 = block(filters[1], filters[0], downsample=conv1x1(filters[1], filters[0], 1))
285
+ """
286
+ self.up_pool3 = up_pooling(filters[2], filters[1])
287
+ self.conv3 = block(filters[2], filters[1], downsample=self.downsample3)
288
+ self.conv4 = block(filters[1], filters[0], downsample=self.downsample4)
289
+
290
+ # init weight
291
+ for m in self.modules():
292
+ if isinstance(m, nn.Conv2d):
293
+ nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu")
294
+ elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
295
+ nn.init.constant_(m.weight, 1)
296
+ nn.init.constant_(m.bias, 0)
297
+
298
+ # Zero-initialize the last BN in each residual branch,
299
+ # so that the residual branch starts with zeros, and each residual block behaves like an identity.
300
+ # This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677
301
+ for m in self.modules():
302
+ if isinstance(m, BasicBlock):
303
+ nn.init.constant_(m.bn2.weight, 0)
304
+
305
+ def forward(self, x):
306
+ x1 = self.conv1(x)
307
+ p1 = self.down_pooling(x1)
308
+ x2 = self.conv2(p1)
309
+
310
+ x3 = self.up_pool3(x2)
311
+ x3 = torch.cat([x3, x1], dim=1)
312
+ x3 = self.conv3(x3)
313
+
314
+ x4 = self.conv4(x3)
315
+
316
+ output = torch.softmax(x4, dim=1)
317
+ # output = torch.sigmoid(x4)
318
+ return output
319
+
320
+
321
+ class Masking1(nn.Module):
322
+ def __init__(self, in_channels, out_channels, block=BasicBlock):
323
+ assert in_channels == out_channels
324
+ super(Masking1, self).__init__()
325
+ filters = [in_channels, in_channels * 2, in_channels * 4, in_channels * 8]
326
+
327
+ self.downsample1 = nn.Sequential(
328
+ conv1x1(filters[0], filters[1], 1),
329
+ nn.BatchNorm2d(filters[1]),
330
+ )
331
+
332
+ self.conv1 = block(filters[0], filters[1], downsample=self.downsample1)
333
+
334
+ self.downsample2 = nn.Sequential(
335
+ conv1x1(filters[1], filters[0], 1),
336
+ nn.BatchNorm2d(filters[0]),
337
+ )
338
+
339
+ self.conv2 = block(filters[1], filters[0], downsample=self.downsample2)
340
+
341
+ # init weight
342
+ for m in self.modules():
343
+ if isinstance(m, nn.Conv2d):
344
+ nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu")
345
+ elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
346
+ nn.init.constant_(m.weight, 1)
347
+ nn.init.constant_(m.bias, 0)
348
+
349
+ # Zero-initialize the last BN in each residual branch,
350
+ # so that the residual branch starts with zeros, and each residual block behaves like an identity.
351
+ # This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677
352
+ for m in self.modules():
353
+ if isinstance(m, BasicBlock):
354
+ nn.init.constant_(m.bn2.weight, 0)
355
+
356
+ def forward(self, x):
357
+ x1 = self.conv1(x)
358
+ x2 = self.conv2(x1)
359
+ output = torch.softmax(x2, dim=1)
360
+ # output = torch.sigmoid(x2)
361
+ return output
362
+
363
+
364
+ def masking(in_channels, out_channels, depth, block=BasicBlock):
365
+ if depth == 1:
366
+ return Masking1(in_channels, out_channels, block)
367
+ elif depth == 2:
368
+ return Masking2(in_channels, out_channels, block)
369
+ elif depth == 3:
370
+ return Masking3(in_channels, out_channels, block)
371
+ elif depth == 4:
372
+ return Masking4(in_channels, out_channels, block)
373
+ else:
374
+ traceback.print_exc()
375
+ raise Exception("depth need to be from 0-3")
models/resmasking.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+
4
+ from .masking import masking
5
+ from .resnet import BasicBlock, ResNet
6
+
7
+
8
+ class ResMasking(ResNet):
9
+ def __init__(self, in_channels=3, num_classes=7):
10
+ super(ResMasking, self).__init__(
11
+ block=BasicBlock,
12
+ layers=[3, 4, 6, 3],
13
+ in_channels=in_channels,
14
+ num_classes=1000,
15
+ )
16
+
17
+ self.fc = nn.Linear(512, num_classes)
18
+
19
+ self.mask1 = masking(64, 64, depth=4)
20
+ self.mask2 = masking(128, 128, depth=3)
21
+ self.mask3 = masking(256, 256, depth=2)
22
+ self.mask4 = masking(512, 512, depth=1)
23
+
24
+ def forward(self, x):
25
+ x = self.conv1(x)
26
+ x = self.bn1(x)
27
+ x = self.relu(x)
28
+ x = self.maxpool(x)
29
+
30
+ x = self.layer1(x)
31
+ m = self.mask1(x)
32
+ x = x * (1 + m)
33
+
34
+ x = self.layer2(x)
35
+ m = self.mask2(x)
36
+ x = x * (1 + m)
37
+
38
+ x = self.layer3(x)
39
+ m = self.mask3(x)
40
+ x = x * (1 + m)
41
+
42
+ x = self.layer4(x)
43
+ m = self.mask4(x)
44
+ x = x * (1 + m)
45
+
46
+ x = self.avgpool(x)
47
+ x = torch.flatten(x, 1)
48
+ x = self.fc(x)
49
+ return x
50
+
51
+
52
+ def resmasking_dropout1(in_channels=3, num_classes=7, weight_path=""):
53
+ del weight_path
54
+
55
+ model = ResMasking(in_channels=in_channels, num_classes=num_classes)
56
+ model.fc = nn.Sequential(
57
+ nn.Dropout(0.4),
58
+ nn.Linear(512, num_classes),
59
+ )
60
+ return model
models/resnet.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+
4
+ __all__ = [
5
+ "ResNet",
6
+ "BasicBlock",
7
+ "conv1x1"
8
+ ]
9
+
10
+
11
+ def conv3x3(in_planes, out_planes, stride=1, groups=1, dilation=1):
12
+ """3x3 convolution with padding"""
13
+ return nn.Conv2d(
14
+ in_planes,
15
+ out_planes,
16
+ kernel_size=3,
17
+ stride=stride,
18
+ padding=dilation,
19
+ groups=groups,
20
+ bias=False,
21
+ dilation=dilation,
22
+ )
23
+
24
+
25
+ def conv1x1(in_planes, out_planes, stride=1):
26
+ """1x1 convolution"""
27
+ return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)
28
+
29
+
30
+ class BasicBlock(nn.Module):
31
+ expansion = 1
32
+ __constants__ = ["downsample"]
33
+
34
+ def __init__(
35
+ self,
36
+ inplanes,
37
+ planes,
38
+ stride=1,
39
+ downsample=None,
40
+ groups=1,
41
+ base_width=64,
42
+ dilation=1,
43
+ norm_layer=None,
44
+ ):
45
+ super(BasicBlock, self).__init__()
46
+ if norm_layer is None:
47
+ norm_layer = nn.BatchNorm2d
48
+ if groups != 1 or base_width != 64:
49
+ raise ValueError("BasicBlock only supports groups=1 and base_width=64")
50
+ if dilation > 1:
51
+ raise NotImplementedError("Dilation > 1 not supported in BasicBlock")
52
+ # Both self.conv1 and self.downsample layers downsample the input when stride != 1
53
+ self.conv1 = conv3x3(inplanes, planes, stride)
54
+ self.bn1 = norm_layer(planes)
55
+ self.relu = nn.ReLU(inplace=True)
56
+ self.conv2 = conv3x3(planes, planes)
57
+ self.bn2 = norm_layer(planes)
58
+ self.downsample = downsample
59
+ self.stride = stride
60
+
61
+ def forward(self, x):
62
+ identity = x
63
+
64
+ out = self.conv1(x)
65
+ out = self.bn1(out)
66
+ out = self.relu(out)
67
+
68
+ out = self.conv2(out)
69
+ out = self.bn2(out)
70
+
71
+ if self.downsample is not None:
72
+ identity = self.downsample(x)
73
+
74
+ out += identity
75
+ out = self.relu(out)
76
+
77
+ return out
78
+
79
+
80
+ class ResNet(nn.Module):
81
+ def __init__(
82
+ self,
83
+ block,
84
+ layers,
85
+ num_classes=1000,
86
+ zero_init_residual=False,
87
+ groups=1,
88
+ width_per_group=64,
89
+ replace_stride_with_dilation=None,
90
+ norm_layer=None,
91
+ in_channels=3,
92
+ ):
93
+ super(ResNet, self).__init__()
94
+ if norm_layer is None:
95
+ norm_layer = nn.BatchNorm2d
96
+ self._norm_layer = norm_layer
97
+
98
+ self.inplanes = 64
99
+ self.dilation = 1
100
+ if replace_stride_with_dilation is None:
101
+ # each element in the tuple indicates if we should replace
102
+ # the 2x2 stride with a dilated convolution instead
103
+ replace_stride_with_dilation = [False, False, False]
104
+ if len(replace_stride_with_dilation) != 3:
105
+ raise ValueError(
106
+ "replace_stride_with_dilation should be None "
107
+ "or a 3-element tuple, got {}".format(replace_stride_with_dilation)
108
+ )
109
+ self.groups = groups
110
+ self.base_width = width_per_group
111
+
112
+ # NOTE: strictly set the in_channels = 3 to load the pretrained model
113
+ self.conv1 = nn.Conv2d(
114
+ 3, self.inplanes, kernel_size=7, stride=2, padding=3, bias=False
115
+ )
116
+ # self.conv1 = nn.Conv2d(in_channels, self.inplanes, kernel_size=7, stride=2, padding=3, bias=False)
117
+ self.bn1 = norm_layer(self.inplanes)
118
+ self.relu = nn.ReLU(inplace=True)
119
+ self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
120
+ self.layer1 = self._make_layer(block, 64, layers[0])
121
+ self.layer2 = self._make_layer(
122
+ block, 128, layers[1], stride=2, dilate=replace_stride_with_dilation[0]
123
+ )
124
+ self.layer3 = self._make_layer(
125
+ block, 256, layers[2], stride=2, dilate=replace_stride_with_dilation[1]
126
+ )
127
+ self.layer4 = self._make_layer(
128
+ block, 512, layers[3], stride=2, dilate=replace_stride_with_dilation[2]
129
+ )
130
+ self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
131
+
132
+ # NOTE: strictly set the num_classes = 1000 to load the pretrained model
133
+ self.fc = nn.Linear(512 * block.expansion, 1000)
134
+
135
+ for m in self.modules():
136
+ if isinstance(m, nn.Conv2d):
137
+ nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu")
138
+ elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
139
+ nn.init.constant_(m.weight, 1)
140
+ nn.init.constant_(m.bias, 0)
141
+
142
+ if zero_init_residual:
143
+ for m in self.modules():
144
+ if isinstance(m, BasicBlock):
145
+ nn.init.constant_(m.bn2.weight, 0)
146
+
147
+ def _make_layer(self, block, planes, blocks, stride=1, dilate=False):
148
+ norm_layer = self._norm_layer
149
+ downsample = None
150
+ previous_dilation = self.dilation
151
+ if dilate:
152
+ self.dilation *= stride
153
+ stride = 1
154
+ if stride != 1 or self.inplanes != planes * block.expansion:
155
+ downsample = nn.Sequential(
156
+ conv1x1(self.inplanes, planes * block.expansion, stride),
157
+ norm_layer(planes * block.expansion),
158
+ )
159
+
160
+ layers = []
161
+ layers.append(
162
+ block(
163
+ self.inplanes,
164
+ planes,
165
+ stride,
166
+ downsample,
167
+ self.groups,
168
+ self.base_width,
169
+ previous_dilation,
170
+ norm_layer,
171
+ )
172
+ )
173
+ self.inplanes = planes * block.expansion
174
+ for _ in range(1, blocks):
175
+ layers.append(
176
+ block(
177
+ self.inplanes,
178
+ planes,
179
+ groups=self.groups,
180
+ base_width=self.base_width,
181
+ dilation=self.dilation,
182
+ norm_layer=norm_layer,
183
+ )
184
+ )
185
+
186
+ return nn.Sequential(*layers)
187
+
188
+ def forward(self, x):
189
+ x = self.conv1(x)
190
+ x = self.bn1(x)
191
+ x = self.relu(x)
192
+ x = self.maxpool(x)
193
+
194
+ x = self.layer1(x)
195
+ x = self.layer2(x)
196
+ x = self.layer3(x)
197
+ x = self.layer4(x)
198
+
199
+ x = self.avgpool(x)
200
+ x = torch.flatten(x, 1)
201
+ x = self.fc(x)
202
+
203
+ return x
models/utils.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ try:
2
+ from torch.hub import load_state_dict_from_url # noqa: F401
3
+ except ImportError:
4
+ from torch.utils.model_zoo import load_url as load_state_dict_from_url # noqa: F401
requirements-hf.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Các thư viện dành riêng cho việc triển khai (deploy)
2
+ gradio==4.36.1
3
+ opencv-python-headless==4.9.0.80
4
+ torch
5
+ torchvision
6
+ numpy
7
+
8
+ # Đường dẫn tới thư viện rmn (Nếu tải trực tiếp lên HF thì nó sẽ cài từ pypi)
9
+ rmn==3.1.2
requirements.txt ADDED
Binary file (2.08 kB). View file
 
rmn/__init__.py ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import cv2
4
+ import numpy as np
5
+ import torch
6
+ from torchvision.transforms import transforms
7
+
8
+ from models import resmasking_dropout1
9
+
10
+
11
+ def show(img, name="disp", width=1000):
12
+ """
13
+ name: name of window, should be name of img
14
+ img: source of img, should in type ndarray
15
+ """
16
+ cv2.namedWindow(name, cv2.WINDOW_GUI_NORMAL)
17
+ cv2.resizeWindow(name, width, 1000)
18
+ cv2.imshow(name, img)
19
+ cv2.waitKey(0)
20
+ cv2.destroyAllWindows()
21
+
22
+
23
+ checkpoint_url = "https://github.com/phamquiluan/ResidualMaskingNetwork/releases/download/v0.0.1/Z_resmasking_dropout1_rot30_2019Nov30_13.32"
24
+ local_checkpoint_path = "pretrained_ckpt"
25
+
26
+ prototxt_url = "https://github.com/phamquiluan/ResidualMaskingNetwork/releases/download/v0.0.1/deploy.prototxt.txt"
27
+ local_prototxt_path = "deploy.prototxt.txt"
28
+
29
+ ssd_checkpoint_url = "https://github.com/phamquiluan/ResidualMaskingNetwork/releases/download/v0.0.1/res10_300x300_ssd_iter_140000.caffemodel"
30
+ local_ssd_checkpoint_path = "res10_300x300_ssd_iter_140000.caffemodel"
31
+
32
+
33
+ def download_checkpoint(remote_url, local_path):
34
+ import requests
35
+ from tqdm import tqdm
36
+
37
+ response = requests.get(remote_url, stream=True)
38
+ total_size_in_bytes = int(response.headers.get("content-length", 0))
39
+ block_size = 1024 # 1 Kibibyte
40
+
41
+ progress_bar = tqdm(
42
+ desc=f"Downloading {local_path}..",
43
+ total=total_size_in_bytes,
44
+ unit="iB",
45
+ unit_scale=True,
46
+ )
47
+
48
+ with open(local_path, "wb") as ref:
49
+ for data in response.iter_content(block_size):
50
+ progress_bar.update(len(data))
51
+ ref.write(data)
52
+
53
+ progress_bar.close()
54
+ if total_size_in_bytes != 0 and progress_bar.n != total_size_in_bytes:
55
+ print("ERROR, something went wrong")
56
+
57
+
58
+ for remote_path, local_path in [
59
+ (checkpoint_url, local_checkpoint_path),
60
+ (prototxt_url, local_prototxt_path),
61
+ (ssd_checkpoint_url, local_ssd_checkpoint_path),
62
+ ]:
63
+ if not os.path.exists(local_path):
64
+ print(f"{local_path} does not exists!")
65
+ download_checkpoint(remote_url=remote_path, local_path=local_path)
66
+
67
+
68
+ def ensure_color(image):
69
+ if len(image.shape) == 2:
70
+ return np.dstack([image] * 3)
71
+ elif image.shape[2] == 1:
72
+ return np.dstack([image] * 3)
73
+ return image
74
+
75
+
76
+ def ensure_gray(image):
77
+ try:
78
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
79
+ except cv2.error:
80
+ pass
81
+ return image
82
+
83
+
84
+ def get_ssd_face_detector():
85
+ ssd_face_detector = cv2.dnn.readNetFromCaffe(
86
+ prototxt=local_prototxt_path,
87
+ caffeModel=local_ssd_checkpoint_path,
88
+ )
89
+ return ssd_face_detector
90
+
91
+
92
+ transform = transforms.Compose(
93
+ transforms=[transforms.ToPILImage(), transforms.ToTensor()]
94
+ )
95
+
96
+ FER_2013_EMO_DICT = {
97
+ 0: "angry",
98
+ 1: "disgust",
99
+ 2: "fear",
100
+ 3: "happy",
101
+ 4: "sad",
102
+ 5: "surprise",
103
+ 6: "neutral",
104
+ }
105
+
106
+ is_cuda = torch.cuda.is_available()
107
+
108
+ image_size = (224, 224)
109
+
110
+
111
+ def get_emo_model():
112
+ emo_model = resmasking_dropout1(in_channels=3, num_classes=7)
113
+ if is_cuda:
114
+ emo_model.cuda(0)
115
+ state = torch.load(local_checkpoint_path, map_location="cpu")
116
+ emo_model.load_state_dict(state["net"])
117
+ emo_model.eval()
118
+ return emo_model
119
+
120
+
121
+ def convert_to_square(xmin, ymin, xmax, ymax):
122
+ # convert to square location
123
+ center_x = (xmin + xmax) // 2
124
+ center_y = (ymin + ymax) // 2
125
+
126
+ square_length = ((xmax - xmin) + (ymax - ymin)) // 2 // 2
127
+ square_length *= 1.1
128
+
129
+ xmin = int(center_x - square_length)
130
+ ymin = int(center_y - square_length)
131
+ xmax = int(center_x + square_length)
132
+ ymax = int(center_y + square_length)
133
+ return xmin, ymin, xmax, ymax
134
+
135
+
136
+ class RMN:
137
+ def __init__(self, face_detector=True):
138
+ if face_detector:
139
+ self.face_detector = get_ssd_face_detector()
140
+ self.emo_model = get_emo_model()
141
+
142
+ @torch.no_grad()
143
+ def detect_emotion_for_single_face_image(self, face_image):
144
+ """
145
+ Params:
146
+ -----------
147
+ face_image : np.ndarray
148
+ a cropped face image
149
+
150
+ Return:
151
+ -----------
152
+ emo_label : str
153
+ dominant emotion label
154
+
155
+ emo_proba : float
156
+ dominant emotion proba
157
+
158
+ proba_list : list
159
+ all emotion label and their proba
160
+ """
161
+ assert isinstance(face_image, np.ndarray)
162
+ face_image = ensure_color(face_image)
163
+ face_image = cv2.resize(face_image, image_size)
164
+
165
+ face_image = transform(face_image)
166
+ if is_cuda:
167
+ face_image = face_image.cuda(0)
168
+
169
+ face_image = torch.unsqueeze(face_image, dim=0)
170
+
171
+ output = torch.squeeze(self.emo_model(face_image), 0)
172
+ proba = torch.softmax(output, 0)
173
+
174
+ # get dominant emotion
175
+ emo_proba, emo_idx = torch.max(proba, dim=0)
176
+ emo_idx = emo_idx.item()
177
+ emo_proba = emo_proba.item()
178
+ emo_label = FER_2013_EMO_DICT[emo_idx]
179
+
180
+ # get proba for each emotion
181
+ proba = proba.tolist()
182
+ proba_list = []
183
+ for emo_idx, emo_name in FER_2013_EMO_DICT.items():
184
+ proba_list.append({emo_name: proba[emo_idx]})
185
+
186
+ return emo_label, emo_proba, proba_list
187
+
188
+ @torch.no_grad()
189
+ def video_demo(self):
190
+ vid = cv2.VideoCapture(0)
191
+
192
+ while True:
193
+ ret, frame = vid.read()
194
+ if frame is None or ret is not True:
195
+ continue
196
+
197
+ try:
198
+ frame = np.fliplr(frame).astype(np.uint8)
199
+
200
+ results = self.detect_emotion_for_single_frame(frame)
201
+ frame = self.draw(frame, results)
202
+
203
+ cv2.rectangle(frame, (1, 1), (220, 25), (223, 128, 255), cv2.FILLED)
204
+ cv2.putText(
205
+ frame,
206
+ f"press q to exit",
207
+ (20, 20),
208
+ cv2.FONT_HERSHEY_SIMPLEX,
209
+ 0.8,
210
+ (0, 0, 0),
211
+ 2,
212
+ )
213
+ cv2.imshow("disp", frame)
214
+ if cv2.waitKey(1) == ord("q"):
215
+ break
216
+
217
+ except Exception as err:
218
+ print(err)
219
+ continue
220
+
221
+ cv2.destroyAllWindows()
222
+
223
+ @staticmethod
224
+ def draw(frame, results):
225
+ """
226
+ Params:
227
+ ---------
228
+ frame : np.ndarray
229
+
230
+ results : list of dict.keys('xmin', 'xmax', 'ymin', 'ymax', 'emo_label', 'emo_proba')
231
+
232
+ Returns:
233
+ ---------
234
+ frame : np.ndarray
235
+ """
236
+ for r in results:
237
+ xmin = r["xmin"]
238
+ xmax = r["xmax"]
239
+ ymin = r["ymin"]
240
+ ymax = r["ymax"]
241
+ emo_label = r["emo_label"]
242
+ emo_proba = r["emo_proba"]
243
+
244
+ label_size, base_line = cv2.getTextSize(
245
+ f"{emo_label}: 000", cv2.FONT_HERSHEY_SIMPLEX, 0.8, 2
246
+ )
247
+
248
+ # draw face
249
+ cv2.rectangle(frame, (xmin, ymin), (xmax, ymax), (179, 255, 179), 2)
250
+
251
+ cv2.rectangle(
252
+ frame,
253
+ (xmax, ymin + 1 - label_size[1]),
254
+ (xmax + label_size[0], ymin + 1 + base_line),
255
+ (223, 128, 255),
256
+ cv2.FILLED,
257
+ )
258
+ cv2.putText(
259
+ frame,
260
+ f"{emo_label} {int(emo_proba * 100)}",
261
+ (xmax, ymin + 1),
262
+ cv2.FONT_HERSHEY_SIMPLEX,
263
+ 0.8,
264
+ (0, 0, 0),
265
+ 2,
266
+ )
267
+
268
+ return frame
269
+
270
+ def detect_faces(self, frame):
271
+ h, w = frame.shape[:2]
272
+ blob = cv2.dnn.blobFromImage(
273
+ cv2.resize(frame, (300, 300)),
274
+ 1.0,
275
+ (300, 300),
276
+ (104.0, 177.0, 123.0),
277
+ False,
278
+ False,
279
+ )
280
+ self.face_detector.setInput(blob)
281
+ faces = self.face_detector.forward()
282
+
283
+ face_results = []
284
+ for i in range(0, faces.shape[2]):
285
+ confidence = faces[0, 0, i, 2]
286
+ if confidence < 0.5:
287
+ continue
288
+ xmin, ymin, xmax, ymax = (
289
+ faces[0, 0, i, 3:7] * np.array([w, h, w, h])
290
+ ).astype("int")
291
+ xmin, ymin, xmax, ymax = convert_to_square(xmin, ymin, xmax, ymax)
292
+ if xmax <= xmin or ymax <= ymin:
293
+ continue
294
+
295
+ face_results.append(
296
+ {
297
+ "xmin": xmin,
298
+ "ymin": ymin,
299
+ "xmax": xmax,
300
+ "ymax": ymax,
301
+ }
302
+ )
303
+ return face_results
304
+
305
+ @torch.no_grad()
306
+ def detect_emotion_for_single_frame(self, frame):
307
+ gray = ensure_gray(frame)
308
+
309
+ results = []
310
+ face_results = self.detect_faces(frame)
311
+ print(f"num faces: {len(face_results)}")
312
+
313
+ for face in face_results:
314
+ xmin = face["xmin"]
315
+ ymin = face["ymin"]
316
+ xmax = face["xmax"]
317
+ ymax = face["ymax"]
318
+
319
+ face_image = gray[ymin:ymax, xmin:xmax]
320
+
321
+ if face_image.shape[0] < 10 or face_image.shape[1] < 10:
322
+ continue
323
+ (
324
+ emo_label,
325
+ emo_proba,
326
+ proba_list,
327
+ ) = self.detect_emotion_for_single_face_image(face_image)
328
+
329
+ results.append(
330
+ {
331
+ "xmin": xmin,
332
+ "ymin": ymin,
333
+ "xmax": xmax,
334
+ "ymax": ymax,
335
+ "emo_label": emo_label,
336
+ "emo_proba": emo_proba,
337
+ "proba_list": proba_list,
338
+ }
339
+ )
340
+ return results
train.py ADDED
@@ -0,0 +1,533 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime
2
+ import os
3
+ import random
4
+
5
+ import cv2
6
+ import imgaug
7
+ import matplotlib.pyplot as plt
8
+ import numpy as np
9
+ import pandas as pd
10
+ import torch
11
+ import torch.nn as nn
12
+ import torch.nn.functional as F
13
+ from torch.optim.lr_scheduler import ReduceLROnPlateau
14
+ from torch.utils.data import DataLoader, Dataset
15
+ from torch.utils.tensorboard import SummaryWriter
16
+ from torchvision import transforms
17
+ from tqdm import tqdm
18
+ from sklearn.metrics import ConfusionMatrixDisplay, confusion_matrix
19
+
20
+ from models import resmasking_dropout1
21
+ from utils.augmenters.augment import seg
22
+ from utils.metrics.metrics import accuracy
23
+ from utils.radam import RAdam
24
+
25
+
26
+ FER_2013_EMO_DICT = {
27
+ 0: "angry",
28
+ 1: "disgust",
29
+ 2: "fear",
30
+ 3: "happy",
31
+ 4: "sad",
32
+ 5: "surprise",
33
+ 6: "neutral",
34
+ }
35
+
36
+
37
+ class FER2013Dataset(Dataset):
38
+ def __init__(
39
+ self,
40
+ stage,
41
+ data_path,
42
+ image_size,
43
+ use_tta=False,
44
+ tta_size=48,
45
+ ):
46
+ self._stage = stage
47
+ self._data_path = data_path
48
+ self._image_size = (image_size, image_size)
49
+ self._use_tta = use_tta
50
+ self._tta_size = tta_size
51
+
52
+ csv_path = os.path.join(self._data_path, f"{self._stage}.csv")
53
+ self._data = pd.read_csv(csv_path)
54
+
55
+ self._pixels = self._data["pixels"].tolist()
56
+ self._targets = self._data["emotion"].tolist()
57
+
58
+ self._transform = transforms.Compose(
59
+ [
60
+ transforms.ToPILImage(),
61
+ transforms.ToTensor(),
62
+ ]
63
+ )
64
+
65
+ def is_tta(self):
66
+ return self._use_tta
67
+
68
+ def __len__(self):
69
+ return len(self._pixels)
70
+
71
+ def __getitem__(self, idx):
72
+ pixels = self._pixels[idx]
73
+ pixels = list(map(int, pixels.split(" ")))
74
+ image = np.asarray(pixels).reshape(48, 48).astype(np.uint8)
75
+
76
+ image = cv2.resize(image, self._image_size)
77
+ image = np.dstack([image] * 3)
78
+
79
+ if self._stage == "train":
80
+ image = seg(image=image)
81
+
82
+ target = int(self._targets[idx])
83
+
84
+ if self._stage == "test" and self._use_tta:
85
+ images = [seg(image=image) for _ in range(self._tta_size)]
86
+ images = [self._transform(tta_image) for tta_image in images]
87
+ return images, target
88
+
89
+ image = self._transform(image)
90
+ return image, target
91
+
92
+
93
+ def set_seed(seed):
94
+ random.seed(seed)
95
+ np.random.seed(seed)
96
+ imgaug.seed(seed)
97
+
98
+ torch.manual_seed(seed)
99
+ torch.cuda.manual_seed_all(seed)
100
+ torch.backends.cudnn.deterministic = True
101
+ torch.backends.cudnn.benchmark = False
102
+
103
+
104
+ def resolve_device(device_name):
105
+ if device_name.startswith("cuda") and torch.cuda.is_available():
106
+ return torch.device(device_name)
107
+ return torch.device("cpu")
108
+
109
+
110
+ def build_datasets(data_path, image_size, use_tta, tta_size):
111
+ train_set = FER2013Dataset(
112
+ stage="train",
113
+ data_path=data_path,
114
+ image_size=image_size,
115
+ )
116
+ val_set = FER2013Dataset(
117
+ stage="val",
118
+ data_path=data_path,
119
+ image_size=image_size,
120
+ )
121
+ test_set = FER2013Dataset(
122
+ stage="test",
123
+ data_path=data_path,
124
+ image_size=image_size,
125
+ use_tta=use_tta,
126
+ tta_size=tta_size,
127
+ )
128
+ return train_set, val_set, test_set
129
+
130
+
131
+ def build_dataloaders(train_set, val_set, test_set, batch_size, num_workers, use_tta):
132
+ pin_memory = torch.cuda.is_available()
133
+
134
+ train_loader = DataLoader(
135
+ train_set,
136
+ batch_size=batch_size,
137
+ num_workers=num_workers,
138
+ pin_memory=pin_memory,
139
+ shuffle=True,
140
+ )
141
+ val_loader = DataLoader(
142
+ val_set,
143
+ batch_size=batch_size,
144
+ num_workers=num_workers,
145
+ pin_memory=pin_memory,
146
+ shuffle=False,
147
+ )
148
+
149
+ test_loader = None
150
+ if not use_tta:
151
+ test_loader = DataLoader(
152
+ test_set,
153
+ batch_size=1,
154
+ num_workers=num_workers,
155
+ pin_memory=pin_memory,
156
+ shuffle=False,
157
+ )
158
+
159
+ return train_loader, val_loader, test_loader
160
+
161
+
162
+ def visualize_dataset(data_path):
163
+ stages = ("train", "val", "test")
164
+ class_ids = list(FER_2013_EMO_DICT.keys())
165
+ class_labels = [FER_2013_EMO_DICT[class_id] for class_id in class_ids]
166
+ counts = pd.Series(0, index=class_ids, dtype=np.int64)
167
+
168
+ for stage in stages:
169
+ csv_path = os.path.join(data_path, f"{stage}.csv")
170
+ df = pd.read_csv(csv_path)
171
+ stage_counts = df["emotion"].value_counts()
172
+ counts = counts.add(stage_counts, fill_value=0).astype(int)
173
+
174
+ plt.figure(figsize=(8, 4))
175
+ plt.bar(class_labels, counts.values)
176
+ plt.title("Class Distribution")
177
+ plt.xlabel("Emotion")
178
+ plt.ylabel("Samples")
179
+ plt.xticks(rotation=20)
180
+ plt.tight_layout()
181
+ plt.show()
182
+
183
+
184
+ def train_one_epoch(model, loader, criterion, optimizer, device):
185
+ model.train()
186
+ total_loss = 0.0
187
+ total_acc = 0.0
188
+
189
+ for images, targets in tqdm(loader, total=len(loader), leave=False):
190
+ images = images.to(device, non_blocking=True)
191
+ targets = targets.to(device, non_blocking=True)
192
+
193
+ outputs = model(images)
194
+ loss = criterion(outputs, targets)
195
+ acc = accuracy(outputs, targets)[0]
196
+
197
+ total_loss += loss.item()
198
+ total_acc += acc.item()
199
+
200
+ optimizer.zero_grad()
201
+ loss.backward()
202
+ optimizer.step()
203
+
204
+ num_batches = len(loader)
205
+ return total_loss / num_batches, total_acc / num_batches
206
+
207
+
208
+ def validate_one_epoch(model, loader, criterion, device):
209
+ model.eval()
210
+ total_loss = 0.0
211
+ total_acc = 0.0
212
+
213
+ with torch.no_grad():
214
+ for images, targets in tqdm(loader, total=len(loader), leave=False):
215
+ images = images.to(device, non_blocking=True)
216
+ targets = targets.to(device, non_blocking=True)
217
+
218
+ outputs = model(images)
219
+ loss = criterion(outputs, targets)
220
+ acc = accuracy(outputs, targets)[0]
221
+
222
+ total_loss += loss.item()
223
+ total_acc += acc.item()
224
+
225
+ num_batches = len(loader)
226
+ return total_loss / num_batches, total_acc / num_batches
227
+
228
+
229
+ def eval_test_without_tta(model, loader, device):
230
+ model.eval()
231
+ total_acc = 0.0
232
+
233
+ with torch.no_grad():
234
+ for images, targets in tqdm(loader, total=len(loader), leave=False):
235
+ images = images.to(device, non_blocking=True)
236
+ targets = targets.to(device, non_blocking=True)
237
+
238
+ outputs = model(images)
239
+ acc = accuracy(outputs, targets)[0]
240
+ total_acc += acc.item()
241
+
242
+ return total_acc / len(loader)
243
+
244
+
245
+ def eval_test_with_tta(model, dataset, device):
246
+ model.eval()
247
+ total_acc = 0.0
248
+
249
+ with torch.no_grad():
250
+ for idx in tqdm(range(len(dataset)), total=len(dataset), leave=False):
251
+ images, target = dataset[idx]
252
+
253
+ images = torch.stack(images, dim=0).to(device, non_blocking=True)
254
+ target = torch.LongTensor([target]).to(device, non_blocking=True)
255
+
256
+ outputs = model(images)
257
+ outputs = F.softmax(outputs, dim=1)
258
+ outputs = torch.sum(outputs, dim=0, keepdim=True)
259
+
260
+ acc = accuracy(outputs, target)[0]
261
+ total_acc += acc.item()
262
+
263
+ return total_acc / len(dataset)
264
+
265
+
266
+ def save_checkpoint(path, model, train_params, metrics):
267
+ state = {
268
+ "net": model.state_dict(),
269
+ "config": train_params,
270
+ **metrics,
271
+ }
272
+ torch.save(state, path)
273
+
274
+
275
+ def train(
276
+ data_path="data",
277
+ image_size=224,
278
+ lr=1e-4,
279
+ weight_decay=1e-3,
280
+ batch_size=48,
281
+ num_workers=2,
282
+ device_name="cuda:0",
283
+ max_epoch_num=50,
284
+ max_plateau_count=8,
285
+ plateau_patience=2,
286
+ log_dir="log",
287
+ checkpoint_dir="checkpoint",
288
+ seed=1234,
289
+ use_tta=True,
290
+ tta_size=10,
291
+ ):
292
+ train_params = {
293
+ "data_path": data_path,
294
+ "image_size": image_size,
295
+ "lr": lr,
296
+ "weight_decay": weight_decay,
297
+ "batch_size": batch_size,
298
+ "num_workers": num_workers,
299
+ "device_name": device_name,
300
+ "max_epoch_num": max_epoch_num,
301
+ "max_plateau_count": max_plateau_count,
302
+ "plateau_patience": plateau_patience,
303
+ "log_dir": log_dir,
304
+ "checkpoint_dir": checkpoint_dir,
305
+ "seed": seed,
306
+ "use_tta": use_tta,
307
+ "tta_size": tta_size,
308
+ }
309
+
310
+ set_seed(seed)
311
+ device = resolve_device(device_name)
312
+
313
+ cwd = os.getcwd()
314
+ log_root = os.path.join(cwd, log_dir)
315
+ ckpt_root = os.path.join(cwd, checkpoint_dir)
316
+ os.makedirs(log_root, exist_ok=True)
317
+ os.makedirs(ckpt_root, exist_ok=True)
318
+
319
+ start_time = datetime.datetime.now().replace(microsecond=0)
320
+ run_name = f"resmasking_dropout1_train_{start_time.strftime('%d%m%Y_%H%M%S')}"
321
+
322
+ writer = SummaryWriter(os.path.join(log_root, run_name))
323
+ checkpoint_path = os.path.join(ckpt_root, f"{run_name}.pt")
324
+
325
+ print("Start training")
326
+ print(train_params)
327
+ print(f"Device: {device}")
328
+
329
+ model = resmasking_dropout1(
330
+ in_channels=3,
331
+ num_classes=7,
332
+ ).to(device)
333
+
334
+ train_set, val_set, test_set = build_datasets(
335
+ data_path=data_path,
336
+ image_size=image_size,
337
+ use_tta=use_tta,
338
+ tta_size=tta_size,
339
+ )
340
+ train_loader, val_loader, test_loader = build_dataloaders(
341
+ train_set=train_set,
342
+ val_set=val_set,
343
+ test_set=test_set,
344
+ batch_size=batch_size,
345
+ num_workers=num_workers,
346
+ use_tta=use_tta,
347
+ )
348
+
349
+ criterion = nn.CrossEntropyLoss().to(device)
350
+ optimizer = RAdam(
351
+ params=model.parameters(),
352
+ lr=lr,
353
+ weight_decay=weight_decay,
354
+ )
355
+ scheduler = ReduceLROnPlateau(
356
+ optimizer,
357
+ patience=plateau_patience,
358
+ min_lr=1e-6,
359
+ )
360
+
361
+ train_losses = []
362
+ val_losses = []
363
+ train_accs = []
364
+ val_accs = []
365
+
366
+ best_val_loss = 1e9
367
+ best_val_acc = 0.0
368
+ best_train_loss = 1e9
369
+ best_train_acc = 0.0
370
+
371
+ plateau_count = 0
372
+ current_epoch = 0
373
+
374
+ while current_epoch < max_epoch_num and plateau_count <= max_plateau_count:
375
+ current_epoch += 1
376
+
377
+ train_loss, train_acc = train_one_epoch(
378
+ model=model,
379
+ loader=train_loader,
380
+ criterion=criterion,
381
+ optimizer=optimizer,
382
+ device=device,
383
+ )
384
+ val_loss, val_acc = validate_one_epoch(
385
+ model=model,
386
+ loader=val_loader,
387
+ criterion=criterion,
388
+ device=device,
389
+ )
390
+
391
+ train_losses.append(train_loss)
392
+ val_losses.append(val_loss)
393
+ train_accs.append(train_acc)
394
+ val_accs.append(val_acc)
395
+
396
+ if current_epoch == 1 or val_acc > best_val_acc:
397
+ plateau_count = 0
398
+ best_val_acc = val_acc
399
+ best_val_loss = val_loss
400
+ best_train_acc = train_acc
401
+ best_train_loss = train_loss
402
+
403
+ save_checkpoint(
404
+ path=checkpoint_path,
405
+ model=model,
406
+ train_params=train_params,
407
+ metrics={
408
+ "best_val_loss": best_val_loss,
409
+ "best_val_acc": best_val_acc,
410
+ "best_train_loss": best_train_loss,
411
+ "best_train_acc": best_train_acc,
412
+ "train_losses": train_losses,
413
+ "val_losses": val_losses,
414
+ "train_accs": train_accs,
415
+ "val_accs": val_accs,
416
+ "current_epoch": current_epoch,
417
+ },
418
+ )
419
+ else:
420
+ plateau_count += 1
421
+
422
+ scheduler.step(100.0 - val_acc)
423
+
424
+ writer.add_scalar("Accuracy/Train", train_acc, current_epoch)
425
+ writer.add_scalar("Accuracy/Val", val_acc, current_epoch)
426
+ writer.add_scalar("Loss/Train", train_loss, current_epoch)
427
+ writer.add_scalar("Loss/Val", val_loss, current_epoch)
428
+
429
+ consume_time = str(datetime.datetime.now() - start_time)
430
+ message = (
431
+ f"E{current_epoch:03d} "
432
+ f"{train_loss:.3f}/{val_loss:.3f}/{best_val_loss:.3f} "
433
+ f"{train_acc:.3f}/{val_acc:.3f}/{best_val_acc:.3f} "
434
+ f"| p{plateau_count:02d} Time {consume_time[:-7]}"
435
+ )
436
+ print(message)
437
+
438
+ best_state = torch.load(checkpoint_path, map_location=device)
439
+ model.load_state_dict(best_state["net"])
440
+
441
+ if use_tta:
442
+ test_acc = eval_test_with_tta(model, test_set, device)
443
+ else:
444
+ test_acc = eval_test_without_tta(model, test_loader, device)
445
+
446
+ best_state["test_acc"] = test_acc
447
+ torch.save(best_state, checkpoint_path)
448
+
449
+ consume_time = str(datetime.datetime.now() - start_time)
450
+ writer.add_text(
451
+ "Summary",
452
+ f"Converged after {current_epoch} epochs, consume {consume_time[:-7]}",
453
+ )
454
+ writer.add_text("Results", f"Best validation accuracy: {best_val_acc:.3f}")
455
+ writer.add_text("Results", f"Best training accuracy: {best_train_acc:.3f}")
456
+ writer.add_text("Results", f"Test accuracy: {test_acc:.3f}")
457
+ writer.close()
458
+
459
+ print(f"Best checkpoint saved at: {checkpoint_path}")
460
+ print(f"Test accuracy: {test_acc:.3f}")
461
+
462
+
463
+ def eval_test_with_tta_conf_matrix(checkpoint_path):
464
+
465
+ checkpoint = torch.load(checkpoint_path)
466
+ config = checkpoint.get("config")
467
+
468
+ data_path = config.get("data_path")
469
+ image_size = config.get("image_size")
470
+ device_name = config.get("device_name")
471
+ use_tta = config.get("use_tta")
472
+ tta_size = config.get("tta_size")
473
+
474
+ if not use_tta:
475
+ raise Exception
476
+
477
+ device = resolve_device(device_name)
478
+
479
+ model = resmasking_dropout1(
480
+ in_channels=3,
481
+ num_classes=7,
482
+ ).to(device)
483
+ model.load_state_dict(checkpoint["net"])
484
+ model.eval()
485
+
486
+ _, _, test_set = build_datasets(
487
+ data_path=data_path,
488
+ image_size=image_size,
489
+ use_tta=True,
490
+ tta_size=tta_size,
491
+ )
492
+
493
+ y_true = []
494
+ y_pred = []
495
+
496
+ total_acc = 0.0
497
+ with torch.no_grad():
498
+ for idx in tqdm(range(len(test_set)), total=len(test_set), leave=False):
499
+ images, target = test_set[idx]
500
+ images = torch.stack(images, dim=0).to(device, non_blocking=True)
501
+ target = torch.LongTensor([target]).to(device, non_blocking=True)
502
+
503
+ outputs = model(images)
504
+ outputs = F.softmax(outputs, dim=1)
505
+ outputs = torch.sum(outputs, dim=0, keepdim=True)
506
+ pred = torch.argmax(outputs, dim=1).item()
507
+
508
+ acc = accuracy(outputs, target)[0]
509
+ total_acc += acc.item()
510
+
511
+ y_true.append(int(target))
512
+ y_pred.append(int(pred))
513
+
514
+ class_ids = list(FER_2013_EMO_DICT.keys())
515
+ class_labels = [FER_2013_EMO_DICT[class_id] for class_id in class_ids]
516
+ cm = confusion_matrix(y_true, y_pred, labels=class_ids)
517
+
518
+ acc = total_acc / len(test_set) if len(test_set) > 0 else 0.0
519
+ print(f"Test accuracy: {acc:.3f}")
520
+
521
+ fig, ax = plt.subplots(figsize=(8, 8))
522
+ disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=class_labels)
523
+ disp.plot(ax=ax, cmap="Blues", colorbar=True, values_format="d")
524
+ ax.set_title("Confusion Matrix")
525
+ plt.xticks(rotation=45, ha="right")
526
+ plt.tight_layout()
527
+ plt.show()
528
+
529
+
530
+ if __name__ == "__main__":
531
+ # visualize_dataset(data_path="data")
532
+ # eval_test_with_tta_conf_matrix("checkpoint/branch1.pt")
533
+ train()
utils/__init__.py ADDED
File without changes
utils/augmenters/augment.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from imgaug import augmenters as iaa
2
+
3
+ seg = iaa.Sequential(
4
+ [
5
+ iaa.Fliplr(p=0.5),
6
+ iaa.Affine(rotate=(-30, 30)),
7
+ ]
8
+ )
utils/metrics/__init__.py ADDED
File without changes
utils/metrics/metrics.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+
4
+ def accuracy(output, target):
5
+ with torch.no_grad():
6
+ batch_size = target.size(0)
7
+ pred = torch.argmax(output, dim=1)
8
+ correct = pred.eq(target).float().sum(0)
9
+ acc = correct * 100 / batch_size
10
+ return [acc]
utils/radam.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+
3
+ import torch
4
+ from torch.optim.optimizer import Optimizer
5
+
6
+
7
+ class RAdam(Optimizer):
8
+ def __init__(self, params, lr=1e-3, betas=(0.9, 0.999), eps=1e-8, weight_decay=0):
9
+ defaults = dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay)
10
+ self.buffer = [[None, None, None] for ind in range(10)]
11
+ super(RAdam, self).__init__(params, defaults)
12
+
13
+ def __setstate__(self, state):
14
+ super(RAdam, self).__setstate__(state)
15
+
16
+ def step(self, closure=None):
17
+
18
+ loss = None
19
+ if closure is not None:
20
+ loss = closure()
21
+
22
+ for group in self.param_groups:
23
+
24
+ for p in group["params"]:
25
+ if p.grad is None:
26
+ continue
27
+ grad = p.grad.data.float()
28
+ if grad.is_sparse:
29
+ raise RuntimeError("RAdam does not support sparse gradients")
30
+
31
+ p_data_fp32 = p.data.float()
32
+
33
+ state = self.state[p]
34
+
35
+ if len(state) == 0:
36
+ state["step"] = 0
37
+ state["exp_avg"] = torch.zeros_like(p_data_fp32)
38
+ state["exp_avg_sq"] = torch.zeros_like(p_data_fp32)
39
+ else:
40
+ state["exp_avg"] = state["exp_avg"].type_as(p_data_fp32)
41
+ state["exp_avg_sq"] = state["exp_avg_sq"].type_as(p_data_fp32)
42
+
43
+ exp_avg, exp_avg_sq = state["exp_avg"], state["exp_avg_sq"]
44
+ beta1, beta2 = group["betas"]
45
+
46
+ exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=1 - beta2)
47
+ exp_avg.mul_(beta1).add_(grad, alpha=1 - beta1)
48
+
49
+ state["step"] += 1
50
+ buffered = self.buffer[int(state["step"] % 10)]
51
+ if state["step"] == buffered[0]:
52
+ N_sma, step_size = buffered[1], buffered[2]
53
+ else:
54
+ buffered[0] = state["step"]
55
+ beta2_t = beta2 ** state["step"]
56
+ N_sma_max = 2 / (1 - beta2) - 1
57
+ N_sma = N_sma_max - 2 * state["step"] * beta2_t / (1 - beta2_t)
58
+ buffered[1] = N_sma
59
+
60
+ # more conservative since it's an approximated value
61
+ if N_sma >= 5:
62
+ step_size = (
63
+ group["lr"]
64
+ * math.sqrt(
65
+ (1 - beta2_t)
66
+ * (N_sma - 4)
67
+ / (N_sma_max - 4)
68
+ * (N_sma - 2)
69
+ / N_sma
70
+ * N_sma_max
71
+ / (N_sma_max - 2)
72
+ )
73
+ / (1 - beta1 ** state["step"])
74
+ )
75
+ else:
76
+ step_size = group["lr"] / (1 - beta1 ** state["step"])
77
+ buffered[2] = step_size
78
+
79
+ if group["weight_decay"] != 0:
80
+ p_data_fp32.add_(
81
+ p_data_fp32, alpha=-group["weight_decay"] * group["lr"]
82
+ )
83
+
84
+ # more conservative since it's an approximated value
85
+ if N_sma >= 5:
86
+ denom = exp_avg_sq.sqrt().add_(group["eps"])
87
+ p_data_fp32.addcdiv_(exp_avg, denom, value=-step_size)
88
+ else:
89
+ p_data_fp32.add_(exp_avg, alpha=-step_size)
90
+
91
+ p.data.copy_(p_data_fp32)
92
+
93
+ return loss
94
+