Cuprins:

Sortare robotică a mărgelelor: 3 pași (cu imagini)
Sortare robotică a mărgelelor: 3 pași (cu imagini)

Video: Sortare robotică a mărgelelor: 3 pași (cu imagini)

Video: Sortare robotică a mărgelelor: 3 pași (cu imagini)
Video: Aplicație sortare RoboGuide 2024, Noiembrie
Anonim
Image
Image
Sortarea robotică a mărgelelor
Sortarea robotică a mărgelelor
Sortarea robotică a mărgelelor
Sortarea robotică a mărgelelor
Sortarea robotică a mărgelelor
Sortarea robotică a mărgelelor

În acest proiect, vom construi un robot pentru a sorta mărgelele Perler după culoare.

Întotdeauna mi-am dorit să construiesc un robot de sortare a culorilor, așa că atunci când fiica mea s-a interesat de fabricarea mărgelelor Perler, am văzut acest lucru ca pe o oportunitate perfectă.

Margelele Perler sunt folosite pentru a crea proiecte de artă topite prin plasarea multor mărgele pe un panou și apoi topindu-le împreună cu un fier de călcat. În general, cumpărați aceste mărgele în pachete gigant de 22 000 de mărgele de culoare mixtă și petreceți mult timp căutând culoarea dorită, așa că m-am gândit că sortarea lor va spori eficiența artistică.

Lucrez pentru Phidgets Inc. așa că am folosit mai ales Phidgets pentru acest proiect - dar acest lucru se poate face folosind orice hardware adecvat.

Pasul 1: Hardware

Iată ce am folosit pentru a construi acest lucru. L-am construit 100% cu piese de pe phidgets.com și lucruri pe care le aveam întinse prin casă.

Plăci Phidgets, Motoare, Hardware

  • HUB0000 - VINT Hub Phidget
  • 1108 - Senzor magnetic
  • 2x STC1001 - 2.5A Phidget Stepper
  • 2x 3324 - 42STH38 NEMA-17 Bipolar Gearless Stepper
  • 3x 3002 - Cablu Phidget 60cm
  • 3403 - Hub cu 4 porturi USB2.0
  • 3031 - Coadă femelă 5,5x2,1mm
  • 3029 - Cablu răsucit cu 2 fire 100 '
  • 3604 - LED alb de 10 mm (sac de 10)
  • 3402 - Cameră web USB

Alte părți

  • Alimentare 24VDC 2.0A
  • Resturi de lemn și metal din garaj
  • Cravate cu fermoar
  • Recipient din plastic cu fundul tăiat

Pasul 2: Proiectați robotul

Proiectează robotul
Proiectează robotul
Proiectează robotul
Proiectează robotul
Proiectează robotul
Proiectează robotul

Trebuie să proiectăm ceva care să poată lua o singură margelă din buncărul de intrare, să o așezăm sub camera web și apoi să o mutăm în coșul corespunzător.

Pickup de mărgele

Am decis să fac prima parte cu 2 bucăți de placaj rotund, fiecare cu o gaură găurită în același loc. Piesa de jos este fixă, iar piesa de sus este atașată la un motor pas cu pas, care o poate roti sub o buncăr plină cu margele. Când gaura se deplasează sub buncăr, ea preia o singură margelă. O pot roti apoi sub camera web și apoi o pot roti până când se potrivește cu gaura din partea inferioară, moment în care cade.

În această imagine, testez că sistemul poate funcționa. Totul este fix, cu excepția bucății rotunde superioare de placaj, care este atașată la un motor pas cu pas, care nu este vizibil dedesubt. Camera web nu a fost încă montată. Folosesc doar Panoul de control Phidget pentru a mă orienta către motor în acest moment.

Depozitare margele

Următoarea parte este de a proiecta sistemul de coșuri pentru a păstra fiecare culoare. Am decis să folosesc un al doilea motor pas cu pas pentru a susține și a roti un container rotund cu compartimente uniform distanțate. Aceasta poate fi utilizată pentru a roti compartimentul corect sub orificiul din care va cădea talonul.

Am construit asta folosind carton și bandă adezivă. Cel mai important lucru aici este consistența - fiecare compartiment ar trebui să aibă aceeași dimensiune, iar întregul lucru ar trebui să fie ponderat uniform, astfel încât să se învârtă fără să sară.

Îndepărtarea mărgelei se realizează prin intermediul unui capac strâns care expune un singur compartiment la un moment dat, astfel încât mărgelele să poată fi turnate.

aparat foto

Camera web este montată peste placa superioară dintre buncăr și locația orificiului inferior al plăcii. Acest lucru permite sistemului să privească talonul înainte de a-l lăsa. Un LED este utilizat pentru a ilumina margelele de sub cameră, iar lumina ambientală este blocată, pentru a oferi un mediu de iluminare consistent. Acest lucru este foarte important pentru detectarea precisă a culorilor, deoarece iluminarea ambientală poate arunca într-adevăr culoarea percepută.

Detecția locației

Este important ca sistemul să poată detecta rotația separatorului de margele. Aceasta este utilizată pentru a configura poziția inițială la pornire, dar și pentru a detecta dacă motorul pas cu pas a ieșit din sincronizare. În sistemul meu, o margele se va bloca uneori în timp ce este preluată, iar sistemul trebuia să poată detecta și gestiona această situație - făcând o copie de rezervă puțin și încercând din nou.

Există o mulțime de moduri de a rezolva acest lucru. Am decis să folosesc un senzor magnetic 1108, cu un magnet încorporat în marginea plăcii superioare. Acest lucru îmi permite să verific poziția la fiecare rotație. O soluție mai bună ar fi probabil un codificator pe motorul pas cu pas, dar aveam un 1108 așezat în jur, așa că l-am folosit.

Termină robotul

În acest moment, totul a fost rezolvat și testat. Este timpul să montăm totul frumos și să trecem la software-ul de scriere.

Cele 2 motoare pas cu pas sunt acționate de controlere pas cu pas STC1001. Un hub HUB000 - USB VINT este utilizat pentru rularea controlerelor pas cu pas, precum și pentru citirea senzorului magnetic și pentru acționarea LED-ului. Camera web și HUB0000 sunt ambele atașate la un mic hub USB. O coadă 3031 și unele fire sunt utilizate împreună cu o sursă de alimentare de 24V pentru a alimenta motoarele.

Pasul 3: Scrieți codul

Image
Image

C # și Visual Studio 2015 sunt utilizate pentru acest proiect. Descărcați sursa din partea de sus a acestei pagini și urmați - secțiunile principale sunt prezentate mai jos

Inițializare

În primul rând, trebuie să creăm, să deschidem și să inițializăm obiectele Phidget. Acest lucru se face în evenimentul de încărcare a formularului și în handlerele de atașare Phidget.

private void Form1_Load (expeditor obiect, EventArgs e) {

/ * Inițializați și deschideți Phidgets * /

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

bottom. HubPort = 1;

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

magSensor. HubPort = 2;

magSensor. IsHubPortDevice = adevărat; magSensor. Attach + = MagSensor_Attach; magSensor. Detach + = MagSensor_Detach; magSensor. SensorChange + = MagSensor_SensorChange; magSensor. Open ();

led. HubPort = 5;

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

private void Led_Attach (expeditor obiect, Phidget22. Events. AttachEventArgs e) {

ledAttachedChk. Checked = adevărat; led. State = adevărat; ledChk. Checked = adevărat; }

private void MagSensor_Attach (expeditor obiect, Phidget22. Events. AttachEventArgs e) {

magSensorAttachedChk. Checked = adevărat; magSensor. SensorType = VoltageRatioSensorType. PN_1108; magSensor. DataInterval = 16; }

private void Bottom_Attach (expeditor obiect, Phidget22. Events. AttachEventArgs e) {

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

private void Top_Attach (expeditor obiect, Phidget22. Events. AttachEventArgs e) {

topAttachedChk. Checked = adevărat; top. CurrentLimit = topCurrentLimit; top. Engaged = adevărat; top. RescaleFactor = -1; top. VelocityLimit = -topVelocityLimit; top. Acceleration = -topAccel; top. DataInterval = 100; }

De asemenea, citim orice informație de culoare salvată în timpul inițializării, astfel încât o rulare anterioară poate fi continuată.

Poziționarea motorului

Codul de manipulare a motorului constă din funcții de comoditate pentru deplasarea motoarelor. Motoarele pe care le-am folosit sunt 3, 200 1/16 pași pe rotație, așa că am creat o constantă pentru asta.

Pentru motorul de sus, există 3 poziții pe care dorim să le putem trimite motorului: camera web, gaura și magnetul de poziționare. Există o funcție pentru a călători în fiecare dintre aceste poziții:

private void nextMagnet (Boolean wait = false) {

double posn = top. Position% stepsPerRev;

top. TargetPosition + = (stepsPerRev - posn);

dacă (așteptați)

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);

dacă (așteptați)

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);

dacă (așteptați)

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

Înainte de a începe o cursă, placa superioară este aliniată cu ajutorul senzorului magnetic. Funcția alignMotor poate fi apelată în orice moment pentru a alinia placa superioară. Această funcție transformă mai întâi rapid placa până la 1 rotație completă până când vede datele magnetului peste un prag. Apoi face o copie de rezervă puțin și se deplasează din nou încet, captând datele senzorului pe măsură ce merge. În cele din urmă, setează poziția la locația maximă a datelor magnetului și resetează poziția compensată la 0. Astfel, poziția maximă a magnetului ar trebui să fie întotdeauna la (sus. Poziție% stepsPerRev)

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

// Găsește magnetul

top. DataInterval = top. MinDataInterval;

sawMagnet = fals;

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

int tryCount = 0;

încearcă din nou:

top. TargetPosition + = stepsPerRev;

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

if (! sawMagnet) {

if (tryCount> 3) {Console. WriteLine ("Alinierea a eșuat"); top. Engaged = false; bottom. Engaged = false; runtest = fals; întoarcere; }

tryCount ++;

Console. WriteLine ("Suntem blocați? Încercăm o copie de rezervă …"); top. TargetPosition - = 600; while (top. IsMoving) Thread. Sleep (100);

du-te din nou;

}

top. VelocityLimit = -100;

magData = Listă nouă> (); magSensor. SensorChange + = magSensorCollectPositionData; top. TargetPosition + = 300; while (top. IsMoving) Thread. Sleep (100);

magSensor. SensorChange - = magSensorCollectPositionData;

top. VelocityLimit = -topVelocityLimit;

KeyValuePair max = magData [0];

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

top. AddPositionOffset (-max. Key);

magSensorMax = Valoare maximă;

top. TargetPosition = 0;

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

Console. WriteLine ("Alinierea a reușit");

}

Listă> magData;

private void magSensorCollectPositionData (expeditor obiect, Phidget22. Events. VoltageRatioInputSensorChangeEventArgs e) {magData. Add (new KeyValuePair (top. Position, e. SensorValue)); }

private void magSensorStopMotor (expeditor obiect, Phidget22. Events. VoltageRatioInputSensorChangeEventArgs e) {

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

În cele din urmă, motorul inferior este controlat prin trimiterea acestuia într-una din pozițiile containerului de margele. Pentru acest proiect, avem 19 poziții. Algoritmul alege o cale mai scurtă și se rotește fie în sensul acelor de ceasornic, fie în sens invers acelor de ceasornic.

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; targetPosn dublu = (posn * stepsPerRev) / beadCompartments;

curent dubluPosn = bottom. Position% stepsPerRev;

double posnDiff = targetPosn - currentPosn;

// Păstrați-l ca pași complet

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

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

dacă (așteptați)

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

aparat foto

OpenCV este folosit pentru a citi imagini de pe camera web. Firul camerei este pornit înainte de a porni firul principal de sortare. Acest fir citește continuu în imagini, calculează o culoare medie pentru o anumită regiune folosind Mean și actualizează o variabilă de culoare globală. Firul folosește, de asemenea, HoughCircles pentru a încerca să detecteze fie o margelă, fie gaura din placa superioară, pentru a rafina zona pe care o urmărește pentru detectarea culorii. Pragul și numerele HoughCircles au fost determinate prin încercări și erori și depind în mare măsură de camera web, iluminare și spațiu.

bool runVideo = true; bool videoRunning = false; Captură VideoCapture; Thread cvThread; Culoare detectatăCuloare; Boolean detection = false; int detectCnt = 0;

private void cvThreadFunction () {

videoRunning = false;

capture = new VideoCapture (selectedCamera);

folosind (Fereastră fereastră = fereastră nouă ("captură")) {

Imagine Mat = Mat nou (); Mat image2 = new Mat (); while (runVideo) {capture. Read (imagine); if (image. Empty ()) break;

dacă (detectând)

detectCnt ++; altfel detectCnt = 0;

if (detectarea || circleDetectChecked || showDetectionImgChecked) {

Cv2. CvtColor (imagine, imagine2, ColorConversionCodes. BGR2GRAY); Mat thir = image2. Threshold ((double) Properties. Settings. Default.videoThresh, 255, ThresholdTypes. Binary); tris = tris. GaussianBlur (nou OpenCvSharp. Size (9, 9), 10);

if (showDetectionImgChecked)

imagine = treier;

if (detectarea || circleDetectChecked) {

CircleSegment margelă = tris. HoughCircles (HoughMethods. Gradient, 2, /*thres. Rows/4*/ 20, 200, 100, 20, 65); if (bead. Length> = 1) {image. Circle (bead [0]. Center, 3, nou Scalar (0, 100, 0), -1); image. Circle (bead [0]. Center, (int) bead [0]. Radius, nou Scalar (0, 0, 255), 3); if (bead [0]. Radius> = 55) {Properties. Settings. Default.x = (zecimal) bead [0]. Center. X + (zecimal) (bead [0]. Radius / 2); Properties. Settings. Default.y = (zecimal) bead [0]. Center. Y - (zecimal) (bead [0]. Radius / 2); } else {Properties. Settings. Default.x = (zecimal) bead [0]. Center. X + (zecimal) (bead [0]. Radius); Properties. Settings. Default.y = (zecimal) bead [0]. Center. Y - (zecimal) (bead [0]. Radius); } Properties. Settings. Default.size = 15; Properties. Settings. Default.height = 15; } altceva {

CircleSegment cercuri = treburi. 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 (); Lista ys = cercuri. Selectați (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 (medianY> image. Height - 15) medianY = image. Height - 15;

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

dacă (detectează) {

Properties. Settings. Default.x = medianX - 7; Properties. Settings. Default.y = medianY - 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 = Mat nou (imagine, 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 (imagine);

Cv2. WaitKey (1); videoRunning = adevărat; }

videoRunning = false;

} }

private void cameraStartBtn_Click (expeditor obiect, EventArgs e) {

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

cvThread = thread nou (nou ThreadStart (cvThreadFunction)); runVideo = adevărat; cvThread. Start (); cameraStartBtn. Text = "stop"; while (! videoRunning) Thread. Sleep (100);

updateColorTimer. Start ();

} altceva {

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

Culoare

Acum, suntem capabili să determinăm culoarea unei mărgele și să decidem pe baza acelei culori în care container să o lăsăm.

Acest pas se bazează pe compararea culorilor. Vrem să putem distinge culorile pentru a limita falsul pozitiv, dar, de asemenea, să permitem suficient prag pentru a limita negativele false. Compararea culorilor este de fapt surprinzător de complexă, deoarece modul în care computerele stochează culorile ca RGB și modul în care oamenii percep culorile nu se corelează liniar. Pentru a înrăutăți lucrurile, trebuie luată în considerare și culoarea luminii sub care se vede o culoare.

Există algoritm complicat pentru calcularea diferenței de culoare. Folosim CIE2000, care afișează un număr aproape de 1 dacă 2 culori ar fi indistincte pentru un om. Folosim biblioteca ColorMine C # pentru a face aceste calcule complicate. S-a constatat că o valoare DeltaE de 5 oferă un compromis bun între fals pozitiv și fals negativ.

Deoarece există adesea mai multe culori decât containere, ultima poziție este rezervată ca un coș de gunoi. În general, le pun deoparte pentru a rula cu mașina la o a doua trecere.

Listă

culori = listă nouă (); listă ColorPanels = listă nouă (); List colorTxts = new List (); List colorCnts = new List ();

const int numColorSpots = 18;

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

Console. WriteLine ("Găsirea culorii …");

var cRGB = Rgb nou ();

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

int bestMatch = -1;

match dublu Delta = 100;

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

var RGB = Rgb nou ();

RGB. R = culori . R; RGB. G = culori . G; RGB. B = culori . B;

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

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

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

if (colors. Count <numColorSpots) {Console. WriteLine ("Culoare nouă!"); culori. Adăugați (c); this. BeginInvoke (new Action (setBackColor), nou obiect {colors. Count - 1}); writeOutColors (); return (colors. Count - 1); } else {Console. WriteLine ("Culoare necunoscută!"); returnează unknownColorIndex; }}

Logica de sortare

Funcția de sortare reunește toate piesele pentru a sorta de fapt mărgele. Această funcție rulează într-un fir dedicat; deplasarea plăcii superioare, detectarea culorii mărgelei, plasarea acesteia într-un coș, asigurându-vă că placa superioară rămâne aliniată, numărarea mărgelelor etc. De asemenea, se oprește din funcționare atunci când coșul de gunoi devine plin - În caz contrar, ajungem doar cu margele debordante.

Thread colorTestThread; Runtest boolean = fals; void colorTest () {

if (! top. Engaged)

top. Engaged = adevărat;

if (! bottom. Engaged)

bottom. Engaged = adevărat;

while (runtest) {

nextMagnet (adevărat);

Thread. Sleep (100); încercați {if (magSensor. SensorValue <(magSensorMax - 4)) alignMotor (); } catch {alignMotor (); }

nextCamera (adevărat);

detectare = adevărat;

while (detectCnt <5) Thread. Sleep (25); Console. WriteLine ("Detect Count:" + detectCnt); detectarea = false;

Culoare c = Color detectat;

this. BeginInvoke (Acțiune nouă (setColorDet), obiect nou {c}); int i = findColorPosition (c);

SetBottomPosition (i, adevărat);

nextHole (adevărat); colorCnts ++; this. BeginInvoke (Acțiune nouă (setColorTxt), obiect nou {i}); Thread. Sleep (250);

if (colorCnts [unknownColorIndex]> 500) {

top. Engaged = false; bottom. Engaged = false; runtest = fals; this. BeginInvoke (nouă acțiune (setGoGreen), nulă); întoarcere; }}}

private void colourTestBtn_Click (expeditor obiect, EventArgs e) {

if (colourTestThread == null ||! colourTestThread. IsAlive) {colourTestThread = thread nou (nou ThreadStart (colourTest)); runtest = adevărat; colourTestThread. Start (); colourTestBtn. Text = "STOP"; colourTestBtn. BackColor = Color. Red; } else {runtest = false; colourTestBtn. Text = "GO"; colourTestBtn. BackColor = Color. Green; }}

În acest moment, avem un program de lucru. Unele bucăți de cod au fost lăsate în afara articolului, așa că aruncați o privire la sursă pentru al rula efectiv.

Concurs de optică
Concurs de optică

Premiul II la Concursul de Optică

Recomandat: