Home β€Ί .NET on AWS β€Ί Using Rekognition with .NET

Using Rekognition with .NET

Image and video analysis with Amazon Rekognition from .NET: content moderation, label detection, face analysis, and CDK setup.

What Is Rekognition?

Amazon Rekognition is a managed computer vision service. You send it an image (or video), and it returns structured data: labels ("car", "sunset", "person"), faces with attributes (age range, emotions, glasses), text in images, content moderation flags, or custom labels you've trained.

No ML expertise required. No model training (for built-in features). No GPUs to manage. You call an API, get JSON back.

Common .NET Use Cases

  • Content moderation: Automatically flag inappropriate user-uploaded images before they're visible
  • Document processing: Detect text in scanned documents (though Textract is better for structured extraction)
  • User verification: Compare a selfie against an ID photo for identity verification
  • Asset tagging: Auto-tag images in a media library with descriptive labels
  • Safety compliance: Detect PPE (hard hats, vests) in construction site photos

Content Moderation

The most common use case for SaaS applications. Automatically detecting inappropriate content in user uploads:

using Amazon.Rekognition;
using Amazon.Rekognition.Model;

var rekognitionClient = new AmazonRekognitionClient();

// Moderate an image from S3
var response = await rekognitionClient.DetectModerationLabelsAsync(new DetectModerationLabelsRequest
{
    Image = new Image
    {
        S3Object = new S3Object
        {
            Bucket = "user-uploads",
            Name = $"photos/{userId}/{imageId}.jpg",
        },
    },
    MinConfidence = 70f, // Only return labels with 70%+ confidence
});

// Check for unsafe content
var unsafeLabels = response.ModerationLabels
    .Where(l => l.Confidence > 80)
    .ToList();

if (unsafeLabels.Any())
{
    // Flag or reject the upload
    var reasons = unsafeLabels.Select(l => $"{l.Name} ({l.Confidence:F0}%)");
    await _moderationService.FlagContentAsync(imageId, reasons);
    
    return new ModerationResult
    {
        IsApproved = false,
        Reason = $"Content flagged: {string.Join(", ", unsafeLabels.Select(l => l.Name))}",
    };
}

Moderation label categories

Rekognition returns a hierarchy of moderation labels:

  • Explicit Nudity β†’ Nudity, Graphic Male Nudity, Sexual Activity, etc.
  • Suggestive β†’ Female Swimwear, Male Swimwear, Revealing Clothes
  • Violence β†’ Graphic Violence, Weapon Violence
  • Visually Disturbing β†’ Emaciated Bodies, Corpses
  • Drugs & Tobacco β†’ Drug Use, Drug Paraphernalia
  • Hate Symbols β†’ Nazi Party, White Supremacy
  • Gambling β†’ Gambling

You decide the threshold per category. A dating app might allow swimwear; a children's platform wouldn't.

Label Detection

Identify objects, scenes, and concepts in images:

var response = await rekognitionClient.DetectLabelsAsync(new DetectLabelsRequest
{
    Image = new Image
    {
        S3Object = new S3Object
        {
            Bucket = "product-images",
            Name = imageKey,
        },
    },
    MaxLabels = 20,
    MinConfidence = 75f,
});

var labels = response.Labels.Select(l => new ImageLabel
{
    Name = l.Name,
    Confidence = l.Confidence,
    Categories = l.Categories?.Select(c => c.Name).ToList(),
    BoundingBoxes = l.Instances?.Select(i => i.BoundingBox).ToList(),
}).ToList();

// Example output: "Dog" (98%), "Animal" (98%), "Pet" (95%), "Golden Retriever" (87%)

Face Detection and Analysis

var response = await rekognitionClient.DetectFacesAsync(new DetectFacesRequest
{
    Image = new Image
    {
        Bytes = new MemoryStream(imageBytes),
    },
    Attributes = new List<string> { "ALL" },
});

foreach (var face in response.FaceDetails)
{
    var info = new FaceInfo
    {
        AgeRange = $"{face.AgeRange.Low}-{face.AgeRange.High}",
        Gender = face.Gender.Value,
        Emotions = face.Emotions
            .Where(e => e.Confidence > 70)
            .Select(e => e.Type.Value)
            .ToList(),
        Smile = face.Smile.Value,
        Sunglasses = face.Sunglasses.Value,
        EyesOpen = face.EyesOpen.Value,
        BoundingBox = face.BoundingBox,
    };
}

Event-Driven Moderation Pipeline

The production pattern: user uploads an image to S3, which triggers a Lambda that moderates it before it's accessible:

// Lambda triggered by S3 upload
public async Task Handler(S3Event s3Event, ILambdaContext context)
{
    foreach (var record in s3Event.Records)
    {
        var bucket = record.S3.Bucket.Name;
        var key = Uri.UnescapeDataString(record.S3.Object.Key);
        
        // Run moderation
        var moderationResult = await _rekognitionClient.DetectModerationLabelsAsync(
            new DetectModerationLabelsRequest
        {
            Image = new Image
            {
                S3Object = new S3Object { Bucket = bucket, Name = key },
            },
            MinConfidence = 70f,
        });
        
        var isUnsafe = moderationResult.ModerationLabels.Any(l => l.Confidence > 80);
        
        if (isUnsafe)
        {
            // Move to quarantine bucket
            await _s3Client.CopyObjectAsync(bucket, key, "quarantine-bucket", key);
            await _s3Client.DeleteObjectAsync(bucket, key);
            
            // Notify admin
            await _notificationService.NotifyModerationFlagAsync(key, moderationResult.ModerationLabels);
        }
        else
        {
            // Mark as approved in metadata
            await _s3Client.CopyObjectAsync(new CopyObjectRequest
            {
                SourceBucket = bucket,
                SourceKey = key,
                DestinationBucket = bucket,
                DestinationKey = key,
                MetadataDirective = S3MetadataDirective.REPLACE,
                Metadata = { ["x-amz-meta-moderation-status"] = "approved" },
            });
        }
    }
}

CDK Setup (C#)

using Amazon.CDK;
using Amazon.CDK.AWS.IAM;
using Amazon.CDK.AWS.S3;
using Amazon.CDK.AWS.Lambda.EventSources;

// Grant Rekognition access
moderationFunction.AddToRolePolicy(new PolicyStatement(new PolicyStatementProps
{
    Actions = new[]
    {
        "rekognition:DetectModerationLabels",
        "rekognition:DetectLabels",
        "rekognition:DetectFaces",
    },
    Resources = new[] { "*" }, // Rekognition doesn't have resource-level permissions
}));

// S3 trigger for automatic moderation
var uploadBucket = new Bucket(this, "UserUploads", new BucketProps
{
    BucketName = "user-uploads",
    BlockPublicAccess = BlockPublicAccess.BLOCK_ALL,
});

moderationFunction.AddEventSource(new S3EventSource(uploadBucket, new S3EventSourceProps
{
    Events = new[] { EventType.OBJECT_CREATED },
    Filters = new[] { new NotificationKeyFilter { Prefix = "photos/" } },
}));

// Rekognition needs access to read from S3
uploadBucket.GrantRead(moderationFunction);

Cost

  • Image moderation: $1.00 per 1,000 images (first 1M/month), drops with volume
  • Label detection: $1.00 per 1,000 images
  • Face detection: $1.00 per 1,000 images

At 100K user uploads per month, content moderation costs ~$100. That's far cheaper than manual moderation or hosting your own ML model.

Rekognition vs Bedrock Multimodal

You might wonder: why not just send images to Claude or another multimodal LLM through Bedrock?

Cost: Rekognition runs $1 per 1,000 images. Bedrock multimodal (Claude Sonnet) costs $4-10 per 1,000 images depending on resolution and output tokens. At scale, that's a significant difference.

Speed: Rekognition returns in 200-500ms. An LLM takes 2-5 seconds per image.

Determinism: Rekognition gives you the same result for the same image every time. LLMs are non-deterministic. You might get slightly different answers on repeated calls.

Where LLMs win: Context and reasoning. Rekognition sees nudity; it can't distinguish between pornography and a medical textbook illustration. An LLM can. Rekognition flags a knife; it can't tell a kitchen scene from a threat.

The hybrid approach (recommended for production)

public async Task<ModerationResult> ModerateAsync(string bucket, string key)
{
    // Step 1: Fast, cheap Rekognition pass
    var rekResult = await _rekognitionClient.DetectModerationLabelsAsync(
        new DetectModerationLabelsRequest
    {
        Image = new Image { S3Object = new S3Object { Bucket = bucket, Name = key } },
        MinConfidence = 50f,
    });

    var highConfidenceFlags = rekResult.ModerationLabels
        .Where(l => l.Confidence > 90).ToList();
    var borderlineFlags = rekResult.ModerationLabels
        .Where(l => l.Confidence is > 50 and <= 90).ToList();

    // Auto-reject obvious violations
    if (highConfidenceFlags.Any())
        return ModerationResult.Rejected(highConfidenceFlags);

    // Auto-approve if nothing flagged
    if (!borderlineFlags.Any())
        return ModerationResult.Approved();

    // Step 2: Borderline cases go to Bedrock for contextual judgment
    var imageBytes = await DownloadImageAsync(bucket, key);
    var bedrockResult = await _bedrockClient.ConverseAsync(new ConverseRequest
    {
        ModelId = "anthropic.claude-sonnet-4-20250514",
        Messages = new List<Message>
        {
            new()
            {
                Role = ConversationRole.User,
                Content = new List<ContentBlock>
                {
                    new() { Image = new ImageBlock
                    {
                        Source = new ImageSource { Bytes = new MemoryStream(imageBytes) },
                        Format = ImageFormat.Jpeg,
                    }},
                    new() { Text = $"This image was flagged for possible: " +
                        $"{string.Join(", ", borderlineFlags.Select(f => f.Name))}. " +
                        "Is this content actually inappropriate for a general audience, " +
                        "or is it benign in context? Respond with APPROVE or REJECT " +
                        "and a one-sentence reason." },
                },
            },
        },
        InferenceConfig = new InferenceConfiguration { MaxTokens = 100, Temperature = 0.1f },
    });

    var decision = bedrockResult.Output.Message.Content[0].Text;
    return decision.Contains("APPROVE", StringComparison.OrdinalIgnoreCase)
        ? ModerationResult.Approved()
        : ModerationResult.Rejected(borderlineFlags);
}

This gives you Rekognition's speed and cost for 95% of images, with LLM-level judgment for the ambiguous 5%. At 100K images/month with 5% borderline, you're paying ~$100 for Rekognition + ~$25 for Bedrock on borderline cases, versus $400-1000 to send everything through Bedrock.

Tips

  • Set confidence thresholds per category. Don't use the same threshold for all moderation categories. Violence detection might need 90% confidence to avoid false positives; explicit content might use 70%.
  • Use S3 references over bytes for images already in S3. Avoids downloading the image to Lambda and re-uploading it. Rekognition reads from S3 directly.
  • Image size limits: Max 15MB for S3 references, 5MB for inline bytes. Resize large uploads before analysis.
  • Combine with human review. Use Rekognition for the first pass (auto-approve clear content, auto-reject obvious violations), then send borderline cases to human moderators.
  • Rekognition doesn't detect AI-generated content (deepfakes, generated images). If that's a concern, you need additional tools.

Further Reading

Looking for hands-on help? View my .NET on AWS services β†’

Need content moderation or image analysis?

Drop me a message β€” I typically respond within one business day.