Entwickler-Ecke

Sonstiges (.NET) - Intergerwerte scheinbar ohne Grund auf negativem Maximum


Kasko - Mi 08.08.18 01:51
Titel: Intergerwerte scheinbar ohne Grund auf negativem Maximum
Ich habe mir als Ziel gesetzt dieses Programm nachzubauen: https://youtu.be/sYd_-pAfbBw

Dafür zeichne ich eine Ellipse und mische die Pixel durch. Aber ich möchte, dass die Pixel nicht nur auf dem Rand der Ellipse liegen, sondern das ganze Innere der Ellipse ausnutzen. Dafür nutze ich folgenden Satz: Je weiter ein Pixel von seiner ursprünglichen, richtigen Position entfernt ist, desto näher liegt er am Mittelpunkt. Dafür nutze ich den Winkel zwischen 2 Vektoren. Wenn ein Pixel genau gegenüber seiner ursprünglichen Position liegt, dann ist er maximal weit entfernt und der Winkel zwischen den Vektoren vom Ellipsenmittelpunkt zur jetzigen und zur ursprünglichen Position beträgt 180°. Der Prozentsatz wie weit der Pixel nach außen gezeichnet wird errechnet sich demnach: 1 - (winkel / 180.0f). Diesen Prozentsatz wende ich dann auf den Vektor vom Mittelpunkt zur jetzigen Position an und erhalte die neue, "gemischte" Position.

Das Problem ist jetzt aber, dass manchmal und scheinbar zufällig Punkte nicht gesetzt werden und die x- und y-Werte dann einen Wert von -2147482688 (negatives Maximum) besitzen. Dies geschieht nicht immer schon beim Mischen aber spätestens beim Sortieren. Dabei tritt es immer an unterschiedlichen Stellen auf.

Hier der relevante Code:


C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
110:
111:
112:
113:
114:
115:
116:
117:
118:
119:
120:
121:
122:
123:
124:
125:
126:
127:
128:
129:
130:
131:
#region Initialisation

public void Initialise(int xRadius, int yRadius) {
    this.xRadius = xRadius;
    this.yRadius = yRadius;

    origin = new Vector2i(xRadius, yRadius);

    int count = 4 * (xRadius + yRadius);
    float angleIncrement = 360.0f / count;

    points = new SortingEllipsePoint[count];

    for (int i = 1; i <= count; i++) {
        points[i - 1] = new SortingEllipsePoint(i, GetEllipsePoint(xRadius, yRadius, origin, i * angleIncrement - 90.0f), Rainbow(i/(float)count));
    }

    SetImage();
}

#endregion

#region Perparation

public void BeginShuffle() {
    new Thread(() => Shuffle()).Start();
}

private void Shuffle() {
    int displayCount = 0;

    for (int i = 0; i < points.Length; i++, displayCount++) {
        SwapPoints(i, rnd.Next(0, points.Length));

        if (displayCount == 30) {
            SetImage();
            displayCount = 0;
        }
    }

    OnShuffleFinish?.Invoke(thisnew EventArgs());
}

#endregion

#region Sorting

public void InsertionSort() {

    new Thread(() => {
        for (int i = 1; i < points.Length; i++) {
            SortingEllipsePoint insert = points[i];
            int j = i;

            while (j > 0 && points[j - 1].CompareTo(insert) > 0) {
                Assign(j, points[j - 1]);
                j--;
            }

            Assign(j, insert);
            swapCount++;

            if (swapCount > 20) {
                SetImage();
                swapCount = 0;
            }
        }

        swapCount = 0;
        SetImage();
    }).Start();
}

#endregion

#region Swap and Assign

private void Assign(int fromint to) {
    points[to] = new SortingEllipsePoint(points[from].value, points[to].maxPoint, points[from].color);
    points[to].point = GetPoint(points[to]);
}

private void Assign(int to, SortingEllipsePoint point) {
    points[to] = new SortingEllipsePoint(point.value, points[to].maxPoint, point.color);
    points[to].point = GetPoint(points[to]);
}

private void SwapPoints(int first, int second) {
    SortingEllipsePoint point = points[first];

    points[first] = new SortingEllipsePoint(points[second].value, points[first].maxPoint, points[second].color);
    points[second] = new SortingEllipsePoint(point.value, points[second].maxPoint, point.color);

    points[first].point = GetPoint(points[first]);
    points[second].point = GetPoint(points[second]);
}

private Vector2i GetPoint(SortingEllipsePoint point) {
    float angle = SFMLMath.Angle(point.maxPoint - origin, points[point.value - 1].maxPoint - origin);
    float percent = 1 - (angle / 180.0f);

    Vector2i vector = point.maxPoint - origin;

    return origin + new Vector2i((int)(vector.X * percent), (int)(vector.Y * percent));
}

#endregion

#region Presentation

private void SetImage() {
    Color[,] pixels = new Color[2 * xRadius + 12 * yRadius + 1];

    for (int i = 0; i < points.Length; i++) {
        pixels[points[i].point.X, points[i].point.Y] = points[i].color; <<<------ Hier tritt der Fehler auf
    }

    _image = new Image(pixels);

    OnImageChange?.Invoke(thisnew EventArgs());
}

private Vector2i GetEllipsePoint(int dHalfwidthEllipse, int dHalfheightEllipse, Vector2i origin, float t) {
    return new Vector2i((int)(origin.X + dHalfwidthEllipse * Math.Cos(t * Math.PI / 180)), (int)(origin.Y + dHalfheightEllipse * Math.Sin(t * Math.PI / 180)));
}

private Color Rainbow(float progress) {
    // unwichtig
}

#endregion


Hoffe auf Mithilfe, auch wenn es vielleicht etwas länger dauert den Fehler zu finden.

Wenn es weitere Fragen oder Unstimmigkeiten gibt, einfach sagen

LG Kasko ;)


Th69 - Mi 08.08.18 07:20

Hallo Kasko,

laß mal die Threads weg. Tritt es dann auch noch auf?
Denn woher weißt du, welcher der Threads gerade noch läuft? Wenn zwei oder mehr gleichzeitig laufen, überschreiben diese sich ja gegenseitig die Daten (sog "race condition").

Wenn du die Threads benutzt, um die GUI nicht zu blockieren, dann benutze besser Tasks mittels async/await-Methoden (denn diese laufen dann im gleichen Thread [wie die UI, wenn von dort aus gestartet] und du kannst explizit auf deren Beendigung warten).

Ansonsten kannst du auch im Debugger sog. "Haltepunkt-Bedingungen" ("breakpoint conditions") definieren, d.h. wenn du eine Vermutung für den konkreten Fehler hast, dann kannst du z.B. "points[i].point.X = 2147482688" angeben (dadurch läuft der Code jedoch langsamer, insbesondere in größeren Schleifen). Aber sobald diese Bedingung eintritt, wird das Programm an diesem Haltepunkt angehalten und du kannst weiter debuggen (Stacktrace untersuchen, Variablen auswerten etc.).


Kasko - Mi 08.08.18 12:52

Ich habe es jetzt ohne Threads probiert und der Fehler tritt leider trotzdem auf.

Zu der Frage woher ich weiß wann die Threads beendet sind:
Ich habe sowohl für das Mischen als auch für das Sortieren einen EventHandler, dessen Methode ganz am Ende des entsprechenden Threads aufgerufen wird (OnShuffleFinish / OnSortingFinish). Vielleicht ist der Thread danach noch kurz aktiv aber es wird kein weiterer Code ausgeführt der irgendetwas ungewollt überschreiben könnte. Genauso habe ich auch einen EventHandler für das image der Ellipse. Dadurch liest das Programm das image nur aus wenn es gerade gesetzt wurde und nicht wenn es gerade gesetzt wird. Außerdem wird es auch nur ausgelesen, wenn es sich verändert hat.

Was die Haltepunkte angeht, habe ich einen auf die Methode angewendet, mit der die ganzen Punkte errechnet werden und dort ist diese Bedingung nicht erfüllt worden, heißt keiner der Werte war auf -2147482688 oder etwas der gleichen. In SetImage ist der Fehler dann trotzdem aufgetreten.


Th69 - Mi 08.08.18 13:16

Da fällt mir jetzt auch nichts mehr ein (außer intensiver Debuggen).

An deinem Code fällt mir noch auf, daß die Zeilen 80 und 85 sehr eigenartig sind (erst neues Objekt erzeugen und dann einen Member mittels einer Methode des gleichen Objekts setzen???).

Und ist SortingEllipsePoint eine Klasse oder eine Struktur (nur letzteres wäre dann bei deinem InsertionSort richtig - Stichwort: Wertetyp).


Kasko - Mi 08.08.18 13:51

Zu den Zeilen 80 und 85:

Um ein korrektes Berechnen der Punkte auch nach mehrmaligem Vertauschen zu gewährleisten benötige ich nicht nur die momentane Position, sondern auch die "maximale" Position, also den Punkt auf dem Ellipsenrand, auf dem der Punkt liegen würde, wenn er nicht eingerückt werden würde. Vielleicht ist es besser zu verstehen, wenn ich die Klasse SortingEllipsePoint in die Antwort einbinde


C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
public class SortingEllipsePoint : IComparable<SortingEllipsePoint> {
    public Vector2i point, maxPoint;
    public SFML.Graphics.Color color;
    public int value;

    public SortingEllipsePoint(int value, Vector2i maxPoint, SFML.Graphics.Color color) {
        this.value = value;
        this.maxPoint = this.point = maxPoint;
        this.color = color;
    }

    public int CompareTo(SortingEllipsePoint obj) {
        return value < obj.value ? -1 : (value > obj.value ? 1 : 0);
    }
}


maxPoint -> Punkt auf dem Ellipsenrand basierend auf der Reihenfolge der gemischten Pixel
point -> Punkt auf der Strecke zwischen maxPoint und Ursprung der Ellipse basierend auf dem errechneten Prozentsatz -> Position des gezeichneten Pixels

Wenn ich nun einen neuen Punkt erstelle werden maxPoint und point auf den Punkt auf dem Ellipsenrand gesetzt. Ich muss demnach im nachhinein point mittels des Prozentsatzes errechnen um die gewünschte Einrückung zu erreichen. Ich übergebe zwar den selben Punkt, dessen Member point ich setzen möchte, nutzen tu ich in der GetPoint-Methode jedoch nur maxPoint. Theoretisch könnte ich auch nur einen Vector2i übergeben, welche den maxPoint darstellt.

Zu deiner letzten Frage: Wie du am obenliegenden Codeausschnitt siehst, ist SortingEllipsePoint eine Klasse. Wieso ist in meiner Methode InsertionSort lediglich eine Struktur richtig?


Th69 - Mi 08.08.18 18:29

Sorry, hatte mich vertan - du fügst ja in der Assign-Methode eine neue Referenz (new SortingEllipsePoint) hinzu (und änderst nicht die bestehende).

Das beste aber wäre wohl, wenn du dir einen Unit-Test (s. z.B. Grundlagen zum Komponententest [https://msdn.microsoft.com/de-de/library/hh694602.aspx]) für die Sortiermethode schreibst.


Ralf Jansen - Mi 08.08.18 19:43

Alternativ versuche das Problem für uns in ein minimales auch für uns kompilierbares Projekt zu überführen. Also ein Projekt das um alles befreit ist was nicht zum nachvollziehen des Problems nötig ist und auch von uns kompiliert und ausgeführt werden kann. Das könnten wir uns dann theoretisch ansehen (und ist für uns einfacherer als eine statische Analyse eines Codeausschnitts) aber das Verfahren ist eigentlich das Programmierer äquivalent zum Spickzettel schreiben ;) Heißt beim vereinfachen des Problems auf das hier relevante stolpert man meist selbst über das Problem und braucht keine Hilfe mehr. Eben ähnlich wie bei einem selbstgeschriebenen Spickzettel wo man dadurch das man darüber nachdenkt wie man das ganze Wissen das man braucht so zusammenstaucht das es auf einen Spickzettel passt diesen nicht mehr braucht weil man dadurch die Problematik soweit durchleuchtet hat das man es verstanden hat.


Kasko - Do 09.08.18 01:52

Ich bin gerade dabei das Programm zu vereinfachen und es für euch und die meisten anderen auch kompilierbar zu machen, indem ich es von einer SFML.Net Windows Application in eine WinForms Application umbaue. Leider hat sich deine (@Ralf Jansen) Aussage nicht bewahrheitet. Im Gegenteil. Es ist ein weiterer, für mich unerklärlicher Fehler aufgetreten. Da ich jetzt auf Basis einer WinForms Application arbeite, musste ich alle Methoden und Klassen etwas umschreiben. Die Methode, in der der neue Fehler auftritt, ist die SetImage-Methode, welche ja auch Auslöser des vorherigen Fehlers war und noch immer ist. Die Methode sieht nun wie folgt aus:


C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
private void SetImage() {
    lock (lockMe) {
        _image = new Bitmap(width, height);

        for (int i = 0; i < points.Length && points[i] != null; i++) {
            int x = points[i].point.X < 0 ? 0 : (points[i].point.X > width - 1 ? width - 1 : points[i].point.X);
            int y = points[i].point.Y < 0 ? 0 : (points[i].point.Y > height - 1 ? height - 1 : points[i].point.Y);

            _image.SetPixel(x, y, points[i].color);
        }

        OnImageChange?.Invoke(thisnew EventArgs());
    }
}


Der Fehler tritt in Form einer System.ArgumentException in Zeile 3 (Neuinitialisierung von _image). VS sagt mir, dass ein ungültiger Parameter in den Bitmapkonstruktor übergeben wird, was ich mir bei 2, als integer definierten, Werten kaum vorstellen kann.

Irgendwelche Ideen?


Kasko - Do 09.08.18 02:12

Okay hat sich erledigt. Habe vorher wohl sehr schlecht gegoogelt. Ich musste lediglich eine PixelFormat hinzufügen: _image = new Bitmap(width, height, PixelFormat.Format16bppRgb555); Der Fehler tritt zumindest nicht mehr auf, ob er wirklich behoben wurde kann ich nicht sagen.


Kasko - Do 09.08.18 14:50

Okay, ich habe das Projekt jetzt so weit komprimiert wie möglich und es trotzdem noch lauffähig ist. (4 Klassen die nicht standardmäßig vorliegen und lediglich eine geht wirklich über 50 Zeilen hinaus) Bitte seht mir nach, dass der Code nicht kommentiert ist. Der Fehler wird, beim aktuellen Stand des Projektes, nicht auftreten, da die x- und y-Werte in der SetImage-Methode in den Größen der PictureBox gehalten werden. Allerdings werdet ihr links oben in der Ecke des schwarzen Feldes Pixel sehen, die da nicht hingehören. Damit der Fehler wieder auftritt müsst ihr SetPixel direkt mit den Werten points[i].point.x und y ausführen.

Bin natürlich auch offen für Verbesserungsvorschläge oder Meldungen von anderen Fehlern, die bei mir noch nicht aufgetreten sind und vielleicht sogar wie ihr sie in den Griff bekommen habt, ansonsten muss ich das halt machen ;)


Ralf Jansen - Do 09.08.18 20:53

Ich hab leider nicht allzu viel Zeit mir das genau anzusehen um das Problem zu korrigieren. Ich kann aber ziemlich genau auf die Problemstelle zeigen ;)
Ein schneller Debuglauf zeigt mir das im Fehlerfall in deiner GetPoint Methode aus VectorMath.Angle NaN (Not a Number) zurückkommt und wenn man NaN nach int castet kommt da Int.MinValue bei rum.

Wenn du dem Vorschlag von TH69 folgst und Tests schreibst wäre insbesondere mal VectorMath.Angle zu testen.
Ein entsprechender Aufruf der schief läuft wäre zum Beispiel


C#-Quelltext
1:
VectorMath.Angle(new Point(365, -236), new Point(-365236));                    

Da kommt NaN raus. Sollte aber vermutlich 180 sein.


PS:
Zitat:
Der Fehler tritt in Form einer System.ArgumentException in Zeile 3 (Neuinitialisierung von _image). VS sagt mir, dass ein ungültiger Parameter in den Bitmapkonstruktor übergeben wird, was ich mir bei 2, als integer definierten, Werten kaum vorstellen kann.

Das Problem wird eher nicht das Imageformat sein, wie von dir vermutet, sondern das du beim erzeugen eines neuen Bitmaps das alte nicht disposed. Dir geht einfach irgendwann der GDI Speicher aus wenn du das Image häufiger neu erzeugst. Die nicht hilfreiche Fehlermeldung ist dem geschuldet das da GDI drunterliegt. Daran muss mann sich leider gewöhnen wenn man GDI oder GDI+ benutzt. Die Fehlermeldungen haben eigentlich nie einen offensichtlichen Zusammenhang zur eigentlichen Fehlerursache. :evil: Das das Image Format setzen scheinbar hilft liegt wohl eher daran das du ein Speicherplatzsparenderes Format gewählt hast. Dir geht einfach nur später der Speicher aus.


Kasko - Do 09.08.18 23:13

Vielen, vielen Dank @Ralf Jansen, dass du mich in die richtige Richtung gezerrt hast. Das Problem war minimal. Und zwar traten/treten bei der Berechnung des Wertes für den Acos minimale Abweichungen gegenüber dem tatsächlichen Wert auf. Dadurch ist eine Berechnung eines Grenzwertes eines Intervalls, welcher nicht überschritten werden darf, eigentlich schon zum scheitern verurteilt. Und so war es auch. Der berechnete Wert war bei einem Winkel von 180° minimal größer als 1. Ein Acos(a) für a > 1 ist aber nicht definiert und deshalb kam NaN als Ergebnis heraus. ich habe also lediglich diese Zeile eingefügt und nun läuft alles super. (Diese Abweichung war so gering, dass sogar eine Ausgabe des Wertes 1 ausgibt)


C#-Quelltext
1:
a = a < -1 ? -1 : (a > 1 ? 1 : a);                    


Vielen Dank ;)

Wobei alle guten Dinge sind drei, also hier eine weitere Exception. Eine System.InvalidOperationException wird ausgeworfen, wenn ich versuche _image.Dispose() aufzurufen, da es angeblich gerade an einer anderen Stelle benutzt wird. Wenn ich es allerdings einfach neu setze tritt die Ausnahme nicht auch, obwohl ich _image genauso verwende wie durch Dispose :/


Ralf Jansen - Fr 10.08.18 13:38

Den Code an der Stelle fand ich allgemein merkwürdig habe aber mal einfach angenommen das es einfach nur Beispielcode ist und in Wirklichkeit anders aussieht.

Wenn der tatsächliche Code ähnlich aussieht wie in deinem Beispiel solltest du auf keinem Fall da wo du das neue Image erzeugst das alte Image aus zerstören den das alte Image wird ja noch von der PictureBox benutzt.

In deinem Fall ist es ja so das du das Image aus deiner Klasse rausgibst. Diese Klasse kann damit die Lebenszeit dieser Resource nicht steuern (zumindest nicht ohne Aufwand und nachhalten der Nutzer der Resource) da sie nicht weiß wo den die Resource den jetzt überall gelandet ist und genutzt wird. Die Verantwortung des Zerstörens liegt als beim Nutzer und das ist bei dir die Form bzw. die PictureBox wo das angezeigt wird.

Ich würde empfehlen _image in SetImage zu einer reinen lokalen Variable zu machen und die nicht mehr als Property zu veröffentlichen damit sich die Form das da abholen kann.
Das Image sollte dann über die EventArgs des OnImageChange Events rausgegeben werden.

Der Nutzer des Events ist dann für das Zerstören alter Images verantwortlich weil der weiß wann er das Image nicht mehr braucht. Hier ist das der Fall wenn das Image in der PictureBox ausgetauscht wird. Du solltest also wenn du das Image der PictureBox zuweist schauen ob nicht schon ein Image in der PictureBox steckt und dieses disposen.

Nebenbei fand ich es auch komisch das du die Zuweisung des Images mit einem lock schützt. Das löst eigentlich nicht die Crossthread Probleme die man beim zugreifen auf die UI haben kann. Denn wie baust du genau diesen Lock so ein das das System (dessen Code du nicht hast) den auch berücksichtigt? Dein jetziger Code wird unter bestimmten Bedingungen voraussichtlich knallen auch wenn dieser Zustand möglicherweise schwer zu erreichen ist. Zum Beispiel wenn du wild beim Ausführen die Anwendung minimierst, maximierst sollte es irgendwann knallen wenn das austauschen des Images zu dem Zeitpunkt stattfindet wo Windows gerade der Meinung ist neu zeichnen zu müssen und dafür auch auf das Image zugreift. An die Stelle muss eigentlich eine Threadsynchronisierung mit dem Hauptthread her ein lock ist zu wenig.


Kasko - Sa 11.08.18 02:13

Okay die Idee, das Image per ImageChangeEventArgs zu übergeben funktioniert sehr gut. Danke ;)

Was die lock-Anweisungen angeht, stimme ich dir voll und ganz zu. Das ist Schwachsinn, die zu verwenden. Allerdings ist diese Version (die Version in der die lock's noch enthalten sind) total veraltet. Die lock's sind schon lange nicht mehr enthaltenund ich frage mich ehrlich gesagt auch warum ich sie überhaupt implementiert habe. Thread-Sychronization erziele ich durch die Klasse System.Threading.SynchronizationContext.

Hier die Implementierung:


C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
using System.ComponentModel;
using System.Threading;

private readonly SynchronizationContext syncContext;
public event ImageChangeEventHandler OnImageChange = null;

public Ellipse() {
    syncContext = AsyncOperationManager.SynchronizationContext;
}

private void SetImage() {
    Bitmap image = new Bitmap(width, height);

    // ...

    syncContext.Post(ImageChanged, new ImageChangeEventArgs((Bitmap)image.Clone()));
    image.Dispose();
}

private void ImageChanged(object args) {
    if (!(args is ImageChangeEventArgs))
        return;

    OnImageChange?.Invoke(this, (ImageChangeEventArgs)args);
}