Skocz do zawartości

Projekt SMTGun - czyli PnP na sterydach...


Pomocna odpowiedź

(edytowany)

Ciąg dalszy kombinowania z skomplikowanym algorytmem...

Problemem było to, iż algorytm uznawał tylko jeden komponent, a środek komponentu odwróconego prawidłowo był rozpoznawany wyłącznie za pomocą jego punktów krańcowych. Niestety zmiana algorytmu tymczasowo usunęła możliwość rozpoznawania ułożenia komponentu (góra / dół), ale to da się zrobić 😉 [I algorytm nie rozpozna innych komponentów niż szare, ale to da się ogarnąć łącząc obie wersje w jedną 😉]

obraz.thumb.png.035b2a70a05559f87fc2e84af92ad3ce.png

Obraz po przetworzeniu przez algorytm (detekcja komponentów)

obraz.thumb.png.40b2d60775141e44fcac0314118131f7.png

I po analizie informacji (obraz z kamery)

Aktualna wersja niestety nie rozpozna komponentów na czarnym/szarym/białym tle, aczkolwiek każde inne tło jest jak najbardziej akceptowalne 😉

Automatyczna ekspozycja w kamerze najbardziej utrudnia robienie czegokolwiek, bo co chwilę zmienia się jej charakterystyka barwowa i algorytm musi się dostosowywać...

Edytowano przez H1M4W4R1
  • Lubię! 2
Link do komentarza
Share on other sites

Zarejestruj się lub zaloguj, aby ukryć tę reklamę.
Zarejestruj się lub zaloguj, aby ukryć tę reklamę.

jlcpcb.jpg

jlcpcb.jpg

Produkcja i montaż PCB - wybierz sprawdzone PCBWay!
   • Darmowe płytki dla studentów i projektów non-profit
   • Tylko 5$ za 10 prototypów PCB w 24 godziny
   • Usługa projektowania PCB na zlecenie
   • Montaż PCB od 30$ + bezpłatna dostawa i szablony
   • Darmowe narzędzie do podglądu plików Gerber
Zobacz również » Film z fabryki PCBWay

(edytowany)
1 godzinę temu, FlyingDutch napisał:

a w jakim języku masz napisany ten algorytm? Podejrzewam, że w C++ z uzyciem biblioteki "OpenCV"?

W wężu 😉

EDIT: Trochę poprawek i działa ciut lepiej (lepiej usuwa szum otoczenia):

obraz.thumb.png.a930a630e7b39d360af814ed5809948f.png

main.py

import math
import time

import cv2
import numpy as np

import PyCV

# Primary constants
SCALE_MULTIPLIER = 4
PIXEL_MM_RATIO = 29 * 4 / SCALE_MULTIPLIER  # Pixels per mm
AREA_TOLERANCE = 0.3
ANGLE_TOLERANCE = 5
DISTANCE_TOLERANCE = 0.15 * PIXEL_MM_RATIO  # In pixels

# Component view
COMPONENT_TOP_VIEW = True
COMPONENT_BOTTOM_VIEW = False

# Component dictionary
COMPONENT_DICTIONARY = {
    "0201": (0.6, 0.3),
    "0402": (1.0, 0.5),
    "0603": (1.55, 0.85),
    "0805": (2.0, 1.25),
    "1008": (2.5, 2.0),
    "1206": (3.2, 1.6),
    "1210": (3.2, 2.5),
    "1806": (4.5, 1.6),
    "1812": (4.5, 3.2),
    "2010": (5.0, 2.5),
    "2512": (6.3, 3.2)
}


def px_to_mm(px):
    return px / PIXEL_MM_RATIO


def mm_to_px(mm):
    return mm * PIXEL_MM_RATIO


def process():
    # Read camera stream
    capture = cv2.VideoCapture(0)
    # print(f"Capturing for {EXPECTED_COMPONENT} with area of {get_expected_area()}mm2")

    # Infinite loop
    while True:
        start_time = time.time()  # start time of the loop

        # Check if got image
        is_true, image = capture.read()
        if not is_true:
            continue

        # Resize image to lower resolution
        image = PyCV.rescale_image(image, 1/SCALE_MULTIPLIER)
        dx, dy = PyCV.get_center(image)

        d = PyCV.is_gray(image)
        d = PyCV.cleanup(d, 1)
        cv2.imshow("Grayscale", d)

        # Center marker
        PyCV.draw_cross(image)

        contours, hierarchy = cv2.findContours(d, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

        # Check too small contours
        small_rectangles = []
        used_small_rectangles = []

        regular_rectangles = []
        big_rectangles = []

        components = []

        # Find contours and compare component areas
        for cont in contours:

            # Initial math
            rect = cv2.minAreaRect(cont)
            xy, wh, angle = rect
            cx, cy = rect[0]
            cx = int(cx)
            cy = int(cy)

            # Compute width and height
            width, height = wh
            rc_width = round(px_to_mm(width), 2)
            rc_height = round(px_to_mm(height), 2)
            rc_area = rc_width * rc_height

            name: str = "Unknown"
            closest_value = 1000
            found = False

            for c_name, dimensions in COMPONENT_DICTIONARY.items():
                (x, y) = dimensions
                area = x * y

                closeness = abs(1 - (rc_area / area))

                # Check if is closest to current component size
                if closeness < closest_value:
                    name = c_name
                    closest_value = closeness
                    found = True
                continue

            if found:
                big_rectangles.append(rect)

            box = np.int64(cv2.boxPoints(rect))

            # Register object
            regular_rectangles.append(rect)
            components.append(((cx, cy), (rc_width, rc_height), rc_area, angle, COMPONENT_BOTTOM_VIEW, box, name))

        # Get first component
        if len(components) < 1:
            nearest_component = None
        else:
            nearest_component = components[0]
        nearest_distance = 1e6

        # Scan all components to write data and get nearest, also render component contours
        for component in components:

            # Unpack component
            (position_x, position_y), (width, length), area, angle, top_view, contour_box, name = component

            # Validate area tolerance
            (x_dim, y_dim) = COMPONENT_DICTIONARY[name]

            # Ignore invalid area sizes
            if abs(1 - area/(x_dim * y_dim)) > AREA_TOLERANCE:
                continue

            # Draw contours
            if top_view:
                cv2.drawContours(image, [contour_box], 0, (0, 0, 255), 1)
            else:
                cv2.drawContours(image, [contour_box], 0, (255, 0, 0), 1)

            cv2.drawMarker(image, (int(position_x), int(position_y)), (255, 0, 255), cv2.MARKER_CROSS, 20, 1)

            min_x, min_y, max_x, max_y = PyCV.box_min_max_points(contour_box)
            if max_x - min_x > max_y - min_y:
                orientation = "h"
            else:
                orientation = "v"

            # Draw component size
            cv2.putText(image, f"{name}{orientation}", (int(position_x - 15), int(position_y - 12)), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 0, 0), 1, cv2.LINE_AA)

            # Get component distance
            distance_x = position_x - dx
            distance_y = position_y - dy

            distance = math.sqrt(distance_x * distance_x + distance_y * distance_y)  # 2D Euler Space Distance Formula

            # Update component if distance is closer
            if distance < nearest_distance:
                nearest_distance = distance
                nearest_component = component

        # Draw closest component data
        if nearest_component is not None:
            (position_x, position_y), (width, length), area, angle, top_view, contour_box, name = nearest_component

            # Render closest component line
            cv2.line(image, (dx, dy), (int(position_x), int(position_y)), (255, 0, 0), 1, 1)

            # Compute and draw distance to the closest image
            dist_x_mm = round(px_to_mm(position_x - dx), 1)
            dist_y_mm = round(-px_to_mm(position_y - dy), 1)

            cv2.putText(image, f"Distance [{dist_x_mm}, {dist_y_mm}]mm", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 1, cv2.LINE_AA)

            # Draw angle text
            min_x, min_y, max_x, max_y = PyCV.box_min_max_points(contour_box)
            if max_x - min_x > max_y - min_y:
                angle = 90 - abs(angle)
            cv2.putText(image, f"Angle [{round(angle, 1)}d]", (10, 90), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 1, cv2.LINE_AA)

        # Render textual data
        cv2.putText(image, f"Count: {len(components)}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 1, cv2.LINE_AA)

        cv2.putText(image, f"FPS: {round(1.0 / (time.time() - start_time), 1)}", (10, 2*dy-30), cv2.FONT_HERSHEY_SIMPLEX, 1, (128, 128, 128), 1, cv2.LINE_AA)

        # Render image
        cv2.imshow('Processed Image', image)



        # Wait for process exit code
        if cv2.waitKey(1) & 0xFF == ord('d'):
            break

    # Clear camera and screen
    capture.release()
    cv2.destroyAllWindows()


# Execute software
process()

Muszę tu jeszcze posprzątać 😉

PyCV.py

from typing import Sequence

import cv2
import numpy as np
from cv2.typing import Rect


def rescale_image(image: cv2.typing.MatLike, scale: float) -> cv2.typing.MatLike:
    shape: tuple[int, ...] = image.shape
    height: float = int(image.shape[0]) * scale
    width: float = int(image.shape[1]) * scale

    size: tuple[int, int] = (int(width), int(height))

    return cv2.resize(image, size)


def get_center(image: cv2.typing.MatLike) -> tuple[int, int]:
    # Image center
    dx = int(image.shape[1] / 2)
    dy = int(image.shape[0] / 2)

    return dx, dy


def draw_cross(image: cv2.typing.MatLike, color: Sequence[int] = (0, 255, 0)) -> None:
    x, y = get_center(image)  # Get image center
    size = int(max(2*x, 2*y))  # Compute largest size (width or height)

    cv2.drawMarker(image, (x, y), color, cv2.MARKER_CROSS, size, 1)  # Draw cross marker


def box_min_max_points(box) -> tuple[float, float, float, float]:
    min_x = min(box[0][0], box[1][0], box[2][0], box[3][0])
    min_y = min(box[0][1], box[1][1], box[2][1], box[3][1])
    max_x = max(box[0][0], box[1][0], box[2][0], box[3][0])
    max_y = max(box[0][1], box[1][1], box[2][1], box[3][1])

    return min_x, min_y, max_x, max_y


def white(width: int, height: int) -> np.ndarray:
    white_image: np.ndarray = np.zeros((width, height, 3), np.uint8)
    white_image[:] = (255, 255, 255)
    return white_image


def is_gray(image: cv2.typing.MatLike, tolerance_percent: float = 0.1) -> cv2.typing.MatLike:
    (blue, green, red) = cv2.split(image)
    blue = blue.astype(np.float32)
    green = green.astype(np.float32)
    red = red.astype(np.float32)

    a = cv2.divide(green, blue)
    a = cv2.inRange(a, 1 - tolerance_percent, 1 + tolerance_percent)
    a = a.astype(np.uint8) * 255
    a = a * 255

    b = cv2.divide(red, blue)
    b = cv2.inRange(b, 1 - tolerance_percent, 1 + tolerance_percent)
    b = b.astype(np.uint8) * 255
    b = b * 255

    c = cv2.divide(red, green)
    c = cv2.inRange(c, 1 - tolerance_percent, 1 + tolerance_percent)
    c = c.astype(np.uint8) * 255
    c = c * 255

    d = a | b | c

    return d


def cleanup(image: cv2.typing.MatLike, alpha: int = 8, beta: int = 2) -> cv2.typing.MatLike:
    # Morph close
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (alpha, alpha))
    close = cv2.morphologyEx(image, cv2.MORPH_OPEN, kernel, iterations=beta)

    return close


def noise_cleanup(image: cv2.typing.MatLike, alpha: float = 128, beta: int = 32, gamma: int = 120) -> cv2.typing.MatLike:
    blur = cv2.erode(image, (alpha, alpha))
    blur = cv2.erode(blur, (alpha/4, alpha/4))

    blur = cv2.dilate(blur, (alpha, alpha))
    blur = cv2.dilate(blur, (alpha/4, alpha/4))
    blur = cv2.fastNlMeansDenoising(blur, None, gamma, beta, int(1.5 * beta))
    blur = cv2.inRange(blur, 180, 255)

    return blur

To tak gdyby ktoś chciał się pobawić 😉

Edytowano przez H1M4W4R1
  • Lubię! 1
Link do komentarza
Share on other sites

18 godzin temu, FlyingDutch napisał:

Ja przeważnie korzystałem z "OpenCV" w C++, ale muszę przyznać, że użycie Pythona jest dużo wygodniejsze 🙂

Docelowo planuję użyć C++, do testów wolę Pythona, bo mogę szybko zmieniać parametry i sprawdzać efekty 😉

obraz.thumb.png.5798644b3a1327ef83dfb7949c9aa700.png

Względne porównanie skali szarości

obraz.thumb.png.bdf086d3a7aaa86f6b9a3fd2f08a8a4d.png

Bezwzględne porównanie koloru do średniej wartości koloru na obrazie 😉

obraz.thumb.png.bdc11adaec52fc7d055571c547fbf82b.png

Suma logiczna obu poprzednich obrazów

obraz.thumb.png.1c5b0c320730f443c1122c13585f7220.png

Detekcja 😉

Na niebieskim tle wykrywa praktycznie każdy komponent (znaczy wykrywa jego obecność, ale nie dane/informacje). W dodatku nie weryfikuje proporcji komponentu i przewód wykryło jako 2025, ale to już mniejsze szczegóły 😉 Następna rzecz w kolejce to detekcja góra / dół.

  • Lubię! 1
Link do komentarza
Share on other sites

I po kolejnej porcji resztek wigilijnych 😄

obraz.thumb.png.bb9ea5db46b128d4d805ab4c54f733bd.png

Rozpoznawanie ułożenia komponentów (góra/dół)

Algorytm rozpoznaje czy komponent widać z góry, czy z dołu (pod warunkiem prawidłowego natężenia światła). Bazuje na porównywaniu koloru względem czarnego i białego w formie odległości euklidesowej (aka. trójkąt pitagorasa w 3 wymiarach dla niewtajemniczonych). Działa całkiem sprawnie dla elementó pasywnych (w przypadku kondensatorów to i tak nie ma znaczenia, bo można je obracać dowolnie). Przy okazji trochę zwiększyłem skalę obrazu, by zredukować szumy nie tracąc precyzji 😉 

Link do komentarza
Share on other sites

2 godziny temu, MR1979 napisał:

A nie rozważałeś dostosowania gotowego oprogramowania openpnp.org do swojego hardware. To i tak będzie projekt sporych rozmiarów.

Powodzenia!

OpenPNP nie ogarnie takiej ilości kalibracji poza tym jest zbyt drewniane, o jakości dokumentacji kodu nie wspominając. Szybciej będzie to napisać od zera.

Link do komentarza
Share on other sites

Dnia 26.12.2023 o 06:31, H1M4W4R1 napisał:

Na niebieskim tle wykrywa praktycznie każdy komponent (znaczy wykrywa jego obecność, ale nie dane/informacje). W dodatku nie weryfikuje proporcji komponentu i przewód wykryło jako 2025, ale to już mniejsze szczegóły 😉 Następna rzecz w kolejce to detekcja góra / dół.

Z czystej ciekawości: a nierównomierność aktualnego tła nic tutaj nie psuje? Druk 3D wprowadza dodatkowe linie i niejednorodność koloru. Czy podmiana tła na jednolity, kolorowy karton nie byłaby tutaj też jakimś ulepszeniem? Inne podejście to zastosowanie podajnika podświetlanego od spodu. Wtedy powinno być możliwe wykrywanie każdego kształtu, bo każdy obiekt będzie po prostu z automatu ciemniejszą "plamą".

Link do komentarza
Share on other sites

3 minuty temu, Treker napisał:

Z czystej ciekawości: a nierównomierność aktualnego tła nic tutaj nie psuje? Druk 3D wprowadza dodatkowe linie i niejednorodność koloru. Czy podmiana tła na jednolity, kolorowy karton nie byłaby tutaj też jakimś ulepszeniem? Inne podejście to zastosowanie podajnika podświetlanego od spodu. Wtedy powinno być możliwe wykrywanie każdego kształtu, bo każdy obiekt będzie po prostu z automatu ciemniejszą "plamą".

Tutaj chodzi o wybór komponentu z tzw. "bulk box" gdzie wszystko leży rozrzucone i maszyna sobie podnosi te, które są prawidłowo ułożone. Kąt służy do wstępnego ustawienia komponentu w trakcie przemieszczania go do weryfikacji z podświetleniem od spodu. Dopiero potem jest umieszczany na płytce.

I druk 3D praktycznie nic nie psuje 😉 [Poza drobnym szumem tła, który jest kasowany przez kilka przejść odszumiających]

  • Lubię! 1
Link do komentarza
Share on other sites

Bądź aktywny - zaloguj się lub utwórz konto!

Tylko zarejestrowani użytkownicy mogą komentować zawartość tej strony

Utwórz konto w ~20 sekund!

Zarejestruj nowe konto, to proste!

Zarejestruj się »

Zaloguj się

Posiadasz własne konto? Użyj go!

Zaloguj się »
×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.