ASP.NET Core  

Image Recognition in Angular Using ASP.NET Core Backend

Building a Scalable, Real-World Full-Stack Application

Image recognition has become an essential feature in many modern applications. From security systems and document scanning to e-commerce and social media platforms, the ability to detect and classify images in real-time adds tremendous value. Combining Angular on the frontend with an ASP.NET Core backend is a powerful approach for building a scalable image recognition application.

In this article, we will discuss a practical approach to integrating image recognition into an Angular application using an ASP.NET Core backend. We will cover architectural considerations, implementation strategies, performance optimizations, and production-ready best practices.

1. Architecture Overview

A high-level architecture of the system looks like this:

Angular Frontend
    |
    | Upload Image / Fetch Results
    v
ASP.NET Core API
    |
    | Calls Image Recognition Model / Service
    v
Machine Learning Service
    |
    | Returns Prediction
    v
ASP.NET Core API
    |
    v
Angular Frontend

1.1 Frontend Responsibilities

  • Handle user image uploads.

  • Display processed results.

  • Show loading state and progress.

  • Perform client-side validation (file type, size, resolution).

1.2 Backend Responsibilities

  • Accept image uploads.

  • Validate image integrity.

  • Interface with the image recognition model or service (e.g., TensorFlow, ML.NET, OpenCV).

  • Return predictions as structured JSON.

  • Optionally, store images and results in database or cloud storage.

1.3 Machine Learning / Recognition Layer

  • Can be ML.NET for .NET-centric pipelines.

  • TensorFlow or PyTorch REST API is also a viable option.

  • Ensure inference is scalable and does not block the API thread.

2. Angular Frontend Implementation

2.1 Creating the Upload Component

We create a component to allow the user to select an image and upload it.

// image-upload.component.ts
import { Component } from '@angular/core';
import { ImageService } from '../services/image.service';

@Component({
  selector: 'app-image-upload',
  templateUrl: './image-upload.component.html',
  styleUrls: ['./image-upload.component.css']
})
export class ImageUploadComponent {
  selectedFile!: File;
  loading = false;
  prediction: any;

  constructor(private imageService: ImageService) {}

  onFileSelected(event: Event) {
    const input = event.target as HTMLInputElement;
    if (input.files && input.files.length > 0) {
      this.selectedFile = input.files[0];
    }
  }

  uploadImage() {
    if (!this.selectedFile) return;
    this.loading = true;
    this.imageService.uploadImage(this.selectedFile).subscribe({
      next: (result) => {
        this.prediction = result;
        this.loading = false;
      },
      error: (err) => {
        console.error(err);
        this.loading = false;
      }
    });
  }
}

2.2 HTML Template

<div class="upload-container">
  <input type="file" (change)="onFileSelected($event)" accept="image/*" />
  <button (click)="uploadImage()" [disabled]="!selectedFile || loading">
    Upload
  </button>

  <div *ngIf="loading">Processing...</div>

  <div *ngIf="prediction">
    <h3>Prediction Result</h3>
    <p>{{ prediction.label }} - Confidence: {{ prediction.confidence | percent }}</p>
  </div>
</div>

2.3 Image Service

// image.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class ImageService {
  private apiUrl = 'https://localhost:5001/api/images';

  constructor(private http: HttpClient) {}

  uploadImage(file: File): Observable<any> {
    const formData = new FormData();
    formData.append('file', file, file.name);
    return this.http.post<any>(`${this.apiUrl}/recognize`, formData);
  }
}

2.4 Angular Best Practices

  1. Validation: Restrict allowed MIME types and file sizes on client-side before upload.

  2. Loading States: Use a spinner or progress bar.

  3. Error Handling: Provide user-friendly error messages.

  4. OnPush Change Detection: Use OnPush for performance when handling streaming or large results.

  5. Async Pipe: Avoid manual subscriptions if possible; for this example, manual subscription is fine for upload-and-receive patterns.

3. ASP.NET Core Backend Implementation

3.1 Creating a Controller

// Controllers/ImagesController.cs
using Microsoft.AspNetCore.Mvc;
using ImageRecognitionAPI.Services;

namespace ImageRecognitionAPI.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ImagesController : ControllerBase
    {
        private readonly IImageRecognitionService _recognitionService;

        public ImagesController(IImageRecognitionService recognitionService)
        {
            _recognitionService = recognitionService;
        }

        [HttpPost("recognize")]
        public async Task<IActionResult> Recognize(IFormFile file)
        {
            if (file == null || file.Length == 0)
                return BadRequest("No file uploaded.");

            try
            {
                var result = await _recognitionService.RecognizeAsync(file);
                return Ok(result);
            }
            catch (Exception ex)
            {
                // Log exception
                return StatusCode(500, $"Internal server error: {ex.Message}");
            }
        }
    }
}

3.2 Image Recognition Service

For simplicity, we can use ML.NET or a Python service. Here's an ML.NET example:

// Services/ImageRecognitionService.cs
using Microsoft.ML;
using Microsoft.ML.Data;

namespace ImageRecognitionAPI.Services
{
    public interface IImageRecognitionService
    {
        Task<PredictionResult> RecognizeAsync(IFormFile file);
    }

    public class ImageRecognitionService : IImageRecognitionService
    {
        private readonly MLContext _mlContext;
        private readonly ITransformer _model;

        public ImageRecognitionService()
        {
            _mlContext = new MLContext();
            // Load pre-trained ONNX or ML.NET model
            _model = _mlContext.Model.Load("Model.zip", out var inputSchema);
        }

        public async Task<PredictionResult> RecognizeAsync(IFormFile file)
        {
            using var stream = new MemoryStream();
            await file.CopyToAsync(stream);
            var imageData = new ImageData { Image = stream.ToArray() };

            var predictor = _mlContext.Model.CreatePredictionEngine<ImageData, ImagePrediction>(_model);
            var prediction = predictor.Predict(imageData);

            return new PredictionResult
            {
                Label = prediction.PredictedLabel,
                Confidence = prediction.Score.Max()
            };
        }
    }

    public class ImageData
    {
        [ColumnName("input")]
        public byte[] Image { get; set; }
    }

    public class ImagePrediction
    {
        [ColumnName("output_label")]
        public string PredictedLabel { get; set; }

        [ColumnName("output_score")]
        public float[] Score { get; set; }
    }

    public class PredictionResult
    {
        public string Label { get; set; }
        public float Confidence { get; set; }
    }
}

3.3 Production Considerations

  1. Model Loading: Load the model once during service startup, not per request.

  2. Async Processing: Use Task.Run for CPU-bound inference if multiple requests arrive simultaneously.

  3. Memory Management: Dispose of streams and unmanaged resources properly.

  4. Validation: Check image dimensions and type before processing.

  5. Logging: Use structured logging for observability.

4. Real-Time Upload and Progress Feedback

For large images, consider Angular’s HttpClient upload progress.

uploadImage() {
  const formData = new FormData();
  formData.append('file', this.selectedFile);

  this.imageService.uploadImageWithProgress(formData).subscribe({
    next: event => {
      if (event.type === HttpEventType.UploadProgress) {
        this.progress = Math.round((event.loaded / event.total!) * 100);
      } else if (event.type === HttpEventType.Response) {
        this.prediction = event.body;
        this.progress = 0;
      }
    },
    error: err => console.error(err)
  });
}

Backend should support streaming or large uploads with RequestSizeLimit.

5. Security and Best Practices

  1. File Validation: Never trust file extensions; check MIME type and magic bytes.

  2. File Size Limit: Configure MaxRequestBodySize to prevent DoS.

  3. Sanitisation: If saving files temporarily, use a secure temp directory.

  4. CORS Policy: Angular frontend may run on a different domain; configure CORS properly.

  5. Authentication: Use JWT or cookie-based auth for secure endpoints.

  6. Rate Limiting: Protect recognition endpoints against abuse.

6. Performance and Scalability Tips

  1. Batch Processing: For multiple images, process in batches asynchronously.

  2. Caching Results: Cache predictions for repeated images.

  3. Offload Heavy Models: Consider using GPU-accelerated servers or a dedicated ML microservice.

  4. Async Queue: Use a queue like RabbitMQ or Azure Service Bus for high-load scenarios.

  5. WebSockets: For real-time result streaming to Angular without polling.

7. Optional Enhancements

  1. Drag-and-Drop Upload: Enhance UX using Angular CDK or libraries like ngx-file-drop.

  2. Preview Image: Display the selected image before upload.

  3. Multiple Model Support: Let backend select models dynamically based on context.

  4. Image Annotation: Highlight detected objects in frontend using Canvas or SVG overlays.

  5. Mobile Responsiveness: Ensure Angular component works for touch and small screens.

8. Testing

  1. Unit Tests

    • Angular: Test file selection and service calls with Jasmine/Karma.

    • ASP.NET Core: Mock IFormFile and test prediction service.

  2. Integration Tests

    • Use TestServer in ASP.NET Core to test API endpoints end-to-end.

  3. Performance Testing

    • Upload multiple large images to simulate production load.

    • Measure CPU, memory, and response latency.

9. Deployment Considerations

  1. Angular: Build with ng build --prod, serve via Nginx, Apache, or ASP.NET Core static files.

  2. ASP.NET Core API: Deploy on Kestrel behind reverse proxy (Nginx/Apache/IIS).

  3. HTTPS: Ensure TLS for image upload endpoints.

  4. Cloud Storage: For heavy image workloads, store images on AWS S3, Azure Blob Storage, or GCP Cloud Storage.

  5. Containerization: Dockerize frontend and backend for easier deployment and scaling.

Summary

By combining Angular with an ASP.NET Core backend, we can build a production-ready image recognition application that is:

  • User-Friendly: Smooth image upload and real-time predictions.

  • Scalable: Proper async processing, batch support, and queue integration.

  • Secure: File validation, authentication, and rate limiting.

  • Maintainable: Clear separation between frontend, API, and ML service layers.

Key takeaways for senior developers:

  1. Use Angular services and async pipes effectively to handle image upload streams.

  2. Keep backend stateless and async for scalability.

  3. Load ML models once and avoid per-request initialization.

  4. Implement throttling or batch processing for high-frequency image recognition requests.

  5. Monitor performance, memory, and request latency continuously.

With these practices, you can build enterprise-grade image recognition features in Angular applications with an ASP.NET Core backend.