Learn ML.NET image classification in C#. Step-by-step tutorial to train, evaluate, and deploy a custom image classifier in .NET. Start building today!
If you have ever wanted to add image recognition to a .NET app without leaving C#, ML.NET image classification is the fastest way to get there. In this tutorial you will train a custom image classifier in C#, evaluate its accuracy, and deploy the trained model into a production .NET application — all using free, open-source tooling from Microsoft. No Python, no external ML services, and no PhD in deep learning required.
ML.NET is Microsoft's cross-platform machine learning framework for .NET developers. It runs on Windows, macOS, and Linux, integrates directly with your existing C# codebase, and uses transfer learning under the hood so you can build an accurate model with only a few hundred images. By the end of this guide you will understand not just how to build an image classifier, but why each step matters.
What Is ML.NET Image Classification?
ML.NET image classification is the task of assigning a label (a class) to an input image — for example, deciding whether a photo shows a "cat", a "dog", or a "pizza". Under the hood, ML.NET uses a technique called transfer learning. Instead of training a deep neural network from scratch (which would need millions of images and expensive GPUs), it takes a pre-trained model such as ResNet or Inception that already knows how to "see" edges, textures, and shapes, and retrains only the final layers on your custom data.
This is a game-changer for C# machine learning because it means:
- Small datasets work. You can get solid results with 100–200 images per class.
- Training is fast. Minutes on a CPU, seconds on a GPU.
- It stays in .NET. The model saves as a
.zipfile you load with C# — no interop headaches.
Prerequisites and Project Setup
To follow this ML.NET image classification tutorial, you will need the .NET 8 SDK (or later) and any editor — Visual Studio 2022, VS Code, or Rider. Start by creating a console project and adding the required NuGet packages.
// Create the project from the terminal
dotnet new console -n ImageClassifier
cd ImageClassifier
// Add the ML.NET packages needed for deep-learning image classification
dotnet add package Microsoft.ML
dotnet add package Microsoft.ML.Vision
dotnet add package Microsoft.ML.ImageAnalytics
dotnet add package SciSharp.TensorFlow.Redist
The Microsoft.ML.Vision package provides the ImageClassificationTrainer, and SciSharp.TensorFlow.Redist supplies the native TensorFlow backend that powers transfer learning. If you have a supported NVIDIA GPU, you can swap in the GPU redist package for dramatically faster training.
Organizing Your Training Data
ML.NET expects one folder per class, with images inside each folder. The folder name becomes the label. This convention keeps data loading trivial:
/assets
/images
/cat -> 150 cat photos
/dog -> 150 dog photos
/pizza -> 150 pizza photos
Loading and Preparing the Image Data
First, define a class that represents a single input row. We only need the file path and the label — ML.NET reads the pixels for us during training.
using Microsoft.ML.Data;
public class ImageData
{
[LoadColumn(0)]
public string ImagePath { get; set; }
[LoadColumn(1)]
public string Label { get; set; }
}
public class ImagePrediction
{
[ColumnName("PredictedLabel")]
public string PredictedLabel { get; set; }
public float[] Score { get; set; }
}
Next, scan the folders to build an in-memory list of ImageData. This helper walks each subdirectory and uses the folder name as the label — a clean, reusable pattern for any image dataset.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
public static IEnumerable<ImageData> LoadImagesFromDirectory(string folder)
{
var files = Directory.GetFiles(folder, "*",
SearchOption.AllDirectories)
.Where(f => Path.GetExtension(f).ToLower() is ".jpg" or ".jpeg" or ".png");
foreach (var file in files)
{
// The parent folder name is the class label (e.g. "cat")
var label = Directory.GetParent(file).Name;
yield return new ImageData { ImagePath = file, Label = label };
}
}
How to Train the Image Classification Model in C#
Now for the core of this C# machine learning tutorial: building and training the pipeline. The workflow is always the same in ML.NET — create an MLContext, load data into an IDataView, split it into training and test sets, then chain transforms into an EstimatorChain.
using Microsoft.ML;
using Microsoft.ML.Vision;
var mlContext = new MLContext(seed: 1);
// 1. Load images into an IDataView
string imagesFolder = Path.Combine(Environment.CurrentDirectory, "assets", "images");
IEnumerable<ImageData> images = LoadImagesFromDirectory(imagesFolder);
IDataView fullData = mlContext.Data.LoadFromEnumerable(images);
// 2. Shuffle so classes are not grouped together — critical for good training
IDataView shuffledData = mlContext.Data.ShuffleRows(fullData);
// 3. Split: 80% training, 20% testing
var split = mlContext.Data.TrainTestSplit(shuffledData, testFraction: 0.2);
Why shuffle? If cat images all appear first and pizza images last, a mini-batch during training could see only one class at a time, which destabilizes learning. Shuffling ensures each batch is representative of the whole dataset.
Now define the pipeline. Two steps matter: mapping the text label to a numeric key (models work with numbers, not strings), then applying the ImageClassificationTrainer.
// Convert string labels ("cat") to numeric keys the trainer understands
var pipeline = mlContext.Transforms.Conversion.MapValueToKey(
inputColumnName: "Label",
outputColumnName: "LabelKey")
.Append(mlContext.Transforms.LoadRawImageBytes(
outputColumnName: "Image",
imageFolder: null,
inputColumnName: "ImagePath"))
.Append(mlContext.MulticlassClassification.Trainers.ImageClassification(
new ImageClassificationTrainer.Options
{
FeatureColumnName = "Image",
LabelColumnName = "LabelKey",
Arch = ImageClassificationTrainer.Architecture.ResnetV2101,
Epoch = 50,
BatchSize = 20,
LearningRate = 0.01f,
ValidationSet = split.TestSet,
MetricsCallback = m => Console.WriteLine(m)
}))
// Turn the numeric prediction back into a readable label
.Append(mlContext.Transforms.Conversion.MapKeyToValue(
outputColumnName: "PredictedLabel",
inputColumnName: "PredictedLabel"));
Console.WriteLine("Training model... this may take a few minutes.");
ITransformer model = pipeline.Fit(split.TrainSet);
The Arch option selects the pre-trained architecture. ResnetV2101 is accurate but larger; MobilenetV2 is smaller and faster, ideal for edge or mobile deployment. An epoch is one full pass over your training data — 50 is a sensible default for transfer learning. The MetricsCallback lets you watch accuracy climb in real time.
Evaluating Model Accuracy
Never trust a model you have not measured. Run the held-out test set through the model and inspect the metrics. This is where you catch overfitting before it reaches production.
IDataView predictions = model.Transform(split.TestSet);
var metrics = mlContext.MulticlassClassification.Evaluate(
predictions,
labelColumnName: "LabelKey",
predictedLabelColumnName: "PredictedLabel");
Console.WriteLine($"Macro Accuracy: {metrics.MacroAccuracy:P2}");
Console.WriteLine($"Micro Accuracy: {metrics.MicroAccuracy:P2}");
Console.WriteLine($"Log Loss: {metrics.LogLoss:F3}");
Macro accuracy averages accuracy per class and is your best metric when classes are imbalanced — it prevents a dominant class from hiding poor performance on rare ones. Micro accuracy weights every sample equally. A large gap between the two signals class imbalance. Aim for macro accuracy above 90% on a clean dataset; if it is low, add more images or check for mislabeled data.
How to Save and Deploy the ML.NET Model
The whole point of ML.NET image classification in C# is deployment inside real .NET apps. Save the trained model as a single .zip file, then load it anywhere — an ASP.NET Core API, a Blazor app, a WPF desktop tool, or an Azure Function.
// Persist the model and its input schema to disk
mlContext.Model.Save(model, split.TrainSet.Schema, "imageClassifier.zip");
Console.WriteLine("Model saved to imageClassifier.zip");
To make predictions in production, load the model once and reuse a PredictionEngine. Because PredictionEngine is not thread-safe, in a web app you should use PredictionEnginePool from the Microsoft.Extensions.ML package instead of creating engines per request.
// Load the saved model
DataViewSchema schema;
ITransformer loadedModel = mlContext.Model.Load("imageClassifier.zip", out schema);
// Create a prediction engine for single, in-memory predictions
var predictor = mlContext.Model.CreatePredictionEngine<ImageData, ImagePrediction>(loadedModel);
// Classify a brand-new image
var sample = new ImageData { ImagePath = "assets/test/mystery.jpg" };
ImagePrediction result = predictor.Predict(sample);
float confidence = result.Score.Max();
Console.WriteLine($"Predicted: {result.PredictedLabel} ({confidence:P1} confident)");
Deploying in an ASP.NET Core API
For scalable image recognition in C#, register a PredictionEnginePool in Program.cs and inject it into your controllers. The pool manages a thread-safe set of engines for you.
builder.Services.AddPredictionEnginePool<ImageData, ImagePrediction>()
.FromFile(modelName: "classifier", filePath: "imageClassifier.zip", watchForChanges: true);
Best Practices and Common Pitfalls
Getting a model to train is easy; getting a reliable one takes discipline. Keep these ML.NET best practices in mind:
- Balance your classes. Aim for a similar number of images per label. Heavy imbalance biases predictions toward the majority class.
- Use varied, realistic images. Include different lighting, angles, and backgrounds so the model generalizes instead of memorizing.
- Always keep a separate test set. Evaluating on training data gives falsely high accuracy — the classic overfitting trap.
- Reuse the PredictionEngine. Creating one per request is slow and, in web apps, unsafe. Use
PredictionEnginePool. - Match architecture to deployment. Use
MobilenetV2for mobile/edge andResnetV2101when accuracy matters most. - Pin the TensorFlow redist version. Mismatched native binaries are the number-one cause of "DllNotFound" errors at runtime.
A frequent beginner mistake is forgetting the MapKeyToValue step at the end of the pipeline. Without it, your predictions come back as opaque numeric keys instead of human-readable labels like "dog".
Conclusion and Key Takeaways
You now have an end-to-end blueprint for ML.NET image classification — from organizing training data to training with transfer learning, evaluating accuracy, and deploying a custom model in a real C# application. The biggest advantage of doing image recognition in C# with ML.NET is that everything stays inside the .NET ecosystem you already know, so there is no context-switching to Python or costly cloud APIs.
Key takeaways:
- Transfer learning lets you train an accurate image classifier with only a few hundred images.
- The pipeline pattern —
MapValueToKey→LoadRawImageBytes→ImageClassification→MapKeyToValue— is reusable for any dataset. - Always evaluate on a held-out test set and prefer macro accuracy for imbalanced data.
- Deploy with
PredictionEnginePoolfor thread-safe, high-throughput inference.
Try it on your own dataset today: swap in your image folders, hit train, and you will have a production-ready image classifier in .NET within minutes. Once you are comfortable, experiment with different architectures and epoch counts to squeeze out extra accuracy. Happy coding on csharp-coder.com!
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment