Commit ·
e916c8e
1
Parent(s): 87ffbc7
Remove async limitations
Browse filesAdd capability for mobilenet model
- src/classification.py +60 -0
- src/nets/Loss.py +1 -42
- src/nets/resnet50.py +0 -8
- src/nets/vgg16.py +6 -6
- src/streamlit_app.py +25 -89
- src/utils/callbacks.py +0 -1
- src/utils/dataloader.py +1 -21
- src/utils/utils.py +8 -21
src/classification.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import numpy as np
|
| 3 |
+
import tensorflow as tf
|
| 4 |
+
from huggingface_hub import hf_hub_download
|
| 5 |
+
from nets import get_model_from_name
|
| 6 |
+
from utils.utils import cvtColor, get_classes, letterbox_image, preprocess_input
|
| 7 |
+
import tempfile
|
| 8 |
+
|
| 9 |
+
class Classification:
|
| 10 |
+
def __init__(self, model_choice):
|
| 11 |
+
self.model_choice = model_choice
|
| 12 |
+
self.classes_path = "src/model_data/cls_classes.txt"
|
| 13 |
+
self.input_shape = (224, 224)
|
| 14 |
+
self.alpha = 0.25
|
| 15 |
+
|
| 16 |
+
# Download the model from Hugging Face
|
| 17 |
+
cache_dir = os.path.join(tempfile.gettempdir(), "hf_cache")
|
| 18 |
+
os.makedirs(cache_dir, exist_ok=True)
|
| 19 |
+
self.model_path = hf_hub_download(
|
| 20 |
+
repo_id="sudo-paras-shah/micro-expression-casme2",
|
| 21 |
+
filename="ep097.weights.h5" if self.model_choice is "mobilenet" else "ep089.weights.h5",
|
| 22 |
+
cache_dir=cache_dir
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
# Load class names
|
| 26 |
+
self.class_names, self.num_classes = get_classes(self.classes_path)
|
| 27 |
+
|
| 28 |
+
# Load model
|
| 29 |
+
self.load_model()
|
| 30 |
+
|
| 31 |
+
def load_model(self):
|
| 32 |
+
if self.model_choice == "mobilenet":
|
| 33 |
+
self.model = get_model_from_name[self.model_choice](
|
| 34 |
+
input_shape=[self.input_shape[0], self.input_shape[1], 3],
|
| 35 |
+
classes=self.num_classes,
|
| 36 |
+
alpha=self.alpha
|
| 37 |
+
)
|
| 38 |
+
else:
|
| 39 |
+
self.model = get_model_from_name[self.model_choice](
|
| 40 |
+
input_shape=[self.input_shape[0], self.input_shape[1], 3],
|
| 41 |
+
classes=self.num_classes
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
self.model.load_weights(self.model_path)
|
| 45 |
+
print("Model loaded from", self.model_path)
|
| 46 |
+
print("Classes:", self.class_names)
|
| 47 |
+
|
| 48 |
+
def detect_image(self, image):
|
| 49 |
+
image = cvtColor(image)
|
| 50 |
+
image = letterbox_image(image, [self.input_shape[1], self.input_shape[0]])
|
| 51 |
+
image = np.array(image, dtype=np.float32)
|
| 52 |
+
image = preprocess_input(image)
|
| 53 |
+
image = np.expand_dims(image, axis=0)
|
| 54 |
+
|
| 55 |
+
preds = self.model.predict(image)[0]
|
| 56 |
+
class_index = np.argmax(preds)
|
| 57 |
+
class_name = self.class_names[class_index]
|
| 58 |
+
probability = preds[class_index]
|
| 59 |
+
|
| 60 |
+
return class_name, probability
|
src/nets/Loss.py
CHANGED
|
@@ -1,19 +1,8 @@
|
|
| 1 |
import tensorflow as tf
|
|
|
|
| 2 |
from keras import backend as K
|
| 3 |
|
| 4 |
def multi_category_focal_loss2(gamma=2., alpha=1):
|
| 5 |
-
"""
|
| 6 |
-
focal loss for multi category of multi label problem
|
| 7 |
-
适用于多分类或多标签问题的focal loss
|
| 8 |
-
alpha控制真值y_true为1/0时的权重
|
| 9 |
-
1的权重为alpha, 0的权重为1-alpha
|
| 10 |
-
当你的模型欠拟合,学习存在困难时,可以尝试适用本函数作为loss
|
| 11 |
-
当模型过于激进(无论何时总是倾向于预测出1),尝试将alpha调小
|
| 12 |
-
当模型过于惰性(无论何时总是倾向于预测出0,或是某一个固定的常数,说明没有学到有效特征)
|
| 13 |
-
尝试将alpha调大,鼓励模型进行预测出1。
|
| 14 |
-
Usage:
|
| 15 |
-
model.compile(loss=[multi_category_focal_loss2(alpha=0.25, gamma=2)], metrics=["accuracy"], optimizer=adam)
|
| 16 |
-
"""
|
| 17 |
epsilon = 1.e-7
|
| 18 |
gamma = float(gamma)
|
| 19 |
alpha = tf.constant(alpha, dtype=tf.float32)
|
|
@@ -33,18 +22,8 @@ def multi_category_focal_loss2(gamma=2., alpha=1):
|
|
| 33 |
return multi_category_focal_loss2_fixed
|
| 34 |
|
| 35 |
def multi_category_focal_loss1(alpha, gamma=2.0):
|
| 36 |
-
"""
|
| 37 |
-
focal loss for multi category of multi label problem
|
| 38 |
-
适用于多分类或多标签问题的focal loss
|
| 39 |
-
alpha用于指定不同类别/标签的权重,数组大小需要与类别个数一致
|
| 40 |
-
当你的数据集不同类别/标签之间存在偏斜,可以尝试适用本函数作为loss
|
| 41 |
-
Usage:
|
| 42 |
-
model.compile(loss=[multi_category_focal_loss1(alpha=[1,2,3,2], gamma=2)], metrics=["accuracy"], optimizer=adam)
|
| 43 |
-
"""
|
| 44 |
epsilon = 1.e-7
|
| 45 |
alpha = tf.constant(alpha, dtype=tf.float32)
|
| 46 |
-
#alpha = tf.constant([[1],[1],[1],[1],[1]], dtype=tf.float32)
|
| 47 |
-
#alpha = tf.constant_initializer(alpha)
|
| 48 |
gamma = float(gamma)
|
| 49 |
def multi_category_focal_loss1_fixed(y_true, y_pred):
|
| 50 |
y_true = tf.cast(y_true, tf.float32)
|
|
@@ -59,34 +38,17 @@ def multi_category_focal_loss1(alpha, gamma=2.0):
|
|
| 59 |
|
| 60 |
|
| 61 |
def Cross_entropy_loss(y_true, y_pred):
|
| 62 |
-
'''
|
| 63 |
-
:param y_true: ont-hot encoding ,shape is [batch_size,nums_classes]
|
| 64 |
-
:param y_pred: shape is [batch_size,nums_classes],each example defined as probability for per class
|
| 65 |
-
:return:shape is [batch_size,], a list include cross_entropy for per example
|
| 66 |
-
'''
|
| 67 |
y_pred = K.clip(y_pred, K.epsilon(), 1.0 - K.epsilon())
|
| 68 |
crossEntropyLoss = -y_true * tf.log(y_pred)
|
| 69 |
|
| 70 |
return tf.reduce_sum(crossEntropyLoss, -1)
|
| 71 |
|
| 72 |
-
# focal loss with multi label
|
| 73 |
def focal_loss(classes_num, gamma=2., alpha=.25, e=0.1):
|
| 74 |
-
# classes_num contains sample number of each classes
|
| 75 |
def focal_loss_fixed(target_tensor, prediction_tensor):
|
| 76 |
-
'''
|
| 77 |
-
prediction_tensor is the output tensor with shape [None, 100], where 100 is the number of classes
|
| 78 |
-
target_tensor is the label tensor, same shape as predcition_tensor
|
| 79 |
-
'''
|
| 80 |
-
import tensorflow as tf
|
| 81 |
-
from tensorflow.python.ops import array_ops
|
| 82 |
-
from keras import backend as K
|
| 83 |
-
|
| 84 |
-
#1# get focal loss with no balanced weight which presented in paper function (4)
|
| 85 |
zeros = array_ops.zeros_like(prediction_tensor, dtype=prediction_tensor.dtype)
|
| 86 |
one_minus_p = array_ops.where(tf.greater(target_tensor,zeros), target_tensor - prediction_tensor, zeros)
|
| 87 |
FT = -1 * (one_minus_p ** gamma) * tf.log(tf.clip_by_value(prediction_tensor, 1e-8, 1.0))
|
| 88 |
|
| 89 |
-
#2# get balanced weight alpha
|
| 90 |
classes_weight = array_ops.zeros_like(prediction_tensor, dtype=prediction_tensor.dtype)
|
| 91 |
|
| 92 |
total_num = float(sum(classes_num))
|
|
@@ -98,12 +60,9 @@ def focal_loss(classes_num, gamma=2., alpha=.25, e=0.1):
|
|
| 98 |
|
| 99 |
alpha = array_ops.where(tf.greater(target_tensor, zeros), classes_weight, zeros)
|
| 100 |
|
| 101 |
-
#3# get balanced focal loss
|
| 102 |
balanced_fl = alpha * FT
|
| 103 |
balanced_fl = tf.reduce_mean(balanced_fl)
|
| 104 |
|
| 105 |
-
#4# add other op to prevent overfit
|
| 106 |
-
# reference : https://spaces.ac.cn/archives/4493
|
| 107 |
nb_classes = len(classes_num)
|
| 108 |
fianal_loss = (1-e) * balanced_fl + e * K.categorical_crossentropy(K.ones_like(prediction_tensor)/nb_classes, prediction_tensor)
|
| 109 |
|
|
|
|
| 1 |
import tensorflow as tf
|
| 2 |
+
from tensorflow.python.ops import array_ops
|
| 3 |
from keras import backend as K
|
| 4 |
|
| 5 |
def multi_category_focal_loss2(gamma=2., alpha=1):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
epsilon = 1.e-7
|
| 7 |
gamma = float(gamma)
|
| 8 |
alpha = tf.constant(alpha, dtype=tf.float32)
|
|
|
|
| 22 |
return multi_category_focal_loss2_fixed
|
| 23 |
|
| 24 |
def multi_category_focal_loss1(alpha, gamma=2.0):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
epsilon = 1.e-7
|
| 26 |
alpha = tf.constant(alpha, dtype=tf.float32)
|
|
|
|
|
|
|
| 27 |
gamma = float(gamma)
|
| 28 |
def multi_category_focal_loss1_fixed(y_true, y_pred):
|
| 29 |
y_true = tf.cast(y_true, tf.float32)
|
|
|
|
| 38 |
|
| 39 |
|
| 40 |
def Cross_entropy_loss(y_true, y_pred):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
y_pred = K.clip(y_pred, K.epsilon(), 1.0 - K.epsilon())
|
| 42 |
crossEntropyLoss = -y_true * tf.log(y_pred)
|
| 43 |
|
| 44 |
return tf.reduce_sum(crossEntropyLoss, -1)
|
| 45 |
|
|
|
|
| 46 |
def focal_loss(classes_num, gamma=2., alpha=.25, e=0.1):
|
|
|
|
| 47 |
def focal_loss_fixed(target_tensor, prediction_tensor):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
zeros = array_ops.zeros_like(prediction_tensor, dtype=prediction_tensor.dtype)
|
| 49 |
one_minus_p = array_ops.where(tf.greater(target_tensor,zeros), target_tensor - prediction_tensor, zeros)
|
| 50 |
FT = -1 * (one_minus_p ** gamma) * tf.log(tf.clip_by_value(prediction_tensor, 1e-8, 1.0))
|
| 51 |
|
|
|
|
| 52 |
classes_weight = array_ops.zeros_like(prediction_tensor, dtype=prediction_tensor.dtype)
|
| 53 |
|
| 54 |
total_num = float(sum(classes_num))
|
|
|
|
| 60 |
|
| 61 |
alpha = array_ops.where(tf.greater(target_tensor, zeros), classes_weight, zeros)
|
| 62 |
|
|
|
|
| 63 |
balanced_fl = alpha * FT
|
| 64 |
balanced_fl = tf.reduce_mean(balanced_fl)
|
| 65 |
|
|
|
|
|
|
|
| 66 |
nb_classes = len(classes_num)
|
| 67 |
fianal_loss = (1-e) * balanced_fl + e * K.categorical_crossentropy(K.ones_like(prediction_tensor)/nb_classes, prediction_tensor)
|
| 68 |
|
src/nets/resnet50.py
CHANGED
|
@@ -12,17 +12,14 @@ def identity_block(input_tensor, kernel_size, filters, stage, block):
|
|
| 12 |
conv_name_base = 'res' + str(stage) + block + '_branch'
|
| 13 |
bn_name_base = 'bn' + str(stage) + block + '_branch'
|
| 14 |
|
| 15 |
-
# 减少通道数
|
| 16 |
x = Conv2D(filters1, (1, 1), name=conv_name_base + '2a')(input_tensor)
|
| 17 |
x = BatchNormalization(name=bn_name_base + '2a')(x)
|
| 18 |
x = Activation('relu')(x)
|
| 19 |
|
| 20 |
-
# 3x3卷积
|
| 21 |
x = Conv2D(filters2, kernel_size,padding='same', name=conv_name_base + '2b')(x)
|
| 22 |
x = BatchNormalization(name=bn_name_base + '2b')(x)
|
| 23 |
x = Activation('relu')(x)
|
| 24 |
|
| 25 |
-
# 上升通道数
|
| 26 |
x = Conv2D(filters3, (1, 1), name=conv_name_base + '2c')(x)
|
| 27 |
x = BatchNormalization(name=bn_name_base + '2c')(x)
|
| 28 |
|
|
@@ -37,21 +34,17 @@ def conv_block(input_tensor, kernel_size, filters, stage, block, strides=(2, 2))
|
|
| 37 |
conv_name_base = 'res' + str(stage) + block + '_branch'
|
| 38 |
bn_name_base = 'bn' + str(stage) + block + '_branch'
|
| 39 |
|
| 40 |
-
# 减少通道数
|
| 41 |
x = Conv2D(filters1, (1, 1), strides=strides, name=conv_name_base + '2a')(input_tensor)
|
| 42 |
x = BatchNormalization(name=bn_name_base + '2a')(x)
|
| 43 |
x = Activation('relu')(x)
|
| 44 |
|
| 45 |
-
# 3x3卷积
|
| 46 |
x = Conv2D(filters2, kernel_size, padding='same', name=conv_name_base + '2b')(x)
|
| 47 |
x = BatchNormalization(name=bn_name_base + '2b')(x)
|
| 48 |
x = Activation('relu')(x)
|
| 49 |
|
| 50 |
-
# 上升通道数
|
| 51 |
x = Conv2D(filters3, (1, 1), name=conv_name_base + '2c')(x)
|
| 52 |
x = BatchNormalization(name=bn_name_base + '2c')(x)
|
| 53 |
|
| 54 |
-
# 残差边
|
| 55 |
shortcut = Conv2D(filters3, (1, 1), strides=strides,
|
| 56 |
name=conv_name_base + '1')(input_tensor)
|
| 57 |
shortcut = BatchNormalization(name=bn_name_base + '1')(shortcut)
|
|
@@ -101,7 +94,6 @@ def ResNet50(input_shape=[224,224,3], classes=1000):
|
|
| 101 |
# 1,1,2048
|
| 102 |
x = AveragePooling2D((7, 7), name='avg_pool')(x)
|
| 103 |
|
| 104 |
-
# 进行预测
|
| 105 |
# 2048
|
| 106 |
x = Flatten()(x)
|
| 107 |
|
|
|
|
| 12 |
conv_name_base = 'res' + str(stage) + block + '_branch'
|
| 13 |
bn_name_base = 'bn' + str(stage) + block + '_branch'
|
| 14 |
|
|
|
|
| 15 |
x = Conv2D(filters1, (1, 1), name=conv_name_base + '2a')(input_tensor)
|
| 16 |
x = BatchNormalization(name=bn_name_base + '2a')(x)
|
| 17 |
x = Activation('relu')(x)
|
| 18 |
|
|
|
|
| 19 |
x = Conv2D(filters2, kernel_size,padding='same', name=conv_name_base + '2b')(x)
|
| 20 |
x = BatchNormalization(name=bn_name_base + '2b')(x)
|
| 21 |
x = Activation('relu')(x)
|
| 22 |
|
|
|
|
| 23 |
x = Conv2D(filters3, (1, 1), name=conv_name_base + '2c')(x)
|
| 24 |
x = BatchNormalization(name=bn_name_base + '2c')(x)
|
| 25 |
|
|
|
|
| 34 |
conv_name_base = 'res' + str(stage) + block + '_branch'
|
| 35 |
bn_name_base = 'bn' + str(stage) + block + '_branch'
|
| 36 |
|
|
|
|
| 37 |
x = Conv2D(filters1, (1, 1), strides=strides, name=conv_name_base + '2a')(input_tensor)
|
| 38 |
x = BatchNormalization(name=bn_name_base + '2a')(x)
|
| 39 |
x = Activation('relu')(x)
|
| 40 |
|
|
|
|
| 41 |
x = Conv2D(filters2, kernel_size, padding='same', name=conv_name_base + '2b')(x)
|
| 42 |
x = BatchNormalization(name=bn_name_base + '2b')(x)
|
| 43 |
x = Activation('relu')(x)
|
| 44 |
|
|
|
|
| 45 |
x = Conv2D(filters3, (1, 1), name=conv_name_base + '2c')(x)
|
| 46 |
x = BatchNormalization(name=bn_name_base + '2c')(x)
|
| 47 |
|
|
|
|
| 48 |
shortcut = Conv2D(filters3, (1, 1), strides=strides,
|
| 49 |
name=conv_name_base + '1')(input_tensor)
|
| 50 |
shortcut = BatchNormalization(name=bn_name_base + '1')(shortcut)
|
|
|
|
| 94 |
# 1,1,2048
|
| 95 |
x = AveragePooling2D((7, 7), name='avg_pool')(x)
|
| 96 |
|
|
|
|
| 97 |
# 2048
|
| 98 |
x = Flatten()(x)
|
| 99 |
|
src/nets/vgg16.py
CHANGED
|
@@ -1,15 +1,15 @@
|
|
| 1 |
from keras.layers import Conv2D, Dense, Flatten, Input, MaxPooling2D
|
| 2 |
-
from keras.models import Model
|
| 3 |
|
| 4 |
-
def VGG16(input_shape=None, classes=1000):
|
| 5 |
img_input = Input(shape=input_shape) # 224, 224, 3
|
| 6 |
|
| 7 |
# Block 1
|
| 8 |
# 224, 224, 3 -> 224, 224, 64
|
| 9 |
-
x = Conv2D(64, (3, 3),
|
| 10 |
-
activation='relu',
|
| 11 |
-
padding='same',
|
| 12 |
-
name='block1_conv1')(img_input)
|
| 13 |
x = Conv2D(64, (3, 3),
|
| 14 |
activation='relu',
|
| 15 |
padding='same',
|
|
|
|
| 1 |
from keras.layers import Conv2D, Dense, Flatten, Input, MaxPooling2D
|
| 2 |
+
from keras.models import Model
|
| 3 |
|
| 4 |
+
def VGG16(input_shape=None, classes=1000):
|
| 5 |
img_input = Input(shape=input_shape) # 224, 224, 3
|
| 6 |
|
| 7 |
# Block 1
|
| 8 |
# 224, 224, 3 -> 224, 224, 64
|
| 9 |
+
x = Conv2D(64, (3, 3),
|
| 10 |
+
activation='relu',
|
| 11 |
+
padding='same',
|
| 12 |
+
name='block1_conv1')(img_input)
|
| 13 |
x = Conv2D(64, (3, 3),
|
| 14 |
activation='relu',
|
| 15 |
padding='same',
|
src/streamlit_app.py
CHANGED
|
@@ -1,7 +1,4 @@
|
|
| 1 |
import os
|
| 2 |
-
import sys
|
| 3 |
-
import asyncio
|
| 4 |
-
import tempfile
|
| 5 |
import traceback
|
| 6 |
|
| 7 |
os.environ["HOME"] = "/tmp"
|
|
@@ -10,19 +7,12 @@ os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
|
|
| 10 |
os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
|
| 11 |
os.environ["TF_FORCE_GPU_ALLOW_GROWTH"] = "true"
|
| 12 |
|
| 13 |
-
if sys.platform.startswith('linux'):
|
| 14 |
-
try:
|
| 15 |
-
asyncio.get_event_loop()
|
| 16 |
-
except RuntimeError:
|
| 17 |
-
asyncio.set_event_loop(asyncio.new_event_loop())
|
| 18 |
-
|
| 19 |
import cv2
|
| 20 |
import numpy as np
|
| 21 |
from PIL import Image
|
| 22 |
|
| 23 |
import streamlit as st
|
| 24 |
from streamlit_webrtc import VideoProcessorBase, webrtc_streamer, RTCConfiguration
|
| 25 |
-
from huggingface_hub import hf_hub_download
|
| 26 |
from twilio.rest import Client
|
| 27 |
|
| 28 |
account_sid = os.environ.get("ACCOUNT_SID")
|
|
@@ -30,7 +20,7 @@ auth_token = os.environ.get("AUTH_TOKEN")
|
|
| 30 |
ICE_SERVERS = [{"urls": ["stun:stun.l.google.com:19302"]}]
|
| 31 |
if account_sid and auth_token:
|
| 32 |
try:
|
| 33 |
-
twilio_client = Client(account_sid, auth_token)
|
| 34 |
token = twilio_client.tokens.create()
|
| 35 |
try:
|
| 36 |
ICE_SERVERS = [
|
|
@@ -56,86 +46,36 @@ if gpus:
|
|
| 56 |
except Exception as e:
|
| 57 |
print(e)
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
from nets import get_model_from_name
|
| 62 |
-
from utils.utils import (cvtColor, get_classes, letterbox_image, preprocess_input)
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
# --- Classification class (merged from classification.py) ---
|
| 66 |
-
cache_dir = os.path.join(tempfile.gettempdir(), "hf_cache")
|
| 67 |
-
os.makedirs(cache_dir, exist_ok=True)
|
| 68 |
-
|
| 69 |
-
class Classification(object):
|
| 70 |
-
_defaults = {
|
| 71 |
-
"model_path": hf_hub_download(
|
| 72 |
-
repo_id="sudo-paras-shah/micro-expression-casme2",
|
| 73 |
-
filename="ep089.weights.h5",
|
| 74 |
-
cache_dir=cache_dir
|
| 75 |
-
),
|
| 76 |
-
"classes_path": 'src/model_data/cls_classes.txt',
|
| 77 |
-
"input_shape": [224, 224],
|
| 78 |
-
"backbone": 'vgg16',
|
| 79 |
-
"alpha": 0.25
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
-
@classmethod
|
| 83 |
-
def get_defaults(cls, n):
|
| 84 |
-
if n in cls._defaults:
|
| 85 |
-
return cls._defaults[n]
|
| 86 |
-
else:
|
| 87 |
-
return "Unrecognized attribute name '" + n + "'"
|
| 88 |
-
|
| 89 |
-
def __init__(self, **kwargs):
|
| 90 |
-
self.__dict__.update(self._defaults)
|
| 91 |
-
for name, value in kwargs.items():
|
| 92 |
-
setattr(self, name, value)
|
| 93 |
-
self.class_names, self.num_classes = get_classes(self.classes_path)
|
| 94 |
-
self.generate()
|
| 95 |
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
if self.backbone == "mobilenet":
|
| 100 |
-
self.model = get_model_from_name[self.backbone](
|
| 101 |
-
input_shape=[self.input_shape[0], self.input_shape[1], 3],
|
| 102 |
-
classes=self.num_classes,
|
| 103 |
-
alpha=self.alpha
|
| 104 |
-
)
|
| 105 |
-
else:
|
| 106 |
-
self.model = get_model_from_name[self.backbone](
|
| 107 |
-
input_shape=[self.input_shape[0], self.input_shape[1], 3],
|
| 108 |
-
classes=self.num_classes
|
| 109 |
-
)
|
| 110 |
-
self.model.load_weights(self.model_path)
|
| 111 |
-
print('{} model, and classes {} loaded.'.format(model_path, self.class_names))
|
| 112 |
|
| 113 |
-
|
| 114 |
-
image = cvtColor(image)
|
| 115 |
-
image_data = letterbox_image(image, [self.input_shape[1], self.input_shape[0]])
|
| 116 |
-
image_data = np.expand_dims(preprocess_input(np.array(image_data, np.float32)), 0)
|
| 117 |
-
preds = self.model.predict(image_data)[0]
|
| 118 |
-
class_name = self.class_names[np.argmax(preds)]
|
| 119 |
-
probability = np.max(preds)
|
| 120 |
-
return class_name, probability
|
| 121 |
|
| 122 |
# --- Main Streamlit App ---
|
| 123 |
if __name__ == '__main__':
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
@st.cache_resource
|
| 125 |
-
def get_model():
|
| 126 |
-
return Classification()
|
| 127 |
|
| 128 |
-
classificator = get_model()
|
| 129 |
face_cascade = cv2.CascadeClassifier(
|
| 130 |
cv2.data.haarcascades + 'haarcascade_frontalface_alt.xml'
|
| 131 |
)
|
| 132 |
|
| 133 |
-
if face_cascade.empty():
|
| 134 |
-
st.error("Failed to load Haarcascade XML. Check the path.")
|
| 135 |
-
|
| 136 |
-
st.title("Real-Time Micro-Emotion Recognition")
|
| 137 |
-
st.write("Turn on your camera and detect emotions in real-time.")
|
| 138 |
-
|
| 139 |
def face_detect(img):
|
| 140 |
try:
|
| 141 |
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
|
@@ -184,8 +124,7 @@ if __name__ == '__main__':
|
|
| 184 |
img = frame.to_ndarray(format="bgr24")
|
| 185 |
self.frame_count += 1
|
| 186 |
|
| 187 |
-
|
| 188 |
-
if self.frame_count % 2 == 0:
|
| 189 |
img_disp, img_gray, faces = face_detect(img)
|
| 190 |
self.last_faces = faces
|
| 191 |
self.last_img_gray = img_gray
|
|
@@ -214,11 +153,8 @@ if __name__ == '__main__':
|
|
| 214 |
current_class = emotion_class
|
| 215 |
|
| 216 |
if current_class:
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
if len(history) > 10:
|
| 220 |
-
history.pop(0)
|
| 221 |
-
if len(history) >= 3 and len(set(history[-3:])) > 1:
|
| 222 |
self.rapid_change_count += 1
|
| 223 |
else:
|
| 224 |
self.rapid_change_count = 0
|
|
@@ -252,7 +188,7 @@ if __name__ == '__main__':
|
|
| 252 |
|
| 253 |
return frame.from_ndarray(img_disp, format="bgr24")
|
| 254 |
except Exception as e:
|
| 255 |
-
|
| 256 |
return frame
|
| 257 |
|
| 258 |
RTC_CONFIGURATION = RTCConfiguration({"iceServers": ICE_SERVERS})
|
|
@@ -264,7 +200,7 @@ if __name__ == '__main__':
|
|
| 264 |
media_stream_constraints={"video": True, "audio": False},
|
| 265 |
)
|
| 266 |
|
| 267 |
-
history =
|
| 268 |
if len(history) >= 3 and len(set(history[-3:])) > 1:
|
| 269 |
st.warning(
|
| 270 |
"⚠️ Rapid changes in your detected emotional state were observed. "
|
|
|
|
| 1 |
import os
|
|
|
|
|
|
|
|
|
|
| 2 |
import traceback
|
| 3 |
|
| 4 |
os.environ["HOME"] = "/tmp"
|
|
|
|
| 7 |
os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
|
| 8 |
os.environ["TF_FORCE_GPU_ALLOW_GROWTH"] = "true"
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
import cv2
|
| 11 |
import numpy as np
|
| 12 |
from PIL import Image
|
| 13 |
|
| 14 |
import streamlit as st
|
| 15 |
from streamlit_webrtc import VideoProcessorBase, webrtc_streamer, RTCConfiguration
|
|
|
|
| 16 |
from twilio.rest import Client
|
| 17 |
|
| 18 |
account_sid = os.environ.get("ACCOUNT_SID")
|
|
|
|
| 20 |
ICE_SERVERS = [{"urls": ["stun:stun.l.google.com:19302"]}]
|
| 21 |
if account_sid and auth_token:
|
| 22 |
try:
|
| 23 |
+
twilio_client = Client(account_sid, auth_token, region="in1")
|
| 24 |
token = twilio_client.tokens.create()
|
| 25 |
try:
|
| 26 |
ICE_SERVERS = [
|
|
|
|
| 46 |
except Exception as e:
|
| 47 |
print(e)
|
| 48 |
|
| 49 |
+
from collections import deque
|
| 50 |
+
shared_emotion_history = deque(maxlen=20)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
+
import logging
|
| 53 |
+
logging.getLogger("streamlit.runtime.scriptrunner.script_run_context").setLevel(logging.ERROR)
|
| 54 |
+
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
+
from classification import Classification
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
# --- Main Streamlit App ---
|
| 59 |
if __name__ == '__main__':
|
| 60 |
+
st.title("Personal Video Logger")
|
| 61 |
+
st.write("Turn on your camera and talk about anything that worries you or just about your day.")
|
| 62 |
+
|
| 63 |
+
model_choice = st.selectbox(
|
| 64 |
+
"Choose a model:",
|
| 65 |
+
options=["mobilenet", "vgg16"],
|
| 66 |
+
index=0,
|
| 67 |
+
help="Select the model used for emotion classification."
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
@st.cache_resource
|
| 71 |
+
def get_model(model):
|
| 72 |
+
return Classification(model)
|
| 73 |
|
| 74 |
+
classificator = get_model(model_choice)
|
| 75 |
face_cascade = cv2.CascadeClassifier(
|
| 76 |
cv2.data.haarcascades + 'haarcascade_frontalface_alt.xml'
|
| 77 |
)
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
def face_detect(img):
|
| 80 |
try:
|
| 81 |
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
|
|
|
| 124 |
img = frame.to_ndarray(format="bgr24")
|
| 125 |
self.frame_count += 1
|
| 126 |
|
| 127 |
+
if self.frame_count % 1 == 0:
|
|
|
|
| 128 |
img_disp, img_gray, faces = face_detect(img)
|
| 129 |
self.last_faces = faces
|
| 130 |
self.last_img_gray = img_gray
|
|
|
|
| 153 |
current_class = emotion_class
|
| 154 |
|
| 155 |
if current_class:
|
| 156 |
+
shared_emotion_history.append(current_class)
|
| 157 |
+
if len(shared_emotion_history) >= 3 and len(set(list(shared_emotion_history)[-3:])) > 1:
|
|
|
|
|
|
|
|
|
|
| 158 |
self.rapid_change_count += 1
|
| 159 |
else:
|
| 160 |
self.rapid_change_count = 0
|
|
|
|
| 188 |
|
| 189 |
return frame.from_ndarray(img_disp, format="bgr24")
|
| 190 |
except Exception as e:
|
| 191 |
+
logger.exception("Video processing error", e)
|
| 192 |
return frame
|
| 193 |
|
| 194 |
RTC_CONFIGURATION = RTCConfiguration({"iceServers": ICE_SERVERS})
|
|
|
|
| 200 |
media_stream_constraints={"video": True, "audio": False},
|
| 201 |
)
|
| 202 |
|
| 203 |
+
history = list(shared_emotion_history)
|
| 204 |
if len(history) >= 3 and len(set(history[-3:])) > 1:
|
| 205 |
st.warning(
|
| 206 |
"⚠️ Rapid changes in your detected emotional state were observed. "
|
src/utils/callbacks.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
import os
|
| 2 |
-
|
| 3 |
import matplotlib
|
| 4 |
matplotlib.use('Agg')
|
| 5 |
from matplotlib import pyplot as plt
|
|
|
|
| 1 |
import os
|
|
|
|
| 2 |
import matplotlib
|
| 3 |
matplotlib.use('Agg')
|
| 4 |
from matplotlib import pyplot as plt
|
src/utils/dataloader.py
CHANGED
|
@@ -50,13 +50,8 @@ class ClsDatasets(keras.utils.Sequence):
|
|
| 50 |
return np.random.rand()*(b-a) + a
|
| 51 |
|
| 52 |
def get_random_data(self, image, input_shape, jitter=.3, hue=.1, sat=1.5, val=1.5, random=True):
|
| 53 |
-
#
|
| 54 |
-
# 读取图像并转换成RGB图像
|
| 55 |
-
#------------------------------#
|
| 56 |
image = cvtColor(image)
|
| 57 |
-
#------------------------------#
|
| 58 |
-
# 获得图像的高宽与目标高宽
|
| 59 |
-
#------------------------------#
|
| 60 |
iw, ih = image.size
|
| 61 |
h, w = input_shape
|
| 62 |
|
|
@@ -67,9 +62,6 @@ class ClsDatasets(keras.utils.Sequence):
|
|
| 67 |
dx = (w-nw)//2
|
| 68 |
dy = (h-nh)//2
|
| 69 |
|
| 70 |
-
#---------------------------------#
|
| 71 |
-
# 将图像多余的部分加上灰条
|
| 72 |
-
#---------------------------------#
|
| 73 |
image = image.resize((nw,nh), Image.BICUBIC)
|
| 74 |
new_image = Image.new('RGB', (w,h), (128,128,128))
|
| 75 |
new_image.paste(image, (dx, dy))
|
|
@@ -77,9 +69,6 @@ class ClsDatasets(keras.utils.Sequence):
|
|
| 77 |
|
| 78 |
return image_data
|
| 79 |
|
| 80 |
-
#------------------------------------------#
|
| 81 |
-
# 对图像进行缩放并且进行长和宽的扭曲
|
| 82 |
-
#------------------------------------------#
|
| 83 |
new_ar = w/h * self.rand(1-jitter,1+jitter)/self.rand(1-jitter,1+jitter)
|
| 84 |
scale = self.rand(.75, 1.25)
|
| 85 |
if new_ar < 1:
|
|
@@ -90,18 +79,12 @@ class ClsDatasets(keras.utils.Sequence):
|
|
| 90 |
nh = int(nw/new_ar)
|
| 91 |
image = image.resize((nw,nh), Image.BICUBIC)
|
| 92 |
|
| 93 |
-
#------------------------------------------#
|
| 94 |
-
# 将图像多余的部分加上灰条
|
| 95 |
-
#------------------------------------------#
|
| 96 |
dx = int(self.rand(0, w-nw))
|
| 97 |
dy = int(self.rand(0, h-nh))
|
| 98 |
new_image = Image.new('RGB', (w,h), (128,128,128))
|
| 99 |
new_image.paste(image, (dx, dy))
|
| 100 |
image = new_image
|
| 101 |
|
| 102 |
-
#------------------------------------------#
|
| 103 |
-
# 翻转图像
|
| 104 |
-
#------------------------------------------#
|
| 105 |
flip = self.rand()<.5
|
| 106 |
if flip: image = image.transpose(Image.FLIP_LEFT_RIGHT)
|
| 107 |
|
|
@@ -112,9 +95,6 @@ class ClsDatasets(keras.utils.Sequence):
|
|
| 112 |
M = cv2.getRotationMatrix2D((a,b),angle,1)
|
| 113 |
image = cv2.warpAffine(np.array(image), M, (w,h), borderValue=[128,128,128])
|
| 114 |
|
| 115 |
-
#------------------------------------------#
|
| 116 |
-
# 色域扭曲
|
| 117 |
-
#------------------------------------------#
|
| 118 |
hue = self.rand(-hue, hue)
|
| 119 |
sat = self.rand(1, sat) if self.rand()<.5 else 1/self.rand(1, sat)
|
| 120 |
val = self.rand(1, val) if self.rand()<.5 else 1/self.rand(1, val)
|
|
|
|
| 50 |
return np.random.rand()*(b-a) + a
|
| 51 |
|
| 52 |
def get_random_data(self, image, input_shape, jitter=.3, hue=.1, sat=1.5, val=1.5, random=True):
|
| 53 |
+
# Read and convert images to RGB
|
|
|
|
|
|
|
| 54 |
image = cvtColor(image)
|
|
|
|
|
|
|
|
|
|
| 55 |
iw, ih = image.size
|
| 56 |
h, w = input_shape
|
| 57 |
|
|
|
|
| 62 |
dx = (w-nw)//2
|
| 63 |
dy = (h-nh)//2
|
| 64 |
|
|
|
|
|
|
|
|
|
|
| 65 |
image = image.resize((nw,nh), Image.BICUBIC)
|
| 66 |
new_image = Image.new('RGB', (w,h), (128,128,128))
|
| 67 |
new_image.paste(image, (dx, dy))
|
|
|
|
| 69 |
|
| 70 |
return image_data
|
| 71 |
|
|
|
|
|
|
|
|
|
|
| 72 |
new_ar = w/h * self.rand(1-jitter,1+jitter)/self.rand(1-jitter,1+jitter)
|
| 73 |
scale = self.rand(.75, 1.25)
|
| 74 |
if new_ar < 1:
|
|
|
|
| 79 |
nh = int(nw/new_ar)
|
| 80 |
image = image.resize((nw,nh), Image.BICUBIC)
|
| 81 |
|
|
|
|
|
|
|
|
|
|
| 82 |
dx = int(self.rand(0, w-nw))
|
| 83 |
dy = int(self.rand(0, h-nh))
|
| 84 |
new_image = Image.new('RGB', (w,h), (128,128,128))
|
| 85 |
new_image.paste(image, (dx, dy))
|
| 86 |
image = new_image
|
| 87 |
|
|
|
|
|
|
|
|
|
|
| 88 |
flip = self.rand()<.5
|
| 89 |
if flip: image = image.transpose(Image.FLIP_LEFT_RIGHT)
|
| 90 |
|
|
|
|
| 95 |
M = cv2.getRotationMatrix2D((a,b),angle,1)
|
| 96 |
image = cv2.warpAffine(np.array(image), M, (w,h), borderValue=[128,128,128])
|
| 97 |
|
|
|
|
|
|
|
|
|
|
| 98 |
hue = self.rand(-hue, hue)
|
| 99 |
sat = self.rand(1, sat) if self.rand()<.5 else 1/self.rand(1, sat)
|
| 100 |
val = self.rand(1, val) if self.rand()<.5 else 1/self.rand(1, val)
|
src/utils/utils.py
CHANGED
|
@@ -1,36 +1,26 @@
|
|
| 1 |
import numpy as np
|
| 2 |
from PIL import Image
|
| 3 |
|
| 4 |
-
#---------------------------------------------------#
|
| 5 |
-
# 不失真的resize
|
| 6 |
-
#---------------------------------------------------#
|
| 7 |
def letterbox_image(image, size):
|
| 8 |
-
iw, ih
|
| 9 |
-
w, h
|
| 10 |
|
| 11 |
-
scale
|
| 12 |
-
nw
|
| 13 |
-
nh
|
| 14 |
|
| 15 |
-
image = image.resize((nw,nh), Image.BICUBIC)
|
| 16 |
-
new_image = Image.new('RGB', size, (128,128,128))
|
| 17 |
-
new_image.paste(image, ((w-nw)//2, (h-nh)//2))
|
| 18 |
|
| 19 |
return new_image
|
| 20 |
|
| 21 |
-
#---------------------------------------------------#
|
| 22 |
-
# 获得类
|
| 23 |
-
#---------------------------------------------------#
|
| 24 |
def get_classes(classes_path):
|
| 25 |
with open(classes_path, encoding='utf-8') as f:
|
| 26 |
class_names = f.readlines()
|
| 27 |
class_names = [c.strip() for c in class_names]
|
| 28 |
return class_names, len(class_names)
|
| 29 |
|
| 30 |
-
#---------------------------------------------------------#
|
| 31 |
-
# 将图像转换成RGB图像,防止灰度图在预测时报错。
|
| 32 |
-
# 代码仅仅支持RGB图像的预测,所有其它类型的图像都会转化成RGB
|
| 33 |
-
#---------------------------------------------------------#
|
| 34 |
def cvtColor(image):
|
| 35 |
if len(np.shape(image)) == 3 and np.shape(image)[2] == 3:
|
| 36 |
return image
|
|
@@ -38,9 +28,6 @@ def cvtColor(image):
|
|
| 38 |
image = image.convert('RGB')
|
| 39 |
return image
|
| 40 |
|
| 41 |
-
#----------------------------------------#
|
| 42 |
-
# 预处理训练图片
|
| 43 |
-
#----------------------------------------#
|
| 44 |
def preprocess_input(x):
|
| 45 |
x /= 127.5
|
| 46 |
x -= 1.
|
|
|
|
| 1 |
import numpy as np
|
| 2 |
from PIL import Image
|
| 3 |
|
|
|
|
|
|
|
|
|
|
| 4 |
def letterbox_image(image, size):
|
| 5 |
+
iw, ih = image.size
|
| 6 |
+
w, h = size
|
| 7 |
|
| 8 |
+
scale = min(w / iw, h / ih)
|
| 9 |
+
nw = int(iw * scale)
|
| 10 |
+
nh = int(ih * scale)
|
| 11 |
|
| 12 |
+
image = image.resize((nw, nh), Image.BICUBIC)
|
| 13 |
+
new_image = Image.new('RGB', size, (128, 128, 128))
|
| 14 |
+
new_image.paste(image, ((w - nw) // 2, (h - nh) // 2))
|
| 15 |
|
| 16 |
return new_image
|
| 17 |
|
|
|
|
|
|
|
|
|
|
| 18 |
def get_classes(classes_path):
|
| 19 |
with open(classes_path, encoding='utf-8') as f:
|
| 20 |
class_names = f.readlines()
|
| 21 |
class_names = [c.strip() for c in class_names]
|
| 22 |
return class_names, len(class_names)
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
def cvtColor(image):
|
| 25 |
if len(np.shape(image)) == 3 and np.shape(image)[2] == 3:
|
| 26 |
return image
|
|
|
|
| 28 |
image = image.convert('RGB')
|
| 29 |
return image
|
| 30 |
|
|
|
|
|
|
|
|
|
|
| 31 |
def preprocess_input(x):
|
| 32 |
x /= 127.5
|
| 33 |
x -= 1.
|