Inhoudsopgave:

Sorteren van robotparels: 3 stappen (met afbeeldingen)
Sorteren van robotparels: 3 stappen (met afbeeldingen)

Video: Sorteren van robotparels: 3 stappen (met afbeeldingen)

Video: Sorteren van robotparels: 3 stappen (met afbeeldingen)
Video: Potato harvest with AVR Puma 3 at van den Borne aardappelen 2015 2024, November
Anonim
Image
Image
Sorteren van robotparels
Sorteren van robotparels
Sorteren van robotparels
Sorteren van robotparels
Sorteren van robotparels
Sorteren van robotparels

In dit project gaan we een robot bouwen om Perler-kralen op kleur te sorteren.

Ik heb altijd al een kleursorteerrobot willen bouwen, dus toen mijn dochter geïnteresseerd raakte in het maken van Perler-kralen, zag ik dit als een perfecte kans.

Perler-kralen worden gebruikt om samengesmolten kunstprojecten te maken door veel kralen op een pegboard te plaatsen en ze vervolgens samen met een strijkijzer te smelten. Je koopt deze kralen over het algemeen in gigantische kleurenpakketten van 22.000 kralen en besteedt veel tijd aan het zoeken naar de gewenste kleur, dus ik dacht dat het sorteren ervan de kunstefficiëntie zou verhogen.

Ik werk voor Phidgets Inc. dus ik gebruikte voornamelijk Phidgets voor dit project - maar dit kan worden gedaan met behulp van elke geschikte hardware.

Stap 1: Hardware

Hier is wat ik heb gebruikt om dit te bouwen. Ik heb het 100% gebouwd met onderdelen van phidgets.com en dingen die ik in huis had liggen.

Phidgets-borden, motoren, hardware

  • HUB0000 - VINT Hub Phidget
  • 1108 - Magnetische sensor
  • 2x STC1001 - 2.5A Stepper Phidget
  • 2x 3324 - 42STH38 NEMA-17 Bipolaire Gearless Stepper
  • 3x 3002 - Phidget Kabel 60cm
  • 3403 - USB 2.0 4-poorts hub
  • 3031 - Vrouwelijke vlecht 5,5x2,1 mm
  • 3029 - 2-aderige 100' getwiste kabel
  • 3604 - 10 mm witte LED (zak van 10)
  • 3402 - USB-webcam

Overige onderdelen

  • 24VDC 2.0A voeding
  • Hout en metaal uit de garage halen
  • Ritssluitingen
  • Plastic bak waarvan de bodem is afgesneden

Stap 2: Ontwerp de robot

Ontwerp de robot
Ontwerp de robot
Ontwerp de robot
Ontwerp de robot
Ontwerp de robot
Ontwerp de robot

We moeten iets ontwerpen dat een enkele kraal uit de invoertrechter kan halen, het onder de webcam kan plaatsen en het vervolgens in de juiste bak kan verplaatsen.

Kralen Ophalen

Ik besloot om het 1e deel te doen met 2 stukken rond multiplex, elk met een gat geboord op dezelfde plaats. Het onderste stuk is vast en het bovenste stuk is bevestigd aan een stappenmotor, die het kan draaien onder een trechter gevuld met kralen. Wanneer het gat onder de trechter beweegt, pikt het een enkele kraal op. Ik kan het dan onder de webcam draaien en dan verder draaien totdat het overeenkomt met het gat in het onderste stuk, waarna het er doorheen valt.

Op deze foto test ik of het systeem kan werken. Alles zit vast, behalve het bovenste ronde stuk triplex, dat aan de onderkant uit het zicht op een stappenmotor is bevestigd. De webcam is nog niet gemonteerd. Ik gebruik gewoon het Phidget-configuratiescherm om op dit moment naar de motor te gaan.

Kralenopslag

Het volgende deel is het ontwerpen van het bakkensysteem voor het vasthouden van elke kleur. Ik besloot om een tweede stappenmotor hieronder te gebruiken om een ronde container met gelijkmatig verdeelde compartimenten te ondersteunen en te draaien. Dit kan worden gebruikt om het juiste compartiment onder het gat te draaien waar de kraal uit zal vallen.

Ik bouwde dit met behulp van karton en ducttape. Het belangrijkste hier is consistentie - elk compartiment moet dezelfde grootte hebben en het hele ding moet gelijkmatig worden gewogen, zodat het ronddraait zonder over te slaan.

Het verwijderen van kralen wordt bereikt door middel van een goed sluitend deksel dat een enkel compartiment tegelijk blootlegt, zodat de kralen kunnen worden uitgegoten.

Camera

De webcam is gemonteerd over de bovenplaat tussen de trechter en de locatie van het gat in de onderste plaat. Hierdoor kan het systeem naar de kraal kijken voordat deze valt. Een LED wordt gebruikt om de kralen onder de camera te verlichten en het omgevingslicht wordt geblokkeerd om een consistente verlichtingsomgeving te bieden. Dit is erg belangrijk voor nauwkeurige kleurdetectie, omdat omgevingslicht de waargenomen kleur echt kan verstoren.

Locatie detectie

Het is belangrijk dat het systeem de rotatie van de korrelafscheider kan detecteren. Dit wordt gebruikt om de uitgangspositie bij het opstarten in te stellen, maar ook om te detecteren of de stappenmotor niet meer synchroon loopt. In mijn systeem zal een kraal soms vastlopen tijdens het oppakken, en het systeem moest deze situatie kunnen detecteren en afhandelen - door een beetje achteruit te gaan en het opnieuw te proberen.

Er zijn veel manieren om hiermee om te gaan. Ik besloot een 1108 magnetische sensor te gebruiken, met een magneet ingebed in de rand van de bovenplaat. Hierdoor kan ik de positie bij elke rotatie verifiëren. Een betere oplossing zou waarschijnlijk een encoder op de stappenmotor zijn, maar ik had een 1108 liggen, dus die heb ik gebruikt.

Voltooi de robot

Inmiddels is alles uitgewerkt en getest. Het is tijd om alles netjes te monteren en over te gaan op het schrijven van software.

De 2 stappenmotoren worden aangedreven door STC1001 stappencontrollers. Een HUB000 - USB VINT-hub wordt gebruikt voor het uitvoeren van de stappencontrollers, het lezen van de magnetische sensor en het aansturen van de LED. De webcam en HUB0000 zijn beide aangesloten op een kleine USB-hub. Een 3031-pigtail en wat draad worden samen met een 24V-voeding gebruikt om de motoren van stroom te voorzien.

Stap 3: Code schrijven

Image
Image

Voor dit project worden C# en Visual Studio 2015 gebruikt. Download de bron bovenaan deze pagina en volg mee - de belangrijkste secties worden hieronder beschreven

Initialisatie

Eerst moeten we de Phidget-objecten maken, openen en initialiseren. Dit wordt gedaan in de vorm load-gebeurtenis en de Phidget-attach-handlers.

private void Form1_Load (object afzender, EventArgs e) {

/* Initialiseer en open Phidgets */

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

onderkant. HubPort = 1;

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

magSensor. HubPort = 2;

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

led. HubPort = 5;

led. IsHubPortDevice = waar; led. Kanaal = 0; led. Attach += Led_Attach; led. Detach += Led_Detach; led. Open(); }

private void Led_Attach (object afzender, Phidget22. Events. AttachEventArgs e) {

ledAttachedChk. Checked = waar; led. Status = waar; ledChk. Checked = waar; }

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

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

private void Bottom_Attach (object afzender, Phidget22. Events. AttachEventArgs e) {

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

private void Top_Attach(object afzender, Phidget22. Events. AttachEventArgs e) {

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

We lezen ook eventuele opgeslagen kleurinformatie in tijdens de initialisatie, zodat een eerdere run kan worden voortgezet.

Motor positionering

De motor handling code bestaat uit gemaksfuncties voor het verplaatsen van de motoren. De motoren die ik gebruikte zijn 3.200 1/16e stappen per omwenteling, dus ik heb hiervoor een constante gemaakt.

Voor de bovenste motor zijn er 3 posities die we naar de motor willen kunnen sturen: de webcam, het gat en de positioneringsmagneet. Er is een functie om naar elk van deze posities te reizen:

private void nextMagnet(Boolean wait = false) {

dubbele posn = top. Positie % stepsPerRev;

top. TargetPosition += (stepsPerRev - posn);

als (wacht)

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

private void nextCamera(Boolean wait = false) {

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

als (wacht)

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

private void nextHole(Boolean wait = false) {

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

als (wacht)

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

Voordat een run wordt gestart, wordt de bovenplaat uitgelijnd met behulp van de magnetische sensor. De functie alignMotor kan op elk moment worden aangeroepen om de bovenplaat uit te lijnen. Deze functie draait de plaat eerst snel tot 1 volledige omwenteling totdat hij magneetgegevens boven een drempel ziet. Het gaat dan een beetje achteruit en beweegt langzaam weer vooruit, terwijl het sensorgegevens vastlegt. Ten slotte stelt het de positie in op de maximale magneetgegevenslocatie en stelt het de positie-offset opnieuw in op 0. De maximale magneetpositie moet dus altijd op (top. Position % stepsPerRev) zijn

DraaduitlijningMotorDraad; Booleaanse zaagMagneet; dubbele magSensorMax = 0; privé leegte alignMotor() {

// Zoek de magneet

top. DataInterval = top. MinDataInterval;

zaagmagneet = onwaar;

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

int probeerCount = 0;

probeer het opnieuw:

top. TargetPosition += stepsPerRev;

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

if (!sawMagnet) {

if (tryCount > 3) { Console. WriteLine ("Uitlijnen mislukt"); top. Betrokken = false; bottom. Engaged = false; runtest = onwaar; opbrengst; }

probeerCount++;

Console. WriteLine("Zitten we vast? Een back-up proberen…"); top. TargetPosition -= 600; while (top. IsMoving) Thread. Sleep(100);

ga het opnieuw proberen;

}

top. VelocityLimit = -100;

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

magSensor. SensorChange -= magSensorCollectPositionData;

top. VelocityLimit = -topVelocityLimit;

KeyValuePair max = magData[0];

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

top. AddPositionOffset(-max. Key);

magSensorMax = max. Waarde;

top. TargetPosition = 0;

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

Console. WriteLine("Uitlijnen gelukt");

}

Lijst> magData;

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

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

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

Ten slotte wordt de onderste motor bestuurd door deze naar een van de hielcontainerposities te sturen. Voor dit project hebben we 19 functies. Het algoritme kiest een kortste pad en draait met de klok mee of tegen de klok in.

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

dubbele currentPosn = bottom. Positie % stepsPerRev;

dubbele posnDiff = targetPosn - currentPosn;

// Houd het als volledige stappen

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

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

als (wacht)

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

Camera

OpenCV wordt gebruikt om afbeeldingen van de webcam te lezen. De camerathread wordt gestart voordat de hoofdsorteerthread wordt gestart. Deze thread leest voortdurend afbeeldingen in, berekent een gemiddelde kleur voor een specifieke regio met behulp van Mean en werkt een globale kleurvariabele bij. De draad maakt ook gebruik van HoughCircles om te proberen een kraal of het gat in de bovenplaat te detecteren, om het gebied waar het naar kijkt te verfijnen voor de kleurdetectie. De drempel- en HoughCircles-getallen zijn met vallen en opstaan bepaald en zijn sterk afhankelijk van de webcam, verlichting en afstand.

bool runVideo = true; bool videoRunning = false; VideoCapture-opname; Draad cvDraad; Kleur gedetecteerdKleur; Booleaanse detectie = onwaar; int detectCnt = 0;

privé leegte cvThreadFunction() {

videoRunning = onwaar;

capture = nieuwe VideoCapture (selectedCamera);

met (Venstervenster = nieuw venster ("capture")) {

Mat afbeelding = nieuwe Mat(); Mat afbeelding2 = nieuwe Mat(); while (runVideo) { capture. Read (afbeelding); if (image. Empty()) pauze;

als (detectie)

detectCnt++; anders detectCnt = 0;

if (detectie || circleDetectChecked || showDetectionImgChecked) {

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

if (showDetectionImgChecked)

afbeelding = drie;

if (detectie || circleDetectChecked) {

CircleSegment kraal = 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); afbeelding. Circle(kraal[0]. Centrum, (int)kraal[0]. Radius, nieuwe Scalar(0, 0, 255), 3); if (kraal [0]. Radius >= 55) { Properties. Settings. Default.x = (decimaal)kraal[0]. Center. X + (decimaal)(kraal [0]. Radius / 2); Properties. Settings. Default.y = (decimaal)kraal[0]. Center. Y - (decimaal)(kraal[0]. Radius / 2); } else { Properties. Settings. Default.x = (decimaal)kraal[0]. Center. X + (decimaal)(kraal[0]. Radius); Properties. Settings. Default.y = (decimaal)kraal[0]. Center. Y - (decimaal)(kraal[0]. Radius); } Eigenschappen. Instellingen. Standaard.grootte = 15; Eigenschappen. Instellingen. Standaard.hoogte = 15; } anders {

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. Sorteren(); Lijst ys = cirkels. Select(c => c. Center. Y). ToList(); ys. Sorteren();

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

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

if (mediaanX > afbeelding. Breedte - 15)

medianX = afbeelding. Breedte - 15; if (mediaanY > afbeelding. Hoogte - 15) mediaanY = afbeelding. Hoogte - 15;

afbeelding. Circle (mediaanX, mediaanY, 100, nieuw scalar (0, 0, 150), 3);

als (detecteren) {

Eigenschappen. Instellingen. Standaard.x = mediaanX - 7; Eigenschappen. Instellingen. Standaard.y = mediaanY - 7; Eigenschappen. Instellingen. Standaard.grootte = 15; Eigenschappen. Instellingen. Standaard.hoogte = 15; } } } } }

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

Mat beadSample = nieuwe Mat(afbeelding, r);

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

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

venster. ShowImage(afbeelding);

Cv2. WachtKey(1); videoRunning = waar; }

videoRunning = onwaar;

} }

private void cameraStartBtn_Click (objectafzender, EventArgs e) {

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

cvThread = nieuwe Thread (nieuwe ThreadStart (cvThreadFunctie)); runVideo = waar; cvThread. Start(); cameraStartBtn. Text = "stop"; while (!videoRunning) Thread. Sleep(100);

updateColorTimer. Start();

} anders {

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

Kleur

Nu kunnen we de kleur van een kraal bepalen en op basis van die kleur beslissen in welke container we deze moeten laten vallen.

Deze stap is gebaseerd op kleurvergelijking. We willen kleuren van elkaar kunnen onderscheiden om vals-positieven te beperken, maar ook voldoende drempel toestaan om vals-negatieven te beperken. Het vergelijken van kleuren is eigenlijk verrassend ingewikkeld, omdat de manier waarop computers kleuren opslaan als RGB, en de manier waarop mensen kleuren waarnemen, niet lineair correleren. Om het nog erger te maken, moet ook rekening worden gehouden met de kleur van het licht waaronder een kleur wordt bekeken.

Er zijn ingewikkelde algoritmen voor het berekenen van kleurverschil. We gebruiken CIE2000, dat een getal in de buurt van 1 geeft als 2 kleuren niet te onderscheiden zijn voor een mens. We gebruiken de ColorMine C#-bibliotheek om deze gecompliceerde berekeningen uit te voeren. Een DeltaE-waarde van 5 blijkt een goed compromis te bieden tussen vals-positief en vals-negatief.

Omdat er vaak meer kleuren zijn dan containers, is de laatste positie gereserveerd als opvangbak. Ik leg deze over het algemeen opzij om door de machine te rennen bij een tweede doorgang.

Lijst

kleuren = nieuwe lijst ();Lijst colorPanels = nieuwe lijst (); Lijst colorsTxts = new List(); Lijst colorCnts = nieuwe lijst();

const int aantalColorSpots = 18;

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

Console. WriteLine("Kleur zoeken…");

var cRGB = nieuwe RGB();

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

int bestMatch = -1;

dubbele matchDelta = 100;

voor (int i = 0; i < kleuren. Aantal; i++) {

var RGB = nieuwe RGB();

RGB. R = kleuren. R; RGB. G = kleuren. G; RGB. B = kleuren. B;

dubbele delta = cRGB. Compare(RGB, nieuwe CieDe2000Comparison());

//dubbele delta = deltaE (c, kleuren ); Console. WriteLine("DeltaE (" + i. ToString() + "): " + delta. ToString()); if (delta < matchDelta) { matchDelta = delta; besteMatch = ik; } }

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

if (colors. Count < numColorSpots) { Console. WriteLine("Nieuwe kleur!"); kleuren. Toevoegen(c); this. BeginInvoke(new Action(setBackColor), new object { colors. Count - 1 }); writeOutColors(); retour (kleuren. Aantal - 1); } else { Console. WriteLine("Onbekende kleur!"); retourneer unknownColorIndex; } }

Sorteerlogica

De sorteerfunctie brengt alle stukjes bij elkaar om kralen daadwerkelijk te sorteren. Deze functie wordt uitgevoerd in een speciale thread; het verplaatsen van de bovenplaat, het detecteren van de kleur van de kraal, het in een bak plaatsen, ervoor zorgen dat de bovenplaat uitgelijnd blijft, het tellen van de kralen, enz. Het stopt ook met lopen wanneer de opvangbak vol raakt - anders eindigen we gewoon met overvolle kralen.

Thread colourTestThread;Boolean runtest = false; ongeldige kleurTest() {

indien (!top. Betrokken)

top. Betrokken = waar;

if (!bottom. Engaged)

bottom. Engaged = waar;

terwijl (runtest) {

volgendeMagneet(waar);

Draad. Slaap(100); probeer {if (magSensor. SensorValue < (magSensorMax - 4)) alignMotor(); } catch { alignMotor(); }

volgendeCamera(waar);

detecteren = waar;

while (detectCnt < 5) Thread. Sleep(25); Console. WriteLine("Aantal detecteren: " + detectCnt); detecteren = vals;

Kleur c = gedetecteerdKleur;

this. BeginInvoke (nieuwe actie (setColorDet), nieuw object {c}); int i = vindColorPosition(c);

SetBottomPosition(i, waar);

nextHole(true); colorCnts++; this. BeginInvoke (nieuwe actie (setColorTxt), nieuw object { i }); Draad. Slaap (250);

if (colorCnts[unknownColorIndex] > 500) {

top. Betrokken = false; bottom. Engaged = false; runtest = onwaar; this. BeginInvoke (nieuwe actie (setGoGreen), null); opbrengst; } } }

privé leegte colorTestBtn_Click (object afzender, EventArgs e) {

if (colourTestThread == null || !colourTestThread. IsAlive) { colorTestThread = new Thread (new ThreadStart (colourTest)); runtest = waar; colorTestThread. Start(); colourTestBtn. Text = "STOP"; colourTestBtn. BackColor = Kleur. Rood; } else { runtest = false; colourTestBtn. Text = "GO"; colourTestBtn. BackColor = Kleur. Groen; } }

Op dit moment hebben we een werkend programma. Sommige stukjes code zijn weggelaten uit het artikel, dus bekijk de bron om het daadwerkelijk uit te voeren.

Optica wedstrijd
Optica wedstrijd

Tweede prijs in de optiekwedstrijd

Aanbevolen: