Compare commits

..

5 Commits
main ... main

8 changed files with 216 additions and 618 deletions

Binary file not shown.

View File

@ -6,190 +6,17 @@
#define BUFFER_SIZE 100 #define BUFFER_SIZE 100
#define FILE_HEADER_STRING "__info2_image_file_format__" #define FILE_HEADER_STRING "__info2_image_file_format__"
// ===================================================== // TODO Implementieren Sie geeignete Hilfsfunktionen für das Lesen der Bildserie aus einer Datei
// Hilfsfunktion 1
// Datei öffnen + Header prüfen + Metadaten lesen
// =====================================================
static FILE *openFileAndReadHeader(const char *path, // TODO Vervollständigen Sie die Funktion readImages unter Benutzung Ihrer Hilfsfunktionen
unsigned short *count,
unsigned short *width,
unsigned short *height)
{
// Schritt 1: Datei öffnen
FILE *file = fopen(path, "rb");
if (!file) {
fprintf(stderr, "Error: Cannot open file '%s'\n", path);
return NULL;
}
// Schritt 2: Header-String prüfen
char buffer[BUFFER_SIZE] = {0};
size_t headerLen = strlen(FILE_HEADER_STRING);
if (fread(buffer, sizeof(char), headerLen, file) != headerLen) {
fprintf(stderr, "Error: Cannot read file header (file too small?)\n");
fclose(file);
return NULL;
}
if (strncmp(buffer, FILE_HEADER_STRING, headerLen) != 0) {
fprintf(stderr, "Error: Invalid file header. Expected '%s', got: %.24s\n",
FILE_HEADER_STRING, buffer);
fclose(file);
return NULL;
}
// Schritt 3: Metadaten lesen (Reihenfolge: count, width, height)
// WICHTIG: Diese Reihenfolge (Anzahl, Breite, Höhe) entspricht
// der Aufgabenstellung und dem in den Tests verwendeten Format.
if (fread(count, sizeof(unsigned short), 1, file) != 1) {
fprintf(stderr, "Error: Cannot read image count\n");
fclose(file);
return NULL;
}
if (fread(width, sizeof(unsigned short), 1, file) != 1) {
fprintf(stderr, "Error: Cannot read image width\n");
fclose(file);
return NULL;
}
if (fread(height, sizeof(unsigned short), 1, file) != 1) {
fprintf(stderr, "Error: Cannot read image height\n");
fclose(file);
return NULL;
}
// Input-Validierung: Prüfe auf ungültige Dimensionen
if (*count == 0) {
fprintf(stderr, "Error: Image count is 0\n");
fclose(file);
return NULL;
}
if (*width == 0) {
fprintf(stderr, "Error: Image width is 0\n");
fclose(file);
return NULL;
}
if (*height == 0) {
fprintf(stderr, "Error: Image height is 0\n");
fclose(file);
return NULL;
}
// Erfolg: offene Datei zurückgeben, Position ist nach Metadaten
return file;
}
// -----------------------------------------------------
// Hilfsfunktion 2: Speicher für die gesamte Serie anlegen
// -----------------------------------------------------
static GrayScaleImageSeries *allocateSeries(unsigned short count,
unsigned short width,
unsigned short height)
{
GrayScaleImageSeries *series = malloc(sizeof(GrayScaleImageSeries));
if (!series) return NULL;
series->count = count;
series->images = calloc(count, sizeof(GrayScaleImage));
series->labels = calloc(count, sizeof(unsigned char));
if (!series->images || !series->labels)
{
clearSeries(series);
return NULL;
}
// Bilddimensionen in jedes Struktur-Element übernehmen
for (unsigned short i = 0; i < count; i++)
{
series->images[i].width = width;
series->images[i].height = height;
}
return series;
}
// -----------------------------------------------------
// Hilfsfunktion 3: EIN BILD + EIN LABEL lesen
// -----------------------------------------------------
static int readSingleImage(FILE *file, GrayScaleImage *img, unsigned char *label)
{
unsigned int pixelCount = img->width * img->height;
img->buffer = malloc(pixelCount * sizeof(GrayScalePixelType));
if (!img->buffer)
return 0;
if (fread(img->buffer, sizeof(GrayScalePixelType), pixelCount, file) != pixelCount)
return 0;
if (fread(label, sizeof(unsigned char), 1, file) != 1)
return 0;
return 1;
}
// =====================================================
// Hauptfunktion: Liest komplette Bilderserie
// =====================================================
GrayScaleImageSeries *readImages(const char *path) GrayScaleImageSeries *readImages(const char *path)
{ {
unsigned short count = 0, width = 0, height = 0; GrayScaleImageSeries *series = NULL;
// Schritt 1-3: Datei öffnen + Header + Metadaten
FILE *file = openFileAndReadHeader(path, &count, &width, &height);
if (!file) {
// Fehler bereits geloggt von openFileAndReadHeader()
return NULL;
}
// Schritt 4: Bilderserie allokieren
GrayScaleImageSeries *series = allocateSeries(count, width, height);
if (!series) {
fprintf(stderr, "Error: Cannot allocate image series\n");
fclose(file);
return NULL;
}
// Schritt 5: Alle Bilder + Labels lesen
for (unsigned short i = 0; i < count; i++) {
if (!readSingleImage(file, &series->images[i], &series->labels[i])) {
fprintf(stderr, "Error: Cannot read image %u\n", i);
clearSeries(series);
fclose(file);
return NULL;
}
}
fclose(file);
return series; return series;
} }
// ===================================================== // TODO Vervollständigen Sie die Funktion clearSeries, welche eine Bildserie vollständig aus dem Speicher freigibt
// Speicher-Freigabe
// =====================================================
void clearSeries(GrayScaleImageSeries *series) void clearSeries(GrayScaleImageSeries *series)
{ {
if (!series)
return;
if (series->images)
{
for (unsigned int i = 0; i < series->count; i++)
{
free(series->images[i].buffer);
series->images[i].buffer = NULL;
}
free(series->images);
series->images = NULL;
}
if (series->labels)
{
free(series->labels);
series->labels = NULL;
}
free(series);
} }

View File

@ -54,7 +54,6 @@ void test_readImagesReturnsCorrectImageWidth(void)
GrayScaleImageSeries *series = NULL; GrayScaleImageSeries *series = NULL;
const unsigned short expectedWidth = 10; const unsigned short expectedWidth = 10;
const char *path = "testFile.info2"; const char *path = "testFile.info2";
// prepareImageFile(path, width, height, numberOfImages, label)
prepareImageFile(path, expectedWidth, 8, 2, 1); prepareImageFile(path, expectedWidth, 8, 2, 1);
series = readImages(path); series = readImages(path);
TEST_ASSERT_NOT_NULL(series); TEST_ASSERT_NOT_NULL(series);
@ -71,7 +70,6 @@ void test_readImagesReturnsCorrectImageHeight(void)
GrayScaleImageSeries *series = NULL; GrayScaleImageSeries *series = NULL;
const unsigned short expectedHeight = 10; const unsigned short expectedHeight = 10;
const char *path = "testFile.info2"; const char *path = "testFile.info2";
// prepareImageFile(path, width, height, numberOfImages, label)
prepareImageFile(path, 8, expectedHeight, 2, 1); prepareImageFile(path, 8, expectedHeight, 2, 1);
series = readImages(path); series = readImages(path);
TEST_ASSERT_NOT_NULL(series); TEST_ASSERT_NOT_NULL(series);
@ -121,161 +119,6 @@ void test_readImagesFailsOnWrongFileTag(void)
remove(path); remove(path);
} }
// =====================================================
// Tests für Hilfsfunktion imageInput.c
// =====================================================
void test_openFileAndReadHeaderFailsOnZeroImageCount(void)
{
// Test: Datei mit count=0 sollte fehlschlagen
const char *path = "testZeroCount.info2";
FILE *file = fopen(path, "wb");
if (file != NULL) {
const char *fileTag = "__info2_image_file_format__";
unsigned short zero_count = 0;
unsigned short width = 28;
unsigned short height = 28;
fwrite(fileTag, sizeof(fileTag[0]), strlen(fileTag), file);
fwrite(&zero_count, sizeof(unsigned short), 1, file);
fwrite(&height, sizeof(unsigned short), 1, file);
fwrite(&width, sizeof(unsigned short), 1, file);
fclose(file);
}
// readImages sollte NULL zurückgeben bei count=0
TEST_ASSERT_NULL(readImages(path));
remove(path);
}
void test_openFileAndReadHeaderFailsOnZeroWidth(void)
{
// Test: Datei mit width=0 sollte fehlschlagen
const char *path = "testZeroWidth.info2";
FILE *file = fopen(path, "wb");
if (file != NULL) {
const char *fileTag = "__info2_image_file_format__";
unsigned short count = 5;
unsigned short width = 0; // INVALID
unsigned short height = 28;
fwrite(fileTag, sizeof(fileTag[0]), strlen(fileTag), file);
fwrite(&count, sizeof(unsigned short), 1, file);
fwrite(&height, sizeof(unsigned short), 1, file);
fwrite(&width, sizeof(unsigned short), 1, file);
fclose(file);
}
TEST_ASSERT_NULL(readImages(path));
remove(path);
}
void test_openFileAndReadHeaderFailsOnZeroHeight(void)
{
// Test: Datei mit height=0 sollte fehlschlagen
const char *path = "testZeroHeight.info2";
FILE *file = fopen(path, "wb");
if (file != NULL) {
const char *fileTag = "__info2_image_file_format__";
unsigned short count = 5;
unsigned short width = 28;
unsigned short height = 0; // INVALID
fwrite(fileTag, sizeof(fileTag[0]), strlen(fileTag), file);
fwrite(&count, sizeof(unsigned short), 1, file);
fwrite(&height, sizeof(unsigned short), 1, file);
fwrite(&width, sizeof(unsigned short), 1, file);
fclose(file);
}
TEST_ASSERT_NULL(readImages(path));
remove(path);
}
void test_openFileAndReadHeaderFailsOnTruncatedHeader(void)
{
// Test: Datei ist zu kurz für Header
const char *path = "testTruncated.info2";
FILE *file = fopen(path, "wb");
if (file != NULL) {
// Nur 10 Bytes schreiben (Header ist 24 Bytes)
const char *fileTag = "__info2_im";
fwrite(fileTag, 1, 10, file);
fclose(file);
}
TEST_ASSERT_NULL(readImages(path));
remove(path);
}
void test_openFileAndReadHeaderFailsOnMissingCount(void)
{
// Test: Datei hat Header aber kein count Feld
const char *path = "testMissingCount.info2";
FILE *file = fopen(path, "wb");
if (file != NULL) {
const char *fileTag = "__info2_image_file_format__";
fwrite(fileTag, sizeof(fileTag[0]), strlen(fileTag), file);
// count Feld nicht schreiben → EOF beim fread
fclose(file);
}
TEST_ASSERT_NULL(readImages(path));
remove(path);
}
void test_openFileAndReadHeaderSucceedsWithValidData(void)
{
// Test: Valide Datei sollte erfolgreich sein
const char *path = "testValid.info2";
prepareImageFile(path, 28, 28, 5, 3);
GrayScaleImageSeries *series = readImages(path);
TEST_ASSERT_NOT_NULL(series);
TEST_ASSERT_EQUAL_UINT16(5, series->count);
TEST_ASSERT_EQUAL_UINT16(28, series->images[0].width);
TEST_ASSERT_EQUAL_UINT16(28, series->images[0].height);
clearSeries(series);
remove(path);
}
void test_openFileAndReadHeaderCorrectMetadataOrder(void)
{
// Test: Metadaten werden in richtiger Reihenfolge gelesen
const char *path = "testMetadataOrder.info2";
FILE *file = fopen(path, "wb");
if (file != NULL) {
const char *fileTag = "__info2_image_file_format__";
unsigned short count = 10;
unsigned short width = 16; // WICHTIG: width vor height (Anzahl, Breite, Höhe)
unsigned short height = 32;
unsigned char label = 5;
unsigned char pixel_data[16*32];
memset(pixel_data, 128, sizeof(pixel_data));
fwrite(fileTag, sizeof(fileTag[0]), strlen(fileTag), file);
fwrite(&count, sizeof(unsigned short), 1, file);
fwrite(&width, sizeof(unsigned short), 1, file);
fwrite(&height, sizeof(unsigned short), 1, file);
for (int i = 0; i < count; i++) {
fwrite(pixel_data, 1, 16*32, file);
fwrite(&label, 1, 1, file);
}
fclose(file);
}
GrayScaleImageSeries *series = readImages(path);
TEST_ASSERT_NOT_NULL(series);
TEST_ASSERT_EQUAL_UINT16(10, series->count);
TEST_ASSERT_EQUAL_UINT16(32, series->images[0].height); // height korrekt
TEST_ASSERT_EQUAL_UINT16(16, series->images[0].width); // width korrekt
clearSeries(series);
remove(path);
}
void setUp(void) { void setUp(void) {
// Falls notwendig, kann hier Vorbereitungsarbeit gemacht werden // Falls notwendig, kann hier Vorbereitungsarbeit gemacht werden
} }
@ -289,8 +132,6 @@ int main()
UNITY_BEGIN(); UNITY_BEGIN();
printf("\n============================\nImage input tests\n============================\n"); printf("\n============================\nImage input tests\n============================\n");
// Ursprüngliche Tests
RUN_TEST(test_readImagesReturnsCorrectNumberOfImages); RUN_TEST(test_readImagesReturnsCorrectNumberOfImages);
RUN_TEST(test_readImagesReturnsCorrectImageWidth); RUN_TEST(test_readImagesReturnsCorrectImageWidth);
RUN_TEST(test_readImagesReturnsCorrectImageHeight); RUN_TEST(test_readImagesReturnsCorrectImageHeight);
@ -298,15 +139,5 @@ int main()
RUN_TEST(test_readImagesReturnsNullOnNotExistingPath); RUN_TEST(test_readImagesReturnsNullOnNotExistingPath);
RUN_TEST(test_readImagesFailsOnWrongFileTag); RUN_TEST(test_readImagesFailsOnWrongFileTag);
// Neue Tests für kombinierte Funktion (Input-Validierung)
printf("\n--- Tests für Input-Validierung ---\n");
RUN_TEST(test_openFileAndReadHeaderFailsOnZeroImageCount);
RUN_TEST(test_openFileAndReadHeaderFailsOnZeroWidth);
RUN_TEST(test_openFileAndReadHeaderFailsOnZeroHeight);
RUN_TEST(test_openFileAndReadHeaderFailsOnTruncatedHeader);
RUN_TEST(test_openFileAndReadHeaderFailsOnMissingCount);
RUN_TEST(test_openFileAndReadHeaderSucceedsWithValidData);
RUN_TEST(test_openFileAndReadHeaderCorrectMetadataOrder);
return UNITY_END(); return UNITY_END();
} }

105
matrix.c
View File

@ -1,126 +1,35 @@
#include "matrix.h"
#include <stdlib.h> #include <stdlib.h>
#include <string.h>
#include "matrix.h"
// TODO Matrix-Funktionen implementieren
// Matrix erstellen
Matrix createMatrix(unsigned int rows, unsigned int cols) Matrix createMatrix(unsigned int rows, unsigned int cols)
{ {
Matrix m;
if (rows == 0 || cols == 0) {
m.rows = 0;
m.cols = 0;
m.buffer = NULL;
return m;
}
m.rows = rows;
m.cols = cols;
m.buffer = (MatrixType*)calloc(rows * cols, sizeof(MatrixType));
if (!m.buffer) {
m.rows = 0;
m.cols = 0;
}
return m;
} }
// Speicher freigeben
void clearMatrix(Matrix *matrix) void clearMatrix(Matrix *matrix)
{ {
if (!matrix || !matrix->buffer) return;
free(matrix->buffer);
matrix->buffer = NULL;
matrix->rows = 0;
matrix->cols = 0;
} }
// Wert setzen
void setMatrixAt(MatrixType value, Matrix matrix, unsigned int rowIdx, unsigned int colIdx) void setMatrixAt(MatrixType value, Matrix matrix, unsigned int rowIdx, unsigned int colIdx)
{ {
if (rowIdx >= matrix.rows || colIdx >= matrix.cols) return;
matrix.buffer[rowIdx * matrix.cols + colIdx] = value;
} }
// Wert auslesen
MatrixType getMatrixAt(const Matrix matrix, unsigned int rowIdx, unsigned int colIdx) MatrixType getMatrixAt(const Matrix matrix, unsigned int rowIdx, unsigned int colIdx)
{ {
if (rowIdx >= matrix.rows || colIdx >= matrix.cols) return UNDEFINED_MATRIX_VALUE;
return matrix.buffer[rowIdx * matrix.cols + colIdx];
} }
// Addition (mit Broadcasting-Unterstützung für Bias)
Matrix add(const Matrix matrix1, const Matrix matrix2) Matrix add(const Matrix matrix1, const Matrix matrix2)
{ {
Matrix result;
// Fall 1: Exakte Dimensionen (Element-weise Addition)
if (matrix1.rows == matrix2.rows && matrix1.cols == matrix2.cols) {
result = createMatrix(matrix1.rows, matrix1.cols);
for (unsigned int i = 0; i < matrix1.rows * matrix1.cols; i++)
result.buffer[i] = matrix1.buffer[i] + matrix2.buffer[i];
return result;
}
// Fall 2: matrix1 ist (zeilen x 1) Spaltenvektor, matrix2 ist (zeilen x spalten)
// Broadcasting: matrix1's Spalte wird zu jeder Spalte von matrix2 addiert
if (matrix1.rows == matrix2.rows && matrix1.cols == 1) {
result = createMatrix(matrix2.rows, matrix2.cols);
for (unsigned int col = 0; col < matrix2.cols; col++) {
for (unsigned int row = 0; row < matrix2.rows; row++) {
MatrixType val1 = matrix1.buffer[row * matrix1.cols + 0];
MatrixType val2 = matrix2.buffer[row * matrix2.cols + col];
result.buffer[row * result.cols + col] = val1 + val2;
}
}
return result;
}
// Fall 3: matrix2 ist (zeilen x 1) Spaltenvektor, matrix1 ist (zeilen x spalten)
// Broadcasting: matrix2's Spalte wird zu jeder Spalte von matrix1 addiert
if (matrix2.rows == matrix1.rows && matrix2.cols == 1) {
result = createMatrix(matrix1.rows, matrix1.cols);
for (unsigned int col = 0; col < matrix1.cols; col++) {
for (unsigned int row = 0; row < matrix1.rows; row++) {
MatrixType val1 = matrix1.buffer[row * matrix1.cols + col];
MatrixType val2 = matrix2.buffer[row * matrix2.cols + 0];
result.buffer[row * result.cols + col] = val1 + val2;
}
}
return result;
}
// Ungültige Dimensionen - leere Matrix zurückgeben
result.rows = 0;
result.cols = 0;
result.buffer = NULL;
return result;
} }
// Multiplikation
Matrix multiply(const Matrix matrix1, const Matrix matrix2) Matrix multiply(const Matrix matrix1, const Matrix matrix2)
{ {
Matrix result;
// Überprüfe ob Multiplikation möglich ist (Spalten matrix1 == Zeilen matrix2)
if (matrix1.cols != matrix2.rows) {
result.rows = 0;
result.cols = 0;
result.buffer = NULL;
return result;
}
result = createMatrix(matrix1.rows, matrix2.cols);
// Berechne alle Elemente des Ergebnisses
for (unsigned int i = 0; i < matrix1.rows; i++)
{
for (unsigned int j = 0; j < matrix2.cols; j++)
{
// Skalarprodukt: Reihe i von matrix1 × Spalte j von matrix2
MatrixType sum = 0;
for (unsigned int k = 0; k < matrix1.cols; k++)
sum += matrix1.buffer[i * matrix1.cols + k] * matrix2.buffer[k * matrix2.cols + j];
result.buffer[i * result.cols + j] = sum;
}
}
return result;
} }

View File

@ -5,15 +5,9 @@
typedef float MatrixType; typedef float MatrixType;
// Struktur Matrix // TODO Matrixtyp definieren
typedef struct {
MatrixType *buffer; // pointer
unsigned int rows;
unsigned int cols;
} Matrix;
// Funktionen
Matrix createMatrix(unsigned int rows, unsigned int cols); Matrix createMatrix(unsigned int rows, unsigned int cols);
void clearMatrix(Matrix *matrix); void clearMatrix(Matrix *matrix);
void setMatrixAt(MatrixType value, Matrix matrix, unsigned int rowIdx, unsigned int colIdx); void setMatrixAt(MatrixType value, Matrix matrix, unsigned int rowIdx, unsigned int colIdx);
@ -21,4 +15,5 @@ MatrixType getMatrixAt(const Matrix matrix, unsigned int rowIdx, unsigned int co
Matrix add(const Matrix matrix1, const Matrix matrix2); Matrix add(const Matrix matrix1, const Matrix matrix2);
Matrix multiply(const Matrix matrix1, const Matrix matrix2); Matrix multiply(const Matrix matrix1, const Matrix matrix2);
#endif #endif

View File

@ -71,6 +71,32 @@ void test_addFailsOnDifferentInputDimensions(void)
TEST_ASSERT_EQUAL_UINT32(0, result.cols); TEST_ASSERT_EQUAL_UINT32(0, result.cols);
} }
void test_addSupportsBroadcasting(void)
{
MatrixType buffer1[] = {1, 2, 3, 4, 5, 6};
MatrixType buffer2[] = {7, 8};
Matrix matrix1 = {.rows=2, .cols=3, .buffer=buffer1};
Matrix matrix2 = {.rows=2, .cols=1, .buffer=buffer2};
Matrix result1 = add(matrix1, matrix2);
Matrix result2 = add(matrix2, matrix1);
float expectedResults[] = {8, 9, 10, 12, 13, 14};
TEST_ASSERT_EQUAL_UINT32(matrix1.rows, result1.rows);
TEST_ASSERT_EQUAL_UINT32(matrix1.cols, result1.cols);
TEST_ASSERT_EQUAL_UINT32(matrix1.rows, result2.rows);
TEST_ASSERT_EQUAL_UINT32(matrix1.cols, result2.cols);
TEST_ASSERT_EQUAL_INT(sizeof(expectedResults)/sizeof(expectedResults[0]), result1.rows * result1.cols);
TEST_ASSERT_EQUAL_FLOAT_ARRAY(expectedResults, result1.buffer, result1.cols * result1.rows);
TEST_ASSERT_EQUAL_INT(sizeof(expectedResults)/sizeof(expectedResults[0]), result2.rows * result2.cols);
TEST_ASSERT_EQUAL_FLOAT_ARRAY(expectedResults, result2.buffer, result2.cols * result2.rows);
free(result1.buffer);
free(result2.buffer);
}
void test_multiplyReturnsCorrectResults(void) void test_multiplyReturnsCorrectResults(void)
{ {
MatrixType buffer1[] = {1, 2, 3, 4, 5, 6}; MatrixType buffer1[] = {1, 2, 3, 4, 5, 6};
@ -138,7 +164,7 @@ void test_setMatrixAtFailsOnIndicesOutOfRange(void)
Matrix matrixToTest = {.rows=2, .cols=3, .buffer=buffer}; Matrix matrixToTest = {.rows=2, .cols=3, .buffer=buffer};
setMatrixAt(-1, matrixToTest, 2, 3); setMatrixAt(-1, matrixToTest, 2, 3);
TEST_ASSERT_EQUAL_FLOAT_ARRAY(expectedResults, matrixToTest.buffer, matrixToTest.cols * matrixToTest.rows); TEST_ASSERT_EQUAL_FLOAT_ARRAY(expectedResults, matrixToTest.buffer, sizeof(buffer)/sizeof(MatrixType));
} }
void setUp(void) { void setUp(void) {
@ -159,6 +185,7 @@ int main()
RUN_TEST(test_clearMatrixSetsMembersToNull); RUN_TEST(test_clearMatrixSetsMembersToNull);
RUN_TEST(test_addReturnsCorrectResult); RUN_TEST(test_addReturnsCorrectResult);
RUN_TEST(test_addFailsOnDifferentInputDimensions); RUN_TEST(test_addFailsOnDifferentInputDimensions);
RUN_TEST(test_addSupportsBroadcasting);
RUN_TEST(test_multiplyReturnsCorrectResults); RUN_TEST(test_multiplyReturnsCorrectResults);
RUN_TEST(test_multiplyFailsOnWrongInputDimensions); RUN_TEST(test_multiplyFailsOnWrongInputDimensions);
RUN_TEST(test_getMatrixAtReturnsCorrectResult); RUN_TEST(test_getMatrixAtReturnsCorrectResult);

View File

@ -254,8 +254,6 @@ unsigned char *predict(const NeuralNetwork model, const GrayScaleImage images[],
return result; return result;
} }
void clearModel(NeuralNetwork *model) void clearModel(NeuralNetwork *model)
{ {
if(model != NULL) if(model != NULL)

View File

@ -1,219 +1,230 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <math.h>
#include "unity.h" #include "unity.h"
#include "neuralNetwork.h" #include "neuralNetwork.h"
#define FILE_HEADER_STRING "__info2_neural_network_file_format__"
// --------------------------
// Hilfsfunktion zum Erstellen einer Test-Datei für das Netzwerk
// --------------------------
static void prepareNeuralNetworkFile(const char *path, const NeuralNetwork nn) static void prepareNeuralNetworkFile(const char *path, const NeuralNetwork nn)
{ {
FILE *file = fopen(path, "wb"); // TODO
if (!file) return;
// Dateikennzeichnung schreiben
const char *fileTag = "__info2_neural_network_file_format__";
fwrite(fileTag, sizeof(char), strlen(fileTag), file);
// Alle Layer des Netzwerks in die Datei schreiben
for (unsigned int i = 0; i < nn.numberOfLayers; i++)
{
unsigned int inputDim = nn.layers[i].weights.cols;
unsigned int outputDim = nn.layers[i].weights.rows;
// Dimensionen des Layers schreiben
fwrite(&inputDim, sizeof(unsigned int), 1, file);
fwrite(&outputDim, sizeof(unsigned int), 1, file);
// Gewichtsmatrix schreiben
fwrite(nn.layers[i].weights.buffer, sizeof(MatrixType),
nn.layers[i].weights.rows * nn.layers[i].weights.cols, file);
// Biasvektor schreiben
fwrite(nn.layers[i].biases.buffer, sizeof(MatrixType),
nn.layers[i].biases.rows * nn.layers[i].biases.cols, file);
}
// Markierung für das Datei-Ende (keine weiteren Layer)
unsigned int zero = 0;
fwrite(&zero, sizeof(unsigned int), 1, file);
fclose(file);
} }
// --------------------------
// Test: Prüft, ob loadModel richtige Anzahl Layer lädt
// --------------------------
void test_loadModelReturnsCorrectNumberOfLayers(void) void test_loadModelReturnsCorrectNumberOfLayers(void)
{ {
const char *path = "test_nn_file.info2"; const char *path = "some__nn_test_file.info2";
MatrixType wBuf[] = {1,2,3,4,5,6}; MatrixType buffer1[] = {1, 2, 3, 4, 5, 6};
MatrixType bBuf[] = {1,2,3}; MatrixType buffer2[] = {1, 2, 3, 4, 5, 6};
Layer layers[] = {{.weights={wBuf,3,2}, .biases={bBuf,3,1}}}; Matrix weights1 = {.buffer=buffer1, .rows=3, .cols=2};
NeuralNetwork nn = {layers,1}; Matrix weights2 = {.buffer=buffer2, .rows=2, .cols=3};
MatrixType buffer3[] = {1, 2, 3};
MatrixType buffer4[] = {1, 2};
Matrix biases1 = {.buffer=buffer3, .rows=3, .cols=1};
Matrix biases2 = {.buffer=buffer4, .rows=2, .cols=1};
Layer layers[] = {{.weights=weights1, .biases=biases1}, {.weights=weights2, .biases=biases2}};
prepareNeuralNetworkFile(path, nn); NeuralNetwork expectedNet = {.layers=layers, .numberOfLayers=2};
NeuralNetwork netUnderTest;
NeuralNetwork loaded = loadModel(path); prepareNeuralNetworkFile(path, expectedNet);
TEST_ASSERT_EQUAL_INT(1, loaded.numberOfLayers);
clearModel(&loaded); netUnderTest = loadModel(path);
remove(path); remove(path);
TEST_ASSERT_EQUAL_INT(expectedNet.numberOfLayers, netUnderTest.numberOfLayers);
clearModel(&netUnderTest);
} }
// --------------------------
// Test: Prüft Dimensionen der Gewichte
// --------------------------
void test_loadModelReturnsCorrectWeightDimensions(void) void test_loadModelReturnsCorrectWeightDimensions(void)
{ {
const char *path = "test_nn_file.info2"; const char *path = "some__nn_test_file.info2";
MatrixType wBuf[] = {1,2,3,4,5,6}; MatrixType weightBuffer[] = {1, 2, 3, 4, 5, 6};
MatrixType bBuf[] = {1,2,3}; Matrix weights = {.buffer=weightBuffer, .rows=3, .cols=2};
Layer layers[] = {{.weights={wBuf,3,2}, .biases={bBuf,3,1}}}; MatrixType biasBuffer[] = {7, 8, 9};
NeuralNetwork nn = {layers,1}; Matrix biases = {.buffer=biasBuffer, .rows=3, .cols=1};
Layer layers[] = {{.weights=weights, .biases=biases}};
prepareNeuralNetworkFile(path, nn); NeuralNetwork expectedNet = {.layers=layers, .numberOfLayers=1};
NeuralNetwork netUnderTest;
NeuralNetwork loaded = loadModel(path); prepareNeuralNetworkFile(path, expectedNet);
TEST_ASSERT_EQUAL_INT(3, loaded.layers[0].weights.rows);
TEST_ASSERT_EQUAL_INT(2, loaded.layers[0].weights.cols); netUnderTest = loadModel(path);
clearModel(&loaded);
remove(path); remove(path);
TEST_ASSERT_TRUE(netUnderTest.numberOfLayers > 0);
TEST_ASSERT_EQUAL_INT(expectedNet.layers[0].weights.rows, netUnderTest.layers[0].weights.rows);
TEST_ASSERT_EQUAL_INT(expectedNet.layers[0].weights.cols, netUnderTest.layers[0].weights.cols);
clearModel(&netUnderTest);
} }
// --------------------------
// Test: Prüft Dimensionen der Biases
// --------------------------
void test_loadModelReturnsCorrectBiasDimensions(void) void test_loadModelReturnsCorrectBiasDimensions(void)
{ {
const char *path = "test_nn_file.info2"; const char *path = "some__nn_test_file.info2";
MatrixType wBuf[] = {1,2,3,4,5,6}; MatrixType weightBuffer[] = {1, 2, 3, 4, 5, 6};
MatrixType bBuf[] = {1,2,3}; Matrix weights = {.buffer=weightBuffer, .rows=3, .cols=2};
Layer layers[] = {{.weights={wBuf,3,2}, .biases={bBuf,3,1}}}; MatrixType biasBuffer[] = {7, 8, 9};
NeuralNetwork nn = {layers,1}; Matrix biases = {.buffer=biasBuffer, .rows=3, .cols=1};
Layer layers[] = {{.weights=weights, .biases=biases}};
prepareNeuralNetworkFile(path, nn); NeuralNetwork expectedNet = {.layers=layers, .numberOfLayers=1};
NeuralNetwork netUnderTest;
NeuralNetwork loaded = loadModel(path); prepareNeuralNetworkFile(path, expectedNet);
TEST_ASSERT_EQUAL_INT(3, loaded.layers[0].biases.rows);
TEST_ASSERT_EQUAL_INT(1, loaded.layers[0].biases.cols); netUnderTest = loadModel(path);
clearModel(&loaded);
remove(path); remove(path);
TEST_ASSERT_TRUE(netUnderTest.numberOfLayers > 0);
TEST_ASSERT_EQUAL_INT(expectedNet.layers[0].biases.rows, netUnderTest.layers[0].biases.rows);
TEST_ASSERT_EQUAL_INT(expectedNet.layers[0].biases.cols, netUnderTest.layers[0].biases.cols);
clearModel(&netUnderTest);
} }
// --------------------------
// Test: Prüft, dass Gewichte korrekt geladen werden
// --------------------------
void test_loadModelReturnsCorrectWeights(void) void test_loadModelReturnsCorrectWeights(void)
{ {
const char *path = "test_nn_file.info2"; const char *path = "some__nn_test_file.info2";
MatrixType wBuf[] = {1,2,3,4,5,6}; MatrixType weightBuffer[] = {1, 2, 3, 4, 5, 6};
MatrixType bBuf[] = {1,2,3}; Matrix weights = {.buffer=weightBuffer, .rows=3, .cols=2};
Layer layers[] = {{.weights={wBuf,3,2}, .biases={bBuf,3,1}}}; MatrixType biasBuffer[] = {7, 8, 9};
NeuralNetwork nn = {layers,1}; Matrix biases = {.buffer=biasBuffer, .rows=3, .cols=1};
Layer layers[] = {{.weights=weights, .biases=biases}};
prepareNeuralNetworkFile(path, nn); NeuralNetwork expectedNet = {.layers=layers, .numberOfLayers=1};
NeuralNetwork netUnderTest;
NeuralNetwork loaded = loadModel(path); prepareNeuralNetworkFile(path, expectedNet);
int n = loaded.layers[0].weights.rows * loaded.layers[0].weights.cols;
TEST_ASSERT_EQUAL_INT_ARRAY(wBuf, loaded.layers[0].weights.buffer, n); netUnderTest = loadModel(path);
clearModel(&loaded);
remove(path); remove(path);
TEST_ASSERT_TRUE(netUnderTest.numberOfLayers > 0);
TEST_ASSERT_EQUAL_INT(expectedNet.layers[0].weights.rows, netUnderTest.layers[0].weights.rows);
TEST_ASSERT_EQUAL_INT(expectedNet.layers[0].weights.cols, netUnderTest.layers[0].weights.cols);
int n = netUnderTest.layers[0].weights.rows * netUnderTest.layers[0].weights.cols;
TEST_ASSERT_EQUAL_INT_ARRAY(expectedNet.layers[0].weights.buffer, netUnderTest.layers[0].weights.buffer, n);
clearModel(&netUnderTest);
} }
// --------------------------
// Test: Prüft, dass Bias korrekt geladen werden
// --------------------------
void test_loadModelReturnsCorrectBiases(void) void test_loadModelReturnsCorrectBiases(void)
{ {
const char *path = "test_nn_file.info2"; const char *path = "some__nn_test_file.info2";
MatrixType wBuf[] = {1,2,3,4,5,6}; MatrixType weightBuffer[] = {1, 2, 3, 4, 5, 6};
MatrixType bBuf[] = {1,2,3}; Matrix weights = {.buffer=weightBuffer, .rows=3, .cols=2};
Layer layers[] = {{.weights={wBuf,3,2}, .biases={bBuf,3,1}}}; MatrixType biasBuffer[] = {7, 8, 9};
NeuralNetwork nn = {layers,1}; Matrix biases = {.buffer=biasBuffer, .rows=3, .cols=1};
Layer layers[] = {{.weights=weights, .biases=biases}};
prepareNeuralNetworkFile(path, nn); NeuralNetwork expectedNet = {.layers=layers, .numberOfLayers=1};
NeuralNetwork netUnderTest;
NeuralNetwork loaded = loadModel(path); prepareNeuralNetworkFile(path, expectedNet);
int n = loaded.layers[0].biases.rows * loaded.layers[0].biases.cols;
TEST_ASSERT_EQUAL_INT_ARRAY(bBuf, loaded.layers[0].biases.buffer, n); netUnderTest = loadModel(path);
clearModel(&loaded);
remove(path); remove(path);
TEST_ASSERT_TRUE(netUnderTest.numberOfLayers > 0);
TEST_ASSERT_EQUAL_INT(expectedNet.layers[0].weights.rows, netUnderTest.layers[0].weights.rows);
TEST_ASSERT_EQUAL_INT(expectedNet.layers[0].weights.cols, netUnderTest.layers[0].weights.cols);
int n = netUnderTest.layers[0].biases.rows * netUnderTest.layers[0].biases.cols;
TEST_ASSERT_EQUAL_INT_ARRAY(expectedNet.layers[0].biases.buffer, netUnderTest.layers[0].biases.buffer, n);
clearModel(&netUnderTest);
} }
// --------------------------
// Test: predict Funktion
// --------------------------
void test_predictReturnsCorrectLabels(void)
{
GrayScalePixelType img1[] = {10,20,30,40};
GrayScalePixelType img2[] = {5,15,25,35};
GrayScaleImage images[] = {
{.buffer=img1, .width=2, .height=2},
{.buffer=img2, .width=2, .height=2}
};
// Dummy Network für test: ReLU-ähnlich
MatrixType w1[] = {1,0,0,1,1,0,0,1};
MatrixType b1[] = {0,0};
Layer layers[] = {{.weights={w1,2,4}, .biases={b1,2,1}, .activation=NULL}};
NeuralNetwork nn = {layers,1};
unsigned char *labels = predict(nn, images, 2);
TEST_ASSERT_NOT_NULL(labels);
free(labels);
}
// --------------------------
// Test: clearModel setzt Pointer auf NULL
// --------------------------
void test_clearModelSetsMembersToNull(void)
{
MatrixType wBuf[] = {1,2,3,4,5,6};
MatrixType bBuf[] = {1,2,3};
Layer layers[] = {{.weights={wBuf,3,2}, .biases={bBuf,3,1}}};
NeuralNetwork nn = {layers,1};
clearModel(&nn);
TEST_ASSERT_NULL(nn.layers);
TEST_ASSERT_EQUAL_INT(0, nn.numberOfLayers);
}
// --------------------------
// Test: Fehlerhafte Datei (Header falsch)
// --------------------------
void test_loadModelFailsOnWrongFileTag(void) void test_loadModelFailsOnWrongFileTag(void)
{ {
const char *path = "wrong_nn_file.info2"; const char *path = "some_nn_test_file.info2";
NeuralNetwork netUnderTest;
FILE *file = fopen(path, "wb"); FILE *file = fopen(path, "wb");
if(file != NULL) if(file != NULL)
{ {
const char *wrongTag = "wrong_header_string"; const char *fileTag = "info2_neural_network_file_format";
fwrite(wrongTag, sizeof(char), strlen(wrongTag), file);
fwrite(fileTag, sizeof(char), strlen(fileTag), file);
fclose(file); fclose(file);
} }
NeuralNetwork nn = loadModel(path); netUnderTest = loadModel(path);
TEST_ASSERT_NULL(nn.layers);
TEST_ASSERT_EQUAL_INT(0, nn.numberOfLayers);
remove(path); remove(path);
TEST_ASSERT_NULL(netUnderTest.layers);
TEST_ASSERT_EQUAL_INT(0, netUnderTest.numberOfLayers);
} }
// -------------------------- void test_clearModelSetsMembersToNull(void)
// Unity Setup / Teardown {
// -------------------------- const char *path = "some__nn_test_file.info2";
void setUp(void) {} MatrixType weightBuffer[] = {1, 2, 3, 4, 5, 6};
void tearDown(void) {} Matrix weights = {.buffer=weightBuffer, .rows=3, .cols=2};
MatrixType biasBuffer[] = {7, 8, 9};
Matrix biases = {.buffer=biasBuffer, .rows=3, .cols=1};
Layer layers[] = {{.weights=weights, .biases=biases}};
// -------------------------- NeuralNetwork expectedNet = {.layers=layers, .numberOfLayers=1};
// Hauptfunktion zum Ausführen der Tests NeuralNetwork netUnderTest;
// --------------------------
int main(void) prepareNeuralNetworkFile(path, expectedNet);
netUnderTest = loadModel(path);
remove(path);
TEST_ASSERT_NOT_NULL(netUnderTest.layers);
TEST_ASSERT_TRUE(netUnderTest.numberOfLayers > 0);
clearModel(&netUnderTest);
TEST_ASSERT_NULL(netUnderTest.layers);
TEST_ASSERT_EQUAL_INT(0, netUnderTest.numberOfLayers);
}
static void someActivation(Matrix *matrix)
{
for(int i = 0; i < matrix->rows * matrix->cols; i++)
{
matrix->buffer[i] = fabs(matrix->buffer[i]);
}
}
void test_predictReturnsCorrectLabels(void)
{
const unsigned char expectedLabels[] = {4, 2};
GrayScalePixelType imageBuffer1[] = {10, 30, 25, 17};
GrayScalePixelType imageBuffer2[] = {20, 40, 10, 128};
GrayScaleImage inputImages[] = {{.buffer=imageBuffer1, .width=2, .height=2}, {.buffer=imageBuffer2, .width=2, .height=2}};
MatrixType weightsBuffer1[] = {1, -2, 3, -4, 5, -6, 7, -8};
MatrixType weightsBuffer2[] = {-9, 10, 11, 12, 13, 14};
MatrixType weightsBuffer3[] = {-15, 16, 17, 18, -19, 20, 21, 22, 23, -24, 25, 26, 27, -28, -29};
Matrix weights1 = {.buffer=weightsBuffer1, .rows=2, .cols=4};
Matrix weights2 = {.buffer=weightsBuffer2, .rows=3, .cols=2};
Matrix weights3 = {.buffer=weightsBuffer3, .rows=5, .cols=3};
MatrixType biasBuffer1[] = {200, 0};
MatrixType biasBuffer2[] = {0, -100, 0};
MatrixType biasBuffer3[] = {0, -1000, 0, 2000, 0};
Matrix biases1 = {.buffer=biasBuffer1, .rows=2, .cols=1};
Matrix biases2 = {.buffer=biasBuffer2, .rows=3, .cols=1};
Matrix biases3 = {.buffer=biasBuffer3, .rows=5, .cols=1};
Layer layers[] = {{.weights=weights1, .biases=biases1, .activation=someActivation}, \
{.weights=weights2, .biases=biases2, .activation=someActivation}, \
{.weights=weights3, .biases=biases3, .activation=someActivation}};
NeuralNetwork netUnderTest = {.layers=layers, .numberOfLayers=3};
unsigned char *predictedLabels = predict(netUnderTest, inputImages, 2);
TEST_ASSERT_NOT_NULL(predictedLabels);
int n = (int)(sizeof(expectedLabels) / sizeof(expectedLabels[0]));
TEST_ASSERT_EQUAL_UINT8_ARRAY(expectedLabels, predictedLabels, n);
free(predictedLabels);
}
void setUp(void) {
// Falls notwendig, kann hier Vorbereitungsarbeit gemacht werden
}
void tearDown(void) {
// Hier kann Bereinigungsarbeit nach jedem Test durchgeführt werden
}
int main()
{ {
UNITY_BEGIN(); UNITY_BEGIN();
@ -223,9 +234,9 @@ int main(void)
RUN_TEST(test_loadModelReturnsCorrectBiasDimensions); RUN_TEST(test_loadModelReturnsCorrectBiasDimensions);
RUN_TEST(test_loadModelReturnsCorrectWeights); RUN_TEST(test_loadModelReturnsCorrectWeights);
RUN_TEST(test_loadModelReturnsCorrectBiases); RUN_TEST(test_loadModelReturnsCorrectBiases);
RUN_TEST(test_predictReturnsCorrectLabels);
RUN_TEST(test_clearModelSetsMembersToNull);
RUN_TEST(test_loadModelFailsOnWrongFileTag); RUN_TEST(test_loadModelFailsOnWrongFileTag);
RUN_TEST(test_clearModelSetsMembersToNull);
RUN_TEST(test_predictReturnsCorrectLabels);
return UNITY_END(); return UNITY_END();
} }