Śledzenie kuli - openCV

0

Siemka. Możecie mi pomóc zwiększyć dokładność programu lub zaproponować jakieś inne pomysły?

Potrzebuję programu do śledzenia położenia kuli na płaszczyźnie za pomocą kamery. Wymaganiami jest optymalność, chciałbym to zaimplementować na mikrokontrolerze STM32, oraz dokładność i czas reakcji. Nie mogę dopuścić do sytuacji aby kula spadła z płaszczyzny, dlatego muszę szybko znajdować jej położenie by odpowiednio zareagować silnikami, najlepiej min. 30fps.

Napisałem na kompie przykładowy program realizujący takie zadanie, lecz przy ruchu kamery czasami program nie znajduje kuli lub znajduje je tam gdzie ich nie ma. Dopasowanie okręgu nie zawsze jest też precyzyjne. Problem stanowi również rodzaj oświetlenia, a im szybszy ruch tym większe problemy.

Może moglibyście mi doradzić co mógłbym zmienić by poprawić jakość znajdowania położenia kuli lub wskazać coś co warto byłoby przeczytać :D
Poniżej wstawiam kod.

int main(int argc, char** argv) {

    // Load input image
    VideoCapture capture = VideoCapture(1);
    cv::Mat bgr_image;

    while (waitKey(20) != 27)  {
        capture >> bgr_image;
        bgr_image.copyTo(orig_image);

        cv::medianBlur(bgr_image, bgr_image, 3);
        // Convert input image to HSV
        cv::Mat hsv_image;
        cv::cvtColor(bgr_image, hsv_image, cv::COLOR_BGR2HSV);

        // Threshold the HSV image, keep only the red pixels
        cv::Mat lower_red_hue_range;
        cv::Mat upper_red_hue_range;
        cv::inRange(hsv_image, cv::Scalar(0, 100, 100), cv::Scalar(10, 255, 255), lower_red_hue_range);
        cv::inRange(hsv_image, cv::Scalar(160, 100, 100), cv::Scalar(179, 255, 255), upper_red_hue_range);

        // Combine the above two images
        cv::Mat red_hue_image;
        cv::addWeighted(lower_red_hue_range, 1.0, upper_red_hue_range, 1.0, 0.0, red_hue_image);

        cv::GaussianBlur(red_hue_image, red_hue_image, cv::Size(9, 9), 2, 2);

        // Use the Hough transform to detect circles in the combined threshold image
        std::vector<cv::Vec3f> circles;
        cv::HoughCircles(red_hue_image, circles, HOUGH_GRADIENT, 1, red_hue_image.rows / 8, 100, 20, 0, 0);

        // Loop over all detected circles and outline them on the original image
        for (size_t current_circle = 0; current_circle < circles.size(); ++current_circle) {
            cv::Point center(std::round(circles[current_circle][0]), std::round(circles[current_circle][1]));
            int radius = std::round(circles[current_circle][2]);

            cv::circle(orig_image, center, radius, cv::Scalar(0, 255, 0), 5);
        }

        // Show images
        cv::namedWindow("Threshold lower image", cv::WINDOW_AUTOSIZE);
        cv::imshow("Threshold lower image", lower_red_hue_range);
        cv::namedWindow("Threshold upper image", cv::WINDOW_AUTOSIZE);
        cv::imshow("Threshold upper image", upper_red_hue_range);
        cv::namedWindow("Combined threshold images", cv::WINDOW_AUTOSIZE);
        cv::imshow("Combined threshold images", red_hue_image);
        cv::namedWindow("Detected red circles on the input image", cv::WINDOW_AUTOSIZE);
        cv::imshow("Detected red circles on the input image", orig_image);
    }
    return 0;
}
1

Tak na szybko co mi przychodzi do głowy:

  • skoro to jest śledzenie obiektu to możesz zapoznać się z filtrem Kalmana.
  • Jasność zawsze jest problemem w tego typu podejściach. Próbowałbym może dynamicznie wyliczać jasność kolejnych zdjęć i na tej podstawie dososowywać progi.
  • Nie wiem jak to wygląda u Ciebie w praktyce, ale być może można filtrować jakoś śmieci przez ich rozmiar
0
int main(int argc, char** argv) {

    // Load input image
    VideoCapture capture = VideoCapture(1);
    cv::Mat bgr_image;
    cv::Mat orig_image;
    cv::Mat hsv_image;
    cv::Mat thresholded;
    std::vector<cv::Vec3f> circles;
    int hueLower = 0, hueUpper = 180, saturationLower = 100, saturationUpper = 255, valueLower = 100, valueUpper = 255;

    string nameThreshold = "Threshold lower image";
    cv::namedWindow(nameThreshold, cv::WINDOW_AUTOSIZE);
    createTrackbar("Hue lower", nameThreshold, &hueLower, 180, NULL);
    createTrackbar("Hue upper", nameThreshold, &hueUpper, 180, NULL);
    createTrackbar("Saturation lower", nameThreshold, &saturationLower, 255, NULL);
    createTrackbar("Saturation upper", nameThreshold, &saturationUpper, 255, NULL);
    createTrackbar("Value lower", nameThreshold, &valueLower, 255, NULL);
    createTrackbar("Value upper", nameThreshold, &valueUpper, 255, NULL);
    cv::namedWindow("Detected red circles on the input image", cv::WINDOW_AUTOSIZE);

    while (waitKey(20) != 27)
    {
        capture >> bgr_image;
        bgr_image.copyTo(orig_image);

        // MedianBlur
        cv::medianBlur(bgr_image, bgr_image, 3);
        // Convert input image to HSV
        cv::cvtColor(bgr_image, hsv_image, cv::COLOR_BGR2HSV);

        // Threshold the HSV image, keep only the red pixels
        cv::inRange(hsv_image, cv::Scalar(hueLower, saturationLower, valueLower),
            cv::Scalar(hueUpper, saturationUpper, valueUpper), thresholded);

        cv::GaussianBlur(thresholded, thresholded, cv::Size(9, 9), 2, 2);

        // Use the Hough transform to detect circles in the combined threshold image
        cv::HoughCircles(thresholded, circles, HOUGH_GRADIENT, 1, thresholded.rows / 8, 100, 20, 0, 0);

        // Loop over all detected circles and outline them on the original image
        for (size_t current_circle = 0; current_circle < circles.size(); ++current_circle) {
            cv::Point center(std::round(circles[current_circle][0]), std::round(circles[current_circle][1]));
            int radius = std::round(circles[current_circle][2]);
            cv::circle(orig_image, center, radius, cv::Scalar(0, 255, 0), 5);
        }

        // Show images
        cv::imshow(nameThreshold, thresholded);
        cv::imshow("Detected red circles on the input image", orig_image);
    }
    return 0;
}

W takiej wersji nawet to w miarę wykrywa, jest dość dokładnie i nie gubi się przy ruchu. Pozostaje jednak wciąż problem opóźnienia. Po poruszeniu kamerą mija jakieś 100ms do czasu aż na ekranie zobaczę nowe położenie. Wszelkie dodatkowe filtry jeszcze to spowolnią. Najważniejsze w sumie jest zoptymalizowanie tego na co nie mam zbytnio pomysłu bo kod i tak wydaje się ubogi.

0

może resize ramek? Do wykrycia okręgów obstawiam, że nie potrzebujesz dużej rozdzielczości, a to na pewno ładnie zredukuje liczbę operacji. Rozumiem, że puszczasz to póki co na kompie? Trochę dziwne, że to już Ci generuje jakieś opóźnienia.

1

while (waitKey(1) != 27)

0

Obstawiam "Ball On Plate"? :)
Robiliśmy to na kompie też (Qt), kamerka PS3 Eye (niska rozdziałka, dużo FPSów), płytka od ST jakaś Discovery (sterowanie serwami do machania "płaszczyzną", komunikacja po serialu z kompem).
Faktycznie jest opóźnienie obrazu i tego raczej się nie przeskoczy.
Obraz na pewno trzeba było przygotować przed wykrywaniem kulki. Chyba: kontrast, konwersja na skalę szarości, usuwanie szumów.
I z tego co pamiętam, użyty został PID, przewidywany był kierunek ruchu kulki na podstawie X ostatnich jej położeń.
Może to coś Ci pomoże ;)

0

Zmniejszyłem rozdzielczość, lecz poprawa jest nieznaczna, teraz mam jakieś 4-4,5 FPS.

    capture.set(CAP_PROP_FRAME_WIDTH, 640);
    capture.set(CAP_PROP_FRAME_HEIGHT, 480);
0

rzuciłem też okiem, czasami niektórzy faktycznie mają problem z powolnością blura ale mówią o szybszych libkach.
https://dsp.stackexchange.com/questions/50576/fastest-available-algorithm-to-blur-an-image-low-pass-filter

0

piszesz, że filtry dużo czasu zabierają, więc może spróbuj zmniejszyć kernel na 3x3 i porównaj wyniki. A ogólnie to ile fps'ów dostajesz jak wszystko wywalisz?

1

Odpowiadam odnośnie opencv na stm32. Z rok temu szukałem o tym informacji i parę spraw:

  1. To są jakies prób przeróbki opencv na stm32 np.https://medium.com/@deryugin.denis/how-to-run-opencv-on-stm32-mcu-b581f42b0766
  2. O oficjalnym wsparciu zapomnij. Mi były potrzebne jakieś proste operacje typu rgb565 do grey. To sobie sam napisałem.
  3. Zapomnij o wydajności. generalni opencv na arm i x86 używa SIMD żeby przyspieszyć operacje. Tego nie masz w ogóle na stm32. Więc zostaje taki czysty kod C co będzie wolne.

Generalnie zapomnij że na stm32 i rdzeniach typu m4/m7 nie odpalisz opencv a nawet jak odpalisz jakieś zhackowane wersje to będą albo wolne albo uboższe.

Jeśli chcesz możesz zamiast tego próbować jak napisałem metod neuronowych dla stm32. Dla małych rdzeni można użyć cube i zaciągnąć model w keras a on go przerobi tak żeby się dało odpalić na stm32. Tak wykryjesz piłkę ale położenie będzie mniej więcej.

Moim zdaniem zainteresuj się rpi4 albo jetson nano(jeśli chcesz używać ML) lub jakimiś pochodnymi płytkami na linuksie.

edit:
zawsze możesz próbować sam napisać wszystkie algorytmy na stm32 i próbować je optymalizować tam ale to dłubanina.

4

Problem leży w tym, że używasz cv::HoughCircles na złym obrazie.
Ta funkcja służy do wynajdowania okręgów nie kół!
Najpierw musisz przetworzyć obraz tak, by dostać obwiednie kształtów na obrazie.
Po odfiltrowaniu po zakresie kolorów dostajesz koło (i śmieci), najlepiej użyć cv::Canny by dostać krawędzie koło czyli okrąg.
W takiej sytuacji cv::HoughCircles będzie bardziej efektywne.

Troszkę zrefaktorowałem twój kod (możesz przywrócić rozmycie gaussa - a nawet powinieneś), jest lepiej ale nadal można to bardzo poprawić (pod względem czytelności).

Do testowania użyłem czerwonego guzika z pilota :).

#include <opencv2/opencv.hpp>
#include <string>
#include <iostream>

using namespace cv;

class BallTracker {
public:
    BallTracker()
    {
        openCamera();
        setupTrackbars();
    }

    void openCamera()
    {
        capture.setExceptionMode(true);
        capture.open(0);
    }

    void setupTrackbars()
    {
        cv::namedWindow(nameThreshold, cv::WINDOW_AUTOSIZE);
        createTrackbar("Hue lower", nameThreshold, &hueLower, 180);
        createTrackbar("Hue upper", nameThreshold, &hueUpper, 180);
        createTrackbar("Saturation lower", nameThreshold, &saturationLower, 255);
        createTrackbar("Saturation upper", nameThreshold, &saturationUpper, 255);
        createTrackbar("Value lower", nameThreshold, &valueLower, 255);
        createTrackbar("Value upper", nameThreshold, &valueUpper, 255);
        cv::namedWindow(nameCaptureWindow, cv::WINDOW_AUTOSIZE);
    }

    void run()
    {
        while (waitKey(20) != 27) {
            std::vector<cv::Vec3f> circles;
            capture >> orig_image;

            // Convert input image to HSV
            cv::cvtColor(orig_image, hsv_image, cv::COLOR_BGR2HSV);

            // Threshold the HSV image, keep only the red pixels
            cv::inRange(hsv_image, cv::Scalar(hueLower, saturationLower, valueLower),
                cv::Scalar(hueUpper, saturationUpper, valueUpper), thresholded);

            cv::Canny(thresholded, thresholded, 100, 200);

            cv::HoughCircles(thresholded, circles, HOUGH_GRADIENT, 1, thresholded.rows / 8, 100, 20, 5, 200);

            for (size_t current_circle = 0; current_circle < circles.size(); ++current_circle) {
                cv::Point center(std::round(circles[current_circle][0]), std::round(circles[current_circle][1]));
                int radius = std::round(circles[current_circle][2]);
                cv::circle(orig_image, center, radius, detectedCircelColor, 5);
            }

            // Show images
            cv::imshow(nameThreshold, thresholded);
            cv::imshow(nameCaptureWindow, orig_image);
        }
    }

private:
    const String nameThreshold = "Threshold lower image";
    const String nameCaptureWindow = "Detected red circles on the input image";
    const cv::Scalar detectedCircelColor{ 0, 255, 0 };

    VideoCapture capture;
    cv::Mat bgr_image;
    cv::Mat orig_image;
    cv::Mat hsv_image;
    cv::Mat thresholded;

    int hueLower = 0;
    int hueUpper = 8;
    int saturationLower = 150;
    int saturationUpper = 255;
    int valueLower = 100;
    int valueUpper = 255;
};

int main(int argc, char** argv)
{
    try {
        BallTracker tracker;
        tracker.run();
    }
    catch (const std::exception& e) {
        std::cerr << "ERROR! " << e.what() << '\n';
        return 1;
    }

    return 0;
}

BTW, dawaj zawsze kod, który można skopiować bez kombinowania.

0

Cześć. Wracam po dłuższym czasie nieobecności. Postanowiłem sprawdzić najpierw dokładne czasy z mojego kodu dołączając kolejne fragmenty, a później spojrzę na kod Marka i pokombinuję dalej.

  • sam odczyt z kamery: 28.6 fps
  • kopia obrazu: 27.2 fps
  • medianBlur: 21 fps
  • konwersja HSV: 18.1fps
  • threshold: 17.9 fps
  • gaussianBlur: 14.5fps
  • HoughCircles: 7.7fps
  • pokazanie obrazów: bez zmian

Pewnie faktycznie ciężko będzie to zrobić na STMie ale może zdecyduję się na Raspberry. Ewentualnie zostanę przy LabView ale nie zaszkodzi się trochę pobawić z OpenCV :D

0
danielbr3 skomentował(a):

dla twojego kodu mam 13-15fps, ale chyba nic tutaj nie zmieniłeś poza usunięciem tego rozmycia. Z czytelnością faktycznie się nie postarałem u siebie, ostatnio chyba za dużo piszę w czystym C a tutaj chciałem tylko na szybko naklepać coś do testu.

Napisałem wyraźnie w tekście co zmieniłem. Przede wszystkim dodałem transformację Canny!

Po drugie pytanie, było o dokładność nie wydajność.
Do wydajności trzeba dobrać prawidłowo parametry. Przykładowo dla cv::HoughCircles należało by zawęzić rozmiar możliwych promieni okręgu. To powinno znacząco przyspieszyć program (poczytaj jak to działa).

Może lepiej będzie jak dostarczysz kilka przykładowych obrazów. Taki jaki ci działa i taki jakie ci nie działa.

1

Pytanie to co robi labview i jak. Już ci wcześniej zwracałem uwagę na parametry w hough które(szczególnie ostatnie dwa) ostatnie słabo wyglądają.

Ja widzę że problemem jest nie to że opencv jest wolne czy coś tylko nie do końca wiesz co robisz masz przykład na rpi

1 użytkowników online, w tym zalogowanych: 0, gości: 1