Gaussian Mixture Model (GMM) on Riemannian manifold

Cette approche étend les modèles à mélanges Gaussien aux variétés Riemanniennes où les données de rotation comme les quaternions sont naturellement représentées. Plus d'information théorique peut être trouvé dans l'article:

  • M J A Zeestraten, I Havoutis, J Silvério, S Calinon and D G Caldwell. An Approach for Imitation Learning on Riemannian Manifolds. IEEE Robotics and Automation Letters (RA-L) 2(3):1240–1247, June 2017

Dans notre cas, la variété Riemannienne étudiée est la combinaison des espaces cartésiens (position) et Riemanniens (orientation) de chaque articulation du squelette humain formant l'espace de la pose humaine. Plus d'information peut être trouvé dans l'article:

  • Maxime Devanne, Sao Mai Nguyen. Multi-level Motion Analysis for Physical Exercises Assessment in Kinaesthetic Rehabilitation. IEEE-RAS International Conference on Humanoid Robots 2017, Birmingham, UK.

Le code suivant est en matlab. Une version python de l'extension des GMM sur les variétés Riemannienne peut être trouvée ici: https://gitlab.martijnzeestraten.nl/martijn/riepybdlib

Il y a deux fichiers principaux, un pour l'apprentissage d'un modèle à partir de démonstrations, l'autre pour l'évaluation d'une séquence à partir d'un modèle appris.

  • 1. Apprentissage

Le script 'mainLearning.m' permet d'apprendre un modèle à partir de démonstrations. Les premières lignes définissent les paramètres qui sont expliqués dans le code. Principalement, les paramètres qui changeront en fonction du nombre de données d'apprentissage sont:

%% Parameters
nbData = 300; %Number of datapoints
nbSamples =2; %Number of demonstrations
trainName={'data/Assis3Maxime/'}; % folders names from where to load data
nspp=2; %number of skeleton sequence per folder

nbData est le nombre de trames à laquelle chaque séquence sera échantillonnée. nbSamples est le nombre de séquences qui seront chargées et utilisées pour l'apprentissage trainName est le nom de chaque dossier où récupérer les données nspp est le nombre de séquence de squelette à charger dans chaque dossier. Si la valeur est 2, les séquence 1 et 2 seront chargées depuis chaque dossier.

Tout d'abord on charge les données de squelettes d'apprentissage.

%% Data processing
trainName={'data/Assis3Maxime/'};
[model,xIn,uIn,xOut,uOut] = processTrainingData(model,trainName,nspp,registration,fastDP,filt,est,rem,ws,nbData);
% data projected on tangent spaces of the human pose space
u = [uIn; uOut{1}.data; uOut{2}.data; uOut{3}.data; uOut{4}.data; uOut{5}.data; uOut{6}.data; uOut{7}.data; uOut{8}.data; uOut{9}.data; uOut{10}.data; uOut{11}.data; uOut{12}.data; uOut{13}.data; uOut{14}.data; uOut{15}.data];
% original data x (positions 3D and quaternions) in the human pose space
x = [xIn; xOut{1}.data; xOut{2}.data; xOut{3}.data; xOut{4}.data; xOut{5}.data; xOut{6}.data; xOut{7}.data; xOut{8}.data; xOut{9}.data; xOut{10}.data; xOut{11}.data; xOut{12}.data; xOut{13}.data; xOut{14}.data; xOut{15}.data];
model.x=x;

Les données de squelettes ont été capturées à l'aide de la bibliothèque Kinect. Le format est une matrice où chaque ligne est une trame, les colonnes correspondent aux positions 3D et orientations (quaternion) de chaque articulation dans l'ordre de la hiérarchie du squelette. Dans notre cas uniquement les données du haut du corps sont considérées. Si le paramètre registration=1, l'alignement temporel entre les séquences d'apprentissage est effectué, ce qui est utile pour apprendre un modèle idéal.

De plus, pour segmenter la séquence en différent segments temporels correspondant à différents mouvements unitaires, et ainsi utiliser ces segments lors de l'évaluation, les fonctions 'segmentSequence' et 'segmentSequenceKeyPose' sont utilisées. La première fonction segmente le mouvement en considérant les points de coupure comme les points de transitions entre deux mouvements unitaires. La seconde fonction permet d'en plus considérer les phases de pose de maintient comme des segments temporels. Plus de détail sur la méthode de segmentation peuvent être trouvé dans le 2ème article cité plus haut.

Une fois les données chargées, xIn et uIn correspondent aux données de temps (trames), xOut est l'ensemble des données pour chaque articulation séparément dans l'espace des poses et uOut est l'ensemble des données pour chaque articulation séparément projeté dans les espaces tangent correspondant. Ces données temporelles et spatiales sont ensuite concaténées pour passer en entrée de l'apprentissage.

%% GMM learning
[ model ] = learnGMMmodel(model,u,xIn,xOut,nbSamples,nbIterEM,nbIter,nbData);
save('modelExo3','model');

Le modèle GMM est appris à partir des données d'apprentissage. Le nombre de gaussiennes du modèle est décidé par l'intermédiaire du paramètre model.nbStates au début du code. Le paramètre nbIter détermine le nombre d'itérations maximum pour l'apprentissage. Le modèle appris est ensuite sauvegardé.

  • 2. Evaluation

Le script 'mainEvaluation.m' permet d'évaluer une séquence à partir d'un modèle appris. En plus des paramètres identiques à ceux de l'apprentissage, le paramètre Seuil permet de définir le seuil utilisé pour le calcul des scores en pourcentage. C'est un seuil négatif qui plus il est proche de zéro, plus le calcul du score en pourcentage sera strict.

La première étape est de charger les données (modèle appris, une séquence d'apprentissage, la séquence de test à évaluer). La séquence d'apprentissage est utile pour l'alignement temporel.

%% load data
% model
load modelExo3
 
% data train for temporal alignment
dirTrain='data/Assis3Maxime/';
fnameTrain='SkeletonSequence1.txt';
[oriMatTrain,posMatTrain,dataTrain] = loadData(dirTrain,fnameTrain,filt,est,rem,ws,nbData);
 
% data test
dirTest='data/Assis1Maxime/';
fnameTest='SkeletonSequence3.txt';
[oriMatTest_,posMatTest_,dataTest_] = loadData(dirTest,fnameTest,filt,est,rem,ws,nbData);
dataTest{1}=dataTest_;oriMatTestLong{1}=oriMatTest_;posMatTestLong{1}=posMatTest_;

Ensuite la séquence peut être évaluée:

%% Evaluate sequence
for rep=1:length(dataTest)
    % temporal alignment
    if registration==1
        [dataTestAligned,r,allPoses,poses,motion,distFI] = temporalAlignmentEval(model, dataTrain,dataTest{rep},fastDP);
        posMatTest=posMatTestLong{rep}(:,r);
    else
        dataTestAligned=dataTest{rep};
    end
 
    % compute likelihoods
    [Lglobal,Lbodypart,Ljoints] = computeLikelihoods(model,dataTestAligned);
 
    % get scores
    seuils=[seuil seuil seuil seuil seuil seuil];minseuils=[-500 -500 -500 -500 -500 -500]; %default values
    [Sglobal,Sbodypart,Sjoints] = computeScores(model,Lglobal,Lbodypart,Ljoints,seuils,minseuils);
    scoreLA=[Sbodypart{1}.global.global Sbodypart{1}.global.perSegment];
    scoreRA=[Sbodypart{2}.global.global Sbodypart{2}.global.perSegment];
    scoreCol=[Sbodypart{3}.global.global Sbodypart{3}.global.perSegment];
    % For each score, the first value corresponds to global score for the
    % whole sequence, and then for each temporal segment
end

Le score pour chaque partie du corps se trouve dans scoreLA, scoreRA et scoreCol pour le bras gauche, le bras droit et la colonne respectivement. Chaque vecteur contient d'abord le score calculé pour toute la séquence puis pour chaque segment temporel.

  • 3. Apprentissage des erreurs

Le script 'learnErrorFeatures.m' permet d'apprendre les erreurs possibles pour un exercice. L'idée est d'apprendre à partir d'erreurs simulées la différence entre ces erreurs et le mouvement idéal. Ces différences sont apprise par un modèle SVM (Support Vector Machine) qui permet lors de l'analyse du mouvement de classifier l'erreur détectée comme une erreur déjà apprise.

La première partie du code concerne les paramètres qui sont similaires aux parties décrites plus haut.

Ensuite, la seconde partie permet de charger le modèle d'exercice idéal ainsi qu'un exercice de référence:

% model
load modelExo3
exo=3;
 
% data correct train 
dirTrain='data/Assis3Maxime/';
fnameTrain='SkeletonSequence1.txt';
[oriMatTrain,posMatTrain,dataTrain] = loadData(dirTrain,fnameTrain,filt,est,rem,ws,nbData);
cuts=[0 model.cuts 300];

La troisième partie du code génère les caractéristiques d'erreurs pour chaque fichier d'erreur correspondant à la différence entre le mouvement d'erreur et le mouvement idéal appris. Ces différences sont calculées séparément par partie du corps et par segment temporel de l'exercice.

% data Erreur train
cptErr=0;
for err=[1 2 3]
    cptErr=cptErr+1;
    dirTrain=sprintf('data/ErreursAssis3Maxime/erreur%i/',err);
    for nf=1:nspp
        fnameTrain=sprintf('SkeletonSequence%i.txt',nf-1);
        [oriMatTrainErr{nf},posMatTrainErr{nf},dataTrainErr{nf}] = loadData(dirTrain,fnameTrain,filt,est,rem,ws,nbData);
        [dataTrainErr{nf},r,allPoses,poses,motion,distFI] = temporalAlignmentEval(model, dataTrain,dataTrainErr{nf},fastDP);
    end
    Mtr=[];Mte=[];
    for c=1:length(cuts)-1
        for bp=1:3
            diff1=[];diff2=[];
            for nf=1:nspp
                diff{nf}=[];
            end
            for t=cuts(c)+1:cuts(c+1)
                for i=(bp-1)*3+1:(bp-1)*3+3
                    for nf=1:nspp
                        diff{nf}=[diff{nf} logmap(dataTrainErr{nf}{i}.data(:,t),dataTrain{i}.data(:,t))'];
                    end
                end
                if bp<3
                    for i=(bp-1)*3+10:(bp-1)*3+12
                        for nf=1:nspp
                            diff{nf}=[diff{nf} (dataTrain{i}.data(:,t)-dataTrainErr{nf}{i}.data(:,t))'];
                        end
                    end
                end
            end
            Mtr{c}.bodypart{bp}=[];
            for nf=1:nspp
                Mtr{c}.bodypart{bp}=[Mtr{c}.bodypart{bp}; diff{nf}];
            end
        end
    end
    Train{cptErr}=Mtr;
end

Il y a principalement deux choses à modifier dans cette partie. La première et le nombre de type d'erreurs. Dans l'exemple il y a 3 types d'erreurs [1 2 3]. La seconde variable à modifier est le chemin vers le dossier où sont stockés les exemples d'erreur dirTrain. A l'intérieur de ce dossier, il doit y avoir n dossiers appelés erreur#n#n est remplacé par le numéro d'erreur.

La quatrième partie concerne l'apprentissage du modèle SVM à partir des erreurs:

% Learning SVM Modèle
for c=1:length(Train{1})
    for bp=1:length(Train{1}{c}.bodypart)
        X=[];Y=[];
        for err=1:cptErr
            X=[X;Train{err}{c}.bodypart{bp}];
            Y=[Y;err;err];
        end
        u=unique(Y);
        numClasses=length(u);
        for k=1:numClasses
            G1vAll=(Y==u(k));
            svmModelsSegmented{c}.bodypart{bp}.models{k} = fitcsvm(X,G1vAll,'KernelScale','auto','Standardize',true);
            svmModelsSegmented{c}.exercise=exo;
        end
        G1vAll=ones(size(G1vAll));
        svmModelsSegmented{c}.bodypart{bp}.errorModel = fitcsvm(X,G1vAll,'KernelScale','auto','Standardize',true);
    end
end

Enfin, la dernière partie permet de sauvegarder le modèle SVM (le nom fname peut être modifié) :

% Save
fname=sprintf('errorFeatures/errorFeaturesModelAssis3Maxime');
save(fname,'svmModelsSegmented');
  • poppy-kine/gmmriemannian.txt
  • Last modified: 2020/07/03 17:25
  • by mai