Tabla de contenido:

Clasificación robótica de cuentas: 3 pasos (con imágenes)
Clasificación robótica de cuentas: 3 pasos (con imágenes)

Video: Clasificación robótica de cuentas: 3 pasos (con imágenes)

Video: Clasificación robótica de cuentas: 3 pasos (con imágenes)
Video: Cinemática Directa: Parámetros de Denavit Hartenberg 2024, Mes de julio
Anonim
Image
Image
Clasificación robótica de perlas
Clasificación robótica de perlas
Clasificación robótica de perlas
Clasificación robótica de perlas
Clasificación robótica de perlas
Clasificación robótica de perlas

En este proyecto, construiremos un robot para clasificar cuentas Perler por color.

Siempre quise construir un robot de clasificación por color, así que cuando mi hija se interesó en la elaboración de cuentas Perler, vi esta como una oportunidad perfecta.

Las cuentas de Perler se utilizan para crear proyectos de arte fusionado colocando muchas cuentas en un tablero de clavijas y luego fundiéndolas con una plancha. Por lo general, compra estas cuentas en paquetes gigantes de 22, 000 cuentas de colores mezclados y pasa mucho tiempo buscando el color que desea, por lo que pensé que clasificarlas aumentaría la eficiencia del arte.

Trabajo para Phidgets Inc., así que usé principalmente Phidgets para este proyecto, pero esto podría hacerse usando cualquier hardware adecuado.

Paso 1: hardware

Esto es lo que usé para construir esto. Lo construí al 100% con partes de phidgets.com y cosas que tenía por la casa.

Placas Phidgets, Motores, Hardware

  • HUB0000 - Phidget VINT Hub
  • 1108 - Sensor magnético
  • 2x STC1001 - Phidget paso a paso 2.5A
  • 2x 3324 - 42STH38 NEMA-17 Bipolar paso a paso sin engranajes
  • 3x 3002 - Cable Phidget 60cm
  • 3403 - Concentrador de 4 puertos USB2.0
  • 3031 - Coleta hembra 5.5x2.1mm
  • 3029 - Cable trenzado de 100 'de 2 hilos
  • 3604 - LED blanco de 10 mm (bolsa de 10)
  • 3402 - Cámara web USB

Otras partes

  • Fuente de alimentación 24VDC 2.0A
  • Chatarra de madera y metal del garaje.
  • Corbatas con cremallera
  • Recipiente de plástico con el fondo cortado

Paso 2: diseñe el robot

Diseña el Robot
Diseña el Robot
Diseña el Robot
Diseña el Robot
Diseña el Robot
Diseña el Robot

Necesitamos diseñar algo que pueda tomar una sola cuenta de la tolva de entrada, colocarla debajo de la cámara web y luego moverla al contenedor apropiado.

Recogida de cuentas

Decidí hacer la 1ª parte con 2 piezas de madera contrachapada redonda, cada una con un agujero perforado en el mismo lugar. La pieza inferior está fija y la pieza superior está unida a un motor paso a paso, que puede girarlo debajo de una tolva llena de perlas. Cuando el orificio pasa por debajo de la tolva, recoge una sola gota. Luego puedo girarlo debajo de la cámara web y luego girarlo más hasta que coincida con el orificio en la pieza inferior, momento en el que se cae.

En esta imagen, estoy probando que el sistema pueda funcionar. Todo está fijo, excepto la pieza redonda superior de madera contrachapada, que está unida a un motor paso a paso fuera de la vista debajo. La cámara web aún no se ha montado. Solo estoy usando el Panel de control de Phidget para convertirlo en motor en este punto.

Almacenamiento de cuentas

La siguiente parte es diseñar el sistema de contenedores para contener cada color. Decidí usar un segundo motor paso a paso a continuación para apoyar y rotar un contenedor redondo con compartimentos espaciados uniformemente. Esto se puede usar para rotar el compartimiento correcto debajo del orificio del que saldrá la cuenta.

Construí esto usando cartón y cinta adhesiva. Lo más importante aquí es la consistencia: cada compartimento debe tener el mismo tamaño y todo debe tener un peso uniforme para que gire sin saltos.

La eliminación de las cuentas se logra mediante una tapa hermética que deja al descubierto un solo compartimento a la vez, de modo que las cuentas se puedan verter.

Cámara

La cámara web está montada sobre la placa superior entre la tolva y la ubicación del orificio de la placa inferior. Esto permite que el sistema mire la cuenta antes de dejarla caer. Se utiliza un LED para iluminar las cuentas debajo de la cámara y se bloquea la luz ambiental para proporcionar un entorno de iluminación constante. Esto es muy importante para una detección de color precisa, ya que la iluminación ambiental realmente puede alterar el color percibido.

Detección de ubicación

Es importante que el sistema pueda detectar la rotación del separador de perlas. Se utiliza para configurar la posición inicial al arrancar, pero también para detectar si el motor paso a paso se ha desincronizado. En mi sistema, una cuenta a veces se atasca mientras se recoge, y el sistema necesitaba poder detectar y manejar esta situación, retrocediendo un poco y volviendo a intentarlo.

Hay muchas formas de manejar esto. Decidí usar un sensor magnético 1108, con un imán incrustado en el borde de la placa superior. Esto me permite verificar la posición en cada rotación. Una mejor solución probablemente sería un codificador en el motor paso a paso, pero tenía un 1108 por ahí, así que lo usé.

Termina el robot

En este punto, todo ha sido resuelto y probado. Es hora de montar todo bien y pasar al software de escritura.

Los 2 motores paso a paso son impulsados por controladores paso a paso STC1001. Se utiliza un concentrador HUB000 - USB VINT para ejecutar los controladores paso a paso, así como para leer el sensor magnético y activar el LED. La cámara web y el HUB0000 están conectados a un pequeño concentrador USB. Se utiliza una coleta 3031 y algo de cable junto con una fuente de alimentación de 24 V para alimentar los motores.

Paso 3: Escriba el código

Image
Image

C # y Visual Studio 2015 se utilizan para este proyecto. Descargue la fuente en la parte superior de esta página y sígala: las secciones principales se describen a continuación

Inicialización

Primero, debemos crear, abrir e inicializar los objetos Phidget. Esto se hace en el evento de carga de formulario y los controladores adjuntos de Phidget.

Private void Form1_Load (remitente del objeto, EventArgs e) {

/ * Inicializar y abrir 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 = 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 (remitente del objeto, Phidget22. Events. AttachEventArgs e) {

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

private void MagSensor_Attach (remitente del objeto, Phidget22. Events. AttachEventArgs e) {

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

private void Bottom_Attach (remitente del objeto, Phidget22. Events. AttachEventArgs e) {

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

private void Top_Attach (remitente del objeto, Phidget22. Events. AttachEventArgs e) {

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

También leemos cualquier información de color guardada durante la inicialización, por lo que se puede continuar con una ejecución anterior.

Posicionamiento del motor

El código de manejo del motor consta de funciones de conveniencia para mover los motores. Los motores que utilicé son 3, 200 1/16 pasos por revolución, así que creé una constante para esto.

Para el motor superior, hay 3 posiciones que queremos poder enviar al motor: la cámara web, el orificio y el imán de posicionamiento. Hay una función para viajar a cada una de estas posiciones:

nextMagnet vacío privado (espera booleana = falso) {

doble posn = top. Position% stepsPerRev;

top. TargetPosition + = (stepsPerRev - posn);

si (espera)

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

private void nextCamera (espera booleana = falso) {

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

si (espera)

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

private void nextHole (espera booleana = falso) {

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

si (espera)

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

Antes de iniciar una ejecución, la placa superior se alinea mediante el sensor magnético. La función alignMotor se puede llamar en cualquier momento para alinear la placa superior. Esta función primero gira rápidamente la placa hasta 1 revolución completa hasta que ve los datos del imán por encima de un umbral. Luego retrocede un poco y avanza nuevamente lentamente, capturando los datos del sensor a medida que avanza. Finalmente, establece la posición en la ubicación máxima de datos del imán y restablece el desplazamiento de posición a 0. Por lo tanto, la posición máxima del imán siempre debe estar en (top. Position% stepsPerRev)

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

// Encuentra el imán

top. DataInterval = top. MinDataInterval;

sawMagnet = falso;

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

int tryCount = 0;

intentar otra vez:

top. TargetPosition + = stepsPerRev;

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

if (! sawMagnet) {

if (tryCount> 3) {Console. WriteLine ("Falló la alineación"); top. Engaged = falso; bottom. Engaged = falso; runtest = false; regreso; }

tryCount ++;

Console. WriteLine ("¿Estamos atascados? Intentando una copia de seguridad …"); top. TargetPosition - = 600; while (top. IsMoving) Thread. Sleep (100);

Vaya a intentarlo de nuevo;

}

top. VelocityLimit = -100;

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

magSensor. SensorChange - = magSensorCollectPositionData;

top. VelocityLimit = -topVelocityLimit;

KeyValuePair max = magData [0];

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

top. AddPositionOffset (-max. Key);

magSensorMax = max. Value;

top. TargetPosition = 0;

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

Console. WriteLine ("Alinear correctamente");

}

Lista> magData;

privado vacío magSensorCollectPositionData (remitente del objeto, Phidget22. Events. VoltageRatioInputSensorChangeEventArgs e) {magData. Add (nuevo KeyValuePair (top. Position, e. SensorValue)); }

private void magSensorStopMotor (remitente del objeto, Phidget22. Events. VoltageRatioInputSensorChangeEventArgs e) {

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

Por último, el motor inferior se controla enviándolo a una de las posiciones del contenedor de perlas. Para este proyecto, tenemos 19 puestos. El algoritmo elige un camino más corto y gira en sentido horario o antihorario.

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

return (int) Math. Round (((posn * beadCompartments) / (doble) pasosPerRev));

} }

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

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

double currentPosn = bottom. Position% stepsPerRev;

doble posnDiff = targetPosn - currentPosn;

// Mantenlo como pasos completos

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ámara

OpenCV se utiliza para leer imágenes de la cámara web. El hilo de la cámara se inicia antes de comenzar el hilo de clasificación principal. Este hilo lee continuamente imágenes, calcula un color promedio para una región específica usando Mean y actualiza una variable de color global. El hilo también emplea HoughCircles para tratar de detectar una cuenta o el agujero en la placa superior, para refinar el área que está mirando para la detección de color. El umbral y los números de HoughCircles se determinaron mediante prueba y error, y dependen en gran medida de la cámara web, la iluminación y el espaciado.

bool runVideo = verdadero; bool videoRunning = false; Captura de VideoCapture; Thread cvThread; Color detectadoColor; Detección booleana = falso; int detectCnt = 0;

private void cvThreadFunction () {

videoRunning = false;

capture = new VideoCapture (selectedCamera);

usando (Ventana ventana = nueva ventana ("captura")) {

Imagen del tapete = nuevo tapete (); Mat image2 = nuevo Mat (); while (runVideo) {capture. Read (imagen); if (image. Empty ()) break;

si (detectando)

detectCnt ++; else detectCnt = 0;

if (detectando || circleDetectChecked || showDetectionImgChecked) {

Cv2. CvtColor (imagen, imagen2, ColorConversionCodes. BGR2GRAY); Mat thres = image2. Threshold ((doble) Properties. Settings. Default.videoThresh, 255, ThresholdTypes. Binary); thres = thres. GaussianBlur (nuevo OpenCvSharp. Size (9, 9), 10);

si (showDetectionImgChecked)

imagen = thres;

if (detectando || 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; } demás {

CircleSegment círculos = thres. HoughCircles (HoughMethods. Gradient, 2, /*thres. Rows/4*/ 5, 200, 100, 60, 180);

if (círculos. Longitud> 1) {Lista xs = círculos. Seleccionar (c => c. Center. X). ToList (); xs. Sort (); Lista ys = círculos. Seleccione (c => c. Center. Y). ToList (); ys. Sort ();

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

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

si (medianX> image. Width - 15)

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

image. Circle (medianX, medianY, 100, nuevo escalar (0, 0, 150), 3);

si (detectando) {

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

Mat beadSample = nuevo Mat (imagen, r);

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

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

window. ShowImage (imagen);

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

videoRunning = false;

} }

private void cameraStartBtn_Click (remitente del objeto, EventArgs e) {

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

cvThread = nuevo hilo (nuevo ThreadStart (cvThreadFunction)); runVideo = true; cvThread. Start (); cameraStartBtn. Text = "detener"; while (! videoRunning) Thread. Sleep (100);

updateColorTimer. Start ();

} demás {

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

Color

Ahora, podemos determinar el color de una cuenta y decidir en base a ese color en qué recipiente colocarla.

Este paso se basa en la comparación de colores. Queremos poder diferenciar los colores para limitar los falsos positivos, pero también permitir un umbral suficiente para limitar los falsos negativos. Comparar colores es sorprendentemente complejo, porque la forma en que las computadoras almacenan los colores como RGB y la forma en que los humanos perciben los colores no se correlacionan linealmente. Para empeorar las cosas, también debe tenerse en cuenta el color de la luz bajo la que se ve un color.

Hay un algoritmo complicado para calcular la diferencia de color. Usamos CIE2000, que genera un número cercano a 1 si 2 colores serían indistinguibles para un humano. Estamos utilizando la biblioteca ColorMine C # para realizar estos complicados cálculos. Se ha descubierto que un valor DeltaE de 5 ofrece un buen compromiso entre falso positivo y falso negativo.

Como a menudo hay más colores que contenedores, la última posición se reserva como contenedor de recogida. Por lo general, los dejo a un lado para que corran a través de la máquina en una segunda pasada.

Lista

colores = nueva Lista (); Lista de paneles de colores = nueva Lista (); List colorsTxts = new List (); List colorCnts = new List ();

const int numColorSpots = 18;

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

Console. WriteLine ("Buscando color…");

var cRGB = nuevo Rgb ();

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

int bestMatch = -1;

doble coincidenciaDelta = 100;

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

var RGB = nuevo Rgb ();

RGB. R = colores . R; RGB. G = colores . G; RGB. B = colores . B;

doble delta = cRGB. Compare (RGB, nuevo CieDe2000Comparison ());

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

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

if (colors. Count <numColorSpots) {Console. WriteLine ("¡Nuevo color!"); colores. Añadir (c); this. BeginInvoke (nueva acción (setBackColor), nuevo objeto {colors. Count - 1}); writeOutColors (); return (colors. Count - 1); } else {Console. WriteLine ("¡Color desconocido!"); return unknownColorIndex; }}

Lógica de clasificación

La función de clasificación reúne todas las piezas para clasificar las cuentas. Esta función se ejecuta en un hilo dedicado; moviendo la placa superior, detectando el color de las cuentas, colocándolas en un contenedor, asegurándose de que la placa superior permanezca alineada, contando las cuentas, etc. También deja de funcionar cuando el contenedor de recolección se llena; de lo contrario, terminamos con cuentas desbordadas.

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

if (! top. Engaged)

top. Engaged = verdadero;

if (! bottom. Engaged)

bottom. Engaged = verdadero;

while (prueba de ejecución) {

nextMagnet (verdadero);

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

nextCamera (verdadero);

detectando = verdadero;

while (detectCnt <5) Thread. Sleep (25); Console. WriteLine ("Detectar recuento:" + detectCnt); detectar = falso;

Color c = color detectado;

this. BeginInvoke (nueva acción (setColorDet), nuevo objeto {c}); int i = findColorPosition (c);

SetBottomPosition (i, verdadero);

nextHole (verdadero); colorCnts ++; this. BeginInvoke (nueva acción (setColorTxt), nuevo objeto {i}); Thread. Sleep (250);

if (colorCnts [unknownColorIndex]> 500) {

top. Engaged = falso; bottom. Engaged = falso; runtest = false; this. BeginInvoke (nueva acción (setGoGreen), nulo); regreso; }}}

private void colourTestBtn_Click (remitente del objeto, EventArgs e) {

if (colourTestThread == null ||! colourTestThread. IsAlive) {colourTestThread = new Thread (nuevo ThreadStart (colourTest)); runtest = true; colourTestThread. Start (); colourTestBtn. Text = "DETENER"; colourTestBtn. BackColor = Color. Red; } else {prueba de ejecución = falso; colourTestBtn. Text = "IR"; colourTestBtn. BackColor = Color. Green; }}

En este punto, tenemos un programa de trabajo. Algunos fragmentos de código se dejaron fuera del artículo, así que eche un vistazo a la fuente para ejecutarlo.

Concurso de Óptica
Concurso de Óptica

Segundo premio en el Concurso de Óptica

Recomendado: