Table des matières:

Tri robotique de billes : 3 étapes (avec photos)
Tri robotique de billes : 3 étapes (avec photos)

Vidéo: Tri robotique de billes : 3 étapes (avec photos)

Vidéo: Tri robotique de billes : 3 étapes (avec photos)
Vidéo: 🤠 Je VISITE cette FERME FLAMBANT NEUVE - 3 ROBOTS : 150 VACHES LAITIÈRES 🐄 2024, Juillet
Anonim
Image
Image
Tri de billes robotisé
Tri de billes robotisé
Tri de billes robotisé
Tri de billes robotisé
Tri de billes robotisé
Tri de billes robotisé

Dans ce projet, nous allons construire un robot pour trier les perles Perler par couleur.

J'ai toujours voulu construire un robot de tri des couleurs, alors quand ma fille s'est intéressée à la fabrication de perles Perler, j'ai vu cela comme une opportunité parfaite.

Les perles Perler sont utilisées pour créer des projets d'art fusionnés en plaçant de nombreuses perles sur un panneau perforé, puis en les fondant avec un fer à repasser. Vous achetez généralement ces perles dans des packs de couleurs mélangées géantes de 22 000 perles et passez beaucoup de temps à chercher la couleur que vous voulez, alors j'ai pensé que les trier augmenterait l'efficacité artistique.

Je travaille pour Phidgets Inc. J'ai donc utilisé principalement Phidgets pour ce projet - mais cela pourrait être fait en utilisant n'importe quel matériel approprié.

Étape 1: Matériel

Voici ce que j'ai utilisé pour le construire. Je l'ai construit à 100% avec des pièces de phidgets.com et des choses que j'avais dans la maison.

Cartes Phidgets, Moteurs, Matériel

  • HUB0000 - Phidget de hub VINT
  • 1108 - Capteur magnétique
  • 2x STC1001 - Phidget pas à pas 2.5A
  • 2x 3324 - 42STH38 NEMA-17 pas à pas bipolaire sans engrenage
  • 3x 3002 - Câble Phidget 60cm
  • 3403 - Concentrateur 4 ports USB2.0
  • 3031 - Pigtail Femelle 5.5x2.1mm
  • 3029 - Câble torsadé 2 fils 100'
  • 3604 - LED Blanche 10mm (Sac de 10)
  • 3402 - Webcam USB

Autres parties

  • Alimentation 24VDC 2.0A
  • Ferraille de bois et de métal du garage
  • Liens zippés
  • Récipient en plastique avec le fond coupé

Étape 2: Concevoir le robot

Concevoir le robot
Concevoir le robot
Concevoir le robot
Concevoir le robot
Concevoir le robot
Concevoir le robot

Nous devons concevoir quelque chose qui puisse prendre une seule perle de la trémie d'entrée, la placer sous la webcam, puis la déplacer dans le bac approprié.

Ramassage de perles

J'ai décidé de faire la 1ère partie avec 2 morceaux de contreplaqué rond, chacun avec un trou percé au même endroit. La pièce inférieure est fixe et la pièce supérieure est attachée à un moteur pas à pas, qui peut la faire tourner sous une trémie remplie de billes. Lorsque le trou passe sous la trémie, il ramasse un seul cordon. Je peux ensuite le faire pivoter sous la webcam, puis le faire pivoter davantage jusqu'à ce qu'il corresponde au trou de la pièce inférieure, auquel cas il tombe à travers.

Sur cette photo, je teste que le système peut fonctionner. Tout est fixé sauf le morceau de contreplaqué rond supérieur, qui est attaché à un moteur pas à pas hors de vue en dessous. La webcam n'a pas encore été montée. J'utilise simplement le panneau de configuration Phidget pour passer au moteur à ce stade.

Stockage de perles

La prochaine étape consiste à concevoir le système de bacs pour contenir chaque couleur. J'ai décidé d'utiliser un deuxième moteur pas à pas ci-dessous pour soutenir et faire pivoter un conteneur rond avec des compartiments régulièrement espacés. Cela peut être utilisé pour faire pivoter le bon compartiment sous le trou d'où la perle tombera.

Je l'ai construit avec du carton et du ruban adhésif. La chose la plus importante ici est la cohérence - chaque compartiment doit être de la même taille et le tout doit être uniformément pondéré pour qu'il tourne sans sauter.

L'élimination des billes est réalisée au moyen d'un couvercle bien ajusté qui expose un seul compartiment à la fois, de sorte que les billes peuvent être déversées.

Caméra

La webcam est montée sur la plaque supérieure entre la trémie et l'emplacement du trou de la plaque inférieure. Cela permet au système de regarder le cordon avant de le laisser tomber. Une LED est utilisée pour éclairer les perles sous la caméra et la lumière ambiante est bloquée afin de fournir un environnement d'éclairage cohérent. Ceci est très important pour une détection précise des couleurs, car l'éclairage ambiant peut vraiment fausser la couleur perçue.

Détection d'emplacement

Il est important que le système puisse détecter la rotation du séparateur de billes. Ceci est utilisé pour configurer la position initiale lors du démarrage, mais aussi pour détecter si le moteur pas à pas s'est désynchronisé. Dans mon système, une perle se coince parfois lorsqu'elle est ramassée, et le système devait être capable de détecter et de gérer cette situation - en reculant un peu et en essayant de nouveau.

Il existe de nombreuses façons de gérer cela. J'ai décidé d'utiliser un capteur magnétique 1108, avec un aimant intégré dans le bord de la plaque supérieure. Cela me permet de vérifier la position à chaque rotation. Une meilleure solution serait probablement un encodeur sur le moteur pas à pas, mais j'avais un 1108 qui traînait donc je l'ai utilisé.

Terminer le robot

À ce stade, tout a été élaboré et testé. Il est temps de tout monter correctement et de passer à l'écriture de logiciels.

Les 2 moteurs pas à pas sont entraînés par des contrôleurs pas à pas STC1001. Un hub HUB000 - USB VINT est utilisé pour faire fonctionner les contrôleurs pas à pas, ainsi que pour lire le capteur magnétique et piloter la LED. La webcam et le HUB0000 sont tous deux connectés à un petit concentrateur USB. Une queue de cochon 3031 et du fil sont utilisés avec une alimentation 24 V pour alimenter les moteurs.

Étape 3: écrivez le code

Image
Image

C# et Visual Studio 2015 sont utilisés pour ce projet. Téléchargez la source en haut de cette page et suivez - les sections principales sont décrites ci-dessous

Initialisation

Tout d'abord, nous devons créer, ouvrir et initialiser les objets Phidget. Cela se fait dans l'événement de chargement de formulaire et les gestionnaires d'attachement Phidget.

Private void Form1_Load (expéditeur d'objet, EventArgs e) {

/* Initialiser et ouvrir Phidgets */

top. HubPort = 0; top. Attach += Top_Attach; top. Detach += Top_Detach; top. PositionChange += Top_PositionChange; top. Open();

bas. HubPort = 1;

bottom. Attach += Bottom_Attach; bottom. Detach += Bottom_Detach; bottom. PositionChange += Bottom_PositionChange; bas. Ouvrir();

magSensor. HubPort = 2;

magSensor. IsHubPortDevice = true; magSensor. Attach += MagSensor_Attach; magSensor. Detach += MagSensor_Detach; magSensor. SensorChange += MagSensor_SensorChange; magSensor. Open();

led. HubPort = 5;

led. IsHubPortDevice = true; led. Canal = 0; led. Attach += Led_Attach; led. Detach += Led_Detach; led. Ouvert(); }

void privé Led_Attach (expéditeur d'objet, Phidget22. Events. AttachEventArgs e) {

ledAttachedChk. Checked = true; led. État = vrai; ledChk. Checked = vrai; }

private void MagSensor_Attach (expéditeur d'objet, Phidget22. Events. AttachEventArgs e) {

magSensorAttachedChk. Checked = true; magSensor. SensorType = VoltageRatioSensorType. PN_1108; magSensor. DataInterval = 16; }

void privé Bottom_Attach (expéditeur d'objet, Phidget22. Events. AttachEventArgs e) {

bottomAttachedChk. Checked = true; bottom. CurrentLimit = bottomCurrentLimit; bas. Engagé = vrai; bottom. VelocityLimit = bottomVelocityLimit; bottom. Acceleration = bottomAccel; bas. IntervalleDonnées = 100; }

void privé Top_Attach (expéditeur d'objet, Phidget22. Events. AttachEventArgs e) {

topAttachedChk. Checked = true; top. CurrentLimit = topCurrentLimit; top. Engagé = vrai; top. RescaleFactor = -1; top. VelocityLimit = -topVelocityLimit; top. Acceleration = -topAccel; top. DataInterval = 100; }

Nous lisons également toutes les informations de couleur enregistrées lors de l'initialisation, afin qu'une exécution précédente puisse être poursuivie.

Positionnement du moteur

Le code de manipulation des moteurs comprend des fonctions de confort pour déplacer les moteurs. Les moteurs que j'ai utilisés sont de 3 200 pas de 1/16e par tour, j'ai donc créé une constante pour cela.

Pour le moteur supérieur, il y a 3 positions que nous voulons pouvoir envoyer au moteur vers: la webcam, le trou et l'aimant de positionnement. Il existe une fonction pour se rendre à chacun de ces postes:

private void nextMagnet(Boolean wait = false) {

double posn = top. Position % stepsPerRev;

top. TargetPosition += (stepsPerRev - posn);

si (attendre)

while (top. IsMoving) Thread. Sleep(50); }

private void nextCamera(Boolean wait = false) {

double posn = top. Position % stepsPerRev; if (posn < Properties. Settings. Default.cameraOffset) top. TargetPosition += (Properties. Settings. Default.cameraOffset - posn); else top. TargetPosition += ((Properties. Settings. Default.cameraOffset - posn) + stepsPerRev);

si (attendre)

while (top. IsMoving) Thread. Sleep(50); }

private void nextHole(Boolean wait = false) {

double posn = top. Position % stepsPerRev; if (posn < Properties. Settings. Default.holeOffset) top. TargetPosition += (Properties. Settings. Default.holeOffset - posn); else top. TargetPosition += ((Properties. Settings. Default.holeOffset - posn) + stepsPerRev);

si (attendre)

while (top. IsMoving) Thread. Sleep(50); }

Avant de commencer une analyse, la plaque supérieure est alignée à l'aide du capteur magnétique. La fonction alignMotor peut être appelée à tout moment pour aligner la plaque supérieure. Cette fonction fait d'abord rapidement tourner la plaque jusqu'à 1 tour complet jusqu'à ce qu'elle voie des données d'aimant au-dessus d'un seuil. Il recule ensuite un peu et avance à nouveau lentement, capturant les données du capteur au fur et à mesure. Enfin, il définit la position à l'emplacement maximal des données de l'aimant et réinitialise le décalage de position à 0. Ainsi, la position maximale de l'aimant doit toujours être à (top. Position % stepsPerRev)

Thread alignMotorThread;Boolean sawMagnet; double magSensorMax = 0; private void alignMotor() {

//Trouve l'aimant

top. DataInterval = top. MinDataInterval;

sawMagnet = false;

magSensor. SensorChange += magSensorStopMotor; top. VelocityLimit = -1000;

int tryCount = 0;

réessayer:

top. TargetPosition += étapesPerRev;

while (top. IsMoving && !sawMagnet) Thread. Sleep (25);

si (!sawMagnet) {

if (tryCount > 3) { Console. WriteLine("Echec de l'alignement"); top. Engagé = faux; bas. Engagé = faux; test d'exécution = faux; revenir; }

tryCount++;

Console. WriteLine("Sommes-nous bloqués ? Essayer une sauvegarde…"); top. TargetPosition -= 600; while (top. IsMoving) Thread. Sleep(100);

aller essayer à nouveau;

}

top. VelocityLimit = -100;

magData = nouvelle liste>(); magSensor. SensorChange += magSensorCollectPositionData; top. TargetPosition += 300; while (top. IsMoving) Thread. Sleep(100);

magSensor. SensorChange -= magSensorCollectPositionData;

top. VelocityLimit = -topVelocityLimit;

KeyValuePair max = magData[0];

foreach (paire KeyValuePair dans magData) if (pair. Value > max. Value) max = paire;

top. AddPositionOffset(-max. Key);

magSensorMax = valeur max.;

top. TargetPosition = 0;

while (top. IsMoving) Thread. Sleep(100);

Console. WriteLine("Alignement réussi");

}

Liste> magData;

private void magSensorCollectPositionData(expéditeur d'objet, Phidget22. Events. VoltageRatioInputSensorChangeEventArgs e) { magData. Add(new KeyValuePair(top. Position, e. SensorValue)); }

privé void magSensorStopMotor (expéditeur d'objet, Phidget22. Events. VoltageRatioInputSensorChangeEventArgs e) {

if (top. IsMoving && e. SensorValue > 5) { top. TargetPosition = top. Position - 300; magSensor. SensorChange -= magSensorStopMotor; sawMagnet = vrai; } }

Enfin, le moteur inférieur est commandé en l'envoyant à l'une des positions du conteneur de billes. Pour ce projet, nous avons 19 postes. L'algorithme choisit le chemin le plus court et tourne dans le sens des aiguilles d'une montre ou dans le sens inverse.

private int BottomPosition { get { int posn = (int)bottom. Position % stepsPerRev; if (posn < 0) posn += stepPerRev;

return (int)Math. Round(((posn * beadCompartments) / (double)stepsPerRev));

} }

private void SetBottomPosition(int posn, bool wait = false) {

posn = posn % beadCompartments; double targetPosn = (posn * stepPerRev) / beadCompartments;

double CurrentPosn = bottom. Position % stepsPerRev;

double posnDiff = targetPosn - currentPosn;

// Gardez-le en tant qu'étapes complètes

posnDiff = ((int)(posnDiff / 16)) * 16;

if (posnDiff <= 1600) bottom. TargetPosition += posnDiff; else bottom. TargetPosition -= (stepsPerRev - posnDiff);

si (attendre)

while (bottom. IsMoving) Thread. Sleep(50); }

Caméra

OpenCV est utilisé pour lire les images de la webcam. Le fil de caméra est lancé avant de démarrer le fil de tri principal. Ce fil lit continuellement les images, calcule une couleur moyenne pour une région spécifique à l'aide de Mean et met à jour une variable de couleur globale. Le fil utilise également des HoughCircles pour essayer de détecter soit une perle, soit le trou dans la plaque supérieure, afin d'affiner la zone qu'il regarde pour la détection de la couleur. Le seuil et les nombres HoughCircles ont été déterminés par essais et erreurs et dépendent fortement de la webcam, de l'éclairage et de l'espacement.

bool runVideo = true;bool videoRunning = false; Capture vidéo; Fil cvThread; Couleur détectéeCouleur; Détection booléenne = faux; int detectCnt = 0;

vide privé cvThreadFunction() {

videoRunning = false;

capture = new VideoCapture(selectedCamera);

en utilisant (Fenêtre fenêtre = nouvelle fenêtre("capture")) {

Image du tapis = nouveau tapis(); Mat image2 = nouveau Mat(); while (runVideo) { capture. Read(image); if (image. Empty()) pause;

si (détecter)

détecterCnt++; sinon detectCnt = 0;

if (détection || circleDetectChecked || showDetectionImgChecked) {

Cv2. CvtColor(image, image2, ColorConversionCodes. BGR2GRAY); Mat thres = image2. Threshold((double)Properties. Settings. Default.videoThresh, 255, ThresholdTypes. Binary); thres = thres. GaussianBlur(new OpenCvSharp. Size(9, 9), 10);

if (showDetectionImgChecked)

image = trois;

if (détection || circleDetectChecked) {

CercleSegment perle = thres. HoughCircles(HoughMethods. Gradient, 2, /*thres. Rows/4*/ 20, 200, 100, 20, 65); if (bead. Length >= 1) { image. Circle(bead[0]. Center, 3, new Scalar(0, 100, 0), -1); image. Circle(bead[0]. Center, (int)bead[0]. Radius, new Scalar(0, 0, 255), 3); if (bead[0]. Radius >= 55) { Properties. Settings. Default.x = (décimal)bead[0]. Center. X + (décimal)(bead[0]. Radius / 2); Properties. Settings. Default.y = (décimal)bead[0]. Center. Y - (décimal)(bead[0]. Radius / 2); } else { Properties. Settings. Default.x = (décimal)bead[0]. Center. X + (décimal)(bead[0]. Radius); Properties. Settings. Default.y = (décimal)bead[0]. Center. Y - (décimal)(bead[0]. Radius); } Properties. Settings. Default.size = 15; Properties. Settings. Default.height = 15; } autre {

CircleSegment circles = thres. HoughCircles(HoughMethods. Gradient, 2, /*thres. Rows/4*/ 5, 200, 100, 60, 180);

if (circles. Length > 1) { List xs = circles. Select(c => c. Center. X). ToList(); xs. Trier(); Liste ys = circles. Select(c => c. Center. Y). ToList(); ys. Sort();

int medianX = (int)xs[xs. Count / 2];

int medianY = (int)ys[ys. Count / 2];

if (medianX > image. Width - 15)

médianX = image. Largeur - 15; if (medianY > image. Height - 15) medianY = image. Height - 15;

image. Circle(medianX, medianY, 100, new Scalar(0, 0, 150), 3);

si (détection) {

Properties. Settings. Default.x = medianX - 7; Properties. Settings. Default.y = medianY - 7; Properties. Settings. Default.size = 15; Properties. Settings. Default.height = 15; } } } } }

Rect r = new Rect((int)Properties. Settings. Default.x, (int)Properties. Settings. Default.y, (int)Properties. Settings. Default.size, (int)Properties. Settings. Default.height);

Mat beadSample = new Mat(image, r);

Scalaire avgColor = Cv2. Mean(beadSample); détectéColor = Color. FromArgb((int)avgColor[2], (int)avgColor[1], (int)avgColor[0]);

image. Rectangle(r, nouveau scalaire(0, 150, 0));

window. ShowImage(image);

Cv2. WaitKey(1); videoRunning = vrai; }

videoRunning = false;

} }

private void cameraStartBtn_Click (expéditeur d'objet, EventArgs e) {

if (cameraStartBtn. Text == "start") {

cvThread = new Thread(nouveau ThreadStart(cvThreadFunction)); runVideo = vrai; cvThread. Start(); cameraStartBtn. Text = "stop"; while (!videoRunning) Thread. Sleep(100);

updateColorTimer. Start();

} autre {

runVideo = false; cvThread. Join(); cameraStartBtn. Text = "start"; } }

Couleur

Maintenant, nous sommes en mesure de déterminer la couleur d'une perle et de décider en fonction de cette couleur dans quel conteneur la déposer.

Cette étape repose sur la comparaison des couleurs. Nous voulons pouvoir distinguer les couleurs pour limiter les faux positifs, mais aussi permettre un seuil suffisant pour limiter les faux négatifs. La comparaison des couleurs est en fait étonnamment complexe, car la façon dont les ordinateurs stockent les couleurs en RVB et la façon dont les humains perçoivent les couleurs ne sont pas corrélées de manière linéaire. Pour aggraver les choses, la couleur de la lumière sous laquelle une couleur est vue doit également être prise en considération.

Il existe un algorithme compliqué pour calculer la différence de couleur. Nous utilisons CIE2000, qui génère un nombre proche de 1 si 2 couleurs ne peuvent être distinguées pour un humain. Nous utilisons la bibliothèque ColorMine C# pour effectuer ces calculs compliqués. Une valeur DeltaE de 5 s'est avérée offrir un bon compromis entre les faux positifs et les faux négatifs.

Comme il y a souvent plus de couleurs que de conteneurs, la dernière position est réservée comme bac fourre-tout. Je les mets généralement de côté pour qu'ils passent à travers la machine lors d'un deuxième passage.

Lister

couleurs = nouvelle liste (); liste colorPanels = nouvelle liste (); Liste des couleursTxts = new List(); Liste colorCnts = new List();

const int numColorSpots = 18;

const int unknownColorIndex = 18; int findColorPosition(Couleur c) {

Console. WriteLine("Rechercher la couleur…");

var cRGB = nouveau Rgb();

cRGB. R = c. R; cRGB. G = c. G; cRGB. B = c. B;

int bestMatch = -1;

double correspondanceDelta = 100;

for (int i = 0; i <couleurs. Compte; i++) {

var RVB = nouveau Rgb();

RVB. R = couleurs. R; RGB. G = couleurs. G; RVB. B = couleurs. B;

double delta = cRGB. Comparer(RGB, nouveau CieDe2000Comparison());

//double delta = deltaE(c, couleurs); Console. WriteLine("DeltaE (" + i. ToString() + "): " + delta. ToString()); if (delta < matchDelta) { matchDelta = delta; meilleure correspondance = i; } }

if (matchDelta < 5) { Console. WriteLine("Found! (Posn: " + bestMatch + " Delta: " + matchDelta + ")"); renvoie bestMatch; }

if (colors. Count < numColorSpots) { Console. WriteLine("Nouvelle couleur!"); couleurs. Ajouter(c); this. BeginInvoke(new Action(setBackColor), new object { colours. Count - 1 }); writeOutColors(); return (couleurs. Compte - 1); } else { Console. WriteLine("Couleur inconnue !"); return unknownColorIndex; } }

Logique de tri

La fonction de tri rassemble toutes les pièces pour trier réellement les perles. Cette fonction s'exécute dans un thread dédié; déplacer la plaque supérieure, détecter la couleur des billes, la placer dans un bac, s'assurer que la plaque supérieure reste alignée, compter les billes, etc. Il s'arrête également de fonctionner lorsque le bac collecteur est plein - Sinon, nous nous retrouvons avec des billes qui débordent.

Thread colorTestThread;Boolean runtest = false; void colorTest() {

si (!top. Engagé)

top. Engagé = vrai;

si (!bas. Engagé)

bas. Engagé = vrai;

tandis que (test d'exécution) {

nextMagnet(true);

Thread. Sleep(100); try { if (magSensor. SensorValue < (magSensorMax - 4)) alignMotor(); } catch { alignMotor(); }

nextCamera(true);

détection = vrai;

while (detectCnt < 5) Thread. Sleep (25); Console. WriteLine("Nombre de détection: " + detectCnt); détection = faux;

Couleur c = Couleur détectée;

this. BeginInvoke(nouvelle action (setColorDet), nouvel objet { c }); int i = findColorPosition(c);

SetBottomPosition(i, vrai);

nextHole(true); colorCnts++; this. BeginInvoke(new Action(setColorTxt), nouvel objet { i }); Thread. Sommeil(250);

if (colorCnts[unknownColorIndex] > 500) {

top. Engagé = faux; bas. Engagé = faux; test d'exécution = faux; this. BeginInvoke(new Action(setGoGreen), null); revenir; } } }

private void colorTestBtn_Click (expéditeur d'objet, EventArgs e) {

if (colorTestThread == null || !colourTestThread. IsAlive) { colourTestThread = new Thread(new ThreadStart(colorTest)); test d'exécution = vrai; ColorTestThread. Start(); colorTestBtn. Text = "STOP"; colorTestBtn. BackColor = Color. Red; } else { runtest = false; colorTestBtn. Text = "GO"; colorTestBtn. BackColor = Color. Green; } }

À ce stade, nous avons un programme de travail. Certains morceaux de code ont été omis de l'article, alors jetez un œil à la source pour l'exécuter réellement.

Concours d'optique
Concours d'optique

Deuxième prix du concours d'optique

Conseillé: