Skip to content

Commit

Permalink
Merge pull request #8 from Team-Wilhelm/remove-image-background
Browse files Browse the repository at this point in the history
Remove image background + add plant + blob
  • Loading branch information
juuwel authored May 2, 2024
2 parents 3db6ef7 + cd03847 commit 40ea238
Show file tree
Hide file tree
Showing 50 changed files with 314 additions and 85 deletions.
3 changes: 3 additions & 0 deletions Core/Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Azure.AI.Vision.ImageAnalysis" Version="1.0.0-beta.2" />
<PackageReference Include="Azure.Identity" Version="1.11.2" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.20.0-beta.2" />
<PackageReference Include="JWT" Version="10.1.1"/>
<PackageReference Include="MQTTnet" Version="4.3.3.952"/>
<PackageReference Include="Serilog" Version="3.1.1"/>
Expand Down
8 changes: 8 additions & 0 deletions Core/Options/AzureBlobStorageOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Core.Options;

public class AzureBlobStorageOptions
{
public string ConnectionString { get; set; } = null!;
public string PlantImagesContainer { get; set; } = null!;
public string UserProfileImagesContainer { get; set; } = null!;
}
3 changes: 2 additions & 1 deletion Core/Options/AzureVisionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace Core.Options;

public class AzureVisionOptions
{
public string Endpoint { get; set; } = null!;
public string BaseUrl { get; set; } = null!;
public string Key { get; set; } = null!;
public string RemoveBackgroundEndpoint { get; set; } = null!;
}
2 changes: 1 addition & 1 deletion Core/Services/ConditionsLogsService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Infrastructure.Repositories;
using Shared.Dtos;
using Shared.Models.Exceptions;
using Shared.Exceptions;
using Shared.Models.Information;

namespace Core.Services;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Core.Services.External;

public interface IImageBackgroundRemoverService
{
public Task<byte[]> RemoveBackground(byte[] imageBytes);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Net;
using System.Net.Http.Headers;
using Core.Options;
using Microsoft.Extensions.Options;
using Shared.Exceptions;

namespace Core.Services.External.BackgroundRemoval;

public class ImageBackgroundRemoverService(IOptions<AzureVisionOptions> options) : IImageBackgroundRemoverService
{
public async Task<byte[]> RemoveBackground(byte[] imageBytes)
{
var request = options.Value.BaseUrl + options.Value.RemoveBackgroundEndpoint;

var client = new HttpClient();
client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", $"{options.Value.Key}");

HttpResponseMessage response;
using (var content = new ByteArrayContent(imageBytes))
{
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
response = await client.PostAsync(request, content);
}

if (response.StatusCode != HttpStatusCode.OK)
{
throw new AppException("Failed to remove background from image.");
}

// The response is image/png
var removedBgImageBytes = await response.Content.ReadAsByteArrayAsync();
return removedBgImageBytes;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Core.Services.External;

public class MockImageBackgroundRemoverService : IImageBackgroundRemoverService
{
public Task<byte[]> RemoveBackground(byte[] imageBytes)
{
return Task.FromResult(imageBytes);
}
}
38 changes: 38 additions & 0 deletions Core/Services/External/BlobStorage/BlobStorageServiceService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Azure.Storage.Blobs;
using Core.Options;
using Microsoft.Extensions.Options;
using Shared.Exceptions;

namespace Core.Services.External.BlobStorage;

public class BlobStorageServiceService(IOptions<AzureBlobStorageOptions> azureBlobStorageOptions) : IBlobStorageService
{
public async Task<string> SaveImageToBlobStorage(string base64Image, string userEmail, string? blobUrl = null)
{
var imageBytes = Convert.FromBase64String(base64Image);
blobUrl ??= userEmail + "_" + Guid.NewGuid();
var blobClient = new BlobClient(azureBlobStorageOptions.Value.ConnectionString, azureBlobStorageOptions.Value.PlantImagesContainer, blobUrl);
var binaryData = new BinaryData(imageBytes);
await blobClient.UploadAsync(binaryData, true);
return blobClient.Uri.ToString();
}

public async Task<bool> DeleteImageFromBlobStorage(string imageUrl)
{
var blobClient = new BlobClient(new Uri(imageUrl));
return await blobClient.DeleteIfExistsAsync();
}

public async Task<string> GetImageFromBlobStorage(string imageUrl)
{
if (string.IsNullOrEmpty(imageUrl)) return "";

var blobClient = new BlobClient(new Uri(imageUrl));
if (await blobClient.ExistsAsync() == false) throw new NotFoundException("Image not found");

using var memoryStream = new MemoryStream();
await blobClient.DownloadToAsync(memoryStream);
var imageBytes = memoryStream.ToArray();
return Convert.ToBase64String(imageBytes);
}
}
8 changes: 8 additions & 0 deletions Core/Services/External/BlobStorage/IBlobStorageService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Core.Services.External.BlobStorage;

public interface IBlobStorageService
{
public Task<string> SaveImageToBlobStorage(string base64Image, string userEmail, string? blobUrl = null);
public Task<bool> DeleteImageFromBlobStorage(string imageUrl);
public Task<string> GetImageFromBlobStorage(string imageUrl);
}
19 changes: 19 additions & 0 deletions Core/Services/External/BlobStorage/MockBlobStorageService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Core.Services.External.BlobStorage;

public class MockBlobStorageService : IBlobStorageService
{
public Task<string> SaveImageToBlobStorage(string base64Image, string userEmail, string? blobUrl = null)
{
return Task.FromResult("https://www.example.com");
}

public Task<bool> DeleteImageFromBlobStorage(string imageUrl)
{
return Task.FromResult(true);
}

public Task<string> GetImageFromBlobStorage(string imageUrl)
{
return Task.FromResult("base64Image");
}
}
1 change: 0 additions & 1 deletion Core/Services/MqttSubscriberService.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System.Text;
using System.Text.Json;
using Core.Options;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using MQTTnet;
using MQTTnet.Client;
Expand Down
43 changes: 28 additions & 15 deletions Core/Services/PlantService.cs
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
using Infrastructure.Repositories;
using Azure.Storage.Blobs;
using Core.Options;
using Core.Services.External.BlobStorage;
using Infrastructure.Repositories;
using Microsoft.Extensions.Options;
using Shared.Dtos.FromClient.Plant;
using Shared.Dtos.Plant;
using Shared.Exceptions;
using Shared.Models;
using Shared.Models.Exceptions;
using Shared.Models.Information;

namespace Core.Services;

public class PlantService (PlantRepository plantRepository, RequirementService requirementService)
public class PlantService(
PlantRepository plantRepository,
RequirementService requirementService,
IBlobStorageService blobStorageService)
{
private const string DefaultImageUrl =
"https://www.creativefabrica.com/wp-content/uploads/2022/01/20/Animated-Plant-Graphics-23785833-1.jpg";

public async Task<Plant> CreatePlant(CreatePlantDto createPlantDto)
{
if (string.IsNullOrEmpty(createPlantDto.Nickname))
{
createPlantDto.Nickname = GenerateRandomNickname();
}


string? ímageUrl = null;
if (createPlantDto.Base64Image is not null)
{
ímageUrl = await blobStorageService.SaveImageToBlobStorage(createPlantDto.Base64Image, createPlantDto.UserEmail);
}

// Insert plant first to get the plantId
var plant = new Plant
{
PlantId = Guid.NewGuid(),
UserEmail = createPlantDto.UserEmail,
// CollectionId = Guid.Empty, // TODO: fix when collections are implemented
Nickname = createPlantDto.Nickname,
ImageUrl = createPlantDto.ImageUrl ?? DefaultImageUrl,
ImageUrl = ímageUrl ?? "",
DeviceId = createPlantDto.DeviceId
};

Expand All @@ -35,11 +43,10 @@ public async Task<Plant> CreatePlant(CreatePlantDto createPlantDto)
// Create requirements for the plant to crete a link between the two
var requirementsDto = createPlantDto.CreateRequirementsDto;
requirementsDto.PlantId = plant.PlantId;
await requirementService.CreateRequirements(requirementsDto);

plant.Requirements = await requirementService.CreateRequirements(requirementsDto);
return plant;
}

public async Task<Plant> GetPlantById(Guid id, string requesterEmail)
{
var plant = await VerifyPlantExistsAndUserHasAccess(id, requesterEmail);
Expand All @@ -63,14 +70,20 @@ public async Task<Plant> UpdatePlant(UpdatePlantDto updatePlantDto, string reque
requirements = await requirementService.UpdateRequirements(updatePlantDto.UpdateRequirementDto, plant.PlantId);
}

var imageUrl = plant.ImageUrl;
if (updatePlantDto.Base64Image is not null)
{
imageUrl = await blobStorageService.SaveImageToBlobStorage(updatePlantDto.Base64Image, requesterEmail, plant.ImageUrl);
}

// Update the plant
plant = new Plant
{
PlantId = updatePlantDto.PlantId,
UserEmail = plant.UserEmail,
CollectionId = updatePlantDto.CollectionId,
Nickname = updatePlantDto.Nickname,
ImageUrl = updatePlantDto.ImageUrl ?? DefaultImageUrl,
Nickname = updatePlantDto.Nickname ?? plant.Nickname,
ImageUrl = imageUrl,
Requirements = requirements,
ConditionsLogs = plant.ConditionsLogs
};
Expand Down
4 changes: 2 additions & 2 deletions Core/Services/RequirementService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Infrastructure.Repositories;
using Shared.Dtos.FromClient.Requirements;
using Shared.Models.Exceptions;
using Shared.Exceptions;
using Shared.Models.Information;

namespace Core.Services;
Expand All @@ -22,7 +22,7 @@ public async Task<Requirements> CreateRequirements(CreateRequirementsDto createR

public async Task<Requirements?> UpdateRequirements(UpdateRequirementDto updateRequirementDto, Guid plantId)
{
var requirements = await requirementsRepository.GetRequirements(updateRequirementDto.ConditionsId);
var requirements = await requirementsRepository.GetRequirements(updateRequirementDto.RequirementsId);
if (requirements is null) throw new NotFoundException("Requirements not found");
if (requirements.PlantId != plantId) throw new NoAccessException("You don't have access to this plant");

Expand Down
2 changes: 1 addition & 1 deletion Core/Services/UserService.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Infrastructure.Repositories;
using Shared.Dtos.FromClient;
using Shared.Dtos.FromClient.Identity;
using Shared.Models.Exceptions;
using Shared.Exceptions;
using Shared.Models.Identity;

namespace Core.Services;
Expand Down
2 changes: 1 addition & 1 deletion Infrastructure/ApplicationDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
.HasForeignKey<Requirements>(e => e.PlantId);

modelBuilder.Entity<Requirements>()
.HasKey(e => e.ConditionsId);
.HasKey(e => e.RequirementsId);

modelBuilder.Entity<ConditionsLog>()
.HasKey(e => e.ConditionsId);
Expand Down
5 changes: 2 additions & 3 deletions Infrastructure/Repositories/PlantRepository.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Shared.Dtos.FromClient.Plant;
using Shared.Dtos.Plant;
using Shared.Exceptions;
using Shared.Models;
using Shared.Models.Exceptions;
using Shared.Models.Information;

namespace Infrastructure.Repositories;
Expand Down Expand Up @@ -66,7 +65,7 @@ public async Task<Guid> GetPlantIdByDeviceIdAsync(string deviceId)
return plant.PlantId;
}

public async Task<Conditions> GetRequirementsForPlant(Guid plantId)
public async Task<Requirements> GetRequirementsForPlant(Guid plantId)
{
await using var context = await dbContextFactory.CreateDbContextAsync();
var plant = await context.Plants
Expand Down
2 changes: 1 addition & 1 deletion Infrastructure/Repositories/RequirementsRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public class RequirementsRepository(IDbContextFactory<ApplicationDbContext> dbCo
public async Task<Requirements?> GetRequirements(Guid requirementsId)
{
await using var context = await dbContextFactory.CreateDbContextAsync();
return await context.Requirements.FirstOrDefaultAsync(r => r.ConditionsId == requirementsId);
return await context.Requirements.FirstOrDefaultAsync(r => r.RequirementsId == requirementsId);
}

public async Task<Requirements> CreateRequirements(Requirements requirements)
Expand Down
2 changes: 1 addition & 1 deletion Shared/Dtos/FromClient/Identity/RegisterUserDto.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;

namespace Shared.Dtos.FromClient;
namespace Shared.Dtos.FromClient.Identity;

public class RegisterUserDto
{
Expand Down
2 changes: 1 addition & 1 deletion Shared/Dtos/FromClient/Plant/CreatePlantDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ public class CreatePlantDto
public Guid? CollectionId { get; set; }
public string? DeviceId { get; set; }
[MaxLength(50)] public string? Nickname { get; set; }
public string? ImageUrl { get; set; }
public string? Base64Image { get; set; }
public CreateRequirementsDto CreateRequirementsDto { get; set; } = null!;
}
5 changes: 2 additions & 3 deletions Shared/Dtos/FromClient/Plant/UpdatePlantDto.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
using System.ComponentModel.DataAnnotations;
using Shared.Dtos.FromClient.Requirements;
using Shared.Models.Information;

namespace Shared.Dtos.Plant;
namespace Shared.Dtos.FromClient.Plant;

public class UpdatePlantDto
{
[Required] public Guid PlantId { get; set; }
public Guid? CollectionId { get; set; }
public string? DeviceId { get; set; }
[MaxLength(50)] public string? Nickname { get; set; }
public string? ImageUrl { get; set; }
public string? Base64Image { get; set; } // should be null if the image should not be updated
public UpdateRequirementDto? UpdateRequirementDto { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace Shared.Dtos.FromClient.Requirements;

public class UpdateRequirementDto
{
public Guid ConditionsId { get; set; }
public Guid RequirementsId { get; set; }
public RequirementLevel SoilMoistureLevel { get; set; }
public RequirementLevel LightLevel { get; set; }
public RequirementLevel TemperatureLevel { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
namespace Shared.Models.Exceptions;
namespace Shared.Exceptions;

public abstract class AppException : Exception
public class AppException : Exception
{
protected AppException(string message) : base(message)
public AppException(string message) : base(message)
{
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Shared.Models.Exceptions;
namespace Shared.Exceptions;

public class InvalidCredentialsException : AppException
{
Expand Down
Loading

0 comments on commit 40ea238

Please sign in to comment.