Inhaltsverzeichnis:

Roboter-Bead-Sortierung - Gunook
Roboter-Bead-Sortierung - Gunook

Video: Roboter-Bead-Sortierung - Gunook

Video: Roboter-Bead-Sortierung - Gunook
Video: Perler beads sorting machine 2024, Juli
Anonim
Image
Image
Roboter-Bead-Sortierung
Roboter-Bead-Sortierung
Roboter-Bead-Sortierung
Roboter-Bead-Sortierung
Roboter-Bead-Sortierung
Roboter-Bead-Sortierung

In diesem Projekt bauen wir einen Roboter, um Perler Beads nach Farben zu sortieren.

Ich wollte schon immer einen Farbsortierroboter bauen, und als meine Tochter sich für das Perlenbasteln von Perler interessierte, sah ich dies als perfekte Gelegenheit.

Perler Beads werden verwendet, um fusionierte Kunstprojekte zu erstellen, indem viele Perlen auf ein Steckbrett gelegt und dann mit einem Bügeleisen zusammengeschmolzen werden. Sie kaufen diese Perlen im Allgemeinen in riesigen 22.000 Perlen-Mischfarbenpackungen und verbringen viel Zeit damit, nach der gewünschten Farbe zu suchen, also dachte ich, dass das Sortieren die Effizienz der Kunst erhöhen würde.

Ich arbeite für Phidgets Inc., also habe ich für dieses Projekt hauptsächlich Phidgets verwendet - aber dies könnte mit jeder geeigneten Hardware erfolgen.

Schritt 1: Hardware

Hier ist, was ich verwendet habe, um dies zu bauen. Ich habe es zu 100% mit Teilen von phidgets.com gebaut und Sachen, die ich im Haus herumliegen hatte.

Phidgets Boards, Motoren, Hardware

  • HUB0000 - VINT Hub Phidget
  • 1108 - Magnetsensor
  • 2x STC1001 - 2.5A Stepper Phidget
  • 2x 3324 - 42STH38 NEMA-17 Bipolarer getriebeloser Stepper
  • 3x 3002 - Phidget-Kabel 60cm
  • 3403 - USB2.0 4-Port-Hub
  • 3031 - Weiblicher Zopf 5.5x2.1mm
  • 3029 - 2-adriges 100' verdrilltes Kabel
  • 3604 - 10 mm weiße LED (10 Stück)
  • 3402 - USB-Webcam

Andere Teile

  • 24VDC 2.0A Netzteil
  • Schrott Holz und Metall aus der Garage
  • Kabelbinder
  • Plastikbehälter mit abgeschnittenem Boden

Schritt 2: Entwerfen Sie den Roboter

Entwerfen Sie den Roboter
Entwerfen Sie den Roboter
Entwerfen Sie den Roboter
Entwerfen Sie den Roboter
Entwerfen Sie den Roboter
Entwerfen Sie den Roboter

Wir müssen etwas entwerfen, das eine einzelne Perle aus dem Eingabetrichter nehmen, unter die Webcam legen und dann in den entsprechenden Behälter verschieben kann.

Perlenaufnahme

Ich beschloss, den 1. Teil mit 2 runden Sperrholzstücken zu machen, die jeweils mit einem Loch an der gleichen Stelle gebohrt wurden. Das Unterteil ist fest und das Oberteil ist an einem Schrittmotor befestigt, der es unter einem mit Perlen gefüllten Trichter drehen kann. Wenn das Loch unter den Trichter wandert, nimmt es eine einzelne Perle auf. Ich kann es dann unter der Webcam drehen und dann weiter drehen, bis es mit dem Loch im unteren Teil übereinstimmt, woraufhin es durchfällt.

In diesem Bild teste ich, dass das System funktionieren kann. Alles ist fixiert, außer dem oberen runden Stück Sperrholz, das unten unsichtbar an einem Schrittmotor befestigt ist. Die Webcam ist noch nicht montiert. Ich verwende an dieser Stelle nur das Phidget Control Panel, um zum Motor zu wechseln.

Perlenaufbewahrung

Der nächste Teil besteht darin, das Behältersystem für die Aufnahme jeder Farbe zu entwerfen. Ich beschloss, einen zweiten Schrittmotor unten zu verwenden, um einen runden Behälter mit gleichmäßig verteilten Fächern zu unterstützen und zu drehen. Dies kann verwendet werden, um das richtige Fach unter dem Loch zu drehen, aus dem die Perle herausfällt.

Ich baute dies mit Pappe und Klebeband. Das Wichtigste hier ist die Konsistenz – jedes Fach sollte die gleiche Größe haben und das Ganze sollte gleichmäßig gewichtet sein, damit es sich ohne Überspringen dreht.

Das Entfernen der Perlen erfolgt durch einen dicht schließenden Deckel, der jeweils ein einzelnes Fach freilegt, so dass die Perlen ausgegossen werden können.

Kamera

Die Webcam wird über der oberen Platte zwischen dem Trichter und der unteren Plattenlochposition montiert. Dadurch kann das System die Perle betrachten, bevor sie fallen gelassen wird. Eine LED wird verwendet, um die Perlen unter der Kamera zu beleuchten, und das Umgebungslicht wird blockiert, um eine gleichmäßige Beleuchtungsumgebung bereitzustellen. Dies ist sehr wichtig für eine genaue Farberkennung, da die Umgebungsbeleuchtung die wahrgenommene Farbe wirklich verfälschen kann.

Standorterkennung

Es ist wichtig, dass das System die Drehung des Wulstabscheiders erkennen kann. Dies wird verwendet, um die Ausgangsposition beim Start einzustellen, aber auch um zu erkennen, ob der Schrittmotor aus der Synchronisation geraten ist. In meinem System klemmt manchmal eine Perle beim Aufnehmen, und das System musste in der Lage sein, diese Situation zu erkennen und zu handhaben - indem es ein wenig zurücklegte und es erneut versuchte.

Es gibt viele Möglichkeiten, damit umzugehen. Ich beschloss, einen 1108-Magnetsensor zu verwenden, bei dem ein Magnet in die Kante der oberen Platte eingebettet ist. Dadurch kann ich die Position bei jeder Drehung überprüfen. Eine bessere Lösung wäre wahrscheinlich ein Encoder am Schrittmotor, aber ich hatte einen 1108 herumliegen, also habe ich den verwendet.

Beende den Roboter

Zu diesem Zeitpunkt ist alles ausgearbeitet und getestet. Es ist an der Zeit, alles schön zu montieren und mit dem Schreiben von Software fortzufahren.

Die 2 Schrittmotoren werden von STC1001 Schrittmotoren angetrieben. Ein HUB000 - USB VINT Hub wird zum Betrieb der Stepper-Controller sowie zum Lesen des Magnetsensors und zum Ansteuern der LED verwendet. Die Webcam und der HUB0000 sind beide an einen kleinen USB-Hub angeschlossen. Ein 3031 Pigtail und etwas Draht werden zusammen mit einem 24V Netzteil verwendet, um die Motoren mit Strom zu versorgen.

Schritt 3: Code schreiben

Image
Image

C# und Visual Studio 2015 werden für dieses Projekt verwendet. Laden Sie die Quelle oben auf dieser Seite herunter und folgen Sie den Anweisungen - die Hauptabschnitte sind unten aufgeführt

Initialisierung

Zuerst müssen wir die Phidget-Objekte erstellen, öffnen und initialisieren. Dies geschieht im Formularladeereignis und den Phidget-Anfügehandlern.

private void Form1_Load(object sender, EventArgs e) {

/* Phidgets initialisieren und öffnen */

top. HubPort = 0; top. Attach += Top_Attach; top. Detach += Top_Detach; top. PositionChange += Top_PositionChange; oben. Öffnen();

unten. HubPort = 1;

bottom. Attach += Bottom_Attach; bottom. Detach += Bottom_Detach; bottom. PositionChange += Bottom_PositionChange; unten. Öffnen();

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

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

ledAttachedChk. Checked = true; led. Zustand = wahr; ledChk. Checked = true; }

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

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

private void Bottom_Attach(Objektsender, 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(Objektsender, Phidget22. Events. AttachEventArgs e) {

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

Bei der Initialisierung lesen wir auch eventuell gespeicherte Farbinformationen ein, damit ein vorheriger Lauf fortgesetzt werden kann.

Motorpositionierung

Der Motorhandling-Code besteht aus Komfortfunktionen zum Verfahren der Motoren. Die von mir verwendeten Motoren sind 3.200 1/16-Schritte pro Umdrehung, also habe ich dafür eine Konstante erstellt.

Für den oberen Motor gibt es 3 Positionen, an die wir den Motor senden können: die Webcam, das Loch und den Positionierungsmagneten. Zu jeder dieser Positionen gibt es eine Funktion zum Anfahren:

private void nextMagnet(Boolean wait = false) {

double posn = top. Position % stepsPerRev;

top. TargetPosition += (stepsPerRev - posn);

wenn (warte)

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

wenn (warte)

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

wenn (warte)

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

Vor dem Start einer Fahrt wird die Deckplatte mit dem Magnetsensor ausgerichtet. Die Funktion alignMotor kann jederzeit aufgerufen werden, um die Kopfplatte auszurichten. Diese Funktion dreht die Platte zuerst schnell bis zu einer vollen Umdrehung, bis sie Magnetdaten über einem Schwellenwert sieht. Dann fährt es ein wenig zurück und bewegt sich langsam wieder vorwärts und erfasst dabei Sensordaten. Schließlich setzt es die Position auf die maximale Magnetdatenposition und setzt den Positionsoffset auf 0 zurück. Daher sollte die maximale Magnetposition immer bei (top. Position % stepsPerRev) liegen.

Gewinde ausrichtenMotorThread;Boolesche SägeMagnet; doppelter magSensorMax = 0; privat void alignMotor() {

// Finde den Magneten

top. DataInterval = top. MinDataInterval;

SägeMagnet = false;

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

int tryCount = 0;

versuchen Sie es nochmal:

top. TargetPosition += stepsPerRev;

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

if (!sawMagnet) {

if (tryCount > 3) { Console. WriteLine("Ausrichten fehlgeschlagen"); top. Engaged = false; bottom. Engaged = false; runtest = false; Rückkehr; }

tryCount++;

Console. WriteLine("Stehen wir fest? Versuchen Sie es mit einem Backup…"); top. TargetPosition -= 600; while (top. IsMoving) Thread. Sleep(100);

versuche es noch einmal;

}

top. VelocityLimit = -100;

magData = neue Liste>(); magSensor. SensorChange += magSensorCollectPositionData; top. Zielposition += 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 = pair;

top. AddPositionOffset(-max. Key);

magSensorMax = max. Wert;

top. TargetPosition = 0;

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

Console. WriteLine("Ausrichten erfolgreich");

}

Liste> magData;

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

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

if (top. IsMoving && e. SensorValue > 5) { top. TargetPosition = top. Position - 300; magSensor. SensorChange -= magSensorStopMotor; Sägemagnet = wahr; } }

Schließlich wird der untere Motor gesteuert, indem er zu einer der Perlenbehälterpositionen geschickt wird. Für dieses Projekt haben wir 19 Stellen. Der Algorithmus wählt einen kürzesten Weg und dreht sich entweder im oder gegen den Uhrzeigersinn.

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 % Perlenfächer; double targetPosn = (posn * stepsPerRev) / BeadCompartments;

double currentPosn = bottom. Position % stepsPerRev;

double posnDiff = targetPosn - currentPosn;

// Behalte es als vollständige Schritte bei

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

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

wenn (warte)

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

Kamera

OpenCV wird verwendet, um Bilder von der Webcam auszulesen. Der Kamerathread wird vor dem Start des Hauptsortierthreads gestartet. Dieser Thread liest kontinuierlich Bilder ein, berechnet eine durchschnittliche Farbe für eine bestimmte Region mit Mittelwert und aktualisiert eine globale Farbvariable. Der Thread verwendet auch HoughCircles, um zu versuchen, entweder eine Perle oder das Loch in der oberen Platte zu erkennen, um den Bereich zu verfeinern, der für die Farberkennung untersucht wird. Der Schwellenwert und die HoughCircles-Zahlen wurden durch Versuch und Irrtum ermittelt und hängen stark von der Webcam, der Beleuchtung und dem Abstand ab.

bool runVideo = true;bool videoRunning = false; VideoCapture-Erfassung; Thread cvThread; Farbe erkanntFarbe; Boolesche Erkennung = falsch; int DetectCnt = 0;

privat void cvThreadFunction() {

videoRunning = false;

capture = new VideoCapture(selectedCamera);

using (Window window = new Window("capture")) {

Mattenbild = neue Matte(); Mat image2 = new Mat(); while (runVideo) { capture. Read (Bild); if (image. Empty()) break;

wenn (erkennen)

erkennenCnt++; sonst DetectCnt = 0;

if (erkennt || circleDetectChecked || showDetectionImgChecked) {

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

if (showDetectionImgChecked)

Bild = thres;

if (erkennt || 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 = (dezimal)bead[0]. Center. X + (dezimal)(bead[0]. Radius / 2); Properties. Settings. Default.y = (dezimal)bead[0]. Center. Y - (dezimal)(bead[0]. Radius / 2); } else { Properties. Settings. Default.x = (dezimal)bead[0]. Center. X + (dezimal)(bead[0]. Radius); Properties. Settings. Default.y = (dezimal)bead[0]. Center. Y - (dezimal)(bead[0]. Radius); } Properties. Settings. Default.size = 15; Properties. Settings. Default.height = 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. Sort(); Liste 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 = Bild. Breite - 15; if (medianY > image. Height - 15) medianY = image. Height - 15;

image. Circle (MedianX, MedianY, 100, neuer Skalar (0, 0, 150), 3);

wenn (erkennen) {

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 = new Mat(image, r);

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

image. Rectangle(r, neuer Skalar(0, 150, 0));

Fenster. ShowImage(Bild);

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

videoRunning = false;

} }

private void cameraStartBtn_Click(object sender, EventArgs e) {

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

cvThread = neuer Thread (neuer ThreadStart (cvThreadFunction)); runVideo = wahr; cvThread. Start(); cameraStartBtn. Text = "stop"; while (!videoRunning) Thread. Sleep(100);

updateColorTimer. Start();

} anders {

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

Farbe

Jetzt können wir die Farbe einer Perle bestimmen und basierend auf dieser Farbe entscheiden, in welchen Behälter wir sie fallen lassen.

Dieser Schritt basiert auf dem Farbvergleich. Wir möchten in der Lage sein, Farben zu unterscheiden, um False-Positives zu begrenzen, aber auch genügend Schwellenwerte zuzulassen, um False-Negatives zu begrenzen. Der Vergleich von Farben ist tatsächlich überraschend komplex, da die Art und Weise, wie Computer Farben als RGB speichern, und die Art und Weise, wie Menschen Farben wahrnehmen, nicht linear korrelieren. Erschwerend kommt hinzu, dass auch die Lichtfarbe, unter der eine Farbe betrachtet wird, berücksichtigt werden muss.

Es gibt komplizierte Algorithmen zur Berechnung des Farbunterschieds. Wir verwenden CIE2000, das eine Zahl nahe 1 ausgibt, wenn 2 Farben für einen Menschen nicht zu unterscheiden wären. Wir verwenden die ColorMine C#-Bibliothek, um diese komplizierten Berechnungen durchzuführen. Es hat sich herausgestellt, dass ein DeltaE-Wert von 5 einen guten Kompromiss zwischen falsch positiv und falsch negativ bietet.

Da es oft mehr Farben als Behälter gibt, wird der letzte Platz als Sammelbehälter reserviert. Ich lege diese normalerweise beiseite, um in einem zweiten Durchgang durch die Maschine zu laufen.

Aufführen

Farben = neue Liste (); Liste colorPanels = neue Liste (); ListenfarbenTxts = new List(); Liste colorCnts = new List();

const int numColorSpots = 18;

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

Console. WriteLine("Farbe finden…");

var cRGB = neues RGB();

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

int bestMatch = -1;

DoppelmatchDelta = 100;

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

var RGB = neues RGB();

RGB. R = Farben. R; RGB. G = Farben. G; RGB. B = Farben. B;

Doppeldelta = cRGB. Compare(RGB, neuer CieDe2000Comparison());

//doppeltes Delta = deltaE(c, Farben); Console. WriteLine("DeltaE (" + i. ToString() + "): " + delta. ToString()); if (Delta < MatchDelta) { MatchDelta = Delta; bestMatch = i; } }

if (matchDelta < 5) { Console. WriteLine("Gefunden! (Posn: " + bestMatch + " Delta: " + matchDelta + ")"); bestMatch zurückgeben; }

if (colors. Count < numColorSpots) { Console. WriteLine("Neue Farbe!"); Farben. Hinzufügen(c); this. BeginInvoke(new Action(setBackColor), neues Objekt {colors. Count - 1}); writeOutColors(); zurück (Farben. Anzahl - 1); } else { Console. WriteLine("Unbekannte Farbe!"); Rückgabe unbekannterFarbindex; } }

Sortierlogik

Die Sortierfunktion bringt alle Teile zusammen, um die Perlen tatsächlich zu sortieren. Diese Funktion wird in einem dedizierten Thread ausgeführt; Bewegen der oberen Platte, Erkennen der Perlenfarbe, Einlegen in einen Behälter, Sicherstellen, dass die obere Platte ausgerichtet bleibt, Zählen der Perlen usw. Es hört auch auf zu laufen, wenn der Auffangbehälter voll ist - sonst haben wir nur überlaufende Perlen.

Thread colorTestThread;Boolescher Runtest = false; void colorTest() {

if (!top. Engaged)

top. Engaged = true;

if (!bottom. Engaged)

bottom. Engaged = true;

während (Runtest) {

nextMagnet(wahr);

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

nextCamera(wahr);

Erfassen = wahr;

while (detectCnt < 5) Thread. Sleep(25); Console. WriteLine("Erkenne Anzahl: " + DetectCnt); Erkennen = falsch;

Farbe c = erkannte Farbe;

this. BeginInvoke(neue Aktion (setColorDet), neues Objekt { c }); int i = findColorPosition(c);

SetBottomPosition(i, wahr);

nextHole(wahr); colorCnts++; this. BeginInvoke(new Action(setColorTxt), neues Objekt { i}); Thread. Sleep(250);

if (colorCnts[unknownColorIndex] > 500) {

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

private void colourTestBtn_Click(object sender, EventArgs e) {

if (colourTestThread == null || !colourTestThread. IsAlive) { colorTestThread = new Thread(new ThreadStart(colourTest)); Runtest = wahr; colorTestThread. Start(); colorTestBtn. Text = "STOP"; colorTestBtn. BackColor = Color. Red; aufrechtzuerhalten. Sonst { Runtest = false; colorTestBtn. Text = "GO"; colorTestBtn. BackColor = Color. Green; } }

An dieser Stelle haben wir ein Arbeitsprogramm. Einige Code-Stücke wurden aus dem Artikel weggelassen, also werfen Sie einen Blick auf den Quellcode, um ihn tatsächlich auszuführen.

Optik-Wettbewerb
Optik-Wettbewerb

Zweiter Preis beim Optik-Wettbewerb

Empfohlen: