OpenCV. Suche nach Verkehrszeichen mit Konturanalyse in Android

  • Tutorial
Hallo Habr!

Ich möchte meine eigene Implementierung des Verkehrszeichensuchalgorithmus mit anderen teilen.

Warum Konturanalyse?

Die Konturanalyse weist eine eher schwache Störfestigkeit auf, aber die Einfachheit und Geschwindigkeit ermöglichten eine recht erfolgreiche Anwendung dieses Ansatzes.



In der Praxis stellte sich jedoch heraus, dass die Suche nach den erforderlichen Koeffizienten auf der Android-Plattform ziemlich schwierig war (ich habe nicht versucht, OpenCV-Widgets zu verwenden, sondern den Bildschirm in Frames unterteilt, wobei die Einstellungen links und der Videostream von der Rückkamera rechts sind). Die spezifische Implementierung der Benutzeroberfläche und der Projektlogik finden Sie unter dem folgenden Link.

Die Reihenfolge der Operationen zum Suchen eines Verkehrszeichens ist wie folgt:

  • Rauschunterdrückung

    Zum Beispiel durch ein nichtlineares Filter

    cv::medianBlur(original, original, 3);
    

  • Konvertieren eines Bildes von RGBA in HSV

    Bei Verwendung des HSV-Farbmodells wird die rote Grundfarbe eines Verkehrszeichens anhand von Farbton, Sättigung und Helligkeit genauer unterschieden.

    Gleichzeitig meinen sie, wenn es um den Farbton geht, in der Regel genau die Farbe. Die Sättigung gibt an, wie stark die beschriebene Farbe mit Weiß verdünnt ist (z. B. ist Pink eine Mischung aus Rot und Weiß). Das Konzept der Helligkeit ist am schwierigsten zu beschreiben, und mit einigen Annahmen kann Helligkeit als Lichtintensität verstanden werden.

    cv::Mat hsv;
    cv::cvtColor(original, hsv, cv::COLOR_RGB2HSV);
    if (layerType == LAYER_HSV) {
      *(cv::Mat*)matAddress = hsv;
    }
    

  • Hervorheben eines Verkehrszeichens nach Farbe



    Während der Verarbeitung wird das Bild in separate Kanäle H, S und V aufgeteilt, die anschließend nach einer bestimmten Schwelle digitalisiert und durch logische Addition in die endgültige Bildmatrix kombiniert werden.

    std::vector channels;
    cv::split(hsv, channels);
    cv::Mat minHueThreshold = channels[0] < lowerHue;
    if (layerType == LAYER_HUE_LOWER) {
      *(cv::Mat*)matAddress = minHueThreshold;
    }
    cv::Mat maxHueThreshold = channels[0] > upperHue;
    if (layerType == LAYER_HUE_UPPER) {
      *(cv::Mat*)matAddress = maxHueThreshold;
    }
    if (layerType == LAYER_HUE) {
      *(cv::Mat*)matAddress = minHueThreshold | maxHueThreshold;
    }
    cv::Mat saturationThreshold = channels[1] > minSaturation;
    if (layerType == LAYER_SATURATION) {
      *(cv::Mat*)matAddress = saturationThreshold;
    }
    cv::Mat valueThreshold = channels[2] > minValue;
    if (layerType == LAYER_VALUE) {
      *(cv::Mat*)matAddress = valueThreshold;
    }
    cv::Mat colorFiltered =
        (minHueThreshold | maxHueThreshold) & saturationThreshold & valueThreshold;
    if (layerType == LAYER_RED_FILTERED) {
      *(cv::Mat*)matAddress = colorFiltered;
    }
    

  • Erkennen eines Verkehrszeichens anhand der Außenkontur



    Zunächst wird ein Dehnungsvorgang ausgeführt, der Rauschen beseitigt und dazu beiträgt, Bildbereiche zu vereinen, die durch Objekte und Schatten getrennt wurden.

    cv::Mat colorDilated;
    cv::dilate(colorFiltered, colorDilated, cv::Mat());
    if (layerType == LAYER_DILATED) {
      *(cv::Mat*)matAddress = colorDilated;
    }
    

    Verwenden Sie zum Suchen nach Konturen die SimpleBlobDetector-Klasse, die nichts anderes als ein Sonderfall der findContours () - Funktion für runde Objekte ist. Es kann externe und verschachtelte Konturen finden und deren Anhangshierarchie bestimmen.

    cv::SimpleBlobDetector::Params params;
    params.filterByColor = false;
    params.filterByConvexity = false;
    params.filterByInertia = false;
    params.filterByArea = true;
    // A = 254.46900494077 px^2 при минимальном диаметре круга в 9 px
    params.minArea = 255;
    // A = 723822.94738709 px^2 при максимальном диаметре круга в 480 px
    params.maxArea = 723823;
    params.filterByCircularity = true;
    params.minCircularity = 0.85f;
    cv::Ptr detector =
        cv::SimpleBlobDetector::create(params);
    std::vector keyPoints;
    detector->detect(colorDilated, keyPoints);
    

    Der keyPoints-Vektor zeichnet die Koordinaten und den Radius des Straßenschildumrisses auf, die dann auf dem Bildschirm hervorgehoben werden.

Ich kann nicht sagen, dass es unter realen Bedingungen gut funktioniert (vielleicht ist der Erkennungsalgorithmus nicht der beste, aber er erwies sich als der am einfachsten zu verwendende). Und es gibt Schwierigkeiten, die optimalen Koeffizienten zu finden (ich habe genug nach Verkehrszeichen in meiner Stadt "gejagt"; ich habe sogar Zeichen getroffen, die von der Sonne verblasst sind, es war überhaupt kein Rot auf ihnen).

Aber die Einfachheit gefällt dem Auge, so dass es durchaus möglich ist, Anwendungen für die Konturanalyse beispielsweise in der Robotik zu finden.

Von den zusätzlichen Extras über die Erfahrung mit der Verwendung von OpenCV in Android:

Drehen Sie das Kamerabild, wenn es auf dem Kopf steht
extern "C"
JNIEXPORT void JNICALL Java_ru_dksta_prohibitingsigndetector_ActivityMain_rotation(JNIEnv /* *env */,
    jclass /* activity */, jlong matAddress, jint angle) {
    CV_Assert(angle % 90 == 0 && angle <= 360 && angle >= -360);
    cv::Mat* mat = (cv::Mat*) matAddress;
    if (angle == 180 || angle == -180) {
        cv::flip(*mat, *mat, -1);
    }
}


Implementierung von Salt & Pepper Noise
extern "C"
JNIEXPORT void JNICALL Java_ru_dksta_prohibitingsigndetector_ActivityMain_saltPepperNoise(JNIEnv /* *env */,
    jclass /* activity */, jlong matAddress) {
    cv::Mat* mat = (cv::Mat*) matAddress;
    cv::Mat noise = cv::Mat::zeros((*mat).rows, (*mat).cols, CV_8U);
    cv::randu(noise, 0, 255);
    cv::Mat black = noise < 30;
    cv::Mat white = noise > 225;
    (*mat).setTo(255, white);
    (*mat).setTo(0, black);
}


Video von der Kamera mit Verarbeitung im Modus "Bild in Bild" anzeigen
if (secondView) {
  cv::Mat miniView = colorDilated.clone();
  cv::cvtColor(miniView, miniView, cv::COLOR_GRAY2RGB);
  cv::resize(miniView, miniView, cv::Size(), 0.6, 0.6, cv::INTER_LINEAR);
  cv::Size miniSize = miniView.size();
  cv::Size maxSize = original.size();
  int startY = maxSize.height - miniSize.height;
  for (int y = startY; y < maxSize.height; y++) {
    for (int x = 0; x < miniSize.width; x++) {
      (*(cv::Mat*)matAddress).at(cv::Point(x, y)) =
          miniView.at(cv::Point(x, y - startY));
    }
  }
}


Schreiben von Text in OpenCV
cv::Mat* mat = (cv::Mat*)matAddress;
int textStartY = TEXT_LINE_HEIGHT;
std::ostringstream output;
output << std::setw(2) << std::setfill('0') << fpsCount << " FPS";
cv::putText(*mat, output.str(), cv::Point(TEXT_START_X, textStartY), FONT_FACE,
            FONT_SCALE, GREEN, TEXT_THICKNESS);
output.seekp(0);
textStartY += TEXT_LINE_HEIGHT;
cv::putText(*mat, getLayerTypeDesc(layerType),
            cv::Point(TEXT_START_X, textStartY), FONT_FACE, FONT_SCALE, GREEN,
            TEXT_THICKNESS);


Link zum Projekt

Jetzt auch beliebt: