From 9aeb4fe95dbe53b899816fa815efc4f24afd3ba0 Mon Sep 17 00:00:00 2001 From: Emiliana <38952746+emilianavt@users.noreply.github.com> Date: Sat, 7 Mar 2020 13:35:58 +0100 Subject: [PATCH] It is now possible to make certain expressions more important when training SVM models. --- Examples/OpenSeeVRMDriver.cs | 28 ++++++++++++++++++---------- Source/SVMWrapper.cpp | 17 ++++++++++++++++- Unity/OpenSeeExpression.cs | 25 +++++++++++++++++++------ Unity/Plugins/SVMModel.cs | 6 +++--- Unity/Plugins/SVMModel.dll | Bin 76800 -> 77312 bytes 5 files changed, 56 insertions(+), 20 deletions(-) diff --git a/Examples/OpenSeeVRMDriver.cs b/Examples/OpenSeeVRMDriver.cs index d0fcf78..6a8d369 100644 --- a/Examples/OpenSeeVRMDriver.cs +++ b/Examples/OpenSeeVRMDriver.cs @@ -26,6 +26,8 @@ public class OpenSeeVRMDriver : MonoBehaviour { //public bool fixedUpdate = true; [Tooltip("When enabled, the avatar will blink automatically. Otherwise, blinks will be applied according to the face tracking's blink detection, allowing it to wink. Even for tracked eye blinks, the eye blink setting of the current expression is used.")] public bool autoBlink = true; + [Tooltip("When automatic eye blinks are disabled, this sets the threshold for how closed the eyes have to be to be registered as closed. At 0 the eyes will never close.")] + public float eyeClosedThreshold = 0.3f; [Tooltip("When enabled, the avatar's eye will move according to the face tracker's gaze tracking.")] public bool gazeTracking = true; [Tooltip("This is the right eye bone. When either eye bone is not set, the VRM look direction blendshapes are used instead.")] @@ -44,12 +46,12 @@ public class OpenSeeVRMDriver : MonoBehaviour { public OpenSeeEyeBlink eyeBlinker = new OpenSeeEyeBlink(); [Tooltip("This component lets configure your VRM expressions.")] public OpenSeeVRMExpression[] expressions = new OpenSeeVRMExpression[]{ - new OpenSeeVRMExpression("neutral", BlendShapePreset.Neutral, 1f, 1f, true, true, 0x70), - new OpenSeeVRMExpression("fun", BlendShapePreset.Fun, 1f, 1f, true, true, 0x71), - new OpenSeeVRMExpression("joy", BlendShapePreset.Joy, 1f, 1f, true, true, 0x72), - new OpenSeeVRMExpression("angry", BlendShapePreset.Angry, 1f, 1f, true, true, 0x73), - new OpenSeeVRMExpression("sorrow", BlendShapePreset.Sorrow, 1f, 1f, true, true, 0x74), - new OpenSeeVRMExpression("surprise", "Surprised", 1f, 1f, true, true, 0x75) + new OpenSeeVRMExpression("neutral", BlendShapePreset.Neutral, 1f, 1f, true, true, 0x70, 50f), + new OpenSeeVRMExpression("fun", BlendShapePreset.Fun, 1f, 1f, true, true, 0x71, 1f), + new OpenSeeVRMExpression("joy", BlendShapePreset.Joy, 1f, 1f, true, true, 0x72, 1f), + new OpenSeeVRMExpression("angry", BlendShapePreset.Angry, 1f, 1f, true, true, 0x73, 1f), + new OpenSeeVRMExpression("sorrow", BlendShapePreset.Sorrow, 1f, 1f, true, true, 0x74, 1f), + new OpenSeeVRMExpression("surprise", "Surprised", 1f, 1f, true, true, 0x75, 1f) }; [Tooltip("The expression configuration is initialized on startup. If it is changed and needs to be reinitialized, this can be triggered by using this flag or calling InitExpressionMap. This flag is reset to false afterwards.")] public bool reloadExpressions = false; @@ -184,10 +186,12 @@ public class OpenSeeVRMDriver : MonoBehaviour { public bool enableBlinking = true; [Tooltip("This can be set to a virtual key value. If this key is pressed together with shift and control, the expression will trigger as an override.")] public int hotkey = -1; + [Tooltip("This is the weight for this class's error when training the model.")] + public float errorWeight = 1f; [HideInInspector] public BlendShapeKey blendShapeKey; - public OpenSeeVRMExpression(string trigger, BlendShapePreset preset, float weight, float factor, bool visemes, bool blinking, int hotkey) { + public OpenSeeVRMExpression(string trigger, BlendShapePreset preset, float weight, float factor, bool visemes, bool blinking, int hotkey, float errorWeight) { this.trigger = trigger; this.blendShapePreset = preset; this.weight = weight; @@ -195,9 +199,10 @@ public class OpenSeeVRMDriver : MonoBehaviour { this.enableVisemes = visemes; this.enableBlinking = blinking; this.hotkey = hotkey; + this.errorWeight = errorWeight; } - public OpenSeeVRMExpression(string trigger, string name, float weight, float factor, bool visemes, bool blinking, int hotkey) { + public OpenSeeVRMExpression(string trigger, string name, float weight, float factor, bool visemes, bool blinking, int hotkey, float errorWeight) { this.trigger = trigger; this.customBlendShapeName = name; this.weight = weight; @@ -205,12 +210,15 @@ public class OpenSeeVRMDriver : MonoBehaviour { this.enableVisemes = visemes; this.enableBlinking = blinking; this.hotkey = hotkey; + this.errorWeight = errorWeight; } } public void InitExpressionMap() { expressionMap = new Dictionary(); + openSeeExpression.weightMap = new Dictionary(); foreach (var expression in expressions) { + openSeeExpression.weightMap.Add(expression.trigger, expression.errorWeight); if (expression.customBlendShapeName != "") expression.blendShapeKey = new BlendShapeKey(expression.customBlendShapeName); else @@ -382,10 +390,10 @@ public class OpenSeeVRMDriver : MonoBehaviour { return; } float right = 1f; - if (openSeeData.rightEyeOpen > 0.5f) + if (openSeeData.rightEyeOpen > eyeClosedThreshold) right = 0f; float left = 1f; - if (openSeeData.leftEyeOpen > 0.5f) + if (openSeeData.leftEyeOpen > eyeClosedThreshold) left = 0f; vrmBlendShapeProxy.ImmediatelySetValue(new BlendShapeKey(BlendShapePreset.Blink_R), right); vrmBlendShapeProxy.ImmediatelySetValue(new BlendShapeKey(BlendShapePreset.Blink_L), left); diff --git a/Source/SVMWrapper.cpp b/Source/SVMWrapper.cpp index 314bae2..f364d49 100644 --- a/Source/SVMWrapper.cpp +++ b/Source/SVMWrapper.cpp @@ -87,7 +87,7 @@ void destroy_problem(struct svm_problem *problem) { } extern "C" { - __declspec(dllexport) void *__cdecl trainModel(float features[], float labels[], int rows, int cols, int classes, int probability, float C) { + __declspec(dllexport) void *__cdecl trainModel(float features[], float labels[], float weights[], int rows, int cols, int classes, int probability, float C) { if (!(rows > 0 && cols > 0)) return NULL; @@ -115,10 +115,25 @@ extern "C" { sw->param.weight_label = NULL; sw->param.weight = NULL; + if (weights != NULL) { + sw->param.nr_weight = classes; + sw->param.weight_label = new int[classes]; + sw->param.weight = new double[classes]; + for (int i = 0; i < classes; i++) { + sw->param.weight_label[i] = i; + sw->param.weight[i] = (double)weights[i]; + } + } + sw->problem = new struct svm_problem(); make_problem(sw->problem, features, labels, rows, cols, sw->means, sw->sdevs); sw->model = svm_train(sw->problem, &sw->param); + if (weights != NULL) { + delete sw->param.weight_label; + delete sw->param.weight; + } + return sw; } diff --git a/Unity/OpenSeeExpression.cs b/Unity/OpenSeeExpression.cs index 6a81126..9b13028 100644 --- a/Unity/OpenSeeExpression.cs +++ b/Unity/OpenSeeExpression.cs @@ -29,6 +29,8 @@ public class OpenSeeExpression : MonoBehaviour public bool enableProbabilityTraining = false; [Tooltip("This is the SVM training C value.")] public float C = 2f; + [Tooltip("This map allows you to set weights for different classes.")] + public Dictionary weightMap = null; [Header("Toggles")] [Tooltip("When enabled, calibration data will be collected from the given OpenSee component.")] public bool recording = false; @@ -341,12 +343,13 @@ public class OpenSeeExpression : MonoBehaviour List list = new List(expressions[key]); list.RemoveAll(x => x.Length != colsFull); if (list != null && list.Count == maxSamples) { + Debug.Log("[Training info] Adding expression " + key + " to training data."); keys.Add(key); - samples += maxSamples; + samples += list.Count; } else { if (list != null && list.Count > 20) { - //Debug.Log("[Training warning] Expression " + key + " has little data and might be inaccurate."); - //accuracyWarnings.Add("Expression " + key + " has little data and might be inaccurate."); + Debug.Log("[Training warning] Expression " + key + " has little data and might be inaccurate."); + accuracyWarnings.Add("Expression " + key + " has little data and might be inaccurate."); samples += list.Count; keys.Add(key); } else { @@ -364,9 +367,19 @@ public class OpenSeeExpression : MonoBehaviour } keys.Sort(); classLabels = keys.ToArray(); + + float[] weights = null; + if (weightMap != null) { + weights = new float[classLabels.Length]; + for (int i = 0; i < classLabels.Length; i++) { + weights[i] = weightMap[classLabels[i]]; + Debug.Log("[Training info] Adding weight " + weights[i] + " for " + classLabels[i] + "."); + } + } + int train_split = maxSamples * 3 / 4; int test_split = maxSamples - train_split; - int rows_train = (classes + 2) * train_split; + int rows_train = classes * train_split; int rows_test = classes * test_split; float[] X_train = new float[rows_train * cols]; float[] X_test = new float[rows_test * cols]; @@ -389,7 +402,7 @@ public class OpenSeeExpression : MonoBehaviour } int train_split_current = train_split; if (classLabels[i] == "neutral") - train_split_current *= 3; + train_split_current *= 1; for (int j = 0; j < train_split_current; j++) { float factor = 1f; float adder = 0f; @@ -416,7 +429,7 @@ public class OpenSeeExpression : MonoBehaviour int probability = 0; if (enableProbabilityTraining) probability = 1; - model.TrainModel(X_train, y_train, i_train, cols, probability, C); + model.TrainModel(X_train, y_train, weights, i_train, cols, probability, C); confusionMatrix = model.ConfusionMatrix(X_test, y_test, i_test, out accuracy); confusionMatrixString = SVMModel.FormatMatrix(confusionMatrix, classLabels); for (int label = 0; label < classes; label++) { diff --git a/Unity/Plugins/SVMModel.cs b/Unity/Plugins/SVMModel.cs index 9b438ef..705dd2c 100644 --- a/Unity/Plugins/SVMModel.cs +++ b/Unity/Plugins/SVMModel.cs @@ -12,7 +12,7 @@ namespace OpenSee { public class SVMModel { #region DllImport [DllImport("SVMModel", CallingConvention = CallingConvention.Cdecl)] - private static extern System.IntPtr trainModel(float[] features, float[] labels, int rows, int cols, int classes, int probability, float C); + private static extern System.IntPtr trainModel(float[] features, float[] labels, float[] weights, int rows, int cols, int classes, int probability, float C); [DllImport("SVMModel", CallingConvention = CallingConvention.Cdecl)] private static extern void predict(System.IntPtr model, float[] features, [Out] float[] predictions, [Out] double[] probabilities, int rows); @@ -118,7 +118,7 @@ public class SVMModel { } // features is a rows*cols long float array with rows written one after another. labels is a rows long float array with the corresponding classes as integral numbers from 0 to num_classes-1, where all classes have to exist. Training on more than 10000 rows is disabled. - public bool TrainModel(float[] features, float[] labels, int rows, int cols, int probability, float C) { + public bool TrainModel(float[] features, float[] labels, float[] weights, int rows, int cols, int probability, float C) { DestroyModel(); //if (rows > 10000) // return false; @@ -143,7 +143,7 @@ public class SVMModel { maxClasses = max + 1; this.cols = cols; - model = trainModel(features, labels, rows, cols, maxClasses, probability, C); + model = trainModel(features, labels, weights, rows, cols, maxClasses, probability, C); haveModel = true; return true; diff --git a/Unity/Plugins/SVMModel.dll b/Unity/Plugins/SVMModel.dll index a273c8c1d375ce9eb2dded0610d940e518032eea..4f88e280fd0a3a662f93da3068d88220fd7353ab 100644 GIT binary patch delta 8453 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 delta 7973 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