3 Commits
Author SHA1 Message Date
Manuel GlockandClaude Opus 5.5 912ec986f3 NN-Tests: Dateikopf als Konstante FILE_HEADER_STRING
Konstante mit dem Tag des Netzwerk-Formats
(__info2_neural_network_file_format__), nicht dem des Bildformats,
damit loadModel die Testdatei akzeptiert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 12:14:03 +01:00
Manuel GlockandClaude Opus 5.5 9356a19fb0 Optionale Aufgabe: eigene Ziffern malen
Taste D wechselt in den Malmodus (linke Maus malen, rechte Maus radieren,
C leeren). Die Zeichnung wird live klassifiziert; vor der Vorhersage wird
sie wie die MNIST-Daten ueber den Schwerpunkt zentriert. Da raylib.h selbst
einen Typ Matrix definiert, bekommt showMnist eine Klassifizierungsfunktion
aus main.c uebergeben statt neuralNetwork.h einzubinden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 12:19:07 +01:00
Manuel GlockandClaude Opus 5.5 4d6a86f293 Pflichtaufgaben fertig: Matrix-Fixes, Bilder einlesen, NN-Testdatei
- matrix.c: multiply summiert in MatrixType statt int (Nachkommastellen
  gingen verloren), clearMatrix gibt Speicher frei, Indexpruefung pro
  Zeile/Spalte, NULL-Pruefungen in add/multiply
- imageInput.c: fseek-Spruenge entfernt, Dateikopf pruefen, Lesefehler
  behandeln, clearSeries implementiert
- neuralNetworkTests.c: prepareNeuralNetworkFile implementiert
- neuralNetwork.c: clearModel gibt das Layer-Array frei (Speicherleck)
- Zusaetzliche Unittests fuer Matrix und Bildeinlesen
- Testartefakte und Testprogramme in .gitignore, testFile.info2 entfernt

Alle 33 Tests gruen, keine Warnungen, 87.37 % Genauigkeit (wie Referenz).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 12:19:07 +01:00
11 changed files with 588 additions and 96 deletions

No files matched your search

+6
View File
@@ -37,6 +37,12 @@
*.hex
Startcode/mnist
Startcode/runTests
Startcode/runMatrixTests
Startcode/runImageInputTests
Startcode/runNeuralNetworkTests
Startcode/testFile.info2
Startcode/testFile.txt
Startcode/some*nn_test_file.info2
# Debug files
*.dSYM/
+112 -57
View File
@@ -6,88 +6,143 @@
#define BUFFER_SIZE 100
#define FILE_HEADER_STRING "__info2_image_file_format__"
// TODO Implementieren Sie geeignete Hilfsfunktionen für das Lesen der Bildserie aus einer Datei
// Liest den Identifikationstag am Dateianfang und vergleicht ihn mit FILE_HEADER_STRING
static int checkFileHeader(FILE *file)
{
int isValid = 0;
size_t fileHeaderLen = strlen(FILE_HEADER_STRING);
char buffer[BUFFER_SIZE] = {0};
if(fileHeaderLen > BUFFER_SIZE-1)
fileHeaderLen = BUFFER_SIZE-1;
if(fread(buffer, sizeof(char), fileHeaderLen, file) == fileHeaderLen)
isValid = strcmp(buffer, FILE_HEADER_STRING) == 0;
return isValid;
}
// Liest den naechsten unsigned short (Anzahl, Breite bzw. Hoehe); 0 im Fehlerfall.
// Kein fseek noetig: fread setzt den Dateizeiger automatisch hinter die gelesenen Bytes.
static unsigned short readUnsignedShort(FILE *file)
{
unsigned short value = 0;
if(fread(&value, sizeof(unsigned short), 1, file) != 1)
value = 0;
return value;
}
static unsigned short readNumberOfImages(FILE *file)
{
unsigned short numberOfImages = 0;
fseek(file, strlen(FILE_HEADER_STRING), SEEK_SET);
fread(&numberOfImages, sizeof(unsigned short), 1, file);
return numberOfImages;
return readUnsignedShort(file);
}
static unsigned short readImageWidth(FILE *file)
{
unsigned short ImageWidth = 0;
fseek(file, sizeof(unsigned short), SEEK_CUR);
fread(&ImageWidth, sizeof(unsigned short), 1, file);
return ImageWidth;
return readUnsignedShort(file);
}
static unsigned short readImageHeight(FILE *file)
{
unsigned short ImageHeight = 0;
fseek(file, sizeof(unsigned short), SEEK_CUR);
fread(&ImageHeight, sizeof(unsigned short), 1, file);
return ImageHeight;
return readUnsignedShort(file);
}
static GrayScalePixelType *readImage(FILE *file, unsigned short ImageWidth, unsigned short ImageHeight)
// Liest die Pixel eines Bildes in einen neuen Puffer; NULL bei Speicher- oder Lesefehler
static GrayScalePixelType *readImage(FILE *file, unsigned short imageWidth, unsigned short imageHeight)
{
GrayScalePixelType *image = malloc(ImageHeight*ImageWidth*sizeof(GrayScalePixelType));
fseek(file, sizeof(unsigned short), SEEK_CUR);
fread(image, sizeof(char), ImageHeight*ImageWidth, file);
size_t numberOfPixels = (size_t)imageWidth * imageHeight;
GrayScalePixelType *image = (GrayScalePixelType *)malloc(numberOfPixels * sizeof(GrayScalePixelType));
if(image != NULL && fread(image, sizeof(GrayScalePixelType), numberOfPixels, file) != numberOfPixels)
{
free(image);
image = NULL;
}
return image;
}
static GrayScalePixelType readLabel(FILE *file)
// Liest das Label, das direkt auf die Pixel eines Bildes folgt; 0 bei Erfolg, sonst -1
static int readLabel(FILE *file, unsigned char *label)
{
unsigned char label;
fseek(file, sizeof(unsigned short), SEEK_CUR);
fread(&label, sizeof(unsigned char), 1, file);
return label;
return fread(label, sizeof(unsigned char), 1, file) == 1 ? 0 : -1;
}
// TODO Vervollständigen Sie die Funktion readImages unter Benutzung Ihrer Hilfsfunktionen
GrayScaleImageSeries *readImages(const char *path)
// Legt eine leere Serie fuer numberOfImages Bilder an (Bildpuffer werden spaeter von readImage geliefert)
static GrayScaleImageSeries *createSeries(unsigned short numberOfImages)
{
FILE *file = fopen(path, "rb");
if (file == NULL)
GrayScaleImageSeries *series = (GrayScaleImageSeries *)calloc(1, sizeof(GrayScaleImageSeries));
if(series != NULL)
{
return NULL;
// calloc: alle Bildpuffer starten als NULL, damit clearSeries jederzeit gefahrlos aufraeumen kann
series->images = (GrayScaleImage *)calloc(numberOfImages, sizeof(GrayScaleImage));
series->labels = (unsigned char *)calloc(numberOfImages, sizeof(unsigned char));
if(series->images == NULL || series->labels == NULL)
{
clearSeries(series);
series = NULL;
}
else
series->count = numberOfImages;
}
return series;
}
GrayScaleImageSeries *readImages(const char *path)
{
GrayScaleImageSeries *series = NULL;
FILE *file = fopen(path, "rb");
if(file != NULL)
{
if(checkFileHeader(file))
{
unsigned short numberOfImages = readNumberOfImages(file);
unsigned short imageWidth = readImageWidth(file);
unsigned short imageHeight = readImageHeight(file);
if(numberOfImages > 0 && imageWidth > 0 && imageHeight > 0)
series = createSeries(numberOfImages);
for(unsigned int i = 0; series != NULL && i < series->count; i++)
{
series->images[i].width = imageWidth;
series->images[i].height = imageHeight;
series->images[i].buffer = readImage(file, imageWidth, imageHeight);
if(series->images[i].buffer == NULL || readLabel(file, &series->labels[i]) != 0)
{
clearSeries(series);
series = NULL;
}
}
}
fclose(file);
}
unsigned short numberOfImages = readNumberOfImages(file);
unsigned short imageWidth = readImageWidth(file);
unsigned short imageHeight = readImageHeight(file);
GrayScaleImageSeries *series = malloc(sizeof(GrayScaleImageSeries));
if(series == NULL)
{
fclose(file);
return NULL;
}
series->count = numberOfImages;
series->images = malloc(numberOfImages * sizeof(GrayScaleImage));
series->labels = malloc(numberOfImages * sizeof(unsigned char));
for(int i = 0; i < numberOfImages; i++)
{
series->images[i].width = imageWidth;
series->images[i].height = imageHeight;
series->images[i].buffer = readImage(file, imageWidth, imageHeight);
series->labels[i] = readLabel(file);
}
fclose(file);
return series;
}
// TODO Vervollständigen Sie die Funktion clearSeries, welche eine Bildserie vollständig aus dem Speicher freigibt
void clearSeries(GrayScaleImageSeries *series)
{
}
if(series != NULL)
{
if(series->images != NULL)
{
for(unsigned int i = 0; i < series->count; i++)
{
free(series->images[i].buffer);
}
}
free(series->images);
free(series->labels);
free(series);
}
}
+96
View File
@@ -119,6 +119,97 @@ void test_readImagesFailsOnWrongFileTag(void)
remove(path);
}
static void writeRawFile(const char *path, const void *data, size_t size)
{
FILE *file = fopen(path, "wb");
if(file != NULL)
{
fwrite(data, 1, size, file);
fclose(file);
}
}
void test_readImagesReturnsCorrectPixelValues(void)
{
const char *path = "testFile.info2";
const char *fileTag = "__info2_image_file_format__";
const unsigned short header[] = {2, 2, 2};
const GrayScalePixelType expectedPixels1[] = {0, 64, 128, 255};
const GrayScalePixelType expectedPixels2[] = {1, 2, 3, 4};
const unsigned char expectedLabels[] = {7, 3};
GrayScaleImageSeries *series = NULL;
FILE *file = fopen(path, "wb");
TEST_ASSERT_NOT_NULL(file);
fwrite(fileTag, sizeof(char), strlen(fileTag), file);
fwrite(header, sizeof(header[0]), 3, file);
fwrite(expectedPixels1, sizeof(GrayScalePixelType), 4, file);
fwrite(&expectedLabels[0], sizeof(unsigned char), 1, file);
fwrite(expectedPixels2, sizeof(GrayScalePixelType), 4, file);
fwrite(&expectedLabels[1], sizeof(unsigned char), 1, file);
fclose(file);
series = readImages(path);
remove(path);
TEST_ASSERT_NOT_NULL(series);
TEST_ASSERT_EQUAL_UINT8_ARRAY(expectedPixels1, series->images[0].buffer, 4);
TEST_ASSERT_EQUAL_UINT8_ARRAY(expectedPixels2, series->images[1].buffer, 4);
TEST_ASSERT_EQUAL_UINT8_ARRAY(expectedLabels, series->labels, 2);
clearSeries(series);
}
void test_readImagesFailsOnTruncatedImageData(void)
{
const char *path = "testFile.info2";
long fullSize = 0;
char *content = NULL;
FILE *file = NULL;
prepareImageFile(path, 8, 8, 3, 1);
// Datei einlesen und ohne das letzte Byte (Label des letzten Bildes) neu schreiben
file = fopen(path, "rb");
TEST_ASSERT_NOT_NULL(file);
fseek(file, 0, SEEK_END);
fullSize = ftell(file);
rewind(file);
content = (char *)malloc(fullSize);
TEST_ASSERT_NOT_NULL(content);
TEST_ASSERT_EQUAL_INT(fullSize, fread(content, 1, fullSize, file));
fclose(file);
writeRawFile(path, content, fullSize - 1);
free(content);
TEST_ASSERT_NULL(readImages(path));
remove(path);
}
void test_readImagesFailsOnIncompleteHeader(void)
{
const char *path = "testFile.info2";
const char content[] = "__info2_image_file_format__\x02\x00\x08";
writeRawFile(path, content, sizeof(content) - 1);
TEST_ASSERT_NULL(readImages(path));
remove(path);
}
void test_readImagesFailsOnZeroImages(void)
{
const char *path = "testFile.info2";
prepareImageFile(path, 8, 8, 0, 1);
TEST_ASSERT_NULL(readImages(path));
remove(path);
}
void test_clearSeriesAcceptsNull(void)
{
clearSeries(NULL);
TEST_PASS();
}
void setUp(void) {
// Falls notwendig, kann hier Vorbereitungsarbeit gemacht werden
}
@@ -138,6 +229,11 @@ int main()
RUN_TEST(test_readImagesReturnsCorrectLabels);
RUN_TEST(test_readImagesReturnsNullOnNotExistingPath);
RUN_TEST(test_readImagesFailsOnWrongFileTag);
RUN_TEST(test_readImagesReturnsCorrectPixelValues);
RUN_TEST(test_readImagesFailsOnTruncatedImageData);
RUN_TEST(test_readImagesFailsOnIncompleteHeader);
RUN_TEST(test_readImagesFailsOnZeroImages);
RUN_TEST(test_clearSeriesAcceptsNull);
return UNITY_END();
}
+16 -1
View File
@@ -4,6 +4,21 @@
#include "mnistVisualization.h"
#include "neuralNetwork.h"
static int classifyImage(const GrayScaleImage *image, const void *context)
{
const NeuralNetwork *model = (const NeuralNetwork *)context;
unsigned char *prediction = predict(*model, image, 1);
int label = -1;
if(prediction != NULL)
{
label = prediction[0];
free(prediction);
}
return label;
}
int main(int argc, char *argv[])
{
const unsigned int windowWidth = 800;
@@ -34,7 +49,7 @@ int main(int argc, char *argv[])
if(predictions != NULL)
{
showMnist(windowWidth, windowHeight, series, predictions);
showMnist(windowWidth, windowHeight, series, predictions, classifyImage, &model);
free(predictions);
exitCode = EXIT_SUCCESS;
}
+60 -28
View File
@@ -8,7 +8,8 @@ Matrix createMatrix(unsigned int rows, unsigned int cols)
{
Matrix m;
if (rows == 0 || cols == 0) //Ueberprüfen, ob ungültige Werte für Matrix vorhanden
if (rows == 0 || cols == 0)
//Ueberprüfen, ob ungültige Werte für Matrix vorhanden
{
m.rows = 0;
m.cols = 0;
@@ -18,21 +19,40 @@ Matrix createMatrix(unsigned int rows, unsigned int cols)
m.rows = rows;
m.cols = cols;
m.buffer = (MatrixType*)calloc(rows*cols, sizeof(MatrixType)); //Bei Fehlschalg gibt calloc entweder NULL oder Zeiger verschieden von NULL aus
m.buffer = (MatrixType*)calloc(rows*cols, sizeof(MatrixType));
//Bei Fehlschalg gibt calloc entweder NULL oder Zeiger verschieden von NULL aus
if (m.buffer == NULL)
//Kein Speicher: leere Matrix zurueckgeben, damit Groesse und Puffer zusammenpassen
{
m.rows = 0;
m.cols = 0;
}
return m;
}
void clearMatrix(Matrix *matrix)
{
matrix->buffer = 0;
matrix->rows = 0;
matrix->cols = 0;
if (matrix != NULL)
{
free(matrix->buffer);
//Speicher leeren
matrix->buffer = NULL;
matrix->rows = 0;
matrix->cols = 0;
}
}
static int isValidIndex(const Matrix matrix, unsigned int rowIdx, unsigned int colIdx)
{
//Zeile und Spalte einzeln pruefen: sonst wuerde z.B. (0, cols) faelschlich auf (1, 0) zugreifen
return matrix.buffer != NULL && rowIdx < matrix.rows && colIdx < matrix.cols;
}
void setMatrixAt(MatrixType value, Matrix matrix, unsigned int rowIdx, unsigned int colIdx)
{
if((rowIdx * matrix.cols + colIdx) < (matrix.rows*matrix.cols))
if(isValidIndex(matrix, rowIdx, colIdx))
{
matrix.buffer[rowIdx * matrix.cols + colIdx] = value;
}
@@ -40,8 +60,8 @@ void setMatrixAt(MatrixType value, Matrix matrix, unsigned int rowIdx, unsigned
MatrixType getMatrixAt(const Matrix matrix, unsigned int rowIdx, unsigned int colIdx)
{
MatrixType value = 0;
if((rowIdx * matrix.cols + colIdx) < (matrix.rows*matrix.cols))
MatrixType value = UNDEFINED_MATRIX_VALUE;
if(isValidIndex(matrix, rowIdx, colIdx))
{
value = matrix.buffer[rowIdx * matrix.cols + colIdx];
}
@@ -53,28 +73,38 @@ Matrix add(const Matrix matrix1, const Matrix matrix2)
typedef enum {elemt_add, broadcast} Addiermodi;
Addiermodi modus;
if((matrix1.cols == 1 || matrix2.cols ==1) && (matrix1.rows == matrix2.rows))
Matrix m = {NULL, 0, 0};
if(matrix1.buffer == NULL || matrix2.buffer == NULL)
{
modus = broadcast;
return m;
}
else if((matrix1.rows == matrix2.rows) && (matrix1.cols == matrix2.cols))
//Elementweise zuerst pruefen: zwei Spaltenvektoren gleicher Groesse sind ein normaler elementweiser Fall
if((matrix1.rows == matrix2.rows) && (matrix1.cols == matrix2.cols))
{
modus = elemt_add;
}
else if((matrix1.cols == 1 || matrix2.cols ==1) && (matrix1.rows == matrix2.rows))
{
modus = broadcast;
}
else
{
Matrix failure = {NULL, 0, 0};
return failure;
return m;
}
Matrix m;
if(modus == elemt_add)
{
m = createMatrix(matrix1.rows, matrix1.cols);
for(int i = 0; i< matrix1.rows; i++)
if(m.buffer == NULL)
{
for(int j = 0; j < matrix1.cols ; j++)
return m;
}
for(unsigned int i = 0; i< matrix1.rows; i++)
{
for(unsigned int j = 0; j < matrix1.cols ; j++)
{
m.buffer[i*matrix1.cols+j] = matrix1.buffer[i*matrix1.cols+j] + matrix2.buffer[i*matrix1.cols+j];
}
@@ -87,9 +117,9 @@ Matrix add(const Matrix matrix1, const Matrix matrix2)
if(matrix1.cols == 1)
{
m = createMatrix(matrix2.rows, matrix2.cols);
for(int i = 0; i< matrix2.rows; i++)
for(unsigned int i = 0; i< m.rows; i++)
{
for(int j = 0; j < matrix2.cols ; j++)
for(unsigned int j = 0; j < m.cols ; j++)
{
m.buffer[i*matrix2.cols+j] = matrix2.buffer[i*matrix2.cols+j] + matrix1.buffer[i*matrix1.cols];
}
@@ -98,9 +128,9 @@ Matrix add(const Matrix matrix1, const Matrix matrix2)
else
{
m = createMatrix(matrix1.rows, matrix1.cols);
for(int i = 0; i< matrix1.rows; i++)
for(unsigned int i = 0; i< m.rows; i++)
{
for(int j = 0; j < matrix1.cols ; j++)
for(unsigned int j = 0; j < m.cols ; j++)
{
m.buffer[i*matrix1.cols+j] = matrix1.buffer[i*matrix1.cols+j] + matrix2.buffer[i*matrix2.cols];
}
@@ -112,7 +142,7 @@ Matrix add(const Matrix matrix1, const Matrix matrix2)
Matrix multiply(const Matrix matrix1, const Matrix matrix2)
{
if(matrix1.cols != matrix2.rows)
if(matrix1.buffer == NULL || matrix2.buffer == NULL || matrix1.cols != matrix2.rows)
{
Matrix failure = {NULL, 0, 0};
return failure;
@@ -120,16 +150,18 @@ Matrix multiply(const Matrix matrix1, const Matrix matrix2)
Matrix m = createMatrix(matrix1.rows, matrix2.cols);
for(int i = 0; i < matrix1.rows; i++)
//Reihenfolge i-k-j statt i-j-k: matrix2 und m werden zeilenweise am Stueck durchlaufen.
//m startet dank calloc mit 0, daher wird direkt aufaddiert.
for(unsigned int i = 0; i < m.rows; i++)
{
for(int j = 0; j < matrix2.cols ; j++)
for(unsigned int k = 0; k < matrix1.cols; k++)
{
int value = 0;
for(int k = 0; k < matrix1.cols; k++)
MatrixType factor = matrix1.buffer[i*matrix1.cols+k];
//MatrixType statt int, hat die zahlen gerundet.
for(unsigned int j = 0; j < m.cols ; j++)
{
value += getMatrixAt(matrix1, i, k) * getMatrixAt(matrix2, k , j);
m.buffer[i*m.cols+j] += factor * matrix2.buffer[k*matrix2.cols+j];
}
m.buffer[i*matrix2.cols+j] = value;
}
}
return m;
+24
View File
@@ -130,6 +130,28 @@ void test_multiplyFailsOnWrongInputDimensions(void)
TEST_ASSERT_EQUAL_UINT32(0, result.cols);
}
void test_multiplyKeepsFractionalValues(void)
{
MatrixType buffer1[] = {0.5f, 0.25f};
MatrixType buffer2[] = {0.5f, 0.5f};
Matrix matrix1 = {.rows=1, .cols=2, .buffer=buffer1};
Matrix matrix2 = {.rows=2, .cols=1, .buffer=buffer2};
Matrix result = multiply(matrix1, matrix2);
TEST_ASSERT_NOT_NULL(result.buffer);
TEST_ASSERT_EQUAL_FLOAT(0.375f, result.buffer[0]);
free(result.buffer);
}
void test_getMatrixAtFailsOnColumnIndexOutOfRange(void)
{
MatrixType buffer[] = {1, 2, 3, 4, 5, 6};
Matrix matrixToTest = {.rows=2, .cols=3, .buffer=buffer};
TEST_ASSERT_EQUAL_INT(UNDEFINED_MATRIX_VALUE, getMatrixAt(matrixToTest, 0, 3));
}
void test_getMatrixAtReturnsCorrectResult(void)
{
MatrixType buffer[] = {1, 2, 3, 4, 5, 6};
@@ -188,6 +210,8 @@ int main()
RUN_TEST(test_addSupportsBroadcasting);
RUN_TEST(test_multiplyReturnsCorrectResults);
RUN_TEST(test_multiplyFailsOnWrongInputDimensions);
RUN_TEST(test_multiplyKeepsFractionalValues);
RUN_TEST(test_getMatrixAtFailsOnColumnIndexOutOfRange);
RUN_TEST(test_getMatrixAtReturnsCorrectResult);
RUN_TEST(test_getMatrixAtFailsOnIndicesOutOfRange);
RUN_TEST(test_setMatrixAtSetsCorrectValue);
+240 -6
View File
@@ -1,10 +1,21 @@
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include "mnistVisualization.h"
#include "raylib.h"
#define MAX_TEXT_LEN 100
#define BRUSH_RADIUS 2.0f
#define BRUSH_STEP 0.25f
#define BROWSE_TEXT "Use left and right key to navigate ... (D: draw mode)"
#define DRAW_TEXT "Left mouse: draw Right mouse: erase C: clear D: browse"
typedef enum
{
BROWSE_MODE,
DRAW_MODE
} VisualizationMode;
typedef struct
{
@@ -23,6 +34,15 @@ typedef struct
Color color;
} TextLabel;
typedef struct
{
GrayScaleImage image;
Vector2 lastPosition;
int isStroking;
int isEmpty;
int prediction;
} DrawingCanvas;
static TextLabel *createTextLabel(const char *text, unsigned int fontSize, Color color)
{
TextLabel *label = NULL;
@@ -65,6 +85,185 @@ static MnistVisualization *createVisualizationContainer(const GrayScaleImageSeri
return container;
}
static void setLabelText(TextLabel *label, const char *text)
{
strncpy(label->text, text, MAX_TEXT_LEN);
label->text[MAX_TEXT_LEN-1] = '\0';
}
static DrawingCanvas *createDrawingCanvas(unsigned int width, unsigned int height)
{
DrawingCanvas *canvas = (DrawingCanvas *)calloc(1, sizeof(DrawingCanvas));
if(canvas != NULL)
{
canvas->image.buffer = (GrayScalePixelType *)calloc(width * height, sizeof(GrayScalePixelType));
if(canvas->image.buffer != NULL)
{
canvas->image.width = width;
canvas->image.height = height;
canvas->isEmpty = 1;
}
else
{
free(canvas);
canvas = NULL;
}
}
return canvas;
}
static void freeDrawingCanvas(DrawingCanvas *canvas)
{
if(canvas != NULL)
{
free(canvas->image.buffer);
free(canvas);
}
}
static void clearDrawingCanvas(DrawingCanvas *canvas)
{
memset(canvas->image.buffer, 0, canvas->image.width * canvas->image.height * sizeof(GrayScalePixelType));
canvas->isEmpty = 1;
}
// Weicher Pinsel: innerer Bereich voll weiss, nach aussen linear auslaufend (aehnlich den MNIST-Strichen)
static void paintAt(DrawingCanvas *canvas, Vector2 position, int erase)
{
int minX = (int)floorf(position.x - BRUSH_RADIUS);
int maxX = (int)ceilf(position.x + BRUSH_RADIUS);
int minY = (int)floorf(position.y - BRUSH_RADIUS);
int maxY = (int)ceilf(position.y + BRUSH_RADIUS);
for(int y = minY; y <= maxY; y++)
{
for(int x = minX; x <= maxX; x++)
{
if(x >= 0 && y >= 0 && x < (int)canvas->image.width && y < (int)canvas->image.height)
{
float dx = x + 0.5f - position.x;
float dy = y + 0.5f - position.y;
float strength = 2.0f * (1.0f - sqrtf(dx * dx + dy * dy) / BRUSH_RADIUS);
GrayScalePixelType *pixel = &canvas->image.buffer[y * canvas->image.width + x];
if(strength > 0)
{
if(erase)
*pixel = 0;
else
{
float value = strength >= 1.0f ? 255.0f : 255.0f * strength;
if(value > *pixel)
*pixel = (GrayScalePixelType)value;
}
}
}
}
}
}
// Zwischenpunkte interpolieren, damit bei schnellen Mausbewegungen keine Luecken entstehen
static void paintLine(DrawingCanvas *canvas, Vector2 from, Vector2 to, int erase)
{
float dx = to.x - from.x;
float dy = to.y - from.y;
int steps = (int)ceilf(sqrtf(dx * dx + dy * dy) / BRUSH_STEP);
for(int i = 0; i <= steps; i++)
{
float t = steps > 0 ? (float)i / steps : 0.0f;
Vector2 point = {from.x + t * dx, from.y + t * dy};
paintAt(canvas, point, erase);
}
}
// MNIST-Ziffern sind ueber ihren Schwerpunkt zentriert; die Zeichnung wird fuer die Vorhersage genauso verschoben
static void centerByMass(const GrayScaleImage source, GrayScaleImage target)
{
double sum = 0;
double centerX = 0;
double centerY = 0;
memset(target.buffer, 0, target.width * target.height * sizeof(GrayScalePixelType));
for(unsigned int i = 0; i < source.width * source.height; i++)
{
sum += source.buffer[i];
centerX += (double)(i % source.width) * source.buffer[i];
centerY += (double)(i / source.width) * source.buffer[i];
}
if(sum > 0)
{
int shiftX = (int)lround((source.width - 1) / 2.0 - centerX / sum);
int shiftY = (int)lround((source.height - 1) / 2.0 - centerY / sum);
for(int y = 0; y < (int)source.height; y++)
{
for(int x = 0; x < (int)source.width; x++)
{
int newX = x + shiftX;
int newY = y + shiftY;
if(newX >= 0 && newY >= 0 && newX < (int)target.width && newY < (int)target.height)
target.buffer[newY * target.width + newX] = source.buffer[y * source.width + x];
}
}
}
}
static void predictDrawing(DrawingCanvas *canvas, ClassifierFunctionType classify, const void *classifierContext)
{
GrayScaleImage centered = canvas->image;
centered.buffer = (GrayScalePixelType *)malloc(canvas->image.width * canvas->image.height * sizeof(GrayScalePixelType));
canvas->prediction = -1;
if(centered.buffer != NULL)
{
centerByMass(canvas->image, centered);
canvas->prediction = classify(&centered, classifierContext);
free(centered.buffer);
}
}
static void updateDrawingCanvas(DrawingCanvas *canvas, const MnistVisualization *container, ClassifierFunctionType classify, const void *classifierContext, TextLabel *predictionLabel)
{
int isDrawing = IsMouseButtonDown(MOUSE_BUTTON_LEFT);
int isErasing = IsMouseButtonDown(MOUSE_BUTTON_RIGHT);
if(IsKeyReleased(KEY_C))
clearDrawingCanvas(canvas);
if(isDrawing || isErasing)
{
Vector2 mouse = GetMousePosition();
Vector2 position = {(mouse.x - container->position.x) / container->pixelSize.x, (mouse.y - container->position.y) / container->pixelSize.y};
if(canvas->isStroking)
paintLine(canvas, canvas->lastPosition, position, isErasing);
else
paintAt(canvas, position, isErasing);
canvas->lastPosition = position;
canvas->isStroking = 1;
canvas->isEmpty = 0;
predictDrawing(canvas, classify, classifierContext);
}
else
canvas->isStroking = 0;
if(canvas->isEmpty)
setLabelText(predictionLabel, "Draw a digit ...");
else if(canvas->prediction < 0)
setLabelText(predictionLabel, "Prediction failed ...");
else
snprintf(predictionLabel->text, MAX_TEXT_LEN, "Predicted label: %d", canvas->prediction);
}
static void drawTextLabel(const TextLabel *label)
{
DrawText(label->text, label->position.x, label->position.y, label->fontSize, label->color);
@@ -106,6 +305,24 @@ static void drawAll(const MnistVisualization *container, const TextLabel *naviga
EndDrawing();
}
static void drawCanvasAll(const DrawingCanvas *canvas, const MnistVisualization *container, const TextLabel *navigationLabel, const TextLabel *predictionLabel)
{
// Hilfsrahmen: MNIST-Ziffern fuellen etwa die inneren 20x20 von 28x28 Pixeln
const float margin = canvas->image.width / 7.0f;
Rectangle guide = {container->position.x + margin * container->pixelSize.x, container->position.y + margin * container->pixelSize.y,
(canvas->image.width - 2 * margin) * container->pixelSize.x, (canvas->image.height - 2 * margin) * container->pixelSize.y};
BeginDrawing();
ClearBackground(BLACK);
drawDigit(canvas->image, container->position, container->pixelSize);
DrawRectangleLinesEx(guide, 1, DARKGRAY);
drawTextLabel(navigationLabel);
drawTextLabel(predictionLabel);
EndDrawing();
}
static int checkUserInput()
{
int inputResult = 0;
@@ -141,17 +358,19 @@ static void update(MnistVisualization *container, TextLabel *predictionLabel, in
updatePredictionLabel(predictionLabel, container->series->labels[container->currentIdx], container->predictions[container->currentIdx]);
}
void showMnist(unsigned int windowWidth, unsigned int windowHeight, const GrayScaleImageSeries *series, const unsigned char predictions[])
void showMnist(unsigned int windowWidth, unsigned int windowHeight, const GrayScaleImageSeries *series, const unsigned char predictions[], ClassifierFunctionType classify, const void *classifierContext)
{
const Vector2 windowSize = {windowWidth, windowHeight};
VisualizationMode mode = BROWSE_MODE;
MnistVisualization *container = createVisualizationContainer(series, predictions, windowSize);
TextLabel *navigationLabel = createTextLabel("Use left and right key to navigate ...", 20, WHITE);
DrawingCanvas *canvas = container != NULL ? createDrawingCanvas(series->images[0].width, series->images[0].height) : NULL;
TextLabel *navigationLabel = createTextLabel(BROWSE_TEXT, 20, WHITE);
TextLabel *predictionLabel = createTextLabel("", 20, WHITE);
predictionLabel->position.x = 10;
predictionLabel->position.y = windowSize.y - 50;
if(container != NULL && navigationLabel != NULL && predictionLabel != NULL)
if(container != NULL && canvas != NULL && classify != NULL && navigationLabel != NULL && predictionLabel != NULL)
{
InitWindow(windowSize.x, windowSize.y, "MNIST Browser");
@@ -159,15 +378,30 @@ void showMnist(unsigned int windowWidth, unsigned int windowHeight, const GraySc
while (!WindowShouldClose())
{
int updateDirection = checkUserInput();
update(container, predictionLabel, updateDirection);
drawAll(container, navigationLabel, predictionLabel);
if(IsKeyReleased(KEY_D))
{
mode = mode == BROWSE_MODE ? DRAW_MODE : BROWSE_MODE;
setLabelText(navigationLabel, mode == BROWSE_MODE ? BROWSE_TEXT : DRAW_TEXT);
}
if(mode == BROWSE_MODE)
{
int updateDirection = checkUserInput();
update(container, predictionLabel, updateDirection);
drawAll(container, navigationLabel, predictionLabel);
}
else
{
updateDrawingCanvas(canvas, container, classify, classifierContext, predictionLabel);
drawCanvasAll(canvas, container, navigationLabel, predictionLabel);
}
}
}
CloseWindow();
free(container);
freeDrawingCanvas(canvas);
free(navigationLabel);
free(predictionLabel);
}
+5 -1
View File
@@ -3,6 +3,10 @@
#include "imageInput.h"
void showMnist(unsigned int windowWidth, unsigned int windowHeight, const GrayScaleImageSeries *series, const unsigned char predictions[]);
// Liefert das erkannte Label (0-9) eines Bildes oder -1 im Fehlerfall; context wird unveraendert durchgereicht.
// (Die Visualisierung kann neuralNetwork.h nicht einbinden, da raylib.h selbst einen Typ "Matrix" definiert.)
typedef int (*ClassifierFunctionType)(const GrayScaleImage *image, const void *context);
void showMnist(unsigned int windowWidth, unsigned int windowHeight, const GrayScaleImageSeries *series, const unsigned char predictions[], ClassifierFunctionType classify, const void *classifierContext);
#endif
+1
View File
@@ -262,6 +262,7 @@ void clearModel(NeuralNetwork *model)
{
clearLayer(&model->layers[i]);
}
free(model->layers);
model->layers = NULL;
model->numberOfLayers = 0;
}
+28 -2
View File
@@ -4,11 +4,37 @@
#include <math.h>
#include "unity.h"
#include "neuralNetwork.h"
#define FILE_HEADER_STRING "__info2_neural_network_file_format__"
static void prepareNeuralNetworkFile(const char *path, const NeuralNetwork nn)
{
// TODO
FILE *file = fopen(path, "wb");
if(file != NULL)
{
const char *fileTag = FILE_HEADER_STRING;
fwrite(fileTag, sizeof(char), strlen(fileTag), file);
if(nn.numberOfLayers > 0)
{
// Eingangsdimension nur einmal: jede weitere Schicht nutzt die Ausgangsdimension der vorherigen
int inputDimension = (int)nn.layers[0].weights.cols;
fwrite(&inputDimension, sizeof(int), 1, file);
}
for(unsigned int i = 0; i < nn.numberOfLayers; i++)
{
const Layer layer = nn.layers[i];
int outputDimension = (int)layer.weights.rows;
fwrite(&outputDimension, sizeof(int), 1, file);
fwrite(layer.weights.buffer, sizeof(MatrixType), layer.weights.rows * layer.weights.cols, file);
fwrite(layer.biases.buffer, sizeof(MatrixType), layer.biases.rows * layer.biases.cols, file);
}
fclose(file);
}
}
void test_loadModelReturnsCorrectNumberOfLayers(void)
-1
View File
@@ -1 +0,0 @@
some_tag