Sommario:

Ordinamento robotico delle perle: 3 passaggi (con immagini)
Ordinamento robotico delle perle: 3 passaggi (con immagini)

Video: Ordinamento robotico delle perle: 3 passaggi (con immagini)

Video: Ordinamento robotico delle perle: 3 passaggi (con immagini)
Video: Se Vedi Questo In Mare, Esci Subito! 😱 2024, Luglio
Anonim
Image
Image
Smistamento robotico di perline
Smistamento robotico di perline
Smistamento robotico di perline
Smistamento robotico di perline
Ordinamento robotico delle perle
Ordinamento robotico delle perle

In questo progetto, costruiremo un robot per ordinare le perline Perler in base al colore.

Ho sempre voluto costruire un robot per la selezione dei colori, quindi quando mia figlia si è interessata alla creazione di perline Perler, l'ho vista come un'opportunità perfetta.

Le perline Perler vengono utilizzate per creare progetti artistici fusi posizionando molte perline su un pannello forato e quindi fondendole insieme con un ferro da stiro. Generalmente si acquistano queste perline in confezioni giganti da 22.000 perline di colori misti e si passa molto tempo a cercare il colore desiderato, quindi ho pensato che ordinarle avrebbe aumentato l'efficienza artistica.

Lavoro per Phidgets Inc. quindi ho usato principalmente Phidgets per questo progetto, ma questo potrebbe essere fatto utilizzando qualsiasi hardware adatto.

Passaggio 1: hardware

Ecco cosa ho usato per costruirlo. L'ho costruito al 100% con parti di phidgets.com e cose che avevo in giro per casa.

Schede, motori, hardware di Phidget

  • HUB0000 - VINT Hub Phidget
  • 1108 - Sensore magnetico
  • 2x STC1001 - Phidget passo-passo 2,5A
  • 2x 3324 - 42STH38 NEMA-17 Stepper bipolare Gearless
  • 3x 3002 - Cavo Phidget 60cm
  • 3403 - Hub USB 2.0 a 4 porte
  • 3031 - Codino femmina 5,5x2,1 mm
  • 3029 - Cavo a 2 fili intrecciato da 100'
  • 3604 - LED bianco da 10 mm (confezione da 10)
  • 3402 - Webcam USB

Altre parti

  • Alimentazione 24VDC 2.0A
  • Scarta legno e metallo dal garage
  • Fascette
  • Contenitore di plastica con il fondo tagliato

Passaggio 2: progettare il robot

Progetta il Robot
Progetta il Robot
Progetta il Robot
Progetta il Robot
Progetta il Robot
Progetta il Robot

Dobbiamo progettare qualcosa che possa prendere un singolo tallone dalla tramoggia di input, posizionarlo sotto la webcam e quindi spostarlo nel cestino appropriato.

Raccogli perline

Ho deciso di fare la prima parte con 2 pezzi di compensato rotondo, ciascuno con un foro praticato nello stesso punto. Il pezzo inferiore è fisso e il pezzo superiore è collegato a un motore passo-passo, che può ruotarlo sotto una tramoggia piena di perline. Quando il foro passa sotto la tramoggia, raccoglie un solo cordone. Posso quindi ruotarlo sotto la webcam, quindi ruotarlo ulteriormente fino a quando non corrisponde al foro nel pezzo inferiore, a quel punto cade.

In questa immagine, sto testando che il sistema possa funzionare. Tutto è fisso tranne il pezzo rotondo superiore di compensato, che è attaccato a un motore passo-passo nascosto sotto. La webcam non è stata ancora montata. Sto solo usando il pannello di controllo di Phidget per passare al motore a questo punto.

Stoccaggio di perline

La parte successiva è progettare il sistema di contenitori per contenere ogni colore. Ho deciso di utilizzare un secondo motore passo-passo in basso per supportare e ruotare un contenitore rotondo con scomparti equidistanti. Questo può essere usato per ruotare il vano corretto sotto il foro da cui cadrà il tallone.

L'ho costruito usando cartone e nastro adesivo. La cosa più importante qui è la coerenza: ogni scomparto dovrebbe avere le stesse dimensioni e l'intera cosa dovrebbe essere uniformemente ponderata in modo che ruoti senza saltare.

La rimozione delle perline viene eseguita mediante un coperchio a chiusura ermetica che espone un singolo scomparto alla volta, in modo che le perline possano essere versate.

Telecamera

La webcam è montata sulla piastra superiore tra la tramoggia e la posizione del foro della piastra inferiore. Ciò consente al sistema di guardare il tallone prima di lasciarlo cadere. Un LED viene utilizzato per illuminare le perline sotto la fotocamera e la luce ambientale viene bloccata, al fine di fornire un ambiente di illuminazione coerente. Questo è molto importante per un rilevamento accurato del colore, poiché l'illuminazione ambientale può davvero eliminare il colore percepito.

Rilevamento della posizione

È importante che il sistema sia in grado di rilevare la rotazione del separatore del tallone. Questo viene utilizzato per impostare la posizione iniziale all'avvio, ma anche per rilevare se il motore passo-passo è andato fuori sincrono. Nel mio sistema, a volte un tallone si inceppa mentre viene raccolto e il sistema doveva essere in grado di rilevare e gestire questa situazione, facendo un po' di backup e riprovando.

Ci sono molti modi per gestirlo. Ho deciso di utilizzare un sensore magnetico 1108, con un magnete incorporato nel bordo della piastra superiore. Questo mi permette di verificare la posizione ad ogni rotazione. Una soluzione migliore sarebbe probabilmente un encoder sul motore passo-passo, ma avevo un 1108 in giro, quindi l'ho usato.

Finisci il robot

A questo punto, tutto è stato elaborato e testato. È ora di montare tutto bene e passare alla scrittura di software.

I 2 motori passo-passo sono azionati da controller passo-passo STC1001. Un hub HUB000 - USB VINT viene utilizzato per eseguire i controller stepper, nonché per leggere il sensore magnetico e pilotare il LED. La webcam e l'HUB0000 sono entrambi collegati a un piccolo hub USB. Un pigtail 3031 e alcuni cavi vengono utilizzati insieme a un alimentatore da 24 V per alimentare i motori.

Passaggio 3: scrivi il codice

Image
Image

C# e Visual Studio 2015 vengono utilizzati per questo progetto. Scarica la fonte nella parte superiore di questa pagina e segui - le sezioni principali sono descritte di seguito

Inizializzazione

Innanzitutto, dobbiamo creare, aprire e inizializzare gli oggetti Phidget. Questo viene fatto nell'evento di caricamento del modulo e nei gestori di collegamento Phidget.

private void Form1_Load (mittente oggetto, EventArgs e) {

/* Inizializza e apre Phidget */

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

fondo. HubPort = 1;

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

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. Canale = 0; led. Attach += Led_Attach; led. Stacca += Led_Stacca; led. Open(); }

private void Led_Attach(oggetto mittente, Phidget22. Events. AttachEventArgs e) {

ledAttachedChk. Checked = vero; led. Stato = vero; ledChk. Checked = vero; }

private void MagSensor_Attach(oggetto mittente, Phidget22. Events. AttachEventArgs e) {

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

private void Bottom_Attach(oggetto mittente, Phidget22. Events. AttachEventArgs e) {

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

private void Top_Attach(oggetto mittente, Phidget22. Events. AttachEventArgs e) {

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

Durante l'inizializzazione leggiamo anche tutte le informazioni sui colori salvate, quindi è possibile continuare una corsa precedente.

Posizionamento del motore

Il codice di gestione del motore è costituito da funzioni comfort per lo spostamento dei motori. I motori che ho usato sono 3.200 1/16 passi per giro, quindi ho creato una costante per questo.

Per il motore superiore, ci sono 3 posizioni a cui vogliamo essere in grado di inviare al motore: la webcam, il foro e il magnete di posizionamento. C'è una funzione per viaggiare in ciascuna di queste posizioni:

private void nextMagnet(Boolean wait = false) {

double posn = top. Position % stepsPerRev;

top. TargetPosition += (stepsPerRev - posn);

se (aspetta)

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

se (aspetta)

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

se (aspetta)

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

Prima di iniziare una corsa, la piastra superiore viene allineata utilizzando il sensore magnetico. La funzione alignMotor può essere richiamata in qualsiasi momento per allineare la piastra superiore. Questa funzione prima ruota rapidamente la piastra fino a 1 giro completo finché non vede i dati del magnete al di sopra di una soglia. Quindi esegue un piccolo backup e si sposta in avanti di nuovo lentamente, catturando i dati del sensore mentre procede. Infine, imposta la posizione sulla posizione dei dati del magnete massimo e reimposta l'offset di posizione su 0. Pertanto, la posizione del magnete massimo dovrebbe sempre essere (top. Position % stepsPerRev)

Thread alignMotorThread;Boolean sawMagnet; doppio magSensorMax = 0; privato void alignMotor() {

//Trova il magnete

top. DataInterval = top. MinDataInterval;

sawMagnet = falso;

magSensor. SensorChange += magSensorStopMotor; top. LimiteVelocità = -1000;

int tryCount = 0;

riprova:

top. TargetPosition += stepsPerRev;

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

if (!sawMagnet) {

if (tryCount > 3) { Console. WriteLine("Allineamento fallito"); top. Engaged = falso; bottom. Engaged = falso; runtest = falso; Restituzione; }

tryCount++;

Console. WriteLine("Siamo bloccati? Sto provando un backup…"); top. TargetPosition -= 600; while (top. IsMoving) Thread. Sleep(100);

vai a riprovare;

}

top. LimiteVelocità = -100;

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

magSensor. SensorChange -= magSensorCollectPositionData;

top. VelocityLimit = -topVelocityLimit;

KeyValuePair max = magData[0];

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

top. AddPositionOffset(-max. Key);

magSensorMax = max. Value;

top. TargetPosition = 0;

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

Console. WriteLine("Allineamento riuscito");

}

Elenco> magData;

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

private void magSensorStopMotor(oggetto mittente, Phidget22. Events. VoltageRatioInputSensorChangeEventArgs e) {

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

Infine, il motore inferiore viene comandato inviandolo in una delle posizioni del contenitore del tallone. Per questo progetto abbiamo 19 posizioni. L'algoritmo sceglie un percorso più breve e gira in senso orario o antiorario.

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

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

} }

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

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

double currentPosn = bottom. Position % stepsPerRev;

double posnDiff = targetPosn - currentPosn;

// Mantieni i passaggi completi

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

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

se (aspetta)

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

Telecamera

OpenCV viene utilizzato per leggere le immagini dalla webcam. Il thread della fotocamera viene avviato prima di iniziare il thread di ordinamento principale. Questo thread legge continuamente le immagini, calcola un colore medio per una regione specifica utilizzando Media e aggiorna una variabile di colore globale. Il filo utilizza anche HoughCircles per cercare di rilevare un cordone o il foro nella piastra superiore, per perfezionare l'area che sta guardando per il rilevamento del colore. La soglia e i numeri di HoughCircles sono stati determinati attraverso tentativi ed errori e dipendono fortemente dalla webcam, dall'illuminazione e dalla spaziatura.

bool runVideo = true;bool videoRunning = false; Cattura VideoCapture; Discussione cvThread; Colore rilevatoColore; Rilevamento booleano = falso; int rilevaCnt = 0;

private void cvThreadFunction() {

videoRunning = falso;

cattura = new VideoCapture (selectedCamera);

using (Window window = new Window("cattura")) {

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

se (rilevamento)

rilevaCnt++; altrimenti rilevaCnt = 0;

if (rilevazione || 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)

immagine = tre;

if (rilevamento || circleDetectChecked) {

CircleSegment bead = 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 = (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; } altro {

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. Sort(); List 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)

medianX = immagine. Larghezza - 15; if (medianY > image. Height - 15) medianY = image. Height - 15;

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

if (rilevamento) {

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

Rect r = nuovo 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);

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

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

finestra. ShowImage(immagine);

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

videoRunning = falso;

} }

private void cameraStartBtn_Click (mittente oggetto, EventArgs e) {

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

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

updateColorTimer. Start();

} altro {

runVideo = falso; cvThread. Join(); cameraStartBtn. Text = "inizio"; } }

Colore

Ora siamo in grado di determinare il colore di una perlina e decidere in base a quel colore in quale contenitore farla cadere.

Questo passaggio si basa sul confronto dei colori. Vogliamo essere in grado di distinguere i colori per limitare i falsi positivi, ma anche consentire una soglia sufficiente per limitare i falsi negativi. Il confronto dei colori è in realtà sorprendentemente complesso, perché il modo in cui i computer memorizzano i colori come RGB e il modo in cui gli esseri umani percepiscono i colori non sono correlati in modo lineare. A peggiorare le cose, deve essere preso in considerazione anche il colore della luce sotto cui viene visualizzato un colore.

Esistono algoritmi complicati per il calcolo della differenza di colore. Usiamo CIE2000, che emette un numero vicino a 1 se 2 colori sarebbero indistinguibili per un essere umano. Stiamo usando la libreria ColorMine C# per fare questi calcoli complicati. È stato riscontrato che un valore DeltaE di 5 offre un buon compromesso tra falso positivo e falso negativo.

Poiché spesso ci sono più colori dei contenitori, l'ultima posizione è riservata come raccoglitore. In genere li metto da parte per eseguire una seconda passata attraverso la macchina.

Elenco

colori = nuova Lista (); Lista colorPanels = nuova Lista (); Elenco colorTxts = new Elenco(); List colorCnts = new List();

const int numColorSpots = 18;

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

Console. WriteLine("Ricerca colore…");

var cRGB = nuovo RGB();

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

int bestMatch = -1;

doppia corrispondenzaDelta = 100;

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

var RGB = nuovo RGB();

RGB. R = colori. R; RGB. G = colori. G; RGB. B = colori. B;

doppio delta = cRGB. Compare(RGB, nuovo CieDe2000Comparison());

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

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

if (colors. Count < numColorSpots) { Console. WriteLine("Nuovo colore!"); colori. Aggiungi(c); this. BeginInvoke(new Action(setBackColor), new object { colors. Count - 1 }); writeOutColors(); ritorno (colori. Conteggio - 1); } else { Console. WriteLine("Colore sconosciuto!"); return unknownColorIndex; } }

Logica di ordinamento

La funzione di smistamento riunisce tutti i pezzi per ordinare effettivamente le perline. Questa funzione viene eseguita in un thread dedicato; spostando la piastra superiore, rilevando il colore delle perline, posizionandola in un cestino, assicurandosi che la piastra superiore rimanga allineata, contando le perline, ecc. Smette anche di funzionare quando il cestino del raccoglitore si riempie, altrimenti finiamo con perline traboccanti.

Thread colourTestThread;Runtest booleano = false; void colorTest() {

if (!top. Engaged)

top. Engaged = vero;

if (!bottom. Engaged)

bottom. Engaged = vero;

mentre (runtest) {

nextMagnet(true);

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

nextCamera(true);

rilevamento = vero;

while (detectCnt < 5) Thread. Sleep(25); Console. WriteLine("Rileva conteggio: " + detectCnt); rilevamento = falso;

Colore c = colore rilevato;

this. BeginInvoke(new Action (setColorDet), new object { c }); int i = findColorPosition(c);

SetBottomPosition(i, vero);

nextHole(vero); colorCnts++; this. BeginInvoke(new Action(setColorTxt), new object { i }); Filo. Sonno(250);

if (colorCnts[unknownColorIndex] > 500) {

top. Engaged = falso; bottom. Engaged = falso; runtest = falso; this. BeginInvoke(new Action(setGoGreen), null); Restituzione; } } }

private void colourTestBtn_Click(mittente oggetto, EventArgs e) {

if (colourTestThread == null || !colourTestThread. IsAlive) { colourTestThread = new Thread(new ThreadStart(colourTest)); runtest = vero; colorTestThread. Start(); colorTestBtn. Text = "STOP"; colourTestBtn. BackColor = Color. Red; } else { runtest = falso; colorTestBtn. Text = "VAI"; colourTestBtn. BackColor = Color. Green; } }

A questo punto abbiamo un programma di lavoro. Alcuni frammenti di codice sono stati omessi dall'articolo, quindi dai un'occhiata al sorgente per eseguirlo effettivamente.

Concorso di ottica
Concorso di ottica

Secondo Premio al Concorso Ottica

Consigliato: