Scanner 3D Kinect

Envoyé par le 14 Juin 2013

Projet réalisé par :
Adrien Moreau
Ayoub Hadfat
Christophe Masoni
Thomas Gaydier

1.Introduction

Depuis quelques années, Télécom Bretagne dispose d’un FabLab appelé Téléfab, lieu de création de divers projets alliant mécanique, électronique et informatique. Le FabLab s’est doté il y a peu de temps d’une imprimante 3D pour imprimer des objets. Il souhaiterait également pouvoir imprimer des objets à partir d’objets existants, en les scannant au préalable. Ainsi notre groupe de projet a entrepris la conception, à partir d’une Kinect, d’un système permettant de reproduire virtuellement et de manière tridimensionnelle la forme d’un objet que l’on désirerait scanner. Plus précisément, l’objectif est de récupérer, à partir d’un objet de dimensions adaptées aux capacités techniques de la Kinect, un nuage de points destiné ensuite à être traité afin de reconstituer une image en 3D visualisable sur un écran.

Ce projet est constitué de 3 parties :

  • Le plateau tournant : permet de faire tourner l’objet.
  • La Kinect : récupère le nuage de point.
  • Un PC : gère le plateau (grâce à Arduino) et traite le nuage de point pour en faire un modèle 3D.

2

Figure 1 : Les différentes parties du système

Chaque partie sera traité individuellement, la mise en relation se fera à la fin.

2.Le plateau tournant

2.1.Objectifs

Dans un premier temps, pour obtenir simplement une image 3D de l’objet de bonne qualité, nous avons décidé de prendre 3 photos de l’objet à 0°, 120°, et 240° (que nous nommerons par la suite « positions-cibles » de l’objet). Le plateau tournant doit donc permettre la rotation de l’objet à scanner pour récupérer les images aux bons angles.

2.2.Le tourne-disque : base du plateau

Nous avons utilisé comme base du plateau tournant un ancien tourne-disque. Cette partie peut bien sûr être réalisé en construisant le plateau tournant.

Nouvelle image bitmapFigure 2 : Photos du tourne-disque en vue de dessus (à gauche) et de dessous (à droite)

            Nous avons dû scier la pointe en métal au centre du plateau tournant qui servait initialement à mettre le disque sur le plateau tournant (voir le résultat sur la photo de gauche).

2.3.Moteur continu

Le tourne-disque fonctionne avec un moteur continu alimenté par 12V. L’arbre moteur entraîne une courroie qui fait tourner le plateau.

Il est aussi possible d’utiliser un moteur pas-à-pas en mettant en place un régulateur pour moteur à l’aide d’un pont en H.

Néanmoins, il faut garder en tête que le plateau tournant devra s’arrêter aux positions-cibles. Ainsi, il faudra prendre en compte l’inertie restante après désalimentation du moteur. Pour le moteur continu utilisé, nous avons l’avons sous alimenté (alimentation à 3.3V) pour limiter le phénomène d’inertie.

2.4.Comment déclencher la prise de photo de la Kinect aux trois « positions-cibles » de l’objet (0°, 120°, 240°) ?

Nous avons opté pour un système «LED+photorésistances».

2.4.1.Principe

On détecte les 3 positions grâce à une LED qui passe devant une photorésistance aux trois positions voulues, entraînant un pic d’intensité. Il suffit alors de demander à la Kinect de déclencher une photo lorsque l’intensité passe au-dessus d’un certain seuil.

Étudions le principe sur un exemple.

Ici l’objectif est de faire clignoter une LED plus ou moins vite selon la luminosité ambiante.

Matériel :

  • des fils de connexion
  • une breadboard (ou plaque d’essai sans soudure)
  • une résistance de 10 kΩ
  • une photo-résistance
  • une LED
  • une carte Arduino

Nouvelle image bitmap

Figure 3 : illustration de l’utilisation d’Arduino par un exemple simple

Le code Arduino :

/*Lit la luminosité etfait clignoter une LED à vitesse variable.*/

// Numéro de la broche du capteur de luminositéconst int luminosityPin = A0;

// Numéro de la broche de la LEDconst int ledPin = 9;
void setup() {// Initialise la communication avec l'ordinateur
Serial.begin(9600);

// Indique que la broche de la LED est une sortie

pinMode(ledPin, OUTPUT);

}

void loop() {

// Lit la valeur du capteur de luminosité

int sensorValue = analogRead(luminosityPin);

// affiche la valeur du capteur dans le "moniteur" sur l'ordinateur

Serial.print(" Valeur du Capteur = " );

Serial.print(sensorValue);

// Convertit la valeur en durée d'attente pour la LED

// il sera nécessaire d'adapter les valeurs du "mapping" en

// fonction de la luminosité ambiante

int waitTime = map(sensorValue, 0, 1023, 20, 1000);

// Envoie le résultat vers l'ordinateur

Serial.print("\t Delai de clignotement (millisecondes) = ");

Serial.println(waitTime);

// Fait clignoter la LED au rythme de la luminosité

digitalWrite(ledPin, HIGH); // allume la LED

delay(waitTime); // attends la durée choisie

digitalWrite(ledPin, LOW); // éteinds la LED

delay(waitTime); // attends la durée choisie

}

La photorésistance (entre les fils noir et vert sur la figure 3) est branchée en parallèle avec une résistance. Elle est alimentée par le 5V de l’Arduino (fil rouge) et est reliée à une entrée analogique de l’Arduino (fil vert). La photorésistance est reliée à un port de sortie. Le code ci-dessus, écrit sur l’interface Arduino permet de faire clignoter la LED en fonction de la luminosité de la photorésistance.
La partie haute du montage (LED) ne nous intéresse pas. Nous ne voulons en effet pas faire clignoter une LED. En revanche nous voulons déclencher la prise de photographies de la Kinect en fonction des valeurs de luminosité des LED. La partie basse du montage sera donc exactement la même pour notre projet, et sera faite trois fois (pour les trois photorésistances).

2.4.2.Choix de la valeur de la résistance en série avec la photorésistance

Le choix de la résistance est important, il détermine les valeurs de la luminosité captée par la photorésistance lorsqu’elle est sous une luminosité ambiante (telle que l’on peut avoir au FabLab), et lorsqu’elle est éclairée par une LED.
L’important est que ces deux valeurs soient bien distinctes pour pouvoir détecter sans ambiguïté lorsque la photorésistance est éclairée par la LED, indépendamment des contraintes de l’environnement (éclairage accidentel…).
Nous avons testé pour différentes valeurs du résistor la valeur de la résistance de la photorésistance lorsque celle-ci est éclairée par la LED ou pas. Nous avons choisi le résistor qui permettait d’avoir le meilleur contraste de résistance, à savoir 100kΩ.

2.4.3.Que mettre sur la partie tournante : LED ou photorésistance ?

Au début, nous avions l’intention de mettre une photorésistance sur la partie tournante et trois LEDs sur la partie fixe (aux trois positions correspondant à 0°, 120° et 240°) (voir figure ci-dessous).

1
Figure 4 : Schéma du montage initialement prévu

La photorésistance doit être reliée à la breadboard puisque c’est grâce aux pics de sa luminosité que l’on déclenchera la prise de photos de la Kinect. En revanche, les LEDs doivent être uniquement alimentées par source de tension.
Or le fait que la photorésistance, donc la partie reliée à la breadboard, soit sur la partie tournante du plateau pose problème. En effet, les fils s’enrouleront autour de l’objet au fur et à mesure que le plateau tournera, ce qui n’est évidemment pas souhaitable. Le problème est le fait passer les fils par « en dessous »

C’est donc pour cela que nous avons plutôt choisi de faire le montage ci-dessous qui résout
ces problèmes. On a inversé LED et photorésistances (qui sont maintenant trois).

1
Figure 5 : Schéma du montage finalement réalisé

Pour éviter les problèmes de fils, il suffit alors d’alimenter la LED avec une pile qui sera fixée avec
celle-ci sur la partie tournante.

2.4.4.Fixation de la LED et réalisation du circuit d’alimentation

Pour gêner le moins possible la prise de photos de la Kinect, nous avons décidé de fixer le circuit en dessous du plateau et de simplement faire ressortir la LED en perçant un trou. Mais sous le plateau il y a peu de place et il faut que le circuit soit assez plat pour ne pas rentrer en collision avec l’arbre du moteur (moins de 8mm environ).
La solution retenue permettant de concilier à la fois la petite taille, et la forme plate, est d’utiliser comme alimentation une pile plate.
Un autre problème s’est posé : on ne peut pas se contenter de souder la pile directement à la LED. Il faut pouvoir changer la pile quand celle-ci est usée.
Nous avons donc utilisé un morceau de connecteur masculin-féminin. Après découpage d’un morceau de plaque et soudure, voici le résultat ci-dessous (Figure 6).

Nouvelle image bitmap

Figure 6 : Circuit d’alimentation de la LED

2.4.5.Réalisation des circuits des photorésistance

Ce qui nous intéresse est de faire la partie du circuit encadrée en jaune trois fois, pour les trois « positions-cibles ».

1

Figure 7 : Montage à réaliser pour relier chaque photorésistance à l’Arduino

 Ce qui donne après un peu de soudure :

Nouvelle image bitmap

Figure 8 : Photos du montage de la figure 7

Les trois photorésistances seront reliées au 5V de l’Arduino, et les trois masses sur le port GND de l’Arduino, comme respectivement les fils rouge et noir sur la figure 15. Les fils « verts » seront reliés à trois entrées analogiques différentes(ANALOG IN sur la figure 15), pour mesurer indépendamment les valeurs des résistances des trois photorésistances.

Les trois circuits sont fixés sur le plateau aux positions-cibles en utilisant de la colle.

2.5.Alimentation du moteur

L’alimentation est faite grâce à un générateur. Le moteur est aussi relié à l’Arduino pour pouvoir l’arrêter aux positions-cibles. Un problème persiste : l’intensité utilisé par le moteur est élevée et risquerait d’endommager l’Arduino pour éviter cela il faut mettre un transistor entre ces deux éléments.

2.6.Comment fixer les objets sur le plateau ?

Cette question se pose puisqu’elle correspond à l’une des contraintes que l’on s’est fixée : l’objet ne doit pas bouger lorsque le plateau tourne.
La meilleure solution retenue pour sa simplicité est d’utiliser de la pâte à modeler que l’on aplatira entre le plateau et l’objet.

2.7.Code et montage final

Voici le code final de l’arduino :

// Numéro de la broche du capteur de luminosité
const int luminosityPin1 = A0;
const int luminosityPin2 = A1;
const int luminosityPin3 = A2;
// Seuil de luminosité
const int seuil1 = 155;
const int seuil2 = 175;
const int seuil3 = 130;
// Numéro de la broche de la LED
const int ledPin = 9;
//numero de la branche transistor
const int traPin = 2;

// Variable gérant l'intervalle de prise de valeurs
long previousMillis = 20;
long interval = 20;

//état du moteur ( 0= arret , 1 = marche )
int state = 1;

boolean depasseSeuil = false;

void setup(){
Serial.begin(9600);
pinMode (traPin, OUTPUT);
digitalWrite(traPin, HIGH);

} 

void loop() {
unsigned long currentMillis = millis();
if(currentMillis - previousMillis > interval) {
previousMillis = currentMillis; 

// Lit la valeur du capteur de luminosité
int sensorValue1 = analogRead(luminosityPin1);
int sensorValue2 = analogRead(luminosityPin2);
int sensorValue3 = analogRead(luminosityPin3);

if (sensorValue1  > seuil1)  depasseSeuil = true; 
else if (sensorValue2 > seuil2)  depasseSeuil = true; 
else if (sensorValue3 > seuil3)  depasseSeuil = true; 

// affiche la valeur du capteur dans le "moniteur" sur l'ordinateur
Serial.print("\n " );
Serial.print(sensorValue1); //envoit la valeur du capteur a Processing
Serial.print(" /  " );
Serial.print(sensorValue2); //envoit la valeur du capteur a Processing
Serial.print(" /  " );
Serial.print(sensorValue3); //envoit la valeur du capteur a Processing

if(depasseSeuil){
state = 0;
digitalWrite(traPin, LOW);//stop le moteur
delay(3000);
digitalWrite(traPin, HIGH);

Serial.println("start");
}
else
{
  state = 1;
digitalWrite(traPin, HIGH);//relance le moteur
}

depasseSeuil=false;
// Vérifie les valeurs envoyés par Processing
//checkSerial(); 

if(state==1){
digitalWrite(traPin, HIGH);
}
}
}

void checkSerial(){ // vérifie les données envoyés par Processing
if (Serial.available() > 1) {
char trigger = Serial.read();
if(trigger == 'S'){
state = 1;
}
}

}

Voici le montage final de la partie plateau tournant :

schéma montage_bb

Figure 9 : Schéma du montage de la partie plateau tournant

3.Kinect

Attention, dans la partie suivante nous avons décidé de travailler sous Ubuntu 12.04 avec un ordinateur ne contenant aucun USB3.0 car la Kinect ne fonctionne pas avec de tels ports USB. Nous avons aussi utilisé Processing 1.5.1, Arduino 1.0.5.

3.1.Présentation de la Kinect

La Kinect, initialement appelée sous le nom de code Project Natal, est un périphérique qui fut développé par PrimeSense puis commercialisé par Microsoft début novembre 2010 comme contrôleur de jeux vidéo. Très vite les programmeurs du monde entier ont réalisé le plein potentiel de l’appareil et en décembre 2010 pour éviter la multiplication des drivers non officiel, PrimeSense à mis à disposition à la communauté des drivers OpenSource. Comme ceux de l’API OpenNI ensuite complété par ceux de l’API NITE augmentant ainsi les capacités de l’appareil.

Notons qu’ils existent deux types de Kinect : Kinect XBOX 360 ( reconnaissable par le XBOX 360 sur sa façade) et Kinect for Windows ( reconnaissable par le XBOX sur sa façade). Nous avons travaillé avec une Kinect XBOX 360. L’autre type de Kinect pouvant rencontrer différents types d’erreur avec les différentes librairies.

La Kinect permet de récupérer plusieurs informations sur son environnement. Elle capte trois types d’images différentes : une image de profondeur, une image RGB et une image infrarouge. On peut la schématiser de la manière suivante :

Nouvelle image bitmap

Figure 10 : Schéma de la Kinect

La résolution de l’image RGB et de l’image IR est de 640×480 pixels. Les informations de profondeur sont récupérées à partir du capteur infrarouge et modélisé par des niveaux de gris allant de 2048 (blanc) à 0 (noir).

La Kinect est dotée de plusieurs pilotes : un pour la camera et un pour le moteur.

Les pilotes ne font pas de post-traitement sur ​​les données, ils nous donnent juste accès au flux de données brutes de la Kinect et nous permettent de contrôler la LED et le moteur.

Par la suite des API OpenSource ont été développés pour récupérer et travailler ces données. Parmi eux on peut en citer 3 et leur associés l’environnement de travail associé : en effet ces différentes API ne fonctionnent pas toujours sous tous les systèmes d’exploitation.

Librairies

Auteur

API

OS

Openkinect

Daniel Shiffman

OpenKinect/Libfreenect

Mac OS X

dLibs

Thomas Diewald

OpenKinect/Libfreenect

Windows

Simple-OpenNI

Max Rheiner

OpenNI/NITE

Mac OS X, Windows, Linux

Nous avons décidé de travailler sous le système d’exploitation Linux car les ordinateurs du fablab ont une puissance limité et tourneront sous Linux.

C’est pourquoi nous nous sommes tournés vers la bibliothèque de Max Rheiner pour développer la partie logicielle de notre scanner. Intéressons-nous de plus près à Simple-OpenNi.

3.2.Présentation de Simple-OpenNI

OpenNi ( Open Natural Interaction ) est une API qui fournit un soutien pour la reconnaissance visuel.

NITE permet de suivre les mouvements d’un squelette.

Simple-OpenNi est un paquet de fonction associé à la plateforme de programmation Processing, utilisant OpenNi et NITE. Toute les fonctions de OpenNi ne sont pas prises en charge mais il est destiné à un usage plus simple de la bibliothèque de fonctions. Il permet l’utilisation de la Kinect avec le langage de programmation Procesing.

3.3.Présentation du langage de programmation utilisé

Nous avons en plus utilisé le langage de programmation Processing pour communiquer et traiter les données entre le PC et la kinect. En effet, nous avons hésité entre deux langages de programmation : Processing et C#. Le C# est un langage qui se rapproche du Java étudié à Telecom Bretagne. Cependant, il existe avec Processing des modèles de fonctions construits à partir de la librairie libfreenect permettant de travailler facilement avec la Kinect. Ce choix nous permettait aussi de nous appuyer sur la documentation apporté par l’ouvrage Arduino and Kinect Projects.

On récupère grâce à des fonctions déjà prédéfinies dans cette librairie, les informations de profondeurs. On peut les représenter ensuite grâce à un code couleur RGB : plus l’objet est proche ( resp. loin ) et plus la couleur sera chaude ( resp. froide ).

Actuellement, la bibliothèque est capable de nous rendre les données disponibles de quatre façons :

  • Une image RVB (image où chaque pixel est défini par une valeur de Bleu, Rouge et Vert) de la caméra Kinect comme une PImage.
  • Échelle de gris prise à partir de l’image de la caméra IR (infrarouge) en tant que PImage
  • L’image en niveaux de gris avec la luminosité de chaque pixel cartographié à la profondeur (plus clair = plus proche).
  • Données de profondeur brutes (nombre de 11 bits entre 0 et 2048) en tant que int [] array

3.4.Fonctions supplémentaires

Cependant pour reconstituer l’objet en 3D nous avons décidé de travailler avec un nuage de point. Nous l’obtenons en ne récupérant qu’un pixel sur 4 permettant ainsi de reconstituer notre nuage de point avec une perte d’information et de qualité minimum. Toute l’information que récupère la Kinect n’est pas non plus essentielle pour reconstituer notre objet. En effet notre objet scanné possède une taille maximale et il existe une distance minimale entre l’objet et la kinect ; on peut donc considérer que toute les données de profondeur en dehors d’un cube prédéfini (de taille 400*400*400 centré par rapport à la kinect) ne nous intéresse pas.

De plus ce scanner sera utilisé pour l’imprimante 3D qui reconstitue l’objet scanné sans se soucier de sa couleur, les informations RGB de l’objet ne sont donc pas nécessaire. Il est donc inutile de récupérer toute les données de la Kinect.

3.5.Installation de Processing sous Linux

Téléchargez l’archive avec la version de Processing qui convient sur le lien : http://www.processing.org/download/

La version de Processing 2.0 étant encore à l’état de béta, ce qui veut dire que certains bug peuvent exister, nous travaillerons avec la version stable : Processing 1.5.1.

Pour un Linux 32 bit : http://processing.googlecode.com/files/processing-1.5.1-linux.tgz

Pour décompresser l’archive dans votre dossier personnel utilisez la commande :

tar -zxvf processing-xxx.tgz

Un dossier sera automatiquement créer : /home/MonDossierPersonnel/processing-1.5.1.

Pour exécutez Processing, placez-vous dans le répertoire suivant :

cd /home/VotreDossierPersonnel/Processing-xxx/

Puis pour autorisez l’exécution de Processing sur l’ordinateur :

chmod +x processing

Maintenant pour exécuter Processing sur l’ordinateur il suffira de se placer dans le dossier /home/MonDossierPersonnel/processing-1.5.1 et d’entrer la commande :

./processing

Processing requiert Java pour fonctionner, si Java n’est pas installé sur l’ordinateur entrez la commande suivante pour l’obtenir :

sudo apt-get install openjdk-6-jdk

3.6.Installation de Simple-OpenNI sous Linux

Suivez les différentes étapes suivantes pour obtenir les pilotes OpenNi et PrimeSense pour pouvoir travailler avec la Kinect sous une distribution Ubuntu (12.04 x32 lors de notre projet). Si l’on travaille avec une architecture 64 bits, il faudra télécharger les dossiers associés

1ère étape :

Vous aurez besoin de plusieurs logiciels ; entrez la commande suivante pour les installer :

sudo apt-get install git-core cmake freeglut3-dev pkgconfig build-essential libxmu-dev libxi-dev libusb-1.0-0-dev doxygen graphviz mono-complete

Ensuite téléchargez la source contenant OpenNi à partir de Github et stockez là dans un répertoire kinect :

mkdir ~/kinect
cd ~/kinect
git clone https://github.com/OpenNI/OpenNI.git

 

Puis executez le script RedistMaker dans le dossier Platform/Linux et installez les « outpu binairies » :

cd OpenNI/Platform/Linux/CreateRedist/
chmod +x RedistMaker
./RedistMaker
cd ../Redist/OpenNI-Bin-Dev-Linux-x32-v1.5.2.23/
sudo ./install.sh

Ensuite copiez le dossier SensorKinect provenant de Github dans le répertoire Kinect :

cd ~/kinect
git clone git://github.com/avin2/SensorKinect.git

Puis executez le script RedistMaker dans le dossier Platform/Linux et installez les « outpu binairies » :

cd SensorKinect/Platform/Linux/CreateRedist/
chmod +x RedistMaker
./RedistMaker
cd ../Redist/Sensor-Bin-Linux-x32-v5.1.0.25/
chmod +x install.sh
sudo ./install.sh

Ensuite télécharger le dossier NITE-Bin-Dev-Linux-x32-v1.5.2.21 et extrayez l’archive dans le répertoire ~/kinect. L’archive peut être obtenue en cliquant sur le lien suivant : https://code.google.com/p/simple-openni/downloads/detail?name=OpenNI_NITE_Installer-Linux32-0.27.zip&can=2&q=

Puis allez dans le répertoire des données qu’il contient :

cd NITE-Bin-Dev-Linux-x32-v1.5.2.21/Data

Pour que Simple-OpenNi fonctionne il faut maintenant modifier la licence. Pour cela modifiez à l’aide d’un éditeur de texte chaque fichiers : Sample-Scene.xml, Sample-Tracking.xml et ample-user.xml.

Changez : <license Vendor= »PrimeSense » key= » »/>

Avec : <license Vendor= »PrimeSense » key= « 0KOIk2JeIBYCIPWVnMoRKn5cdY4=»/>

Ensuite revenez dans le répertoire NITE :

Cd ..

Et exécutez le script d’installation :

sudo ./install.sh

C’est tout en ce qui concerne Simple-OpenNi !

3.7.Code Processing final

Le code processing ayant permis de récupérer et de représenter les données de la Kinect est le suivant. Il est composé de deux fonctions principales : setup() qui permet d’initialiser les paramètres du code et draw() qui est une boucle infinie et permet de récupérer constamment les données envoyés par la Kinect. On définit ensuite les fonctions secondaires ayant été utilisées dans le code principale.

Voici le code final de Processing :

import processing.opengl.*;

import SimpleOpenNI.*;

import kinectOrbit.*;

// Initialisation des paramètres Orbit and simple-openni

KinectOrbit myOrbit;

SimpleOpenNI kinect;

// Initialisation des listes de vecteurs pour le nuage de points et les couleurs associés

ArrayList<PVector> scanPoints = new ArrayList<PVector>(); // Nuage de Points

ArrayList<PVector> objectPoints = new ArrayList<PVector>(); // Nuage de Points

 

// Variable d'espace

float baseHeight = -5; // hauteur de la base du modèle

float modelWidth = 400;

float modelHeight = 400;

//tout objet contenue dans ce cube sera scanné

PVector axis = new PVector(0, baseHeight, 1050);

// Paramètres du scan

int scanLines = 200; // définie la largeur du scan (pour une valeur de 1 : une seule ligne verticale de l'objet est scanné)

int scanRes = 1; // définie le nombre de pixel que nous prenons dans le scan ( 1 est la valeur correspondant à la plus haute résolution)

boolean scanning; // valeur booléenne associer à l'état de numérisation

boolean arrived; // valeur booléenne : sommes nous à une position devant être scanné ?

float[] shotNumber = new float[3]; // nombre de scan que nous comptons effectuer : ici 3

int currentShot = 0; // valaur du nombre de prise de vue déjà prise

public void setup() {

size(800, 600, OPENGL);

// Orbit

myOrbit = new KinectOrbit(this, 0, "kinect");

myOrbit.drawCS(true);

myOrbit.drawGizmo(true);

myOrbit.setCSScale(200);

myOrbit.drawGround(true);

// Simple-openni

kinect = new SimpleOpenNI(this);

kinect.setMirror(false);

kinect.enableDepth();

for (int i = 0; i < shotNumber.length; i++) {

shotNumber[i] = i * (2 * PI) / shotNumber.length;

}

}

public void draw() {

kinect.update(); // Actualise les données de la Kinect

background(0);

myOrbit.pushOrbit(this); // fonction permettant de gérer la perspective

updateObject(scanLines, scanRes); // actualise les positions des données de l'objet

 

if ( scanning) { //scan à la main

scan();

scanning = false;

} // si on doit scanné et que l'on est sur la bonne position alors on scan

stroke(255,255,255);

drawObjects(); // on dessine les points déjà pris auparavant

drawBoundingBox(); // on dessine le cube autour de l'objet scanné

kinect.drawCamFrustum(); // on dessine l'image de la Kinect

myOrbit.popOrbit(this); // arrète la fonction permettant de gérer la perspective

}

void drawPointCloud(int steps) { // Création du nuage de point "grossier" scanné

int index;

PVector realWorldPoint;

stroke(255);

for (int y = 0; y < kinect.depthHeight(); y += steps) {

for (int x = 0; x < kinect.depthWidth(); x += steps) {

index = x + y * kinect.depthWidth();

realWorldPoint = kinect.depthMapRealWorld()[index];

stroke(150);

point(realWorldPoint.x, realWorldPoint.y, realWorldPoint.z);

}

}

}

void drawObjects() { // Dessine les points scanné

pushStyle();

for (int i = 1; i < objectPoints.size(); i++) {

point(objectPoints.get(i).x, objectPoints.get(i).y, objectPoints.get(i).z + axis.z);

}

for (int i = 1; i < scanPoints.size(); i++) {

point(scanPoints.get(i).x, scanPoints.get(i).y, scanPoints.get(i).z + axis.z);

}

popStyle();

}

void drawBoundingBox() { // dessine le cube ou l'objet est scanné

stroke(255, 0, 0);

line(axis.x, axis.y, axis.z, axis.x, axis.y + 100, axis.z);

noFill();

pushMatrix();

translate(axis.x, axis.x + baseHeight + modelHeight / 2, axis.z);

box(modelWidth, modelHeight, modelWidth);

popMatrix();

}

void scan() { // numérise l'objet et ne prend en considération que les nouveaux points à une distance de plus d'1mm des autres

for (PVector v : scanPoints) {

boolean newPoint = true;

for (PVector w : objectPoints) { // on regarde si les pixels de l'objet scanné ne sont pas trop proches de la prise de vue précédente

if (v.dist(w) < 1)

newPoint = false;

}

if (newPoint) {

objectPoints.add(v.get());

}

}

if (currentShot < shotNumber.length-1) {

currentShot++;

}

else {

scanning = false;

}

arrived = false;

}

// Fonctions auxiliaires

void updateObject(int scanWidth, int step) { // Fonction actualisant l'image et les valeurs scannés

int index;

PVector realWorldPoint;

scanPoints.clear();

float angle = shotNumber[currentShot];

pushMatrix();

translate(axis.x, axis.y, axis.z);

rotateY(angle);

line(0, 0, 100, 0);

popMatrix();

int xMin = (int) (kinect.depthWidth() / 2 - scanWidth / 2);

int xMax = (int) (kinect.depthWidth() / 2 + scanWidth / 2);

for (int y = 0; y < kinect.depthHeight(); y += step) {

for (int x = xMin; x < xMax; x += step) {

index = x + (y * kinect.depthWidth());

realWorldPoint = kinect.depthMapRealWorld()[index];

if (realWorldPoint.y < modelHeight + baseHeight && realWorldPoint.y > baseHeight) {

if (abs(realWorldPoint.x - axis.x) < modelWidth / 2) { // Vérification de x

if (realWorldPoint.z < axis.z + modelWidth / 2 && realWorldPoint.z > axis.z -

modelWidth / 2) { // Vérification de z

PVector rotatedPoint;

realWorldPoint.z -= axis.z;

realWorldPoint.x -= axis.x;

rotatedPoint = vecRotY(realWorldPoint, angle);

scanPoints.add(rotatedPoint.get());

}

}

}

}

}

}

PVector vecRotY(PVector vecIn, float phi) { // Fonction permettant de prendre en compte la rotation de l'objet lors de la représentation sur ordinateur

// On considère une rotation autour de l'axe Y

PVector rotatedVec = new PVector();

rotatedVec.x = vecIn.x * cos(phi) - vecIn.z * sin(phi);

rotatedVec.z = vecIn.x * sin(phi) + vecIn.z * cos(phi);

rotatedVec.y = vecIn.y;

return rotatedVec;

}

public void keyPressed() { // Fonction permettant de définir des actions avec les touches du clavier

switch (key) {

case 'r': // 'r' reinitialise le scan

objectPoints.clear();

currentShot = 0;

scanning = false;

break;

case 's': // 's' démarre le scan de l'objet

scanning = true;

arrived = false;

break;

case 'e': // 'e' exporte le nuage de point ( format PLY )

exportPly('0');

break;

case '+': // '+' augmente la résolution du nuage de points ( scanned lines ++ )

scanLines++;

println(scanLines);

break;

case '-': // '-' diminue la résolution du nuage de points ( scanned lines -- )

scanLines--;

println(scanLines);

break;

}

}

void exportPly(char key) { // Fonction permettant d'exporter le nuage de point au format PLY

PrintWriter output;

String viewPointFileName;

viewPointFileName = "myPoints" + key + ".ply";

output = createWriter(dataPath(viewPointFileName));

output.println("ply");

output.println("format ascii 1.0");

output.println("comment This is your Processing ply file");

output.println("element vertex " + (objectPoints.size()-1));

output.println("property float x");

output.println("property float y");

output.println("property float z");

output.println("end_header");

for (int i = 0; i < objectPoints.size() - 1; i++) {

output.println((objectPoints.get(i).x / 1000) + " "

+ (objectPoints.get(i).y / 1000) + " "

+ (objectPoints.get(i).z / 1000) + " ");

}

output.flush(); // On écrit les données dans le fichier

output.close(); // On termine le fichier

}

4.Traitement des données reçues par l’ordinateur pour former le nuage de points

Après avoir récupéré le nuage de points, il faut désormais reconstruire la surface initiale. Pour cela plusieurs algorithmes existent, ainsi que des logiciels « open source » qui permettent de réaliser cette opération.
Pour notre projet nous avons utilisé MeshLab qui est un logiciel de traitement et d’édition de maillage 3D.
MeshLab lit les fichiers .ply, il suffit donc d’ouvrir le fichier précédemment obtenu. Pour certains fichiers, nous avons rencontré une erreur « Unexpected end of file ».Cette erreur survient dans le cas où le nombre de points affichés est différent du nombre de points déclarés dans le fichier .ply. Dans ce cas, il faut suivre les étapes suivantes :

  •  Activer la vue des points (Points view) dans la barre du haut.
  •  Aller au menu Rendu (Render) et choisir Couleur/Par Sommet (Colors/Per Vertex).

Après cette manipulation, le nuage de points s’affichera et sera coloré. (figure 11)

1Figure 11: Nuage de points 3D

On peut cependant observer des points incohérents, qu’il faudra supprimer manuellement avant de lancer le processus de reconstruction de la forme. Ces points sont parfois dus à un mauvais calibrage de la Kinect par rapport à la plate-forme tournante. Pour supprimer ces points, il faut utiliser l’outil Sélectionner Sommets (Select Vertexes) dans la barre du haut. Après avoir sélectionné les points indésirables, il faut cliquer sur Supprimer Points Sélectionnés (Delete Selected Vertices) dans la barre du haut. Il est possible de sélectionner plusieurs points en gardant enfoncée la touche Control du clavier.(figure 12)

2
Figure 12 : Points incohérents du nuage de points 3D

La prochaine étape est le processus de reconstruction. Tout d’abord, il faut générer le vecteur perpendiculaire à la surface voisine de chaque point. Ceci permet à l’algorithme d’avoir les informations sur la topologie de l’objet nécessaires à la reconstruction. Il faut aller au menu Filtres/Jeu de points (Filters/Point Set) et choisir Générer les normales aux jeux de points (Compute normal for point sets). Dans le menu qui apparaît, il faut choisir 16 comme nombre de points voisins. À la fin du processus, pour afficher les normales, il faut activer Afficher les normales aux sommets (Show Vertex Normals) dans le menu Rendu (Render).
Il ne reste plus qu’à lancer le processus de reconstruction en allant dans Filtres/Jeu de points (Filters/Point Set) et en choisissant Reconstruction de Surface : Poisson (Surface Reconstruction : Poisson). Un menu s’affiche alors, contenant plusieurs paramètres qu’il faut renseigner. Ces paramètres influent sur la précision de la reconstruction, les valeurs qu’il faut
rentrer dépendent de l’objet et de la qualité du scan. Il faut donc essayer de trouver les paramètres optimaux pour chaque objet.

Le modèle 3D est généré à la fin du processus. (figure 13)

5
Figure 13 : Modèle 3D

Pour le colorer, il faut aller dans Filtres/Échantillonnage (Filters/Sampling) et choisir Transfert des attributs des points (Vertex Attributes Transfer), choisir comme source le nuage de points et comme cible le maillage Poisson, cocher Transfert de couleur (Transfer Color) puis appliquer. Si le résultat n’est pas satisfaisant, il faut transférer la géométrie aussi dans le menu
précédent. Il suffit donc de cocher géométrie aussi avant d’appuyer sur appliquer. (figure 14)

5

Figure 14 : Modèle 3D coloré

Ensuite il faut lisser le modèle, pour cela il faut aller dans Filtres/Lissage, Carénage, Déformation (Filters/Smoothing, Fairing, Deformation) et appliquer HC Lissage Laplacien (HC Laplacian Smooth) autant de fois qu’il le faut pour avoir une surface suffisamment lisse. (figure 15)

6 Figure 15 : Modéle 3D coloré et lissé

MeshLab permet d’exporter ce modèle dans différents formats pouvant être utilisés par un
logiciel de modélisation 3D, mais aussi par une imprimante 3D.

5.Le système Final

Nouvelle image bitmap

Figure 16:Schéma du système global

Les différentes parties présentées précédemment communiquent entre elles pour réaliser le scan de l’objet. L’ordinateur commande au plateau de tourner. Quand la lumière est détectée par l’une des photorésistances, l’Arduino envoie un message à l’ordinateur. Dès lors l’ordinateur envoie un ordre à la Kinect pour lancer la capture. Le nuage de points résultant est enregistré grâce à Processing. Ce processus est réitéré trois fois pour récupérer trois nuages de points qui sont ensuite traités par Processing pour construire le nuage 3D.
Ce nuage est enfin traité sur Meshlab pour générer le modèle 3D.

5 Commentaires

  1. Bonjour,
    Votre projet m’intersse beaucoup. Peut-on utiliser Kinect avec Windows 7, pour ensuite imprimer un objet avec une imprimante 3D ?
    J’ai essayé « TriScatch3D » (fourni avec l’impreimante 3D de chez Pearl), en prenant 8 à 20 photos, mais je n’ai rien obtenu de valable, par insuffisance de points traités, je pense.
    Merci si vous pouvez me renseigner sur l’utilisation de Kinect.
    Cordialement.

  2. bonjour,
    j’ai essayé d’installer les packets sur mon nunux, mais la compilation ne se fait pas.
    cd SensorKinect/Platform/Linux/CreateRedist/
    chmod +x RedistMaker
    ./RedistMaker
    une idée?
    merci d’avance

  3. bonjour,
    après pas mal de galères, j’ai enfin réussis a faire fonctionner le tout.
    A quelle distance faut il mettre la kinect?
    y aurait il moyen de mettre en place la fonction du moteur de la kinect afin de pouvoir affiner la position de l’image sur l’objet?
    par contre, je n’ai pas le rendu des couleurs comme sur votre exemple.
    mais continuez sur cette optique de projet, ceci est fort impressionnant.

  4. Bonjour tout le monde.
    Après avoir étudier le tuto précédent, il apparait que un petit problème de librairie se pose lors du lancement du code processing;
    un message d’erreur me dit qu’il ne trouve pas la librairie « kinectOrbit ».
    Je ne trouve pas cette librairie, et je me demandait si quelqu’un aurait une réponse a m’apporter?
    Merci d’avance…peace.

  5. Bonjour,

    je trouve tout ceci fort intéressant, simplement je me demande la plus-value apportée par un plateau tournant automatique.

    je trouve que cette option renchérit le prix du dispositif et n’amène pas grand chose.

    Existe-t-il une possibiilté de supprimer cela ?

    Je ne suis pas du tout programmeur et donc incapable de le faire.

    Par contre je pense être en mesure de créer un plateau tournant « manuellement » et de le bloquer sur les angles appropriés.

    Mais quelquechose m’échappe peut-être ?

Laisser une réponse

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *