Оглавление:

Роботизированная сортировка бус: 3 шага (с изображениями)
Роботизированная сортировка бус: 3 шага (с изображениями)

Видео: Роботизированная сортировка бус: 3 шага (с изображениями)

Видео: Роботизированная сортировка бус: 3 шага (с изображениями)
Видео: Одесса. НАЧАЛОСЬ! 2024, Июль
Anonim
Image
Image
Роботизированная сортировка бусинок
Роботизированная сортировка бусинок
Роботизированная сортировка бусинок
Роботизированная сортировка бусинок
Роботизированная сортировка бусинок
Роботизированная сортировка бусинок

В этом проекте мы создадим робота для сортировки бусинок Perler по цвету.

Я всегда хотел создать робота для сортировки по цвету, поэтому, когда моя дочь заинтересовалась изготовлением бусинок Perler, я увидел в этом прекрасную возможность.

Бусины Perler используются для создания художественных проектов путем размещения множества бусин на перфорированной доске, а затем их плавления утюгом. Обычно вы покупаете эти бусины в гигантских упаковках по 22 000 бусин смешанного цвета и тратите много времени на поиск нужного цвета, поэтому я подумал, что их сортировка повысит эффективность искусства.

Я работаю в Phidgets Inc., поэтому в этом проекте я использовал в основном Phidgets, но это можно было сделать с помощью любого подходящего оборудования.

Шаг 1. Аппаратное обеспечение

Вот что я использовал для этого. Я построил его на 100% из деталей с phidgets.com и вещей, которые лежали у меня дома.

Платы Phidgets, Моторы, Комплектующие

  • HUB0000 - VINT Hub Phidget
  • 1108 - Магнитный датчик
  • 2x STC1001 - шаговый фиджет 2,5 А
  • 2x 3324 - 42STH38 Биполярный безредукторный шаговый двигатель NEMA-17
  • 3x 3002 - кабель Phidget 60см
  • 3403 - 4-портовый концентратор USB2.0
  • 3031 - Женский косичка 5.5x2.1мм
  • 3029 - 2-жильный 100-футовый витой кабель
  • 3604 - 10 мм белый светодиод (10 шт. В упаковке)
  • 3402 - Веб-камера USB

Другие части

  • Источник питания 24VDC 2.0A
  • Лом дерева и металла из гаража
  • Застежки-молнии
  • Пластиковый контейнер с отрезанным дном

Шаг 2: спроектируйте робота

Дизайн робота
Дизайн робота
Дизайн робота
Дизайн робота
Дизайн робота
Дизайн робота

Нам нужно спроектировать что-то, что может взять одну бусину из входного бункера, поместить ее под веб-камеру, а затем переместить в соответствующий контейнер.

Подбор бус

Я решил сделать 1-ю часть из 2 кусков круглой фанеры, каждый с просверленным отверстием в одном и том же месте. Нижняя часть закреплена, а верхняя часть прикреплена к шаговому двигателю, который может вращать его под бункером, заполненным шариками. Когда отверстие проходит под бункером, оно захватывает один шарик. Затем я могу повернуть его под веб-камерой, а затем повернуть дальше, пока он не совпадет с отверстием в нижней части, после чего он провалится.

На этом изображении я проверяю, может ли система работать. Все зафиксировано, за исключением верхнего круглого куска фанеры, который прикреплен к шаговому двигателю вне поля зрения снизу. Веб-камера еще не смонтирована. Я просто использую панель управления Phidget, чтобы переключиться на двигатель.

Хранение бус

Следующая часть - это разработка системы бункеров для хранения каждого цвета. Я решил использовать второй шаговый двигатель внизу, чтобы поддерживать и вращать круглый контейнер с равномерно расположенными отсеками. Это можно использовать для поворота нужного отсека под отверстием, из которого будет выпадать бусинка.

Я построил его из картона и изоленты. Самое главное здесь - последовательность - все отделения должны быть одинакового размера, и все это должно быть равномерно взвешено, чтобы оно вращалось без пропусков.

Удаление шариков осуществляется с помощью плотно закрывающейся крышки, которая открывает доступ к одному отсеку за раз, поэтому шарики можно вылить.

Камера

Веб-камера устанавливается над верхней пластиной между бункером и отверстием в нижней пластине. Это позволяет системе посмотреть на борт перед тем, как уронить его. Светодиод используется для освещения бусинок под камерой, а окружающий свет блокируется, чтобы обеспечить постоянное освещение. Это очень важно для точного определения цвета, поскольку окружающее освещение действительно может нарушить воспринимаемый цвет.

Определение местоположения

Для системы важно иметь возможность обнаруживать вращение сепаратора шариков. Это используется для установки начального положения при запуске, а также для определения того, не рассинхронизировался ли шаговый двигатель. В моей системе бусинка иногда заклинивает, когда ее поднимают, и системе необходимо было уметь обнаруживать и обрабатывать эту ситуацию - путем небольшого резервного копирования и попытки повторить попытку.

Есть много способов справиться с этим. Я решил использовать магнитный датчик 1108 с магнитом, встроенным в край верхней пластины. Это позволяет мне проверять положение при каждом повороте. Лучшим решением, вероятно, был бы энкодер на шаговом двигателе, но у меня был 1108, поэтому я использовал его.

Завершить робота

На данный момент все проработано и протестировано. Пора все красиво смонтировать и перейти к написанию программного обеспечения.

2 шаговых двигателя управляются шаговыми контроллерами STC1001. Концентратор HUB000 - USB VINT используется для управления шаговыми контроллерами, а также для считывания показаний магнитного датчика и управления светодиодом. Веб-камера и HUB0000 подключены к небольшому USB-концентратору. Пигтейл 3031 и некоторые провода используются вместе с источником питания 24 В для питания двигателей.

Шаг 3: напишите код

Image
Image

В этом проекте используются C # и Visual Studio 2015. Загрузите исходный код вверху этой страницы и следуйте инструкциям - основные разделы описаны ниже.

Инициализация

Во-первых, мы должны создать, открыть и инициализировать объекты Phidget. Это делается в событии загрузки формы и обработчиках присоединения Phidget.

private void Form1_Load (отправитель объекта, EventArgs e) {

/ * Инициализируем и открываем 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 (отправитель объекта, Phidget22. Events. AttachEventArgs e) {

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

private void MagSensor_Attach (отправитель объекта, Phidget22. Events. AttachEventArgs e) {

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

private void Bottom_Attach (отправитель объекта, 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 (отправитель объекта, 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; }

Мы также считываем любую сохраненную информацию о цвете во время инициализации, поэтому предыдущий запуск можно продолжить.

Позиционирование двигателя

Код управления двигателем состоит из удобных функций для перемещения двигателей. Моторы, которые я использовал, имеют 3 200 1/16 шагов на оборот, поэтому я создал для этого константу.

Для верхнего двигателя есть 3 положения, которые мы хотим отправить на двигатель: веб-камера, отверстие и позиционирующий магнит. Есть функция для перехода к каждой из этих позиций:

private void nextMagnet (Boolean wait = false) {

double posn = top. Position% stepsPerRev;

top. TargetPosition + = (stepsPerRev - posn);

если (подожди)

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); иначе top. TargetPosition + = ((Properties. Settings. Default.cameraOffset - posn) + stepsPerRev);

если (подожди)

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); иначе top. TargetPosition + = ((Properties. Settings. Default.holeOffset - posn) + stepsPerRev);

если (подожди)

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

Перед началом пробежки верхняя пластина выравнивается с помощью магнитного датчика. Функцию alignMotor можно вызвать в любое время для выравнивания верхней пластины. Эта функция сначала быстро поворачивает пластину на 1 полный оборот, пока не будет обнаружено, что данные магнита превышают пороговое значение. Затем он немного отступает и снова медленно движется вперед, собирая данные датчиков по мере продвижения. Наконец, он устанавливает положение на максимальное местоположение данных магнита и сбрасывает смещение положения на 0. Таким образом, максимальное положение магнита всегда должно быть на (top. Position% stepsPerRev)

Thread alignMotorThread; Boolean sawMagnet; двойной magSensorMax = 0; private void alignMotor () {

// Находим магнит

top. DataInterval = top. MinDataInterval;

sawMagnet = false;

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

int tryCount = 0;

Попробуйте снова:

top. TargetPosition + = stepsPerRev;

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

if (! sawMagnet) {

if (tryCount> 3) {Console. WriteLine ("Ошибка выравнивания"); top. Engaged = false; bottom. Engaged = false; runtest = false; возвращение; }

tryCount ++;

Console. WriteLine («Мы застряли? Пробуем сделать резервную копию…»); top. TargetPosition - = 600; while (top. IsMoving) Thread. Sleep (100);

goto tryagain;

}

top. VelocityLimit = -100;

magData = новый список> (); magSensor. SensorChange + = magSensorCollectPositionData; top. TargetPosition + = 300; while (top. IsMoving) Thread. Sleep (100);

magSensor. SensorChange - = magSensorCollectPositionData;

top. VelocityLimit = -topVelocityLimit;

KeyValuePair max = magData [0];

foreach (пара KeyValuePair в magData) if (pair. Value> max. Value) max = pair;

top. AddPositionOffset (-max. Key);

magSensorMax = макс. значение;

top. TargetPosition = 0;

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

Console. WriteLine («Выровнять успешно»);

}

Список> magData;

private void magSensorCollectPositionData (отправитель объекта, Phidget22. Events. VoltageRatioInputSensorChangeEventArgs e) {magData. Add (новый KeyValuePair (top. Position, e. SensorValue)); }

private void magSensorStopMotor (отправитель объекта, Phidget22. Events. VoltageRatioInputSensorChangeEventArgs e) {

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

Наконец, нижний двигатель управляется путем отправки его в одно из положений контейнера для шариков. Для этого проекта у нас 19 позиций. Алгоритм выбирает кратчайший путь и поворачивает либо по часовой стрелке, либо против часовой стрелки.

private int BottomPosition {получить {int posn = (int) bottom. Position% stepsPerRev; если (posn <0) posn + = stepsPerRev;

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

} }

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

posn = posn% beadCompartments; двойной targetPosn = (posn * stepsPerRev) / beadCompartments;

двойной currentPosn = bottom. Position% stepsPerRev;

double posnDiff = targetPosn - currentPosn;

// Сохранить как полные шаги

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

если (posnDiff <= 1600) bottom. TargetPosition + = posnDiff; иначе bottom. TargetPosition - = (stepsPerRev - posnDiff);

если (подожди)

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

Камера

OpenCV используется для чтения изображений с веб-камеры. Поток камеры запускается до запуска основного потока сортировки. Этот поток постоянно считывает изображения, вычисляет средний цвет для определенной области с использованием среднего значения и обновляет глобальную цветовую переменную. Резьба также использует HoughCircles, чтобы попытаться обнаружить либо шарик, либо отверстие в верхней пластине, чтобы уточнить область, на которую она смотрит, для определения цвета. Пороговые значения и числа HoughCircles были определены методом проб и ошибок и сильно зависят от веб-камеры, освещения и расстояния между ними.

bool runVideo = true; bool videoRunning = false; VideoCapture захват; Thread cvThread; Обнаруженный цветColor; Логическое обнаружение = ложь; int detectCnt = 0;

private void cvThreadFunction () {

videoRunning = false;

захват = новый VideoCapture (selectedCamera);

using (Window window = новое окно ("захват")) {

Изображение Mat = новый Mat (); Mat image2 = новый Mat (); в то время как (runVideo) {capture. Read (изображение); если (image. Empty ()) перерыв;

если (обнаружение)

detectCnt ++; иначе detectCnt = 0;

if (обнаружение || circleDetectChecked || showDetectionImgChecked) {

Cv2. CvtColor (изображение, изображение2, ColorConversionCodes. BGR2GRAY); Mat thres = image2. Threshold ((двойной) Properties. Settings. Default.videoThresh, 255, ThresholdTypes. Binary); thres = thres. GaussianBlur (новый OpenCvSharp. Size (9, 9), 10);

если (showDetectionImgChecked)

image = thres;

if (обнаружение || 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 = (десятичный) bead [0]. Center. X + (десятичный) (bead [0]. Radius / 2); Properties. Settings. Default.y = (десятичный) шарик [0]. Center. Y - (десятичный) (шарик [0]. Радиус / 2); } else {Properties. Settings. Default.x = (десятичный) шарик [0]. Center. X + (десятичный) (шарик [0]. Радиус); Properties. Settings. Default.y = (десятичный) шарик [0]. Center. Y - (десятичный) (шарик [0]. Радиус); } Properties. Settings. Default.size = 15; Properties. Settings. Default.height = 15; } еще {

CircleSegment круги = thres. HoughCircles (HoughMethods. Gradient, 2, /*thres. Rows/4*/ 5, 200, 100, 60, 180);

if (circle. Length> 1) {Список xs = circle. Select (c => c. Center. X). ToList (); xs. Sort (); Список ys = круги. Select (c => c. Center. Y). ToList (); ys. Sort ();

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

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

если (medianX> image. Width - 15)

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

image. Circle (medianX, medianY, 100, новый Скаляр (0, 0, 150), 3);

if (обнаружение) {

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

Rect r = новый Rect ((int) Properties. Settings. Default.x, (int) Properties. Settings. Default.y, (int) Properties. Settings. Default.size, (int) Properties. Settings. Default.height);

Мат beadSample = new Mat (image, r);

Скалярный avgColor = Cv2. Mean (beadSample); DetectedColor = Color. FromArgb ((int) avgColor [2], (int) avgColor [1], (int) avgColor [0]);

image. Rectangle (r, новый Скаляр (0, 150, 0));

window. ShowImage (изображение);

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

videoRunning = false;

} }

private void cameraStartBtn_Click (объект-отправитель, EventArgs e) {

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

cvThread = новый поток (новый ThreadStart (cvThreadFunction)); runVideo = true; cvThread. Start (); cameraStartBtn. Text = "стоп"; while (! videoRunning) Thread. Sleep (100);

updateColorTimer. Start ();

} еще {

runVideo = false; cvThread. Join (); cameraStartBtn. Text = "начало"; }}

Цвет

Теперь мы можем определить цвет бусинки и на основе этого цвета решить, в какой контейнер ее бросить.

Этот шаг основан на сравнении цветов. Мы хотим иметь возможность различать цвета, чтобы ограничить ложное срабатывание, но также позволить достаточный порог для ограничения ложноотрицательных результатов. Сравнение цветов на самом деле удивительно сложно, потому что то, как компьютеры хранят цвета как RGB, и то, как люди воспринимают цвета, не коррелируют линейно. Что еще хуже, необходимо учитывать и цвет света, при котором просматривается цвет.

Есть сложный алгоритм расчета цветового различия. Мы используем CIE2000, который выводит число около 1, если два цвета будут неразличимы для человека. Мы используем библиотеку ColorMine C # для выполнения этих сложных вычислений. Было обнаружено, что значение DeltaE, равное 5, предлагает хороший компромисс между ложноположительным и ложноотрицательным.

Поскольку часто бывает больше цветов, чем контейнеров, последняя позиция зарезервирована как контейнер для сбора. Я обычно откладываю их, чтобы запустить машину на втором проходе.

Список

цвета = новый список (); список colorPanels = новый список (); Список colorsTxts = new List (); Список colorCnts = новый список ();

const int numColorSpots = 18;

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

Console. WriteLine («Подбираем цвет…»);

var cRGB = новый Rgb ();

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

int bestMatch = -1;

двойной matchDelta = 100;

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

var RGB = новый Rgb ();

RGB. R = цвета . R; RGB. G = цвета . G; RGB. B = цвета . B;

двойная дельта = cRGB. Compare (RGB, новый CieDe2000Comparison ());

// двойная дельта = deltaE (c, colors ); Console. WriteLine ("DeltaE (" + i. ToString () + "):" + delta. ToString ()); если (delta <matchDelta) {matchDelta = delta; bestMatch = я; }}

if (matchDelta <5) {Console. WriteLine ("Найдено! (Posn:" + bestMatch + "Delta:" + matchDelta + ")"); вернуть bestMatch; }

if (colors. Count <numColorSpots) {Console. WriteLine ("Новый цвет!"); colors. Add (c); this. BeginInvoke (новое действие (setBackColor), новый объект {colors. Count - 1}); writeOutColors (); return (colors. Count - 1); } else {Console. WriteLine ("Неизвестный цвет!"); return unknownColorIndex; }}

Логика сортировки

Функция сортировки объединяет все кусочки для сортировки бусинок. Эта функция выполняется в выделенном потоке; перемещая верхнюю пластину, определяя цвет шариков, помещая их в контейнер, проверяя, чтобы верхняя пластина оставалась выровненной, подсчитывая шарики и т. д. Он также перестает работать, когда контейнер для уловителя становится полным - в противном случае мы просто получим переполненные бусинки.

Thread colourTestThread; логическое runtest = false; void colourTest () {

если (! наверху задействовано)

top. Engaged = true;

если (! bottom. Engated)

bottom. Engaged = true;

while (runtest) {

nextMagnet (истина);

Thread. Sleep (100); попробуйте {если (magSensor. SensorValue <(magSensorMax - 4)) alignMotor (); } catch {alignMotor (); }

nextCamera (правда);

обнаружение = истина;

в то время как (detectCnt <5) Thread. Sleep (25); Console. WriteLine ("Обнаружить счетчик:" + detectCnt); обнаружение = ложь;

Цвет c = DetectedColor;

this. BeginInvoke (новое действие (setColorDet), новый объект {c}); int i = findColorPosition (c);

SetBottomPosition (я, истина);

nextHole (правда); colorCnts [я] ++; this. BeginInvoke (новое действие (setColorTxt), новый объект {i}); Thread. Sleep (250);

if (colorCnts [unknownColorIndex]> 500) {

top. Engaged = false; bottom. Engaged = false; runtest = false; this. BeginInvoke (новое действие (setGoGreen), null); возвращение; }}}

private void colourTestBtn_Click (отправитель объекта, EventArgs e) {

если (colourTestThread == null ||! colourTestThread. IsAlive) {colourTestThread = новый поток (новый ThreadStart (colourTest)); runtest = true; colourTestThread. Start (); colourTestBtn. Text = "СТОП"; colourTestBtn. BackColor = Color. Red; } else {runtest = false; colourTestBtn. Text = "ИДТИ"; colourTestBtn. BackColor = Color. Green; }}

На данный момент у нас есть рабочая программа. Некоторые фрагменты кода не вошли в статью, поэтому взгляните на исходный код, чтобы запустить его.

Конкурс оптики
Конкурс оптики

Вторая премия в конкурсе оптики

Рекомендуемые: