AWS

Amazon Rekognition - Комп'ютерний зір

Глибоке занурення в Amazon Rekognition. Повна інструкція з інтеграції аналізу зображень, виявлення об'єктів, облич, OCR та модерації контенту з .NET 8 та React. Повноцінний код з малюванням bounding boxes на Canvas.

Amazon Rekognition - Комп'ютерний зір

Amazon Rekognition — це хмарний сервіс комп'ютерного зору (Computer Vision), який працює на базі готових навчених моделей глибокого навчання від AWS. Він не потребує від розробника експертизи в Data Science. Ви передаєте зображення (як потік байтів або посилання на об'єкт в S3) і отримуєте структуровану JSON-відповідь про об'єкти, обличчя, тексти та сцени.

Можливості Amazon Rekognition

Детекція об'єктів та сцен (Labels)

Аналізує зображення та повертає список міток (Label) із рівнем впевненості (Confidence) та координатами об'єктів (BoundingBox). Наприклад: Car (99%), Tree (95%).

Аналіз та порівняння облич

Знаходить обличчя на фото, визначає стать, віковий діапазон, наявність окулярів чи бороди, а також емоції (радість, смуток тощо). Дозволяє порівнювати два фото на схожість облич.

Розпізнавання тексту (OCR)

Виявляє та зчитує друкований та рукописний текст на фотографіях (вивіски, автомобільні номери, документи) у складних умовах (під кутом, при поганому освітленні).

Модерація контенту

Автоматично маркує неприйнятний, агресивний чи відвертий контент, повертаючи ієрархію категорій з рівнем впевненості. Запобігає публікації небажаного вмісту.

Реалізація сервісу на .NET 8

Для роботи встановіть NuGet-пакет:

dotnet add package AWSSDK.Rekognition

Нижче наведено повний та готовий до використання клас сервісу RekognitionService.cs зі всіма DTO та методами для обробки зображень.

Services/RekognitionService.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Amazon.Rekognition;
using Amazon.Rekognition.Model;

namespace AwsAiPlayground.Services;

// DTO для об'єктів
public record BoundingBoxDto(float Left, float Top, float Width, float Height);

public record DetectedLabelDto(
    string Name,
    float Confidence,
    List<string> Categories,
    BoundingBoxDto? BoundingBox);

// DTO для модерації
public record ModerationViolationDto(string Category, string SubCategory, float Confidence);

public record ContentModerationResultDto(
    bool IsSafe,
    List<ModerationViolationDto> Violations);

// DTO для облич
public record FaceDetailDto(
    string Gender,
    string AgeRange,
    string Emotion,
    float Confidence,
    BoundingBoxDto BoundingBox);

public record FaceComparisonResultDto(
    float Similarity,
    BoundingBoxDto TargetBoundingBox);

public sealed class RekognitionService
{
    private readonly IAmazonRekognition _rekognitionClient;

    public RekognitionService(IAmazonRekognition rekognitionClient)
    {
        _rekognitionClient = rekognitionClient;
    }

    /// <summary>
    /// Детектує об'єкти на зображенні з S3.
    /// </summary>
    public async Task<List<DetectedLabelDto>> DetectLabelsAsync(
        string bucketName, 
        string imageKey, 
        float minConfidence = 70f)
    {
        var request = new DetectLabelsRequest
        {
            Image = new Image
            {
                S3Object = new S3Object { Bucket = bucketName, Name = imageKey }
            },
            MaxLabels = 50,
            MinConfidence = minConfidence
        };

        try
        {
            var response = await _rekognitionClient.DetectLabelsAsync(request);

            return response.Labels.Select(l => new DetectedLabelDto(
                Name: l.Name,
                Confidence: l.Confidence,
                Categories: l.Categories.Select(c => c.Name).ToList(),
                BoundingBox: l.Instances.FirstOrDefault()?.BoundingBox is { } bb
                    ? new BoundingBoxDto(bb.Left, bb.Top, bb.Width, bb.Height)
                    : null
            )).ToList();
        }
        catch (AmazonRekognitionException ex)
        {
            throw new Exception($"Error calling Amazon Rekognition DetectLabels: {ex.Message}", ex);
        }
    }

    /// <summary>
    /// Перевіряє зображення на наявність небажаного вмісту.
    /// </summary>
    public async Task<ContentModerationResultDto> ModerateImageAsync(string bucketName, string imageKey)
    {
        var request = new DetectModerationLabelsRequest
        {
            Image = new Image
            {
                S3Object = new S3Object { Bucket = bucketName, Name = imageKey }
            },
            MinConfidence = 50f
        };

        try
        {
            var response = await _rekognitionClient.DetectModerationLabelsAsync(request);

            var violations = response.ModerationLabels.Select(ml => new ModerationViolationDto(
                Category: ml.ParentName ?? ml.Name,
                SubCategory: ml.Name,
                Confidence: ml.Confidence
            )).ToList();

            return new ContentModerationResultDto(
                IsSafe: violations.Count == 0,
                Violations: violations
            );
        }
        catch (AmazonRekognitionException ex)
        {
            throw new Exception($"Error calling Amazon Rekognition ModerateImage: {ex.Message}", ex);
        }
    }

    /// <summary>
    /// OCR розпізнавання тексту на фотографії з S3.
    /// </summary>
    public async Task<string> ExtractTextAsync(string bucketName, string imageKey)
    {
        var request = new DetectTextRequest
        {
            Image = new Image
            {
                S3Object = new S3Object { Bucket = bucketName, Name = imageKey }
            }
        };

        try
        {
            var response = await _rekognitionClient.DetectTextAsync(request);
            
            // Фільтруємо лише цілі рядки (LINES), ігноруючи окремі слова
            var lines = response.TextDetections
                .Where(t => t.Type == TextTypes.LINE)
                .OrderBy(t => t.Geometry.BoundingBox.Top)
                .Select(t => t.DetectedText);

            return string.Join("\n", lines);
        }
        catch (AmazonRekognitionException ex)
        {
            throw new Exception($"Error calling Amazon Rekognition DetectText: {ex.Message}", ex);
        }
    }

    /// <summary>
    /// Порівнює обличчя на джерельному фото з обличчями на цільовому фото.
    /// </summary>
    public async Task<List<FaceComparisonResultDto>> CompareFacesAsync(
        string sourceBucket, string sourceKey,
        string targetBucket, string targetKey,
        float similarityThreshold = 80f)
    {
        var request = new CompareFacesRequest
        {
            SourceImage = new Image { S3Object = new S3Object { Bucket = sourceBucket, Name = sourceKey } },
            TargetImage = new Image { S3Object = new S3Object { Bucket = targetBucket, Name = targetKey } },
            SimilarityThreshold = similarityThreshold
        };

        try
        {
            var response = await _rekognitionClient.CompareFacesAsync(request);

            return response.FaceMatches.Select(m => new FaceComparisonResultDto(
                Similarity: m.Similarity,
                TargetBoundingBox: new BoundingBoxDto(
                    m.Face.BoundingBox.Left,
                    m.Face.BoundingBox.Top,
                    m.Face.BoundingBox.Width,
                    m.Face.BoundingBox.Height)
            )).ToList();
        }
        catch (AmazonRekognitionException ex)
        {
            throw new Exception($"Error calling Amazon Rekognition CompareFaces: {ex.Message}", ex);
        }
    }
}

Інтеграція з React: Візуалізація Bounding Boxes на Canvas

Для того щоб відобразити результати виявлення Rekognition (об'єкти, обличчя) поверх оригінального фото, координати Bounding Box перераховуються у пікселі та малюються на елементі <canvas>.

Координати від AWS Rekognition є відносними (значення від 0 до 1 відносно ширини та висоти зображення).

Повний React компонент ImageAnalyzer

src/components/ImageAnalyzer.tsx
import React, { useRef, useState, useEffect } from 'react';

export interface BoundingBox {
  left: number;
  top: number;
  width: number;
  height: number;
}

export interface DetectedLabel {
  name: string;
  confidence: number;
  boundingBox?: BoundingBox | null;
}

interface ImageAnalyzerProps {
  imageUrl: string;
  labels: DetectedLabel[];
}

export function ImageAnalyzer({ imageUrl, labels }: ImageAnalyzerProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const imageRef = useRef<HTMLImageElement | null>(null);
  const [imgLoaded, setImgLoaded] = useState(false);

  useEffect(() => {
    // Створюємо та завантажуємо зображення
    const img = new Image();
    img.src = imageUrl;
    img.crossOrigin = 'anonymous'; // дозволяє читання пікселів при CORS
    img.onload = () => {
      imageRef.current = img;
      setImgLoaded(true);
    };
    img.onerror = () => {
      console.error('Не вдалося завантажити зображення.');
    };
  }, [imageUrl]);

  useEffect(() => {
    if (!imgLoaded || !imageRef.current || !canvasRef.current) return;

    const canvas = canvasRef.current;
    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    const img = imageRef.current;
    
    // Встановлюємо розміри canvas відповідно до реальних розмірів зображення
    canvas.width = img.naturalWidth;
    canvas.height = img.naturalHeight;

    // Малюємо оригінальне зображення
    ctx.drawImage(img, 0, 0);

    // Малюємо Bounding Boxes для кожного об'єкта
    labels.forEach((label) => {
      if (!label.boundingBox) return;

      const { left, top, width, height } = label.boundingBox;

      // Конвертуємо відносні координати (0..1) в пікселі
      const x = left * canvas.width;
      const y = top * canvas.height;
      const w = width * canvas.width;
      const h = height * canvas.height;

      // Налаштовуємо стилі рамки
      ctx.strokeStyle = '#00ffcc';
      ctx.lineWidth = Math.max(3, canvas.width * 0.003); // товщина рамки адаптується під роздільну здатність
      ctx.strokeRect(x, y, w, h);

      // Малюємо плашку для тексту
      ctx.fillStyle = 'rgba(0, 255, 204, 0.85)';
      const fontSize = Math.max(14, canvas.width * 0.015);
      ctx.font = `bold ${fontSize}px sans-serif`;
      
      const text = `${label.name} (${Math.round(label.confidence)}%)`;
      const textWidth = ctx.measureText(text).width;
      const padding = 6;
      
      // Запобігаємо виходу плашки за верхній край
      const labelY = y - fontSize - padding > 0 ? y - fontSize - padding : y;
      
      ctx.fillRect(x, labelY, textWidth + padding * 2, fontSize + padding);

      // Малюємо сам текст
      ctx.fillStyle = '#000000';
      ctx.fillText(text, x + padding, labelY + fontSize);
    });
  }, [imgLoaded, labels]);

  return (
    <div style={styles.container}>
      <div style={styles.canvasWrapper}>
        <canvas ref={canvasRef} style={styles.canvas} />
      </div>
      <div style={styles.detailsList}>
        <h3 style={styles.listTitle}>Виявлені об'єкти:</h3>
        <ul style={styles.list}>
          {labels.map((l, index) => (
            <li key={index} style={styles.listItem}>
              <span style={styles.labelName}>{l.name}</span>
              <span style={styles.confidenceBadge}>{Math.round(l.confidence)}%</span>
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
}

const styles = {
  container: {
    display: 'flex',
    flexDirection: 'row' as const,
    gap: '24px',
    background: '#1f2937',
    padding: '24px',
    borderRadius: '12px',
    boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)',
    color: '#f3f4f6',
    flexWrap: 'wrap' as const,
  },
  canvasWrapper: {
    flex: '2 1 500px',
    position: 'relative' as const,
    background: '#111827',
    borderRadius: '8px',
    overflow: 'hidden',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
  },
  canvas: {
    maxWidth: '100%',
    maxHeight: '550px',
    objectFit: 'contain' as const,
  },
  detailsList: {
    flex: '1 1 250px',
    background: '#111827',
    padding: '16px',
    borderRadius: '8px',
    border: '1px solid rgba(255, 255, 255, 0.05)',
  },
  listTitle: {
    marginTop: 0,
    borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
    paddingBottom: '8px',
    color: '#60a5fa',
  },
  list: {
    listStyleType: 'none',
    padding: 0,
    margin: 0,
    maxHeight: '400px',
    overflowY: 'auto' as const,
  },
  listItem: {
    display: 'flex',
    justifyContent: 'space-between',
    padding: '8px 4px',
    borderBottom: '1px solid rgba(255, 255, 255, 0.03)',
  },
  labelName: {
    fontWeight: 500,
  },
  confidenceBadge: {
    background: '#10b981',
    color: '#fff',
    padding: '2px 8px',
    borderRadius: '12px',
    fontSize: '0.85rem',
  },
};
Copyright © 2026