Taula de continguts:

Classificació robòtica de comptes: 3 passos (amb imatges)
Classificació robòtica de comptes: 3 passos (amb imatges)

Vídeo: Classificació robòtica de comptes: 3 passos (amb imatges)

Vídeo: Classificació robòtica de comptes: 3 passos (amb imatges)
Vídeo: Основные ошибки при возведении перегородок из газобетона #5 2024, Desembre
Anonim
Image
Image
Classificació robòtica de comptes
Classificació robòtica de comptes
Classificació robòtica de comptes
Classificació robòtica de comptes
Classificació robòtica de comptes
Classificació robòtica de comptes

En aquest projecte, construirem un robot per ordenar les perles Perler per color.

Sempre he volgut construir un robot de classificació del color, de manera que quan la meva filla es va interessar per l’elaboració de perles de perles, vaig veure això com una oportunitat perfecta.

Les perles Perler s’utilitzen per crear projectes d’art fusionats col·locant moltes perles en un tauler de clavilles i després fondre-les juntes amb una planxa. Generalment, compreu aquestes perles en paquets de colors mixts de 22.000 perles gegants i dediqueu molt de temps a cercar el color que desitgeu, de manera que vaig pensar que ordenar-los augmentaria l’eficiència artística.

Treballo per a Phidgets Inc., de manera que he utilitzat sobretot Phidgets per a aquest projecte, però es podria fer amb qualsevol maquinari adequat.

Pas 1: maquinari

Això és el que utilitzava per construir això. El vaig construir al 100% amb peces de phidgets.com i coses que tenia a casa.

Taulers Phidgets, motors, maquinari

  • HUB0000 - Phidget del centre VINT
  • 1108 - Sensor magnètic
  • 2x STC1001 - Phidget Stepper de 2,5 A
  • 2x 3324 - 42STH38 NEMA-17 Bipolar Gearless Stepper
  • 3x 3002 - Cable Phidget de 60 cm
  • 3403 - Hub de 4 ports USB2.0
  • 3031 - Pigtail femella 5,5x2,1mm
  • 3029 - Cable torçat de 100 fils de 2 fils
  • 3604 - LED blanc de 10 mm (bossa de 10)
  • 3402 - Càmera web USB

Altres parts

  • Alimentació 24VDC 2.0A
  • Trencar fusta i metall del garatge
  • Corbates amb cremallera
  • Recipient de plàstic amb el fons tallat

Pas 2: dissenyeu el robot

Dissenya el robot
Dissenya el robot
Dissenya el robot
Dissenya el robot
Dissenya el robot
Dissenya el robot

Hem de dissenyar alguna cosa que pugui treure una sola perla de la tremuja d’entrada, col·locar-la sota la càmera web i, a continuació, traslladar-la a la paperera adequada.

Recollida de comptes

Vaig decidir fer la primera part amb 2 peces de fusta contraxapada rodona, cadascuna amb un forat perforat al mateix lloc. La peça inferior està fixa i la peça superior s’uneix a un motor pas a pas, que la pot girar sota una tremuja plena de perles. Quan el forat es desplaça per sota de la tremuja, recull un únic cordó. A continuació, puc girar-lo sota la càmera web i després girar fins que coincideixi amb el forat de la peça inferior, moment en què cau.

En aquesta imatge, estic provant que el sistema funcioni. Tot està fixat, excepte la peça rodona superior de fusta contraxapada, que s’adjunta a un motor pas a pas fora de la vista per sota. La càmera web encara no s'ha muntat. Només faig servir el tauler de control de Phidget per passar al motor en aquest moment.

Emmagatzematge de comptes

La següent part és dissenyar el sistema de contenidors per contenir cada color. Vaig decidir utilitzar un segon motor pas a pas per recolzar i girar un contenidor rodó amb compartiments separats uniformement. Es pot utilitzar per fer girar el compartiment correcte sota el forat del qual sortirà el cordó.

Vaig construir-ho amb cartró i cinta adhesiva. El més important aquí és la consistència: cada compartiment ha de tenir la mateixa mida i s’ha de ponderar de manera uniforme perquè giri sense saltar-se.

L'eliminació de les perles s'aconsegueix mitjançant una tapa ajustada que exposa un compartiment a la vegada, de manera que es poden abocar les perles.

Càmera

La càmera web està muntada sobre la placa superior entre la tremuja i la ubicació inferior del forat de la placa. Això permet al sistema mirar el cordó abans de deixar-lo caure. Es fa servir un LED per il·luminar les perles sota la càmera i es bloqueja la llum ambiental per proporcionar un entorn d’il·luminació consistent. Això és molt important per a una detecció precisa del color, ja que la il·luminació ambiental realment pot fer desaparèixer el color percebut.

Detecció d'ubicacions

És important que el sistema pugui detectar la rotació del separador de cordons. S’utilitza per configurar la posició inicial en arrencar, però també per detectar si el motor pas a pas s’ha sincronitzat. Al meu sistema, de vegades es produeix un embussament quan es recull i el sistema necessitava per poder detectar i gestionar aquesta situació, fent una còpia de seguretat una mica i provant de nou.

Hi ha moltes maneres de fer-ho. Vaig decidir utilitzar un sensor magnètic 1108, amb un imant incrustat a la vora de la placa superior. Això em permet verificar la posició en cada rotació. Una millor solució probablement seria un codificador al motor pas a pas, però tenia un 1108 estirat al voltant, així que ho vaig fer servir.

Acaba el robot

En aquest punt, tot s’ha resolt i provat. És hora de muntar-ho tot bé i passar al programari d’escriptura.

Els controladors pas a pas STC1001 condueixen els 2 motors pas a pas. Un concentrador HUB000 - USB VINT s’utilitza per executar els controladors pas a pas, així com per llegir el sensor magnètic i accionar el LED. La càmera web i el HUB0000 estan connectats a un petit concentrador USB. S’utilitza una cua 3031 i algun filferro juntament amb una font d’alimentació de 24V per alimentar els motors.

Pas 3: escriure codi

Image
Image

C # i Visual Studio 2015 s’utilitzen per a aquest projecte. Baixeu-vos la font a la part superior d’aquesta pàgina i seguiu-la; a continuació es detallen les seccions principals

Inicialització

En primer lloc, hem de crear, obrir i inicialitzar els objectes Phidget. Això es fa a l’esdeveniment de càrrega del formulari i als controladors d’adjunts de Phidget.

private void Form1_Load (remitent d'objectes, EventArgs e) {

/ * Inicialitzar i obrir Phidgets * /

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

bottom. HubPort = 1;

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

magSensor. HubPort = 2;

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

led. HubPort = 5;

led. IsHubPortDevice = cert; led. Channel = 0; led. Attach + = Led_Attach; led. Detach + = Led_Detach; led. Open (); }

private void Led_Attach (remitent d'objectes, Phidget22. Events. AttachEventArgs e) {

ledAttachedChk. Checked = cert; led. State = true; ledChk. Checked = cert; }

private void MagSensor_Attach (remitent d'objectes, Phidget22. Events. AttachEventArgs e) {

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

private void Bottom_Attach (remitent d'objectes, Phidget22. Events. AttachEventArgs e) {

bottomAttachedChk. Checked = cert; bottom. CurrentLimit = bottomCurrentLimit; bottom. Engaged = cert; bottom. VelocityLimit = bottomVelocityLimit; bottom. Acceleration = bottomAccel; bottom. DataInterval = 100; }

private void Top_Attach (remitent d'objectes, Phidget22. Events. AttachEventArgs e) {

topAttachedChk. Checked = cert; top. CurrentLimit = topCurrentLimit; top. Engaged = true; top. RescaleFactor = -1; top. VelocityLimit = -topVelocityLimit; top. Acceleration = -topAccel; top. DataInterval = 100; }

També llegim qualsevol informació de color desada durant la inicialització, de manera que es pot continuar amb una execució anterior.

Posicionament del motor

El codi de manipulació del motor consta de funcions de comoditat per moure els motors. Els motors que he utilitzat són 3, 200 1/16 passos per revolució, de manera que he creat una constant per a això.

Per al motor superior, hi ha 3 posicions que volem poder enviar al motor: la càmera web, el forat i l’imant de posicionament. Hi ha una funció per viatjar a cadascuna d'aquestes posicions:

private void nextMagnet (espera booleana = falsa) {

posn doble = top. Position% stepsPerRev;

top. TargetPosition + = (stepsPerRev - posn);

si (espera)

mentre que (top. IsMoving) Thread. Sleep (50); }

private void nextCamera (espera booleana = falsa) {

posn doble = 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 (espera)

mentre que (top. IsMoving) Thread. Sleep (50); }

private void nextHole (espera booleana = falsa) {

posn doble = 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 (espera)

mentre que (top. IsMoving) Thread. Sleep (50); }

Abans de començar una carrera, la placa superior s’alinea mitjançant el sensor magnètic. La funció alignMotor es pot cridar en qualsevol moment per alinear la placa superior. Aquesta funció primer fa girar ràpidament la placa fins a 1 volta completa fins que veu les dades de l’imant per sobre d’un llindar. A continuació, fa una còpia de seguretat una mica i avança de nou lentament, capturant les dades del sensor a mesura que avança. Finalment, estableix la posició a la ubicació màxima de les dades de l’imant i restableix la compensació de la posició a 0. Per tant, la posició màxima de l’imant sempre ha d’estar a (superior. Posició% stepsPerRev)

Fil alignMotorThread; Boolean sawMagnet; doble magSensorMax = 0; private void alignMotor () {

// Troba l’imant

top. DataInterval = top. MinDataInterval;

sawMagnet = fals;

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

int TryCount = 0;

torna a provar:

top. TargetPosition + = stepsPerRev;

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

if (! sawMagnet) {

if (tryCount> 3) {Console. WriteLine ("Alinear fallat"); top. Engaged = fals; bottom. Engaged = fals; runtest = fals; tornar; }

tryCount ++;

Console. WriteLine ("Estem atrapats? Provem una còpia de seguretat …"); top. TargetPosition - = 600; mentre que (top. IsMoving) Thread. Sleep (100);

tornar a provar;

}

top. VelocityLimit = -100;

magData = nova llista> (); magSensor. SensorChange + = magSensorCollectPositionData; top. TargetPosition + = 300; mentre (top. IsMoving) Thread. Sleep (100);

magSensor. SensorChange - = magSensorCollectPositionData;

top. VelocityLimit = -topVelocityLimit;

KeyValuePair max = magData [0];

foreach (parell KeyValuePair a magData) if (pair. Value> Valor màx.) max = pair;

top. AddPositionOffset (-max. Key);

magSensorMax = Valor màxim;

top. TargetPosition = 0;

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

Console. WriteLine ("Alinear correctament");

}

Llista> magData;

private void magSensorCollectPositionData (remitent d'objectes, Phidget22. Events. VoltageRatioInputSensorChangeEventArgs e) {magData. Add (new KeyValuePair (top. Position, e. SensorValue)); }

private void magSensorStopMotor (remitent d'objectes, Phidget22. Events. VoltageRatioInputSensorChangeEventArgs e) {

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

Finalment, es controla el motor inferior enviant-lo a una de les posicions del contenidor de comptes. Per a aquest projecte, tenim 19 posicions. L’algorisme tria un camí més curt i gira en sentit horari o antihorari.

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

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

} }

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

posn = posn% beadCompartments; doble targetPosn = (posn * stepsPerRev) / beadCompartments;

doble currentPosn = bottom. Position% stepsPerRev;

posnDiff doble = targetPosn - currentPosn;

// Mantingueu-lo com a passos complets

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

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

si (espera)

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

Càmera

OpenCV s’utilitza per llegir imatges de la càmera web. El fil de la càmera s’inicia abans d’iniciar el fil d’ordenació principal. Aquest fil llegeix contínuament imatges, calcula un color mitjà per a una regió específica mitjançant Mean i actualitza una variable de color global. El fil també fa servir HoughCircles per intentar detectar un cordó o el forat de la placa superior, per refinar la zona que està mirant per a la detecció del color. El llindar i els números de HoughCircles es van determinar mitjançant proves i errors i depenen en gran mesura de la càmera web, la il·luminació i l’espai.

bool runVideo = true; bool videoRunning = false; Captura de VideoCapture; Fil cvThread; Color detectatColor; Detecció booleana = fals; int detectCnt = 0;

private void cvThreadFunction () {

videoRunning = fals;

capture = new VideoCapture (selectedCamera);

using (Window window = finestra nova ("capture")) {

Imatge mat = mat nova (); Mat imatge2 = nova Mat (); while (runVideo) {capture. Read (imatge); if (image. Empty ()) break;

si (detectant)

detectCnt ++; else detectCnt = 0;

if (detectant || circleDetectChecked || showDetectionImgChecked) {

Cv2. CvtColor (imatge, imatge2, ColorConversionCodes. BGR2GRAY); Mat Thir = image2. Threshold ((doble) Properties. Settings. Default.videoThresh, 255, ThresholdTypes. Binary); thris = tris. GaussianBlur (nou OpenCvSharp. Size (9, 9), 10);

if (showDetectionImgChecked)

imatge = batre;

if (detectant || circleDetectChecked) {

CircleSegment bead = thir. HoughCircles (HoughMethods. Gradient, 2, /*thres. Rows/4*/ 20, 200, 100, 20, 65); if (bead. Length> = 1) {image. Circle (bead [0]. Center, 3, nova escala (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 = (decimal) bead [0]. Center. X + (decimal) (bead [0]. Radius / 2); Properties. Settings. Default.y = (decimal) bead [0]. Center. Y - (decimal) (bead [0]. Radius / 2); } else {Properties. Settings. Default.x = (decimal) bead [0]. Center. X + (decimal) (bead [0]. Radius); Properties. Settings. Default.y = (decimal) bead [0]. Center. Y - (decimal) (bead [0]. Radius); } Properties. Settings. Default.size = 15; Properties. Settings. Default.height = 15; } més {

CircleSegment circles = thir. 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. Sort (); Llista ys = cercles. Selecciona (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)

medianX = image. Width - 15; if (medianaY> imatge. Alçada - 15) medianaY = imatge. Alçada - 15;

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

si (detectant) {

Properties. Settings. Default.x = medianaX - 7; Properties. Settings. Default.y = mitjanaY - 7; Properties. Settings. Default.size = 15; Properties. Settings. Default.height = 15; }}}}}

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

Mat beadSample = nova mat (imatge, r);

Scalar avgColor = Cv2. Mean (beadSample); detectColor = Color. FromArgb ((int) avgColor [2], (int) avgColor [1], (int) avgColor [0]);

image. Rectangle (r, nou Scalar (0, 150, 0));

window. ShowImage (imatge);

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

videoRunning = fals;

} }

private void cameraStartBtn_Click (remitent d'objectes, EventArgs e) {

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

cvThread = nou fil (nou ThreadStart (cvThreadFunction)); runVideo = cert; cvThread. Start (); cameraStartBtn. Text = "aturar"; mentre (! videoRunning) Thread. Sleep (100);

updateColorTimer. Start ();

} més {

runVideo = fals; cvThread. Join (); cameraStartBtn. Text = "inici"; }}

Color

Ara podem determinar el color d’un cordó i decidir en funció d’aquest color a quin contenidor voleu deixar-lo caure.

Aquest pas es basa en la comparació de colors. Volem poder distingir els colors per limitar els falsos positius, però també permetre un llindar suficient per limitar els falsos negatius. La comparació dels colors és realment sorprenentment complexa, perquè la manera com els ordinadors emmagatzemen els colors com a RGB i la manera com els humans perceben els colors no es correlacionen linealment. Per empitjorar les coses, també s’ha de tenir en compte el color de la llum on es veu un color.

Hi ha un algorisme complicat per calcular la diferència de color. Utilitzem CIE2000, que genera un nombre proper a 1 si 2 colors serien indistingibles per a un ésser humà. Estem utilitzant la biblioteca ColorMine C # per fer aquests càlculs complicats. S'ha trobat un valor DeltaE de 5 que ofereix un bon compromís entre falsos positius i falsos negatius.

Com que sovint hi ha més colors que contenidors, l'última posició es reserva com a paperera catchall. En general, els deixo de banda perquè funcionin amb la màquina en una segona passada.

Llista

colors = new List (); List colorPanels = new List (); Colors de la llista Textos = nova Llista (); Llista colorCnts = nova Llista ();

const int numColorSpots = 18;

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

Console. WriteLine ("Cercar color …");

var cRGB = Rgb nou ();

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

int bestMatch = -1;

partit dobleDelta = 100;

per a (int i = 0; i <colors. Count; i ++) {

var RGB = Rgb nou ();

RGB. R = colors . R; RGB. G = colors . G; RGB. B = colors . B;

double delta = cRGB. Compare (RGB, new CieDe2000Comparison ());

// doble delta = deltaE (c, colors ); Console. WriteLine ("DeltaE (" + i. ToString () + "):" + delta. ToString ()); if (delta <matchDelta) {matchDelta = delta; bestMatch = i; }}

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

if (colors. Count <numColorSpots) {Console. WriteLine ("Nou color!"); colors. Afegeix (c); this. BeginInvoke (nova Acció (setBackColor), nou objecte {colors. Count - 1}); writeOutColors (); retorn (colors. Count - 1); } else {Console. WriteLine ("Color desconegut!"); torna unknownColorIndex; }}

Ordenació de la lògica

La funció d’ordenació reuneix totes les peces per ordenar els comptes. Aquesta funció s'executa en un fil dedicat; moure la placa superior, detectar el color de la perla, col·locar-la en un contenidor, assegurar-se que la placa superior es manté alineada, comptar les perles, etc. També deixa d’executar-se quan la paperera es fa plena. Si no, acabem amb comptes desbordants.

Fil colorTestThread; Runtest booleà = fals; void colorTest () {

if (! top. Engaged)

top. Engaged = true;

if (! bottom. Engaged)

bottom. Engaged = cert;

while (runtest) {

nextMagnet (cert);

Fil. Dorm (100); proveu {if (magSensor. SensorValue <(magSensorMax - 4)) alignMotor (); } catch {alignMotor (); }

nextCamera (cert);

detectar = cert;

while (detectCnt <5) Thread. Sleep (25); Console. WriteLine ("Detecta el recompte:" + detectCnt); detectar = fals;

Color c = Color detectat;

this. BeginInvoke (nova Acció (setColorDet), nou objecte {c}); int i = findColorPosition (c);

SetBottomPosition (i, cert);

nextHole (cert); colorCnts ++; this. BeginInvoke (nova Acció (setColorTxt), nou objecte {i}); Fil. Dorm (250);

if (colorCnts [unknownColorIndex]> 500) {

top. Engaged = fals; bottom. Engaged = fals; runtest = fals; this. BeginInvoke (nova acció (setGoGreen), nul·la); tornar; }}}

private void colourTestBtn_Click (remitent d'objectes, EventArgs e) {

if (colourTestThread == null ||! colourTestThread. IsAlive) {colourTestThread = nou fil (nou ThreadStart (colorTest)); runtest = cert; colorTestThread. Start (); colourTestBtn. Text = "PARAR"; colourTestBtn. BackColor = Color. Red; } else {runtest = false; colourTestBtn. Text = "VES"; colourTestBtn. BackColor = Color. Green; }}

En aquest moment, tenim un programa de treball. Alguns fragments de codi van quedar fora de l'article, així que mireu la font per executar-lo realment.

Concurs d’ Optptica
Concurs d’ Optptica

Accèssit al Concurs d’ Optptica

Recomanat: