Índice:

Classificação robótica de esferas: 3 etapas (com imagens)
Classificação robótica de esferas: 3 etapas (com imagens)

Vídeo: Classificação robótica de esferas: 3 etapas (com imagens)

Vídeo: Classificação robótica de esferas: 3 etapas (com imagens)
Vídeo: As 3 etapas da inteligência artificial e por que 3ª pode ser fatal 2024, Julho
Anonim
Image
Image
Classificação robótica de esferas
Classificação robótica de esferas
Classificação robótica de esferas
Classificação robótica de esferas
Classificação robótica de esferas
Classificação robótica de esferas

Neste projeto, estaremos construindo um robô para classificar as contas de Perler por cor.

Sempre quis construir um robô de classificação de cores, então, quando minha filha se interessou pela criação de pérolas de Perler, vi isso como uma oportunidade perfeita.

As contas de Perler são usadas para criar projetos de arte fundidos, colocando muitas contas em um pegboard e, em seguida, derretendo-as com um ferro. Você geralmente compra essas contas em pacotes gigantes de 22.000 contas com várias cores e passa muito tempo procurando a cor que deseja, então pensei que classificá-las aumentaria a eficiência da arte.

Eu trabalho para a Phidgets Inc., então usei principalmente Phidgets para este projeto - mas isso poderia ser feito usando qualquer hardware adequado.

Etapa 1: Hardware

Aqui está o que usei para construir isso. Eu o construí 100% com peças do phidgets.com e coisas que eu tinha espalhadas pela casa.

Placas Phidgets, Motores, Hardware

  • HUB0000 - VINT Hub Phidget
  • 1108 - Sensor Magnético
  • 2x STC1001 - 2,5A Stepper Phidget
  • 2x 3324 - 42STH38 NEMA-17 Bipolar Gearless Stepper
  • 3x 3002 - Cabo Phidget 60 cm
  • 3403 - Hub USB 2.0 de 4 portas
  • 3031 - Pigtail Fêmea 5,5x2,1mm
  • 3029 - cabo trançado de 2 fios de 100 '
  • 3604 - LED branco de 10 mm (bolsa de 10)
  • 3402 - Webcam USB

Outras Partes

  • Fonte de alimentação 24VDC 2.0A
  • Sucata de madeira e metal da garagem
  • Gravatas zip
  • Recipiente de plástico com o fundo cortado

Etapa 2: projetar o robô

Projete o robô
Projete o robô
Projete o robô
Projete o robô
Projete o robô
Projete o robô

Precisamos projetar algo que possa pegar um único grânulo do funil de entrada, colocá-lo sob a webcam e, em seguida, movê-lo para o compartimento apropriado.

Coleta de Contas

Resolvi fazer a 1ª parte com 2 peças de contraplacado redondo, cada uma com um furo no mesmo local. A peça inferior é fixada e a peça superior é conectada a um motor de passo, que pode girá-la sob uma tremonha cheia de miçangas. Quando o orifício passa por baixo da caçamba, ele pega um único grânulo. Posso, então, girá-lo sob a webcam e girar ainda mais até que ele corresponda ao orifício na parte inferior, ponto em que ele cai.

Nesta foto, estou testando se o sistema pode funcionar. Tudo é consertado, exceto a peça redonda superior de madeira compensada, que está presa a um motor de passo fora do campo de visão embaixo. A webcam ainda não foi montada. Estou apenas usando o painel de controle Phidget para ligar o motor neste momento.

Armazenamento de grânulos

A próxima parte é projetar o sistema de escaninho para conter cada cor. Decidi usar um segundo motor de passo abaixo para apoiar e girar um recipiente redondo com compartimentos espaçados uniformemente. Isso pode ser usado para girar o compartimento correto sob o orifício de onde o cordão cairá.

Eu construí usando papelão e fita adesiva. O mais importante aqui é a consistência - cada compartimento deve ter o mesmo tamanho e tudo deve ter o peso uniforme para que gire sem pular.

A remoção do grânulo é realizada por meio de uma tampa de encaixe apertado que expõe um único compartimento de cada vez, para que os grânulos possam ser despejados.

Câmera

A webcam é montada sobre a placa superior entre a tremonha e o local do orifício da placa inferior. Isso permite que o sistema olhe para o cordão antes de largá-lo. Um LED é usado para iluminar as contas sob a câmera e a luz ambiente é bloqueada, a fim de fornecer um ambiente de iluminação consistente. Isso é muito importante para a detecção precisa de cores, pois a iluminação ambiente pode realmente prejudicar a percepção das cores.

Detecção de localização

É importante que o sistema seja capaz de detectar a rotação do separador de cordões. Isso é usado para definir a posição inicial durante a inicialização, mas também para detectar se o motor de passo saiu de sincronia. No meu sistema, às vezes um cordão emperra ao ser pego, e o sistema precisava ser capaz de detectar e lidar com essa situação - recuando um pouco e tentando novamente.

Existem muitas maneiras de lidar com isso. Decidi usar um sensor magnético 1108, com um ímã embutido na borda da placa superior. Isso me permite verificar a posição em cada rotação. Uma solução melhor provavelmente seria um codificador no motor de passo, mas eu tinha um 1108 por perto, então o usei.

Concluir o robô

Neste ponto, tudo foi resolvido e testado. É hora de montar tudo bem e começar a escrever software.

Os 2 motores de passo são acionados por controladores de passo STC1001. Um hub HUB000 - USB VINT é usado para operar os controladores de passo, bem como ler o sensor magnético e acionar o LED. A webcam e o HUB0000 estão conectados a um pequeno hub USB. Um pigtail 3031 e alguns fios são usados junto com uma fonte de alimentação de 24 V para alimentar os motores.

Etapa 3: escrever o código

Image
Image

C # e Visual Studio 2015 são usados para este projeto. Baixe o código-fonte no topo desta página e acompanhe - as seções principais estão descritas abaixo

Inicialização

Primeiro, devemos criar, abrir e inicializar os objetos Phidget. Isso é feito no evento de carregamento do formulário e nos manipuladores de anexos do Phidget.

private void Form1_Load (object sender, EventArgs e) {

/ * Inicializar e 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 (object sender, Phidget22. Events. AttachEventArgs e) {

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

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

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

private void Bottom_Attach (objeto remetente, 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 (objeto remetente, 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; }

Também lemos todas as informações de cores salvas durante a inicialização, para que uma execução anterior possa ser continuada.

Posicionamento do motor

O código de manuseio do motor consiste em funções convenientes para mover os motores. Os motores que usei têm 3.200 passos de 1/16 por rotação, então criei uma constante para isso.

Para o motor superior, há 3 posições que queremos enviar ao motor: a webcam, o orifício e o ímã de posicionamento. Existe uma função para viajar para cada uma dessas posições:

private void nextMagnet (Boolean wait = false) {

posição dupla = top. Position% stepsPerRev;

top. TargetPosition + = (stepsPerRev - posn);

se (espere)

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

private void nextCamera (Boolean wait = false) {

posição dupla = 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 (espere)

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

private void nextHole (Boolean wait = false) {

posição dupla = 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 (espere)

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

Antes de iniciar uma corrida, a placa superior é alinhada usando o sensor magnético. A função alignMotor pode ser chamada a qualquer momento para alinhar a placa superior. Esta função primeiro gira rapidamente a placa até 1 volta completa até que veja os dados magnéticos acima de um limite. Em seguida, ele recua um pouco e avança lentamente novamente, capturando os dados do sensor à medida que avança. Finalmente, ele define a posição para a localização máxima dos dados do ímã e redefine o deslocamento da posição para 0. Assim, a posição máxima do ímã deve estar sempre em (top. Position% stepsPerRev)

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

// Encontre o ímã

top. DataInterval = top. MinDataInterval;

sawMagnet = false;

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

int tryCount = 0;

tente novamente:

top. TargetPosition + = stepsPerRev;

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

if (! sawMagnet) {

if (tryCount> 3) {Console. WriteLine ("Falha no alinhamento"); top. Engaged = false; bottom. Engaged = false; runtest = false; Retorna; }

tryCount ++;

Console. WriteLine ("Estamos travados? Tentando um backup…"); top. TargetPosition - = 600; while (top. IsMoving) Thread. Sleep (100);

goto tryagain;

}

top. VelocityLimit = -100;

magData = nova 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 em 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 ("Alinhamento bem-sucedido");

}

Lista> magData;

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

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

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

Por último, o motor inferior é controlado enviando-o para uma das posições do contêiner de grânulos. Para este projeto, temos 19 posições. O algoritmo está escolhendo um caminho mais curto e gira no sentido horário ou anti-horário.

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

double currentPosn = bottom. Position% stepsPerRev;

double posnDiff = targetPosn - currentPosn;

// Mantenha-o como etapas completas

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

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

se (espere)

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

Câmera

OpenCV é usado para ler imagens da webcam. O thread da câmera é iniciado antes de iniciar o thread de classificação principal. Este tópico lê continuamente as imagens, calcula uma cor média para uma região específica usando a Média e atualiza uma variável de cor global. A linha também emprega HoughCircles para tentar detectar um cordão ou o orifício na placa superior, para refinar a área que está olhando para a detecção de cor. O limite e os números de HoughCircles foram determinados por tentativa e erro e dependem muito da webcam, iluminação e espaçamento.

bool runVideo = true; bool videoRunning = false; Captura VideoCapture; Thread cvThread; ColorectedColor; Detecção booleana = falso; int detectCnt = 0;

private void cvThreadFunction () {

videoRunning = false;

captura = novo VideoCapture (selectedCamera);

usando (janela janela = nova janela ("captura")) {

Imagem do tapete = novo tapete (); Mat image2 = novo Mat (); while (runVideo) {capture. Read (imagem); if (image. Empty ()) quebra;

if (detectando)

detectCnt ++; senão detectCnt = 0;

if (detectando || circleDetectChecked || showDetectionImgChecked) {

Cv2. CvtColor (imagem, imagem2, ColorConversionCodes. BGR2GRAY); Mat thres = image2. Threshold ((duplo) Properties. Settings. Default.videoThresh, 255, ThresholdTypes. Binary); thres = thres. GaussianBlur (novo OpenCvSharp. Size (9, 9), 10);

if (showDetectionImgChecked)

imagem = thres;

if (detectando || circleDetectChecked) {

CircleSegment bead = thres. HoughCircles (HoughMethods. Gradient, 2, /*thres. Rows/4*/ 20, 200, 100, 20, 65); if (cordão. Comprimento> = 1) {imagem. Círculo (cordão [0]. Centro, 3, novo Escalar (0, 100, 0), -1); imagem. Circle (bead [0]. Center, (int) bead [0]. Radius, new Scalar (0, 0, 255), 3); if (cordão [0]. Radius> = 55) {Propriedades. Settings. Default.x = (decimal) cordão [0]. Center. X + (decimal) (cordão [0]. Radius / 2); Properties. Settings. Default.y = (decimal) bead [0]. Center. Y - (decimal) (bead [0]. Radius / 2); } else {Properties. Settings. Default.x = (decimal) talão [0]. Center. X + (decimal) (talão [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; } outro {

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 = image. Width - 15; if (medianaY> imagem. Altura - 15) medianaY = imagem. Altura - 15;

imagem. Círculo (medianX, medianY, 100, novo escalar (0, 0, 150), 3);

if (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 = novo Rect ((int) Properties. Settings. Default.x, (int) Properties. Settings. Default.y, (int) Properties. Settings. Default.size, (int) Properties. Settings. Default.height);

Mat beadSample = novo Mat (imagem, r);

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

imagem. Retângulo (r, novo escalar (0, 150, 0));

window. ShowImage (imagem);

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

videoRunning = false;

} }

private void cameraStartBtn_Click (objeto remetente, EventArgs e) {

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

cvThread = novo Tópico (novo ThreadStart (cvThreadFunction)); runVideo = true; cvThread. Start (); cameraStartBtn. Text = "parar"; while (! videoRunning) Thread. Sleep (100);

updateColorTimer. Start ();

} outro {

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

Cor

Agora, podemos determinar a cor de uma conta e decidir com base nessa cor em qual recipiente colocá-la.

Esta etapa depende da comparação de cores. Queremos ser capazes de distinguir as cores para limitar os falsos positivos, mas também permitir um limite suficiente para limitar os falsos negativos. Comparar cores é surpreendentemente complexo, porque a maneira como os computadores armazenam cores como RGB e a maneira como os humanos percebem as cores não se correlacionam linearmente. Para piorar as coisas, a cor da luz sob a qual uma cor está sendo visualizada também deve ser levada em consideração.

Existem algoritmos complicados para calcular a diferença de cores. Usamos CIE2000, que produz um número próximo a 1 se 2 cores forem indistinguíveis para um humano. Estamos usando a biblioteca ColorMine C # para fazer esses cálculos complicados. Foi descoberto que um valor DeltaE de 5 oferece um bom meio-termo entre falso positivo e falso negativo.

Como geralmente há mais cores do que recipientes, a última posição é reservada como uma caixa geral. Eu geralmente os coloco de lado para passar pela máquina em uma segunda passagem.

Lista

cores = nova Lista (); Lista colorPanels = nova Lista (); Lista colorsTxts = new List (); List colorCnts = new List ();

const int numColorSpots = 18;

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

Console. WriteLine ("Encontrando cor…");

var cRGB = novo Rgb ();

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

int bestMatch = -1;

matchDelta duplo = 100;

para (int i = 0; i <cores. Contagem; i ++) {

var RGB = novo Rgb ();

RGB. R = cores . R; RGB. G = cores . G; RGB. B = cores . B;

delta duplo = cRGB. Compare (RGB, novo CieDe2000Comparison ());

// delta duplo = deltaE (c, cores ); 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 ("Nova cor!"); cores. Adicionar (c); this. BeginInvoke (nova ação (setBackColor), novo objeto {cores. Contagem - 1}); writeOutColors (); retorno (cores. Contagem - 1); } else {Console. WriteLine ("Cor desconhecida!"); return unknownColorIndex; }}

Lógica de classificação

A função de classificação reúne todas as peças para realmente classificar as contas. Esta função é executada em um thread dedicado; movendo a placa superior, detectando a cor do cordão, colocando-a em uma lixeira, certificando-se de que a placa superior fique alinhada, contando as contas, etc. Ele também para de funcionar quando a lixeira fica cheia - caso contrário, acabaremos com contas transbordando.

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

if (! top. Engaged)

top. Engaged = true;

if (! bottom. Engaged)

bottom. Engaged = true;

while (runtest) {

nextMagnet (verdadeiro);

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

nextCamera (verdadeiro);

detectando = verdadeiro;

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

Cor c = cor detectada;

this. BeginInvoke (nova Ação (setColorDet), novo objeto {c}); int i = findColorPosition (c);

SetBottomPosition (i, true);

nextHole (verdadeiro); colorCnts ++; this. BeginInvoke (nova ação (setColorTxt), novo objeto {i}); Thread. Sleep (250);

if (colorCnts [unknownColorIndex]> 500) {

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

private void colourTestBtn_Click (objeto remetente, EventArgs e) {

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

Neste ponto, temos um programa de trabalho. Alguns pedaços de código foram deixados de fora do artigo, então dê uma olhada no código-fonte para realmente executá-lo.

Concurso de Óptica
Concurso de Óptica

Segundo Prêmio no Concurso de Óptica

Recomendado: