Umrechnung pixel zu mm einer Höhe

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
digerid
User
Beiträge: 4
Registriert: Freitag 3. Juni 2022, 06:47

Hey
ich habe ein Code geschrieben, mit dem ich die Höhe einer Kontur in mm messen möchte mittels OpenCV. Mein jetziger Stand ist, dass ich die für mich wichtigen Konturen finde, die Höhe des minAreaRect() finde und in pixeln anzeigen kann. Ich habe auch eine Kontur, von der ich die Höhe in pixel bestimmen kann und von der ich die Höhe in mm kenne, sodass ich den Wert pixels_per_mm ausrechnen kann, das funktioniert alles. Nur bestimme ich diesen Wert innerhalb meiner Funktion (recht weit "rechts"), sodass, wenn ich ihn abfrage einen Fehler bekomme: UnboundLocalError: local variable 'px_per_mm' referenced before assignment. Dass ich diesen Fehler bekomme kann ich nachvollziehen, allerdings weiß ich nicht, wie ich die Funktion umschreiben kann, damit ich die Variable px_per_mm in der letzten Zeile (cv.putText()) verwenden kann. Da die Frage mit Python und weniger mit OpenCV zusammenhängt, frage ich hier.

Code:

Code: Alles auswählen

def contour_areas(contours):
    areas = []
    for cnt in contours:
        cont_area = cv.contourArea(cnt)
        if cont_area >= 2500:
            areas.append(cont_area)
            cv.drawContours(imgcontours_min_area, cnt, -1, (0, 255, 255), 2)
             
            rect = cv.minAreaRect(cnt)
            box = cv.boxPoints(rect)
            box = np.int0(box)
            cv.drawContours(imgcontours_min_area,[box],0,(0,0,255),2)
            (x, y), (w, h), angle = rect

            height = []
            if h >= 0.8*w and h <= 1.2*w:
                height.append(h)
                print(height)
                px_per_mm = h / 20
                print(px_per_mm)

            cv.putText(imgcontours_min_area, "w={},h={}".format(round(w, 2), round(h, 2)), (int(x-w/2.2),int(y-h/1.5)), cv.FONT_HERSHEY_SIMPLEX, 0.7, (0,0,255), 2)

    return areas
Das Problem ist nun, wenn ich in die Zeile

Code: Alles auswählen

cv.putText(imgcontours_min_area, "w={},h={}".format(round(w, 2), round(h, 2)), (int(x-w/2.2),int(y-h/1.5)), cv.FONT_HERSHEY_SIMPLEX, 0.7, (0,0,255), 2)
nun als

Code: Alles auswählen

cv.putText(imgcontours_min_area, "w={},h={}".format(round(w, 2), round(h/px_per_mm, 2)), (int(x-w/2.2),int(y-h/1.5)), cv.FONT_HERSHEY_SIMPLEX, 0.7, (0,0,255), 2)
schreibe, dass die Variable px_per_mm nicht benutzt werden kann, da sie ja "weiter rechts" definiert wird (so zumindest mein Verständnis).

Vielen Dank!
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Hallo!

Das Problem bei dir schleicht sich im zweiten if-Statement ein. Nur wenn die Bedingung

Code: Alles auswählen

h >= 0.8*w and h <= 1.2*w
erfüllt ist, wir dein Wert für `px_per_mm` gesetzt. Sonst nicht. Du bekommst also die Fehlermeldung, wenn die Bedingung nicht erfüllt ist. Jetzt musst du dir überlegen, wie sich dein Programm verhalten soll. Gibt es einen sinnvollen Wert für `px_per_mm` wenn der if-Block nicht ausgeführt wird? Dann muss der Wert gesetzt werden. Oder soll der Text vielleicht nur hinzugefügt werden, wenn die Bedingung erfüllt ist? Dann musst du das Schreiben des Texts in den if-Block ziehen.

Das Runden mittels `round` macht hier übrigens nicht viel Sinn und bezweckt nicht das, was du dir vorstellst. Wenn du eine Ausgabe gerundet haben möchtest, dann solltest du dir die Syntax für die String-Formatierung etwas genauer ansehen. Dazu gibt es einige Optionen.
Das Leben ist wie ein Tennisball.
digerid
User
Beiträge: 4
Registriert: Freitag 3. Juni 2022, 06:47

Hi, danke für die Antwort, macht Sinn.

Ich benutze dieses if statement, da ich ein Rechteck auf mein Bild gezeichnet habe, anhand dessen ich die Beziehung pixel_per_mm herstelle. Und diesen Wert (bei mir 6.6 pixel pro mm) wollte ich dann im cv.putText() verwenden um andere Höhen (wo h >= 0.8*w and h <= 1.2*w nicht gilt) von pixeln in mm umzurechnen.

Meinen Gedanken nach kann es nicht so schwierig sein, immerhin habe ich diesen Wert schon. Könntest du mir helfen was ich machen muss, um den Wert pixelpermm auch für Höhen zu benutzen, wo h >= 0.8*w and h <= 1.2*w nicht zutrifft?

Das Runden hat bei mir schon geholfen, ich gucke mir den Befehl dennoch mal genauer an!

Danke!
Sirius3
User
Beiträge: 17757
Registriert: Sonntag 21. Oktober 2012, 17:20

Wir wissen nicht, was Du mit der if-Abfrage bezwecken willst, und warum Du nur dann `pixel_per_mm` ausrechnest.
Die triviale Lösung wäre ja, das außerhalb der if-Abfrage zu machen, wenn das möglich ist.

Code: Alles auswählen

def contour_areas(contours):
    areas = []
    for cnt in contours:
        cont_area = cv.contourArea(cnt)
        if cont_area >= 2500:
            areas.append(cont_area)
            cv.drawContours(imgcontours_min_area, cnt, -1, (0, 255, 255), 2)
             
            rect = cv.minAreaRect(cnt)
            box = cv.boxPoints(rect)
            box = np.int0(box)
            cv.drawContours(imgcontours_min_area,[box],0,(0,0,255),2)
            (x, y), (w, h), angle = rect
            px_per_mm = h / 20

            height = []
            if h >= 0.8*w and h <= 1.2*w:
                height.append(h)

            cv.putText(imgcontours_min_area, f"w={w:.2f},h={h/px_per_mm:.2f}", (int(x - w / 2.2), int(y - h / 1.5)), cv.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)

    return areas
Die if-Abfrage und die Liste `height` sind so natürlich ziemlich unsinnig.
digerid
User
Beiträge: 4
Registriert: Freitag 3. Juni 2022, 06:47

Mithilfe der if Abfrage finde ich in den Konturen diejenige, die quadratisch ist, denn dieses Quadrat hat eine Kantenlänge von 20 mm, sodass ich damit die Beziehung "px_per_mm" aufstellen kann. Diese Beziehung möchte ich dann aber auch für die anderen Konturen nutzen, um die Höhe, die ich bereits in px habe, in Millimeter umrechnen zu können. Diese anderen Konturen haben allerdings ein anderes Seitenverhätnis (damit ich in der if Schleife natürlich nur eine Kontur finde..)
Benutzeravatar
__blackjack__
User
Beiträge: 13119
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@digerid: Anmerkungen zum Quelltext: Funktions und Methodennamen werden üblicherweise nach der Tätigkeit benannt, die sie durchführen. `contour_areas()` wäre ein guter Name für das Ergebnis dieser Funktion, aber nicht für die Funktion selbst.

Namen sollten keine kryptischen Abkürzungen enthalten, oder gar nur daraus bestehen. Also nicht nur schreiben `cnt` wenn man `contour` meint. Ist auch ziemlich nah an einem englischen „schmuzigen Wort“. Bei `cont_area` ist das Problem das `cont` verschiedene Sachen abkürzen kann, beispielsweise `continue(d)`. Oder `contagious` oder `contrary` oder …

`height` als Name für eine Liste ist irreführend weil die ja nicht *eine* Höhe enthält, sondern mehrere. Wenn man die `heights` nennt, dann braucht man den Namen der tatsächlich für eine einzelne Höhe steht, auch nicht mit `h` abkürzen.

Funktionen und Methoden bekommen alles was sie ausser Konstanten benötigen als Argument(e) übergeben. Der Funktion hier fehlt das Bild auf dem gemalt wird. Das ist keine Konstante, kommt aber ”magisch” aus der ”Umgebung”.

Und auch hier wieder der Hinweis zu kryptischen Abkürzungen. Und man sollte die normale Reihenfolge von Worten in Namen beibehalten. Ein `contour_image` ist etwas anderes als ein `image_contour`.

Wenn man einen Wert gegen zwei andere Vergleicht und das mit ``and`` verknüpft, kann man das in der Regel mit verketteten Operatoren lesbarer ausdrücken.

Zwischenstand (ungetestet):

Code: Alles auswählen

RED = (0, 0, 255)
YELLOW = (0, 255, 255)


def get_contour_areas(contours, contours_min_area_image):
    areas = []
    for contour in contours:
        contour_area = cv.contourArea(contour)
        if contour_area >= 2500:
            areas.append(contour_area)

            cv.drawContours(contours_min_area_image, contour, -1, YELLOW, 2)

            rect = cv.minAreaRect(contour)
            box = np.int0(cv.boxPoints(rect))
            cv.drawContours(contours_min_area_image, [box], 0, RED, 2)

            (x, y), (width, height), _angle = rect
            #
            # FIXME Die Liste `heights` macht so keinen Sinn und wird auch
            # nirgends verwendet.
            #
            heights = []
            if 0.8 * width <= height <= 1.2 * width:
                heights.append(height)

            pixel_per_mm = height / 20
            cv.putText(
                contours_min_area_image,
                f"w={width:.2f},h={height / pixel_per_mm:.2f}",
                (int(x - width / 2.2), int(y - height / 1.5)),
                cv.FONT_HERSHEY_SIMPLEX,
                0.7,
                RED,
                2,
            )

    return areas
Wenn Du die quadratische contour brauchst um diesen Wert zu berechnen, und diesen Wert bei allen Kontouren brauchst, dann kannst Du das nicht in einer Schleife lösen wo beides gilt, denn das kann ja offensichtlich nur funktionieren wenn die quadratische contour die erste ist. Das ist sie nicht, denn genau deshalb läufst Du ja in das Problem das Du versuchst etwas zu benutzen was zu dem Zeitpunkt noch gar nicht berechnet wurde.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
__blackjack__
User
Beiträge: 13119
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@digerid: Wie identifizierst Du eigentlich das Quadrat, denn alleine das Seitenverhältnis eines in der Kontur gezeichneten Rechtecks kann ja auch bei völlig anderen Konturen nahe 1 sein. Kannst Du sicherstellen, dass das Quadrat entsprechend ausgerichtet ist, so das der Winkel von dem `RotatedRect` ungefähr rechtwinklig sein muss? Und ein Vergleich zwischen Konturfläche und Fläche des `RotatedRect` sollte ungefähr gleich sein, wobei auch das Kriterium nicht wirklich wasserdicht ist.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
digerid
User
Beiträge: 4
Registriert: Freitag 3. Juni 2022, 06:47

Erstmal danke für deine allgemeinen Hinweise!! Ich versuche mich seit einem Monat eigenständig an Python und kann sowas sehr gut gebrauchen!!

Ich habe meinen Code jetzt mal versucht zu vereinfachen, sieht tatsächlich sehr viel übersichtlicher aus! Im Endeffekt muss ich die area der Konturen auch nicht speichern, sodass mein Code nun ein Stück anders aussieht, s.u.

Leider funktioniert der Code, den du mir umgeschrieben hast, nicht, denn er loopt durch die Konturen und nimmt die Höhe der jeweiligen Kontur (bzw des cv.minAreaRect()) teilt diese durch 20, und im Befehl cv.putText() teilt er dann durch "pixels_per_mm" und ist somit wieder bei 20. Demnach hat jede Kontur die Höhe 20 mm, was nicht stimmt, da er ja immer einen anderen Wert für "pixels_per_mm" berechnet pro Kontur.

Du hast recht, dass die Liste "heights" keine Verwendung findet, hab ich rausgenommen.
Wenn Du die quadratische contour brauchst um diesen Wert zu berechnen, und diesen Wert bei allen Kontouren brauchst, dann kannst Du das nicht in einer Schleife lösen wo beides gilt, denn das kann ja offensichtlich nur funktionieren wenn die quadratische contour die erste ist. Das ist sie nicht, denn genau deshalb läufst Du ja in das Problem das Du versuchst etwas zu benutzen was zu dem Zeitpunkt noch gar nicht berechnet wurde.
Genau das ist mein Problem, habe jetzt auch nachvollziehen können wie es entsteht, aber eine Lösung bsier leider nicht gefunden.
@digerid: Wie identifizierst Du eigentlich das Quadrat, denn alleine das Seitenverhältnis eines in der Kontur gezeichneten Rechtecks kann ja auch bei völlig anderen Konturen nahe 1 sein. Kannst Du sicherstellen, dass das Quadrat entsprechend ausgerichtet ist, so das der Winkel von dem `RotatedRect` ungefähr rechtwinklig sein muss? Und ein Vergleich zwischen Konturfläche und Fläche des `RotatedRect` sollte ungefähr gleich sein, wobei auch das Kriterium nicht wirklich wasserdicht ist.
In meinem Fall ist die Kontur tatsächlich die einzige relevante, die auch nur annähernd in dem Seitenverhältnis steht. Unrelevante filtere ich ja schon mittels area>=2500 raus, und das was da dann entsprechendes Seitenverhältnis hat ist immer das Rechteck. Dieses Rechteck habe ich extra auf das Blatt eingefügt, um den Wert pixels_per_mm zu bestimmen. Ich dachte diesen Wert kann ich dann ganz einfach benutzen.

Kann ich evtl eine ganz neue Funktion aufstellen, wo ich erstmal nur "pixels_per_mm" bestimme und diesen Wert dann auch für andere Konturen nutzen kann?

Code: Alles auswählen

# Find Contours / Draw Contours Area >= 2500   
RED = (0, 0, 255)
YELLOW = (0, 255, 255)      
imgcontours = img.copy()
contours, hierarchy = cv.findContours(imgROI, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_NONE, offset=(ROIx1, ROIy1))

def contour_filter():
    for contour in contours:
        if cv.contourArea(contour) >= 2500:
            drawContours(imgcontours, contour, -1, YELLOW, 2)

            rect = cv.minAreaRect(contour)
            box = cv.boxPoints(rect)
            box = np.int0(box)
            cv.drawContours(imgcontours,[box],0, RED, 2)
            (x, y), (height, width), angle = rect            # w und h tauschen müssen, Bild ist Hochformat?

            posh = int(x-width/2.2) ; posw = int(y-height/1.5)
            cv.putText(imgcontours, f"w={width:.2f},h={height:.2f}", (posh, posw), cv.FONT_HERSHEY_SIMPLEX, 0.7, RED, 2) #x, y in integer sonst falscher data type; w, h mit round für 2 Nachkommastellen  

contour_filter()
Komischerweise musste ich "width" und "height" tauschen (siehe Kommentar), ich denke das liegt vielleicht am Format oder so, aber mit dem Tausch passt und funktioniert alles wie bisher!
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Statt mit so einer unzuverlässigen Heuristik arbeitet man bei sowas eigentlich mit einem bekannten Marker, zb einem Fiducal. Das ist trivial zu lokalisieren, und dann kann man die Kontur darum auch einfach bestimmen & als Kalibrationskörper benutzen.
Antworten