Auf meiner Reise durch das AI Universum stellte sich die Frage, welches Problem lösen wir. Da finden sich Taxi Fare und House Prices als Regression Beispiele. Oder Hund und Auto Erkennung in Bildern in der Kategorie Vision.
Also konstruieren wir uns ein Problem. Ein Whiteboard in das der Benutzer per Ink/Pen Figuren zeichnet, die AI erkennt was es ist und an die gleiche Stelle eine korrekt unverwackelte Skizze erstellt, Gibt es schon.
Lösung soll mit Blazor sein. Dazu brauch ich eine Trainings Web App. Sogar Server basiert, damit ich keinen Aufwand mit speichern habe. Recht unspektakulär nehme ich dazu SVG
1: <divid="Whiteboard" @ref="Whiteboard"
2: @onmousemove="MoveMouse" @onmousedown="DownMouse" @onmouseup="UpMouse"
3: @ontouchmove="MoveTouch" @ontouchstart="DownTouch" @ontouchend="UpTouch"draggable="false"
4: style="border:2px solid black;display:inline-block;
max-height:80vh;max-width:100vw;overflow:hidden;touch-action: none;">
5: <svgstyle="width:800px; height:800px;">
6: @foreach (var (line, i) in Lines.WithIndex())
7: {
8: <LineComponentX1=@line.prevXY1=@line.prevY
X2=@line.currXY2=@line.currYStroke=@line.colorStrokeWidth=@line.lineWidth @key="i"/>
9: }
10: </svg>
11: </div>
Die Idee ist eine Liste von Linien zu einer Figur zu verbinden. Für einen Kreis nicht ganz so optimal, aber was solls.
1: protected List<Line> Lines = new List<Line>();
2: protectedbool flag = false;
3: protecteddouble prevX = 0;
4: protecteddouble currX = 0;
5: protecteddouble prevY = 0;
6: protecteddouble currY = 0;
7: protectedint count = 0;
8: protectedint status = 0;
9: protectedstring color = "#000000";
10: protectedfloat lineWidth = 2;
11: ElementReference Whiteboard;
12: BoundingClientRect DIVOffset;
13: int counter;
14: protectedoverride async Task OnAfterRenderAsync(bool firstRender)
15: {
16: if (firstRender)
17: {
18: DIVOffset = await JSRuntime.InvokeAsync<BoundingClientRect>(
"eval", "document.getElementById('Whiteboard').getBoundingClientRect()");
19: }
20:
21: }
Der Trick beliebigen JavaScript Code per Eval auszuführen ist nicht fein aber Effektiv um die Position des Boards zu erhalten. Dann führe ich zwei Hilfsklassen ein
1: publicclass Line
2: {
3: public Line() { }
4: public Line(double prevX, double prevY, double currX, double currY, string color, float lineWidth)
5: {
6: this.prevX = prevX;
7: this.prevY = prevY;
8: this.currX = currX;
9: this.currY = currY;
10: this.color = color;
11: this.lineWidth = lineWidth;
12: }
13: publicdouble prevX { get; set; }
14: publicdouble currX { get; set; }
15: publicdouble prevY { get; set; }
16: publicdouble currY { get; set; }
17: publicstring color { get; set; }
18: publicfloat lineWidth { get; set; }
19:
20: }
21:
22: publicclass BoundingClientRect
23: {
24: publicdouble X { get; set; }
25: publicdouble Y { get; set; }
26: publicdouble Width { get; set; }
27: publicdouble Height { get; set; }
28: publicdouble Top { get; set; }
29: publicdouble Right { get; set; }
30: publicdouble Bottom { get; set; }
31: publicdouble Left { get; set; }
32: }
Den folgenden C# Blazor Code erläutere ich nicht im Detail.
1: private async Task MoveMouse(MouseEventArgs e)
2: {
3: await Move(e.ClientX, e.ClientY);
4: }
5: private async Task MoveTouch(TouchEventArgs e)
6: {
7: await Move(e.Touches[0].ClientX, e.Touches[0].ClientY);
8: }
9: private async Task Move(double clientX, double clientY)
10: {
11: if (flag)
12: {
13: prevX = currX;
14: prevY = currY;
15: currX = clientX - DIVOffset.X;
16: currY = clientY - DIVOffset.Y;
17: var addedLine = new Line(prevX, prevY, currX, currY, color, lineWidth);
18: Lines.Add(addedLine);
19: status++;
20: count++;
21: this.StateHasChanged();
22: }
23: }
24: private async Task DownMouse(MouseEventArgs e)
25: {
26: await Down(e.ClientX, e.ClientY);
27: }
28: private async Task DownTouch(TouchEventArgs e)
29: {
30: await Down(e.Touches[0].ClientX, e.Touches[0].ClientY);
31: }
Schon etwas spannender ist der Code wenn der Stift den Bildschirm verlässt. Die Figur ist dann fertig gezeichnet. Allerdings als SVG. Wir brauchen eine Library
NuGet\Install-Package Svg.Skia
Aus dem Line Array wird ein SVG String zusammengesetzt. Danach (Zeile 23) ein SK Bitmap generiert. Ab Zeile 25 wird das SVG in das Bitmap Objekt gezeichnet und am Ende als png (Zeile 23) auf der Festplatt des Webservers abgelegt.
1: privatevoid UpTouch(TouchEventArgs e)
2: {
3: flag = false;
4: using (var svg = new SKSvg())
5: {
6: var sr = new StringBuilder();
7: sr.Append("<svg style=\"width: 800px; height: 800px; \">\n");
8: foreach (var line in Lines)
9: {
10: sr.Append($@"<line x1=""{line.prevX.ToString(
CultureInfo.InvariantCulture)}"" y1 =""{line.prevY.ToString(
CultureInfo.InvariantCulture)}""
11: x2 =""{line.currX.ToString(CultureInfo.InvariantCulture)}""
y2 =""{line.currY.ToString(CultureInfo.InvariantCulture)}"" stroke=""black""
12: stroke-width =""{line.lineWidth.ToString(CultureInfo.InvariantCulture)}"" />");
13: }
14: sr.Append("</svg>");
15: }
16: var cs = SKColorSpace.CreateSrgb();
17: var farbe = SKColor.Parse("#ffffff");
18: svg.FromSvg(sr.ToString());
19: if (svg.Picture != null)
20: {
21: var width = (int)svg.Picture.CullRect.Width;
22: var height = (int)svg.Picture.CullRect.Height;
23: using (var bitmap = new SKBitmap(width, height))
24: {
25: using (var canvas = new SKCanvas(bitmap))
26: {
27: canvas.Clear(farbe);
28: canvas.DrawPicture(svg.Picture);
29: canvas.Flush();
30: }
31:
32: using (var stream = File.OpenWrite($@"c:\temp\viereck{counter}.png"))
33: {
34: bitmap.Encode(stream, SKEncodedImageFormat.Png, 100);
35: }
36:
37: }
38: Lines.Clear();
39: counter++;
40: }
41: }
42:
Für mein Training habe ich jeweils 50 mal Dreieck, Kreis und Viereck gezeichnet. Den Code geändert und das Blazor Programm neu gestartet.
Im letzten Schritt kann man die Grafiken für zwei verschiedene Model Builder Szenarien aufbereiten, Bild Klassifizierung und Bild Erkennung.
Für die Bilderkennung werden drei Unterordner angelegt, die den Labels entsprechen und die PNG jeweils in den passenden Ordner verschoben,
Deutlich mehr Aufwand wird benötigt um Objekte in Bildern zu erkennen. Dazu müssen die Labels definiert werden und die Objekte in Position und Größte markiert werden. Auch hier gibt es eine Reihe von Formaten. Microsoft setzt auf VoTT und bietet dafür einen Editor.
Ich habe hier nur jeweils ein Objekt pro Png um das ganze schneller zu erledigen. In der Praxis werden die Objekte auch mehrfach in Bildern vorhanden sein. Ein Hund, Auto oder Person.
Am Ende werden die Markierungen exportiert. In ein JSON, CSV, Tensorflow oder Azure Custom Vision Service Format. Um nur einen Auszug zu nennen.
Mit diesen Daten können wir dann ein Training starten. Das aber in einem anderen Blog Beitrag.