Speed-up von Koordinaten Suche

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
Finn_h
User
Beiträge: 12
Registriert: Samstag 30. Mai 2020, 08:51

Hallo,
ich habe einen Code geschrieben, der bei größeren Datenmengen zu langsam ist und bräuchte Anregungen wie ich diesen schneller machen kann.
Zu dem Code:
Ich trenne mit OpenCv dunkle von hellen Pixeln und schreibe die x-y-Koordinaten der dunklen Pixel in eine Liste. Diese Liste importiere ich dann in meinen "Problem Code". In diesem Teil soll die Liste so sortiert werden, dass die Liste möglichst meanderförmig von unten nach oben abgefahren werden. Das funktioniert für kleine Bilder (also kurze Listen) auch sehr gut. Bei größeren Bildern (also langen Listen) funktioniert das ebenfalls, dauert aber zu lange. Ich hoffe man kann sich das Problem vorstellen, würde gerne ein Bild der turtle Simulation hochladen, das geht aber nicht.
Die CPU Auslastung befindet sich aber die ganze Zeit bei ca. 25%. D.h. der Code läuft nur über einen von 4 Kerne des Prozessors (?)

Mein Idee wären:
1.) numpy zu verwenden, weil das schneller sein soll als Python Listen (stimmt das?)
2.) Multiprocessoring zu verwenden. Da weiß ich allerdings nicht welche Programmteile ich parallel laufen lassen könnte.

Über ein paar Anregungen wäre ich dankbar.

Hier der Code:

Code: Alles auswählen

from pixelfinder_v_iii import koo_list, height, width
import time

start = time.perf_counter()

def find_neigbors(koo_list):
    potential_neighbors = []
    neighbors_work = []

    for j in range(len(koo_list)):
        x_coordinate = koo_list[j][0]
        y_coordinate = koo_list[j][1]

        potential_neighbors.append((x_coordinate + 1, y_coordinate))
        potential_neighbors.append((x_coordinate - 1, y_coordinate))
        potential_neighbors.append((x_coordinate, y_coordinate + 1))
        #potentail_neighbors.append((x_coordinate+1, y_coordinate + 1))
        #potential_neighbors.append((x_coordinate -1, y_coordinate + 1))


        for i in range(len(potential_neighbors)):
            if potential_neighbors[i] in koo_list:
                neighbors_work.append(koo_list.index((potential_neighbors[i])))
        neighbors.append(tuple(neighbors_work))
        neighbors_work.clear()
        potential_neighbors.clear()
    return neighbors


def startpoints(coordinates_work_list, height, width):
    kleinste_y = height
    potential_startpoints = []
    for i in range(len(coordinates_work_list)):
        aktuelle_y = coordinates_work_list[i][1]
        if aktuelle_y < kleinste_y:
            kleinste_y = aktuelle_y
            potential_startpoints.clear()
            potential_startpoints.append(coordinates_work_list[i])
        elif aktuelle_y == kleinste_y:
            potential_startpoints.append(coordinates_work_list[i])
    kleinste_x = width
    for i in range(len(potential_startpoints)):
        aktuelle_x = potential_startpoints[i][0]
        if aktuelle_x < kleinste_x:
            kleinste_x = aktuelle_x

    return ((kleinste_x,kleinste_y))


def search_for_neighbor(neigbors, used_coordinates):
    for i in range(len(neigbors)):
        if neigbors[i] not in used_coordinates:
            return neigbors[i]
            pass;


print("Ab hier beginnt der pathfinder")

neighbors = []

neighbors = find_neigbors(koo_list)

used_coordinates = []
path_work = []
path = []
coordinates_work_list = koo_list


while len(used_coordinates) < len(koo_list):

    startpunkt = (startpoints(coordinates_work_list, height, width))

    path_work.append(koo_list.index(startpunkt))
    used_coordinates.append(koo_list.index(startpunkt))

    while True:
        next_point = (search_for_neighbor(neighbors[used_coordinates[-1]], used_coordinates))
        if next_point != None:
            used_coordinates.append(next_point)
            path_work.append(next_point)
        else:
            break;

    path.append(tuple(path_work))

    coordinates_work_list = []
    for j in range((len(koo_list))):
        if j not in used_coordinates:
            coordinates_work_list.append(koo_list[j])

    path_work.clear()

print(path)

finish = time.perf_counter()
print(f"abgeschlossen in {round(finish-start,2)} Sekunden")
Benutzeravatar
__blackjack__
User
Beiträge: 13112
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Finn_h: Mal so ganz allgemein sind da ein paar Ecken die komisch sind.

Beispielsweise die Ausführung. Das sieht so aus, als wenn das von einem anderen Modul aus ausgeführt wird in dem es importiert wird und sich dann aus diesem oder einem weiteren Modul die Werte auf denen es operiert wiederum per Import holt. Das ist sehr sehr sehr falsch!

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Kein Code der irgendwelche Ergebnisse berechnet. So etwas gehört in eine Funktion. Und alles was Funktionen und Methoden ausser Konstanten benötigen, bekommen sie als Argument(e) übergeben. Man verwendet keine globalen Variablen. Weder in dem man auf Modulebene Datenstrukturen anlegt und manipuliert, und schon gar nicht per ``global``. Falls Du ``global`` irgendwo verwendest, dann vergiss sofort, dass es das gibt, und schreib den Code sauber ohne globalen Zustand.

`find_neigbors()` fügt beispielsweise Elemente an die globale `neighbours`-Liste an *und* gibt die als Ergebnis zurück. Das ist verwirrend. Entweder übergibt man `neighbours` und das wird in der Funktion verändert und *nicht* zurückgegeben, denn wenn es als Argument übergeben wurde, hat der Aufrufer das Objekt ja bereits. Oder man erstellt in `find_neigbors()` eine neue Liste und gibt die dann an den Aufrufer zurück. Das wäre die sauberere, übersichtlichere Lösung.

Geunddatentypen wie `list` haben nichts in Namen zu suchen. Man ändert den Typ während der Entwicklung gerne mal und dann hat man entweder falsche, irreführende Namen im Quelltext, oder man muss die immer überall anpassen.

Namen sollten auch nicht abgekürzt werden oder kryptische Prä- und/oder Postfixe enthalten. Wenn man `coordinates` meint, sollte man bitte nicht `koos` schreiben.

Da sind ein paar überflüssige Semikolons im Quelltext. Das ist Python, kein C, C#, C++, Java, oder was auch immer, wo Anweisungen mit einem Semikolon abgeschlossen werden. ``pass`` in eiem Block der davor Code enthält macht auch keinen Sinn.

``for i in range(len(sequence)):`` nur um dann mit dem `i` auf ein Element in `sequence` zuzugreifen ist in Python ein „anti pattern“. Man kann direkt über die Elemente von `sequence` iterieren, ohne den Umweg über einen unnötigen Laufindex. Hör auf in irgendeiner anderen Programmiersprache zu programmieren, schreibe echtes Python. Oder nimm halt tatsächlich die Programmiersprache in der Du eigentlich programmieren willst.

Es gibt da Listen mit Koordinaten und Listen mit Indexwerten in andere Listen und man kann den Unterschied nicht an den Namen erkennen. Das ist verwirrend. Die sollte man passend benennen.

Das nächste was den Code fehleranfällig und ”unsauber” macht ist die Verwendung von `list.clear()`. Das verwendet man nur wenn man tatsächlich eine vorhandene Liste leeren muss die irgendwo anders im Code verwendet wird wo es wichtig ist, das diese Änderung sichtbar ist. Ansonsten verwendet man das nicht, weil das eben auch irgendwo anders im Code Auswirkungen haben kann mit denen man nicht rechnet und die dann zu schwer zu findenen Fehlern führen. Wenn man eine leere Liste braucht, dann erstellt man einfach eine leere Liste. Du kannst Dir dann auch die beiden `tuple()`-Aufrufe sparen, die den Listeninhalt von so einer mit `clear()` wiederverwendeten kopieren, weil das wiederverwenden sonst Probleme macht.

Der Code wird dann auch einfacher weil man nicht mehr überall mit `append()` arbeiten muss, sondern an bestimmten Stellen auch einfach gleich eine literale Liste mit den entsprechenden Werten hinschreiben kann.

Die erste Funktion schrumpft damit auf das hier zusammen:

Code: Alles auswählen

def find_neigbors_indices(pixels_coordinates):
    neighbor_indices = list()
    for x, y in pixels_coordinates:
        pixel_neighbor_indices = list()
        for potential_neighbor in [(x + 1, y), (x - 1, y), (x, y + 1)]:
            if potential_neighbor in pixels_coordinates:
                pixel_neighbor_indices.append(
                    pixels_coordinates.index(potential_neighbor)
                )
        neighbor_indices.append(pixel_neighbor_indices)
    
    return neighbor_indices
Davon die innere Schleife in eine „list comprehension“ umgeschrieben:

Code: Alles auswählen

def find_neigbors_indices(pixels_coordinates):
    neighbor_indices = list()
    for x, y in pixels_coordinates:
        neighbor_indices.append(
            [
                pixels_coordinates.index(potential_neighbor)
                for potential_neighbor in [(x + 1, y), (x - 1, y), (x, y + 1)]
                if potential_neighbor in pixels_coordinates
            ]
        )
    return neighbor_indices
Und dann die äussere Schleife:

Code: Alles auswählen

def find_neigbors_indices(pixels_coordinates):
    return [
        [
            pixels_coordinates.index(potential_neighbor)
            for potential_neighbor in [(x + 1, y), (x - 1, y), (x, y + 1)]
            if potential_neighbor in pixels_coordinates
        ]
        for x, y in pixels_coordinates
    ]
`startpoints()` ist kein guter Funktionsname weil der keine Tätigkeit beschreibt und weil so der Name `startpoints` für eine Objekt das Startpunkte repräsentiert, verloren ist. Die Mehrzahl in dem Namen ist ausserdem falsch, weil da nur *ein* Startpunkt ermittelt wird. Also `get_start_point()`.

Das kleinste `x` lässt sich leichter mit einer `min()` und einem Generatorausdruck ermitteln.

Wobei die Funktion gar nicht wirklich sicherstellt, dass es die Koordinate die am Ende heraus kommt, auch tatsächlich in den Übergebenen Koordinaten der Pixel gibt. Soll das so, oder ist das ein Fehler?

In `search_for_neighbor()` kann der Programmfluss am Ende der Funktion ankommen und da steht dann gar kein explizites ``return`` mehr. Da fragt sich der Leser ob das ein Fehler ist oder ob die Funktion noch nicht zuende ausprogrammiert wurde. Erst wenn man die Stelle sieht, wo die Funktion aufgerufen wird, sieht man, dass sich hier auf das implizite `None` verlassen wird. Das sollte explizit in der Funktion stehen.

`coordinates_work_list` hiesse vom Inhalt her besser `remaining_pixels_coordinates`, dann weiss der Leser gleich, was die Werte darin für eine Bedeutung haben. Und man kann die ``while`` Bedingung der ersten ``while``-Schleife auch kürzer formulieren, nämlich einfach ``while remaining_pixels_coordinates:``. Die Art wie diese Liste am Ende von jedem Schleifendurchlauf erstellt wird ist übrigens was die Laufzeit angeht ziemlich pervers. Das tut schon fast körperlich weh das zu lesen.

Wenn man bei `used_coordinates` darauf verzichtet auf das letzte Element mit ``…[-1]`` zuzugreifen, ist man nicht mehr daran gebunden, dass das eine Liste sein muss, und damit könnte man die ``in``-Operation zumindest schon mal beschleunigen, in dem man daraus eine Menge macht (`set`).

`round()` verwendet man nur wenn man mit gerundeten Zwischenergebnissen weiterrechnen will. Für die Ausgabe von einer begrenzten Anzahl von Nachkommastellen verwendet man die Möglichkeiten der Zeichenkettenformatierung.

Zwischenstand (ungetestet):

Code: Alles auswählen

import time


def find_neigbors_indices(pixels_coordinates):
    return [
        [
            pixels_coordinates.index(potential_neighbor)
            for potential_neighbor in [(x + 1, y), (x - 1, y), (x, y + 1)]
            if potential_neighbor in pixels_coordinates
        ]
        for x, y in pixels_coordinates
    ]


def get_start_point(pixels_coordinates, height, width):
    potential_startpoints = list()
    smallest_y = height
    for pixel_coordinates in pixels_coordinates:
        y = pixel_coordinates[1]
        if y < smallest_y:
            smallest_y = y
            potential_startpoints = [pixel_coordinates]
        elif y == smallest_y:
            potential_startpoints.append(pixel_coordinates)

    smallest_x = min((x for x, _ in potential_startpoints), default=width)
    return (smallest_x, smallest_y)


def search_for_neighbor_index(neigbor_indices, used_coordinates_indices):
    for index in neigbor_indices:
        if index not in used_coordinates_indices:
            return index
    return None


def find_path(pixels_coordinates, height, width):
    start = time.perf_counter()
    print("Ab hier beginnt der pathfinder")
    pixels_neighbors_indices = find_neigbors_indices(pixels_coordinates)
    used_coordinates_indices = set()
    path = list()
    remaining_pixels_coordinates = pixels_coordinates
    while remaining_pixels_coordinates:
        start_point = get_start_point(
            remaining_pixels_coordinates, height, width
        )
        point_index = pixels_coordinates.index(start_point)
        used_coordinates_indices.add(point_index)
        path_segment_indices = [point_index]
        while True:
            point_index = search_for_neighbor_index(
                pixels_neighbors_indices[point_index], used_coordinates_indices
            )
            if point_index is None:
                break

            used_coordinates_indices.add(point_index)
            path_segment_indices.append(point_index)

        path.append(path_segment_indices)
        #
        # XXX Butt ugly inefficient!
        #
        remaining_pixels_coordinates = [
            pixels_coordinates[i]
            for i in range((len(pixels_coordinates)))
            if i not in used_coordinates_indices
        ]

    print(path)
    finish = time.perf_counter()
    print(f"abgeschlossen in {finish - start:.2f} Sekunden")
Das ist jetzt erst einmal nur (fast) genau das was Dein Quelltext gemacht hat, nur etwas mehr in Python. Das sieht aber immer noch sehr danach aus wie etwas das eigentlich in C geschrieben werden wollte.

Was hier nämlich immer noch kein Python ist, sind die Listen die für *alles* verwendet werden und das herumgehampel mit Indexwerten. Das sollte man ohne die undurchsichtigen Indirektionen über Indexwerte lösen, in dem man die Koordinaten selbst verwendet. Und nicht alles muss eine Liste sein. Es gibt auch Mengen und Wörterbücher. Wenn die Koordinaten Tupel (oder was ähnlich ”hashbares”) sind, dann kann man die Koordinaten in Mengen als Elemente nutzen und in Wörterbüchern als Schlüssel. Zum Beispiel um eine Abbildung von Koordinaten auf alle Nachbarn zu erstellen, in der man direkt über die Koordinaten an die Koordinaten der Nachbarn kommen kann. ``in`` und ``index()`` in Listen sind hier ziemliche Performance-Killer, weil beides linear vom Anfang der Liste sucht und alle Elemente anschaut bis zu dem was gesucht wird. Oder wirklich alle, wenn das gesuchte Element gar nicht enthalten ist.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Man schreibt keine Schleifen über einen Index. Man löscht den Inhalt einer Liste nicht, sondern erzeugt einfach bei Bedarf eine neue. Man benutzt keine globalen Variablen: `neighbors` kommt aus dem nichts, wird aber trotzdem zurückgegeben.

Code: Alles auswählen

def find_neigbors(coordinates):
    neighbors = []
    for x, y in coordinates:
        potential_neighbors = [
            (x + 1, y),
            (x - 1, y),
            (x, y + 1),
            #(x + 1, y + 1),
            #(x - 1, y + 1),
        ]
         neighbors_work = []
        for neighbor in potential_neighbors:
            try:
                index = coordinates.index(neighbor)
                neighbors_work.append(index)
            except ValueError:
                pass
        neighbors.append(tuple(neighbors_work))
    return neighbors
Der Rest des Codes müßte auch erstmal so geschrieben werden, dass es Python ist.
In `search_for_neighbor` ist das `pass` überflüssig, wie auch alle ; im Programm. Man sollte bei einer Funktion nicht das implizite None benutzen.

Code: Alles auswählen

def search_for_neighbor(neigbors, used_coordinates):
    for neighbor in neigbors:
        if neigbor not in used_coordinates:
            return neigbor
    return None
`startpoints` ist ein ziemlich kompliziert geschriebenes `min`:

Code: Alles auswählen

def startpoints(coordinates):
    return min(coordinates, key=lambda c: (c[1], c[0]))
Und dann ist der Funktionsname noch totaler Quatsch, weil hier nur EIN Startpunkt gefunden wird.

Das Umbenennen von `coordinates_work_list = koo_list` ist überflüssig, warum benutzt Du nicht gleich einen sprechenden Namen wie `coordinates`.

Statt `round` benutzt man Format-Anweisungen:

Code: Alles auswählen

print(f"abgeschlossen in {finish-start:.2f} Sekunden")
Statt irgendwelche Listen zu durchsuchen, würde man wohl eine Matrix verwenden, wo belegte Pixel den Wert des Index haben.
Finn_h
User
Beiträge: 12
Registriert: Samstag 30. Mai 2020, 08:51

@__blackjack__ & Sirius3: danke für die Hilfe. Ich habe mir das in den letzten Tagen mal angesehen und einiges verändert.
Wieso ist es so falsch aus anderen Modulen zu importieren? Meine Idee war es das ganze modular aufzubauen, um einfacher mit Mehreren an dem Projekt arbeiten zu können. Trotzdem habe ich den Code jetzt so verändert, dass ich alles in einem Modul erledige. D.h. ich habe die Pixeldetektion sowie die Simulation in Funktionen verpackt und dem Code hinzugefügt. Allerdings wird es m.M.n. unübersichtlicher dadurch.
Global habe ich allerdings nirgends verwendet.
Mit anderen Programmiersprache (mal abgesehen von VBA) hatte ich bisher noch nicht zu tun, weshalb der Code so nach C aussieht weiß ich daher nicht.

Ich habe außerdem das "herumgehampel" mit den Indexwerten beseitigt und benutze jetzt die Koordinaten Tupels selbst. Ich hoffe der Code ist jetzt etwas mehr Python-like.

Wie würde man jetzt vorgehen, wenn man anstatt der Listen Mengen, Wörterbücher oder Matrizen verwenden wollte? Ein kleiner Denkanstoß würde genügen.

Hier noch mein aktueller Code (die Funktion "turn_coordinate_system" wirkt wahrscheinlich befremdlich, diese verwende ich um das Koordinaten System in die untere linke Ecke zu verlegen, wo es meinem und dem turtle Verständnis nach hingehört) ansonsten hoffe ich, dass der Code weniger körperliche Schmerzen auslöst.

Code: Alles auswählen

import time
import cv2
import numpy as np
import sys
import turtle

def nothing(x):
    pass

def mask(img_show):
    while True:

        RGB_frame = cv2.cvtColor(img_show, cv2.COLOR_BGR2RGB)

        #weißer filter

        limit = cv2.getTrackbarPos("Filter", "Trackbar")

        low_white = np.array([limit, limit, limit])
        high_white = np.array([256, 256, 256])

        white_mask = cv2.inRange(RGB_frame, low_white, high_white)
        white = cv2.bitwise_and(img_show, img_show, mask=white_mask)


        cv2.imshow("Gefiltertes Bild", white)
        cv2.imshow("Maske: Alle schwarzen Stellen werden abgefahren", white_mask)

        key = cv2.waitKey(1)

        if key == 27:
            cv2.destroyAllWindows()
            return limit

def turn_coordinate_system(pixels_coordinates, img, limit):
    for i in range((len(img[0]))):
        for j in range((len(img))):
            if img[j][i][0] <= limit and img[j][i][1] <= limit and img[j][i][2] <= limit:
                pixels_coordinates.append(tuple((i, (len(img) - j))))


def find_neigbors(pixels_coordinates):
    return [
        [
            potential_neighbor
            for potential_neighbor in [(x + 1, y), (x - 1, y), (x, y + 1)]
            if potential_neighbor in pixels_coordinates
        ]
        for x, y in pixels_coordinates
    ]


def get_next_point(path, pixels_coordinates, height, width):
    if len(path) == 0:
        return get_lowest_left_point(pixels_coordinates, height, width)
    else:
        x, y = path[-1][-1]
        for potentital_next_point in [(x,y-1),(x-1,y+1),(x+1,y+1),(x-1,y-1),(x+1,y-1),(x,y-2),(x,y+2),(x-2,y),(x+2,y),(x-2,y+2),(x+2,y+2),(x-2,y-2),(x+2,y-2)]:
            if potentital_next_point in pixels_coordinates:
                return potentital_next_point
        return get_lowest_left_point(pixels_coordinates, height, width)

def get_lowest_left_point(pixels_coordinates, height, width):
    potential_startpoints = list()
    smallest_y = height
    for pixel_coordinates in pixels_coordinates:
        y = pixel_coordinates[1]
        if y < smallest_y:
            smallest_y = y
            potential_startpoints = [pixel_coordinates]
        elif y == smallest_y:
            potential_startpoints.append(pixel_coordinates)

    smallest_x = min((x for x, _ in potential_startpoints), default=width)
    return (smallest_x, smallest_y)


def search_for_neighbor(pixel_neigbors, used_coordinates):
    for point in pixel_neigbors:
        if point not in used_coordinates:
            return point
    return None


def find_path(pixels_coordinates, height, width):
    start = time.perf_counter()
    pixels_neighbors = find_neigbors(pixels_coordinates)
    used_coordinates = set()
    path = list()
    remaining_pixels_coordinates = pixels_coordinates

    while remaining_pixels_coordinates:
        start_point = get_next_point(
            path,remaining_pixels_coordinates, height, width
        )
        point = start_point
        used_coordinates.add(point)
        path_segment = [point]

        while True:
            point = (search_for_neighbor(pixels_neighbors[pixels_coordinates.index((point))], used_coordinates))
            if point is None:
                break


            used_coordinates.add(point)
            path_segment.append(point)
        path.append(path_segment)
        #
        # XXX Butt ugly inefficient!
        #
        remaining_pixels_coordinates = [
            pixels_coordinates[i]
            for i in range((len(pixels_coordinates)))
            if pixels_coordinates[i] not in used_coordinates
        ]
    finish = time.perf_counter()
    print(f"abgeschlossen in {finish - start:.2f} Sekunden")

    return path

def simulation(path, ENLARGEMNET, offset_x, offset_y):
    for c in range(len(path)):
        turtle.up()
        turtle.goto(path[c][0][0] * ENLARGEMNET - offset_x, path[c][0][1] * ENLARGEMNET - offset_y)
        turtle.down()
        for i in range(len(path[c])):
            turtle.goto(path[c][i][0] * ENLARGEMNET - offset_x, path[c][i][1] * ENLARGEMNET - offset_y)
    turtle.Screen().exitonclick()

def main():
    # pixel Detektion
    ENLARGEMENT = 2
    img = cv2.imread("test.PNG")
    np.set_printoptions(threshold=sys.maxsize)
    pixels_coordinates = []

    height, width = img.shape[:2]

    img_show = cv2.resize(img, (0, 0), fx=ENLARGEMENT, fy=ENLARGEMENT)
    cv2.imwrite("Hintergrund_turtle.png", img_show)

    cv2.imshow("Orgninalbild", img_show)

    cv2.namedWindow("Trackbar")
    cv2.createTrackbar("Filter", "Trackbar", 0, 256, nothing)

    limit = (mask(img_show))

    turn_coordinate_system(pixels_coordinates, img, limit)

    #ab hier path
    path = find_path(pixels_coordinates, height, width)
    print(path)

    # ab hier Simulation

    offset_x = (((len(img[0])) * ENLARGEMENT) / 2)
    offset_y = (((len(img) * ENLARGEMENT)) / 2)
    PEN_SIZE = 2

    turtle.setup(width=len(img[0]) * ENLARGEMENT * 1.3, height=len(img) * ENLARGEMENT * 1.5, startx=100, starty=100)
    turtle.up()
    turtle.bgpic("Hintergrund_turtle.png")
    turtle.speed(5)
    turtle.color("red")
    turtle.pensize(PEN_SIZE)

    simulation(path, ENLARGEMENT, offset_x, offset_y)


if __name__ == "__main__":
    main()
PS: Wir sind Maschinenbauer und streben programmiertechnisch keine absolute Perfektion an.
Benutzeravatar
__blackjack__
User
Beiträge: 13112
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Finn_h: Es ist nicht falsch aus anderen Modulen zu importieren. Konstanten, Funktionen, und Klassen. Aber keine Variablen die da irgendwo auf Modulebene rumlungern. Der Code auf Modulebene wird nur einmal ausgeführt, wenn das Modul importiert wird. Man kann das also gar nicht vernünftig testen, ohne das man immer wieder das komplette Programm startet. Und der Code und die Beziehungen werden so auch total unübersichtlich. Man sieht ja beispielsweise nicht das und wo `width`, `height`, und `koo_list` in anderen Modulen weiterverarbeitet werden.

Wobei mir das Modul jetzt auch nicht so gross erscheint, dass man es unbedingt aufteilen müsste. Was die Zusammenarbeit angeht, dafür gibt es Versionsverwaltungen wie Git oder Mercurial.

Zum Programm selbst: Ich finde die Diskrepanz zwischen dem was man per Schwellwert grafisch auswählen kann und dem was dann hinterher tatsächlich von Turtle gezeichnet wird viel zu gross. Die Erwartungshaltung ist doch eigentlich, dass alles was schwarz ist, am Ende von Turtle rot nachgezeichnet wird. Dem ist aber nicht so, weil man grafisch einen Schwellenwert für ein hochskaliertes und dadurch etwas weichgezeichnetes Bild auswählt, den dann aber auf das Originalbild anwendet, wo er nicht die gleichen Pixel auswählt.

Konstanten werden üblicherweise am Anfang eines Moduls definiert und nicht lokal in Funktionen. Und Funktionsargumente sind keine Konstanten.

Namen sollte man nicht abkürzen. Wenn man `image` meint sollte man nicht `img` schreiben.

Man sollte Namen nicht zu weit von der Stelle definieren wo sie letztendlich verwendet werden. Das macht den Code unübersichtlicher, weil man sich mehr merken muss oder eben noch mal zurückgehen muss, um zu sehen wo etwas definiert wird. Es erschwert auch das Warten vom Code, wenn man beispielsweise etwas als eigene Funktion herausziehen will, und sich dann erst einmal alles zusammensuchen muss, statt einfach einen bereits zusammen stehenden Block an Zeilen zu verschieben. Bei zu langen Funktionen übersieht man auch leichter irgendwelche Überreste die gar nicht mehr verwendet werden.

Die `nothing()`-Funktion ist ein bisschen Overkill. Da kann man einfach einen ``lambda``-Ausdruck verwenden. Oder besser: man verwendet das tatsächlich statt da in einer Endlosschleife völlig unnötig Rechenzeit zu verbraten.

Das Umwandeln von BGR nach RGB macht keinen Sinn wenn man den gleichen Schwellwert auf alle drei Komponenten anwendet.

`turn_coordinate_system()` sollte keine leere Liste bekommen und die füllen, sondern selbst eine Liste mit den Koordinaten erstellen. Der Funktionsname ist auch irreführend. Ich hätte da erwartet, dass die Funktion Koordinaten bekommt und die umrechnet und war dann erst mal überrascht warum die eine leere Koordinatenliste übergeben bekommt.

Da werden Sachen in Python-Code gemacht, die eigentlich Numpy und/oder OpenCV erledigen könnten. Zum Beispiel wird da für jedes einzelne Pixel noch mal die Maske bestimmt. Statt über das Bild zu gehen, würde man hier besser über die Maske iterieren und die per Numpy-Operationen a) in die passenden Wahrheitswerte wandeln, und b) das Array horizontals spiegeln. Dann braucht man den y-Wert nicht selbst für jedes Pixel anpassen.

Das `get_lowest_left_point()` eigentlich ein Einzeiler mit `min()` ist, hatte Sirius3 ja schon gezeigt.

Bei dem Teil für die Simulation wird plötzlich wieder ``len(img[0])`` und ``len(img)`` verwendet, wo die beiden Werte doch bereits unter den schönen Namen `width` und `height` zur Verfügung stehen.

In `simulation()` ist wieder das `for i in range(len(sequence)):` „anti pattern“ zweimal drin. Und den ersten Punkt muss man nicht gesondert behandeln weil `turtle.down()` idempotent ist. Das muss also nur an der richtigen Stelle stehen.

Beim `remaining_pixels_coordinates` bestimmen am Ende der ``while``-Schleife kann man noch sehr einfach den Index `i` beseitigen und direkt über die Pixel iterieren. Was dann da immer noch blöd ist, ist das man da jedes mal über alle Pixelkoordinaten iterieren muss, auch wenn die Menge der verbleibenden Pixel ja immer kleiner wird.

Ein fieser `index()`-Aufruf ist in der Funktion auch noch drin. Da könnte man beispielsweise statt einer Liste, ein Wörterbuch erstellen, das Pixel auf Nachbarn abbildet und wo man dann in O(1) an die Nachbarn kommt, unabhängig von der Anzahl der Pixel die in die Funktion rein kommen. Dadurch wird dann auch gleich die `get_next_point()`-Funktion schneller weil da der ``in``-Test von profitiert.

Das es die gesamten Koordinaten, die noch verbleinden Koordinaten, und die bereits benutzen Koordinaten jeweils als Datenstruktur gibt ist auch etwas zu viel des guten. Eigentlich braucht man ja nur die noch verbleibenden Koordinaten. Ob eine Koordinate schon benutzt wurde, kann man ja einfach testen in dem man schaut ob die *nicht* in den verbleibenden Koordinaten ist.

Zwischenstand der zumindest für mein Beispielbild das ich hier verwendet habe spürbar schneller ist:

Code: Alles auswählen

#!/usr/bin/env python3
import sys
import time
import turtle
from functools import partial
from operator import itemgetter

import cv2
import numpy as np

ENLARGEMENT = 2
PEN_SIZE = 2


def calculate_mask(image, limit):
    return cv2.inRange(
        image, np.array([limit, limit, limit]), np.array([256, 256, 256])
    )


def update_display(image, limit):
    white_mask = calculate_mask(image, limit)
    white = cv2.bitwise_and(image, image, mask=white_mask)
    cv2.imshow("Gefiltertes Bild", white)
    cv2.imshow("Maske: Alle schwarzen Stellen werden abgefahren", white_mask)


def get_coordinates(image, limit):
    mask = np.flipud(calculate_mask(image, limit))
    ys, xs = np.nonzero(mask == 0)
    return list(zip(xs, ys))


def find_neigbors(pixels_coordinates):
    coordinates_to_neighbors = {
        coordinates: [] for coordinates in pixels_coordinates
    }
    for x, y in pixels_coordinates:
        coordinates_to_neighbors[x, y] = [
            potential_neighbor
            for potential_neighbor in [(x + 1, y), (x - 1, y), (x, y + 1)]
            if potential_neighbor in coordinates_to_neighbors
        ]
    return coordinates_to_neighbors


def get_lowest_left_point(pixels_coordinates):
    return min(pixels_coordinates, key=itemgetter(1, 0))


def get_next_point(path, pixels_coordinates):
    if len(path) == 0:
        return get_lowest_left_point(pixels_coordinates)
    else:
        x, y = path[-1][-1]
        for potentital_next_point in [
            (x, y - 1),
            (x - 1, y + 1),
            (x + 1, y + 1),
            (x - 1, y - 1),
            (x + 1, y - 1),
            (x, y - 2),
            (x, y + 2),
            (x - 2, y),
            (x + 2, y),
            (x - 2, y + 2),
            (x + 2, y + 2),
            (x - 2, y - 2),
            (x + 2, y - 2),
        ]:
            if potentital_next_point in pixels_coordinates:
                return potentital_next_point
        return get_lowest_left_point(pixels_coordinates)


def search_for_neighbor(neighbors, coordinates_to_neighbors):
    for neighbor in neighbors:
        if neighbor in coordinates_to_neighbors:
            return neighbor
    return None


def find_path(pixels_coordinates):
    start = time.perf_counter()
    coordinates_to_neighbors = find_neigbors(pixels_coordinates)
    path = list()
    while coordinates_to_neighbors:
        point = get_next_point(path, coordinates_to_neighbors)
        neighbors = coordinates_to_neighbors.pop(point)
        path_segment = [point]
        while True:
            point = search_for_neighbor(neighbors, coordinates_to_neighbors)
            if point is None:
                break
            neighbors = coordinates_to_neighbors.pop(point)
            path_segment.append(point)

        path.append(path_segment)

    finish = time.perf_counter()
    print(f"abgeschlossen in {finish - start:.2f} Sekunden")

    return path


def run_simulation(path, enlargemnet, offset_x, offset_y):
    for segment in path:
        turtle.up()
        for x, y in segment:
            turtle.goto(
                x * enlargemnet - offset_x, y * enlargemnet - offset_y,
            )
            turtle.down()
    turtle.Screen().exitonclick()


def main():
    np.set_printoptions(threshold=sys.maxsize)
    #
    # Pixel-Detektion.
    #
    image = cv2.imread("test.png")

    display_image = cv2.resize(image, (0, 0), fx=ENLARGEMENT, fy=ENLARGEMENT)
    cv2.imwrite("Hintergrund_turtle.png", display_image)
    cv2.imshow("Originalbild", display_image)
    update_display(display_image, 0)
    cv2.namedWindow("Trackbar")
    cv2.createTrackbar(
        "Filter", "Trackbar", 0, 256, partial(update_display, display_image)
    )
    while True:
        key = cv2.waitKey()
        if key == 27:
            limit = cv2.getTrackbarPos("Filter", "Trackbar")
            cv2.destroyAllWindows()
            break
    #
    # Ab hier Pfad bestimmen.
    #
    path = find_path(get_coordinates(image, limit))
    print(path)
    #
    # Ab hier Simulation.
    #
    height, width = image.shape[:2]
    turtle.setup(
        width=width * ENLARGEMENT * 1.3,
        height=height * ENLARGEMENT * 1.5,
        startx=100,
        starty=100,
    )
    turtle.up()
    turtle.bgpic("Hintergrund_turtle.png")
    turtle.speed(5)
    turtle.color("red")
    turtle.pensize(PEN_SIZE)
    run_simulation(
        path, ENLARGEMENT, width * ENLARGEMENT / 2, height * ENLARGEMENT / 2
    )


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Finn_h
User
Beiträge: 12
Registriert: Samstag 30. Mai 2020, 08:51

@__blackjack__: Danke nochmal für die Hilfe. Der Code läuft so tatsächlich sehr viel schneller auch für größere Bilder. Die Funktion get_next_point habe ich nochmals überarbeitet und es wahrscheinlich wieder viel zu kompliziert gemacht. Wenn du da nochmal drüber schauen könntest wäre ich dir sehr dankbar.

Ansonst an alle die es interessiert: Ich habe noch eine Funktion (write_g_code) geschrieben, die aus dem Path einen G_code erstellt den wir auch schon von einer Maschine haben lesen lassen, also funktionstüchtig ist.

Code: Alles auswählen

#!/usr/bin/env python3
import sys
import time
import turtle
from functools import partial
from operator import itemgetter

import cv2
import numpy as np

ENLARGEMENT = 0.3
PEN_SIZE = 2


def calculate_mask(image, limit):
    return cv2.inRange(
        image, np.array([limit, limit, limit]), np.array([256, 256, 256])
    )


def update_display(image, limit):
    white_mask = calculate_mask(image, limit)
    white = cv2.bitwise_and(image, image, mask=white_mask)
    cv2.imshow("Gefiltertes Bild", white)
    cv2.imshow("Maske: Alle schwarzen Stellen werden abgefahren", white_mask)


def get_coordinates(image, limit):
    mask = np.flipud(calculate_mask(image, limit))
    ys, xs = np.nonzero(mask == 0)
    return list(zip(xs, ys))


def find_neigbors(pixels_coordinates):
    coordinates_to_neighbors = {
        coordinates: [] for coordinates in pixels_coordinates
    }
    for x, y in pixels_coordinates:
        coordinates_to_neighbors[x, y] = [
            potential_neighbor
            for potential_neighbor in [(x + 1, y), (x - 1, y), (x, y + 1)]
            if potential_neighbor in coordinates_to_neighbors
        ]
    return coordinates_to_neighbors


def get_lowest_left_point(pixels_coordinates):
    return min(pixels_coordinates, key=itemgetter(1, 0))


def get_next_point(path, pixels_coordinates):
    if len(path) == 0:
        return get_lowest_left_point(pixels_coordinates)
    else:
        x, y = path[-1][-1]
        SEARCH_RADIUS = 4
        search_radius_counter = 1
        while search_radius_counter <= SEARCH_RADIUS:
            circle_around_point = []
            for j in range(-search_radius_counter, search_radius_counter + 1):
                circle_around_point.append((x + j, y - search_radius_counter))
                circle_around_point.append((x + j, y + search_radius_counter))
            for j in range(-search_radius_counter + 1, search_radius_counter):
                circle_around_point.append((x - search_radius_counter, y + j))
                circle_around_point.append((x + search_radius_counter, y + j))
            #
            # Negative Werte entfernen.
            #
            positiv_circle_around_point = []
            for xs, ys in circle_around_point:
                if xs >= 0 and ys >= 0:
                    positiv_circle_around_point.append((xs, ys))
            #
            # Sortieren nach: Direkt zuerst, diagonal anschließend.
            #
            potentital_next_points = []
            for xss, yss in positiv_circle_around_point:
                if x == xss or y == yss:
                    potentital_next_points.append((xss, yss))

            for xss, yss in positiv_circle_around_point:
                if (xss, yss) not in potentital_next_points:
                    potentital_next_points.append((xss, yss))

            for next_point in potentital_next_points:
                if next_point in pixels_coordinates:
                    return next_point

            search_radius_counter = search_radius_counter + 1

        return get_lowest_left_point(pixels_coordinates)


def search_for_neighbor(neighbors, coordinates_to_neighbors):
    for neighbor in neighbors:
        if neighbor in coordinates_to_neighbors:
            return neighbor
    return None


def find_path(pixels_coordinates):
    start = time.perf_counter()
    coordinates_to_neighbors = find_neigbors(pixels_coordinates)
    path = list()
    while coordinates_to_neighbors:
        point = get_next_point(path, coordinates_to_neighbors)
        neighbors = coordinates_to_neighbors.pop(point)
        path_segment = [point]
        while True:
            point = search_for_neighbor(neighbors, coordinates_to_neighbors)
            if point is None:
                break
            neighbors = coordinates_to_neighbors.pop(point)
            path_segment.append(point)

        path.append(path_segment)

    finish = time.perf_counter()
    print(f"abgeschlossen in {finish - start:.2f} Sekunden")

    return path


def run_simulation(path, enlargemnet, offset_x, offset_y):
    for segment in path:
        turtle.up()
        for x, y in segment:
            turtle.goto(
                x * enlargemnet - offset_x, y * enlargemnet - offset_y,
            )
            turtle.down()
    turtle.Screen().exitonclick()

def write_g_code(path):
    g_code_file = open("g_code.txt", "w")
    g_code_file.write("G28 \n")
    for j in range(len(path)):
        for i in range(len(path[j])):
            x, y = path[j][i]
            if i == 0:
                g_code_file.write("G00 X%4i Y%4i \n" % (x, y))
                g_code_file.write("M03 \n")
            else:
                g_code_file.write("G01 X%4i Y%4i \n" %(x,y))
        g_code_file.write("M05 \n")

    g_code_file.close()


def main():
    np.set_printoptions(threshold=sys.maxsize)
    #
    # Pixel-Detektion.
    #
    image = cv2.imread("shapes.png")

    display_image = cv2.resize(image, (0, 0), fx=ENLARGEMENT, fy=ENLARGEMENT)
    cv2.imwrite("Hintergrund_turtle.png", display_image)
    cv2.imshow("Originalbild", display_image)
    update_display(display_image, 0)
    cv2.namedWindow("Trackbar")
    cv2.createTrackbar(
        "Filter", "Trackbar", 0, 256, partial(update_display, display_image)
    )
    while True:
        key = cv2.waitKey()
        if key == 27:
            limit = cv2.getTrackbarPos("Filter", "Trackbar")
            cv2.destroyAllWindows()
            break
    #
    # Ab hier Pfad bestimmen.
    #
    path = find_path(get_coordinates(image, limit))
    print(path)
    #
    # Ab hier Simulation.
    #
    height, width = image.shape[:2]
    turtle.setup(
        width=width * ENLARGEMENT * 1.3,
        height=height * ENLARGEMENT * 1.5,
        startx=100,
        starty=100,
    )
    turtle.up()
    turtle.bgpic("Hintergrund_turtle.png")
    turtle.speed(10)
    turtle.color("red")
    turtle.pensize(PEN_SIZE)
    run_simulation(
        path, ENLARGEMENT, width * ENLARGEMENT / 2, height * ENLARGEMENT / 2
    )
    #
    # G Code erstellen
    #
    write_g_code(path)


if __name__ == "__main__":
    main()
Benutzeravatar
__blackjack__
User
Beiträge: 13112
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Finn_h: Ups, da ist ja noch ein Tab offen. 🙂

Konstanten (`SEARCH_RADIUS`) definiert man am Anfang des Moduls, nicht in Funktionen.

Die `while`-Schleife für `search_radius_counter` ist eine ``for``-Schleife.

Das entfernen der Negativen Werte verstehe ich nicht. Zu grosse Werte filterst Du ja auch nicht aus, und das erledigt sich ja beides schon automatisch dadurch, dass beide Varianten nicht in `pixels_coordinates` enthalten sind.

Bei Namen die „radius“ enthalten und `circle_around_point` ist es schon ein bisschen überraschend, wenn das dann Quadrate beschreibt. 😉

Beim Kommentar „Sortieren nach…“ hätte ich einen `sort()`-Aufruf erwartet.

Beim Schreiben vom G-Code sollte man die Datei mit ``with`` zusammen öffnen und man sollte bei Textdateien immer explizit eine Kodierung angeben.

Da ist wieder das „anti pattern“ erst unnötigerweise über Indexzahlen zu iterieren und mit denen auf `path` zuzugreifen, statt gleich über die Elemente von `path` zu iterieren. Eine Stufe tiefer dann das gleiche. Da wird `i` noch verwendet um in jedem Durchgang zu prüfen ob es der erste Durchgang ist. Da kann man das erste Element aber einfach *vor* der Schleife gesondert behandeln.

Ungetestet:

Code: Alles auswählen

#!/usr/bin/env python3
import sys
import time
import turtle
from functools import partial
from operator import itemgetter

import cv2
import numpy as np

ENLARGEMENT = 0.3
PEN_SIZE = 2
SEARCH_RADIUS = 4


def calculate_mask(image, limit):
    return cv2.inRange(
        image, np.array([limit, limit, limit]), np.array([256, 256, 256])
    )


def update_display(image, limit):
    white_mask = calculate_mask(image, limit)
    white = cv2.bitwise_and(image, image, mask=white_mask)
    cv2.imshow("Gefiltertes Bild", white)
    cv2.imshow("Maske: Alle schwarzen Stellen werden abgefahren", white_mask)


def get_coordinates(image, limit):
    mask = np.flipud(calculate_mask(image, limit))
    ys, xs = np.nonzero(mask == 0)
    return list(zip(xs, ys))


def find_neigbors(pixels_coordinates):
    coordinates_to_neighbors = {
        coordinates: [] for coordinates in pixels_coordinates
    }
    for x, y in pixels_coordinates:
        coordinates_to_neighbors[x, y] = [
            potential_neighbor
            for potential_neighbor in [(x + 1, y), (x - 1, y), (x, y + 1)]
            if potential_neighbor in coordinates_to_neighbors
        ]
    return coordinates_to_neighbors


def get_lowest_left_point(pixels_coordinates):
    return min(pixels_coordinates, key=itemgetter(1, 0))


def get_next_point(path, pixels_coordinates):
    if path:
        x, y = path[-1][-1]
        for search_radius_counter in range(1, SEARCH_RADIUS + 1):
            circle_around_point = []
            for j in range(-search_radius_counter, search_radius_counter + 1):
                circle_around_point.append((x + j, y - search_radius_counter))
                circle_around_point.append((x + j, y + search_radius_counter))

            for j in range(-search_radius_counter + 1, search_radius_counter):
                circle_around_point.append((x - search_radius_counter, y + j))
                circle_around_point.append((x + search_radius_counter, y + j))
            #
            # Sortieren nach: Direkt zuerst, diagonal anschließend.
            #
            circle_around_point.sort(
                key=lambda point: x == point[0] or y == point[1]
            )
            for next_point in circle_around_point:
                if next_point in pixels_coordinates:
                    return next_point

    return get_lowest_left_point(pixels_coordinates)


def search_for_neighbor(neighbors, coordinates_to_neighbors):
    for neighbor in neighbors:
        if neighbor in coordinates_to_neighbors:
            return neighbor
    return None


def find_path(pixels_coordinates):
    start = time.perf_counter()
    coordinates_to_neighbors = find_neigbors(pixels_coordinates)
    path = list()
    while coordinates_to_neighbors:
        point = get_next_point(path, coordinates_to_neighbors)
        neighbors = coordinates_to_neighbors.pop(point)
        path_segment = [point]
        while True:
            point = search_for_neighbor(neighbors, coordinates_to_neighbors)
            if point is None:
                break
            neighbors = coordinates_to_neighbors.pop(point)
            path_segment.append(point)

        path.append(path_segment)

    finish = time.perf_counter()
    print(f"abgeschlossen in {finish - start:.2f} Sekunden")

    return path


def run_simulation(path, enlargemnet, offset_x, offset_y):
    for segment in path:
        turtle.up()
        for x, y in segment:
            turtle.goto(
                x * enlargemnet - offset_x, y * enlargemnet - offset_y,
            )
            turtle.down()
    turtle.Screen().exitonclick()


def write_g_code(path):
    with open("g_code.txt", "w", encoding="ascii") as g_code_file:
        g_code_file.write("G28 \n")
        for segment in map(iter, path):
            x, y = next(segment)
            g_code_file.write(f"G00 X{x:4} Y{y:4} \nM03 \n")
            g_code_file.writelines(f"G01 X{x:4} Y{y:4} \n" for x, y in segment)
            g_code_file.write("M05 \n")


def main():
    np.set_printoptions(threshold=sys.maxsize)
    #
    # Pixel-Detektion.
    #
    image = cv2.imread("shapes.png")

    display_image = cv2.resize(image, (0, 0), fx=ENLARGEMENT, fy=ENLARGEMENT)
    cv2.imwrite("Hintergrund_turtle.png", display_image)
    cv2.imshow("Originalbild", display_image)
    update_display(display_image, 0)
    cv2.namedWindow("Trackbar")
    cv2.createTrackbar(
        "Filter", "Trackbar", 0, 256, partial(update_display, display_image)
    )
    while True:
        key = cv2.waitKey()
        if key == 27:
            limit = cv2.getTrackbarPos("Filter", "Trackbar")
            cv2.destroyAllWindows()
            break
    #
    # Ab hier Pfad bestimmen.
    #
    path = find_path(get_coordinates(image, limit))
    print(path)
    #
    # Ab hier Simulation.
    #
    height, width = image.shape[:2]
    turtle.setup(
        width=width * ENLARGEMENT * 1.3,
        height=height * ENLARGEMENT * 1.5,
        startx=100,
        starty=100,
    )
    turtle.up()
    turtle.bgpic("Hintergrund_turtle.png")
    turtle.speed(10)
    turtle.color("red")
    turtle.pensize(PEN_SIZE)
    run_simulation(
        path, ENLARGEMENT, width * ENLARGEMENT / 2, height * ENLARGEMENT / 2
    )
    #
    # G Code erstellen
    #
    write_g_code(path)


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten