Innehållsförteckning:

Sortering av robotpärlor: 3 steg (med bilder)
Sortering av robotpärlor: 3 steg (med bilder)

Video: Sortering av robotpärlor: 3 steg (med bilder)

Video: Sortering av robotpärlor: 3 steg (med bilder)
Video: Lära Med Leksaker | Play Doh Ägg Med Fordon | Barnprogram För Små Barn 2024, Juli
Anonim
Image
Image
Sortering av robotpärlor
Sortering av robotpärlor
Robotsortering av pärlor
Robotsortering av pärlor
Robotsortering av pärlor
Robotsortering av pärlor

I detta projekt kommer vi att bygga en robot för att sortera Perler -pärlor efter färg.

Jag har alltid velat bygga en färgsorteringsrobot, så när min dotter blev intresserad av Perler -pärlhantverk såg jag detta som ett perfekt tillfälle.

Perlerpärlor används för att skapa sammansmälta konstprojekt genom att placera många pärlor på en pegbräda och sedan smälta dem tillsammans med ett järn. Du köper vanligtvis dessa pärlor i gigantiska 22 000 pärlblandade färgpaket och lägger mycket tid på att leta efter den färg du vill ha, så jag trodde att sortering av dem skulle öka konsteffektiviteten.

Jag arbetar för Phidgets Inc. så jag använde mestadels Phidgets för detta projekt - men det kan göras med lämplig hårdvara.

Steg 1: Hårdvara

Här är vad jag brukade bygga detta. Jag byggde den till 100% med delar från phidgets.com och saker som jag hade liggande i huset.

Phidgets brädor, motorer, hårdvara

  • HUB0000 - VINT Hub Phidget
  • 1108 - Magnetisk sensor
  • 2x STC1001 - 2,5A Stepper Phidget
  • 2x 3324 - 42STH38 NEMA -17 Bipolär växellös steg
  • 3x 3002 - Phidget -kabel 60cm
  • 3403 - USB2.0 4 -Port Hub
  • 3031 - Pigtail hona 5.5x2.1mm
  • 3029 - 2 -trådig 100 'vriden kabel
  • 3604 - 10 mm vit LED (påse med 10)
  • 3402 - USB -webbkamera

Andra delar

  • 24VDC 2.0A nätaggregat
  • Skrot trä och metall från garaget
  • Buntband
  • Plastbehållare med botten avskuret

Steg 2: Designa roboten

Designa roboten
Designa roboten
Designa roboten
Designa roboten
Designa roboten
Designa roboten

Vi måste designa något som kan ta en enda pärla från inmatningsbehållaren, placera den under webbkameran och sedan flytta den till lämpligt fack.

Bead Pickup

Jag bestämde mig för att göra den första delen med 2 bitar av rund plywood, var och en med ett hål borrat på samma ställe. Bottenstycket är fixerat och toppstycket fästs på en stegmotor som kan rotera det under en behållare fylld med pärlor. När hålet rör sig under behållaren tar det upp en enda pärla. Jag kan sedan rotera den under webbkameran och sedan rotera ytterligare tills den matchar hålet i bottenstycket, vid vilken tidpunkt den faller igenom.

På den här bilden testar jag att systemet kan fungera. Allt är fixat utom den övre runda plywoodbiten, som är fäst vid en stegmotor utom synhåll under. Webbkameran har inte monterats än. Jag använder bara Phidget -kontrollpanelen för att gå till motorn vid denna tidpunkt.

Pärlförvaring

Nästa del är att designa kärlsystemet för att hålla varje färg. Jag bestämde mig för att använda en andra stegmotor nedan för att stödja och rotera en rund behållare med jämnt fördelade fack. Detta kan användas för att rotera rätt fack under hålet som pärlan tappar ur.

Jag byggde detta med kartong och tejp. Det viktigaste här är konsistens - varje fack ska ha samma storlek och det hela ska vara jämnt viktat så att det snurrar utan att hoppa över.

Pärlborttagning sker med hjälp av ett tätt passande lock som exponerar ett enda fack i taget, så att pärlorna kan hällas ut.

Kamera

Webbkameran är monterad över topplattan mellan behållaren och det nedre plattans hål. Detta gör att systemet kan titta på pärlan innan den tappas. En lysdiod används för att belysa pärlorna under kameran och omgivande ljus blockeras för att skapa en konsekvent belysningsmiljö. Detta är mycket viktigt för exakt färgdetektering, eftersom omgivande belysning verkligen kan slänga upp den upplevda färgen.

Platsdetektering

Det är viktigt för systemet att kunna detektera rotationen av pärlavskiljaren. Detta används för att ställa in startpositionen vid start, men också för att upptäcka om stegmotorn har kommit ur synk. I mitt system kan en pärla ibland fastna medan den tas upp, och systemet behövde kunna upptäcka och hantera denna situation - genom att säkerhetskopiera lite och försöka igen.

Det finns många sätt att hantera detta. Jag bestämde mig för att använda en 1108 magnetisk sensor, med en magnet inbäddad i kanten på topplattan. Detta gör att jag kan verifiera positionen vid varje rotation. En bättre lösning skulle förmodligen vara en kodare på stegmotorn, men jag hade en 1108 liggande så jag använde det.

Slutför roboten

Vid denna tidpunkt har allt blivit utarbetat och testat. Det är dags att montera allt snyggt och gå vidare till skrivprogramvara.

De två stegmotorerna drivs av STC1001 stegstyrenheter. En HUB000 - USB VINT -hubb används för att köra stegkontrollerna, samt läsa av den magnetiska sensorn och driva lysdioden. Webbkameran och HUB0000 är båda anslutna till en liten USB -hubb. En 3031 pigtail och lite tråd används tillsammans med en 24V strömförsörjning för att driva motorerna.

Steg 3: Skriv kod

Image
Image

C# och Visual Studio 2015 används för detta projekt. Ladda ner källan högst upp på denna sida och följ med - huvudavsnitten beskrivs nedan

Initiering

Först måste vi skapa, öppna och initiera Phidget -objekten. Detta görs i formbelastningshändelsen och Phidget bifogar hanterare.

private void Form1_Load (objektavsändare, EventArgs e) {

/ * Initiera och öppna Phidgets */

top. HubPort = 0; top. Attach += Top_Attach; top. Detach += Top_Detach; top. PositionChange += Top_PositionChange; övre. Öppna ();

bottom. HubPort = 1;

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

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. Channel = 0; led. Attach += Led_Attach; led. Detach += Led_Detach; led. Open (); }

private void Led_Attach (objektavsändare, Phidget22. Events. AttachEventArgs e) {

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

private void MagSensor_Attach (objektavsändare, Phidget22. Events. AttachEventArgs e) {

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

private void Bottom_Attach (objektavsändare, Phidget22. Events. AttachEventArgs e) {

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

private void Top_Attach (objektavsändare, Phidget22. Events. AttachEventArgs e) {

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

Vi läser också in all sparad färginformation under initialiseringen, så en tidigare körning kan fortsätta.

Motorpositionering

Motorhanteringskoden består av bekvämlighetsfunktioner för att flytta motorerna. De motorer jag använde är 3 200 1/16 steg per varv, så jag skapade en konstant för detta.

För toppmotorn finns det tre positioner som vi vill kunna skicka till motorn till: webbkameran, hålet och positioneringsmagneten. Det finns en funktion för att resa till var och en av dessa positioner:

private void nextMagnet (Boolean wait = false) {

double posn = top. Position % stepsPerRev;

top. TargetPosition += (stepsPerRev - posn);

om (vänta)

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); annars top. TargetPosition + = ((Properties. Settings. Default.cameraOffset - posn) + stepsPerRev);

om (vänta)

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); annars top. TargetPosition + = ((Properties. Settings. Default.holeOffset - posn) + stepsPerRev);

om (vänta)

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

Innan en körning påbörjas justeras topplattan med hjälp av den magnetiska sensorn. AlignMotor -funktionen kan när som helst kallas för att justera topplattan. Denna funktion vrider först upp plattan till 1 fullständigt varv tills den ser magnetdata över ett tröskelvärde. Den backar sedan upp lite och går sakta framåt igen och fångar sensordata när det går. Slutligen ställer det in positionen till den maximala magnetdataplatsen och återställer positionsförskjutningen till 0. Således bör den maximala magnetpositionen alltid vara på (top. Position % stepsPerRev)

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

// Hitta magneten

top. DataInterval = top. MinDataInterval;

sawMagnet = falskt;

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

int tryCount = 0;

försök igen:

top. TargetPosition += stepsPerRev;

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

om (! sawMagnet) {

if (tryCount> 3) {Console. WriteLine ("Justering misslyckades"); top. Engaged = false; bottom. Engaged = false; runtest = false; lämna tillbaka; }

tryCount ++;

Console. WriteLine ("Har vi fastnat? Försöker säkerhetskopiera …"); top. TargetPosition -= 600; while (top. IsMoving) Thread. Sleep (100);

återigen försöka;

}

top. VelocityLimit = -100;

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

magSensor. SensorChange -= magSensorCollectPositionData;

top. VelocityLimit = -topVelocityLimit;

KeyValuePair max = magData [0];

foreach (KeyValuePair -par i magData) if (pair. Value> max. Value) max = pair;

top. AddPositionOffset (-max. Key);

magSensorMax = max. värde;

top. TargetPosition = 0;

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

Console. WriteLine ("Justera lyckades");

}

List> magData;

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

private void magSensorStopMotor (objektavsändare, Phidget22. Events. VoltageRatioInputSensorChangeEventArgs e) {

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

Slutligen styrs bottenmotorn genom att skicka den till en av pärlbehållarpositionerna. För detta projekt har vi 19 tjänster. Algoritmen väljer en kortaste väg och vänder antingen medurs eller moturs.

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

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

} }

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

posn = posn % beadCompartment; dubbel targetPosn = (posn * stepsPerRev) / beadCompartment;

double currentPosn = bottom. Position % stepsPerRev;

dubbel posnDiff = targetPosn - currentPosn;

// Behåll det som hela steg

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

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

om (vänta)

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

Kamera

OpenCV används för att läsa bilder från webbkameran. Kameratråden startas innan huvudsorteringstråden startas. Denna tråd läser kontinuerligt i bilder, beräknar en genomsnittlig färg för en specifik region med medelvärde och uppdaterar en global färgvariabel. Tråden använder också HoughCircles för att försöka upptäcka antingen en pärla eller hålet i topplattan för att förfina området det tittar på för färgdetektering. Tröskelvärdet och HoughCircles -siffrorna bestämdes genom försök och fel och beror mycket på webbkameran, belysningen och avståndet.

bool runVideo = true; bool videoRunning = false; VideoCapture -inspelning; Tråd cvThread; Färg detekteradColor; Boolsk detektering = falsk; int detectCnt = 0;

private void cvThreadFunction () {

videoRunning = false;

capture = ny VideoCapture (vald kamera);

använder (fönsterfönster = nytt fönster ("capture")) {

Mattbild = ny Mat (); Matta image2 = ny Mat (); medan (runVideo) {capture. Read (bild); om (image. Empty ()) bryts;

om (upptäcker)

detectCnt ++; annars detectCnt = 0;

if (detekterar || circleDetectChecked || showDetectionImgChecked) {

Cv2. CvtColor (image, image2, ColorConversionCodes. BGR2GRAY); Matt tröskel = image2. Threshold ((dubbel) Properties. Settings. Default.videoThresh, 255, ThresholdTypes. Binary); tröskor = tröskor. GaussianBlur (ny OpenCvSharp. Size (9, 9), 10);

if (showDetectionImgChecked)

bild = tröskor;

if (detekterar || circleDetectChecked) {

CircleSegment pärla = tröskor. 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 (pärla [0]. Center, (int) pärla [0]. Radius, ny Scalar (0, 0, 255), 3); if (pärla [0]. Radius> = 55) {Properties. Settings. Default.x = (decimal) pärla [0]. Center. X + (decimal) (pärla [0]. Radius / 2); Properties. Settings. Default.y = (decimal) pärla [0]. Center. Y - (decimal) (pärla [0]. Radius / 2); } annat {Properties. Settings. Default.x = (decimal) pärla [0]. Center. X + (decimal) (pärla [0]. Radius); Properties. Settings. Default.y = (decimal) pärla [0]. Center. Y - (decimal) (pärla [0]. Radius); } Properties. Settings. Default.size = 15; Properties. Settings. Default.height = 15; } annat {

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

if (cirklar. Längd> 1) {Lista xs = cirklar. Välj (c => c. Center. X). ToList (); xs. Sort (); Lista ys = cirklar. Välj (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, ny Scalar (0, 0, 150), 3);

om (upptäcker) {

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

Matt beadSample = ny matta (bild, r);

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

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

fönster. ShowImage (bild);

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

videoRunning = false;

} }

private void cameraStartBtn_Click (objektavsändare, EventArgs e) {

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

cvThread = ny tråd (ny ThreadStart (cvThreadFunction)); runVideo = true; cvThread. Start (); cameraStartBtn. Text = "stopp"; medan (! videoRunning) Thread. Sleep (100);

updateColorTimer. Start ();

} annat {

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

Färg

Nu kan vi bestämma färgen på en pärla och bestämma utifrån den färgen i vilken behållare vi ska släppa den.

Detta steg är beroende av färgjämförelse. Vi vill kunna skilja färger åt för att begränsa falskt positivt, men också tillåta tillräckligt med tröskel för att begränsa falska negativ. Att jämföra färger är faktiskt förvånansvärt komplext, eftersom datorns sätt att lagra färger som RGB, och hur människor uppfattar färger inte korrelerar linjärt. För att göra saken värre måste man också ta hänsyn till färgen på det ljus som en färg ses under.

Det finns komplicerad algoritm för att beräkna färgskillnad. Vi använder CIE2000, som matar ut ett tal nära 1 om 2 färger skulle vara oskiljbara för en människa. Vi använder ColorMine C# -biblioteket för att göra dessa komplicerade beräkningar. Ett DeltaE -värde på 5 har visat sig erbjuda en bra kompromiss mellan falskt positivt och falskt negativt.

Eftersom det ofta finns fler färger än behållare, är den sista positionen reserverad som en uppsamlingsbehållare. Jag ställer vanligtvis dessa åt sidan för att köra genom maskinen vid ett andra pass.

Lista

färger = ny lista (); lista färgpaneler = ny lista (); List colourTxts = new List (); List colorCnts = new List ();

const int numColorSpots = 18;

const int unknownColorIndex = 18; int findColorPosition (färg c) {

Console. WriteLine ("Hitta färg …");

var cRGB = ny Rgb ();

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

int bestMatch = -1;

dubbel matchDelta = 100;

för (int i = 0; i <colors. Count; i ++) {

var RGB = ny Rgb ();

RGB. R = färger . R; RGB. G = färger . G; RGB. B = färger . B;

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

// dubbel delta = deltaE (c, färger ); Console. WriteLine ("DeltaE (" + i. ToString () + "):" + delta. ToString ()); om (delta <matchDelta) {matchDelta = delta; bestMatch = i; }}

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

if (colors. Count <numColorSpots) {Console. WriteLine ("Ny färg!"); färger. Lägg till (c); this. BeginInvoke (ny åtgärd (setBackColor), nytt objekt {colors. Count - 1}); writeOutColors (); retur (färger.räkning - 1); } annat {Console. WriteLine ("Okänd färg!"); retur unknownColorIndex; }}

Sorteringslogik

Sorteringsfunktionen sammanför alla bitar för att faktiskt sortera pärlor. Denna funktion körs i en dedikerad tråd; flytta topplattan, upptäcka pärlfärgen, placera den i en papperskorg, se till att topplattan håller sig i linje, räkna pärlorna etc. Det slutar också att springa när avfallsbehållaren blir full - Annars hamnar vi bara med överfulla pärlor.

Thread colourTestThread; Boolean runtest = false; void colourTest () {

om (! top. Engaged)

top. Engaged = true;

om (! bottom. Engaged)

bottom. Engaged = true;

medan (runtest) {

nextMagnet (true);

Tråd. Sömn (100); prova {if (magSensor. SensorValue <(magSensorMax - 4)) alignMotor (); } fånga {alignMotor (); }

nextCamera (true);

detektera = sant;

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

Färg c = detekteradColor;

this. BeginInvoke (ny åtgärd (setColorDet), nytt objekt {c}); int i = findColorPosition (c);

SetBottomPosition (i, true);

nextHole (true); colorCnts ++; this. BeginInvoke (ny åtgärd (setColorTxt), nytt objekt {i}); Tråd. Sömn (250);

if (colorCnts [unknownColorIndex]> 500) {

top. Engaged = false; bottom. Engaged = false; runtest = false; this. BeginInvoke (ny åtgärd (setGoGreen), null); lämna tillbaka; }}}

private void colourTestBtn_Click (objektavsändare, EventArgs e) {

if (colourTestThread == null ||! colourTestThread. IsAlive) {colourTestThread = new Thread (new ThreadStart (colourTest)); runtest = true; colourTestThread. Start (); colourTestBtn. Text = "STOPP"; colourTestBtn. BackColor = Color. Red; } annat {runtest = false; colourTestBtn. Text = "GO"; colourTestBtn. BackColor = Color. Green; }}

Vid denna tidpunkt har vi ett arbetsprogram. Några bitar av kod lämnades utanför artikeln, så ta en titt på källan för att faktiskt köra den.

Optik tävling
Optik tävling

Andra pris i optiktävlingen

Rekommenderad: