Skip to content

Commit

Permalink
Add support for processing stationary images
Browse files Browse the repository at this point in the history
Introduced the `StationaryImage` entity and corresponding repository to handle stationary images. Updated the image processing service and controller to support processing images from stationary cameras. Also added database schema changes and necessary DTO adjustments to accommodate the new image type.
  • Loading branch information
saschadoemer committed Aug 16, 2024
1 parent 777e034 commit 246564c
Show file tree
Hide file tree
Showing 12 changed files with 248 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package de.app.fivegla.controller.dto.request;

import de.app.fivegla.controller.dto.request.inner.DroneImage;
import de.app.fivegla.controller.dto.request.inner.CameraImage;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
Expand All @@ -21,11 +21,11 @@ public class ImageProcessingRequest {
private String transactionId;

@NotBlank
@Schema(description = "The id of the drone.")
private String droneId;
@Schema(description = "The id of the camera.")
private String cameraId;

@Schema(description = "The images to process.")
private List<DroneImage> images;
private List<CameraImage> images;

@Schema(description = "A custom group ID, which can be used to group devices / measurements. This is optional, if not set, the default group will be used.")
protected String groupId;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package de.app.fivegla.controller.dto.request;

import de.app.fivegla.controller.dto.request.inner.CameraImage;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;

import java.util.List;

/**
* Request for image processing.
*/
@Getter
@Setter
@Schema(description = "Request for image processing.")
public class StationaryImageProcessingRequest {

@NotBlank
@Schema(description = "The id of the camera.")
private String cameraId;

@Schema(description = "The images to process.")
private List<CameraImage> images;

@Schema(description = "A custom group ID, which can be used to group devices / measurements. This is optional, if not set, the default group will be used.")
protected String groupId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
@Getter
@Setter
@Schema(description = "A single image to process.")
public class DroneImage {
public class CameraImage {

@Schema(description = "The channel of the image.")
private ImageChannel imageChannel;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import de.app.fivegla.controller.api.BaseMappings;
import de.app.fivegla.controller.dto.request.GetAllImagesForTransactionRequest;
import de.app.fivegla.controller.dto.request.ImageProcessingRequest;
import de.app.fivegla.controller.dto.request.StationaryImageProcessingRequest;
import de.app.fivegla.controller.dto.response.*;
import de.app.fivegla.integration.imageprocessing.ImageProcessingIntegrationService;
import de.app.fivegla.integration.imageprocessing.OrthophotoIntegrationService;
Expand Down Expand Up @@ -81,7 +82,47 @@ public ResponseEntity<? extends Response> processImage(@Valid @RequestBody @Para
var group = groupService.getOrDefault(tenant, request.getGroupId());
var oids = new ArrayList<String>();
request.getImages().forEach(droneImage -> {
var oid = imageProcessingIntegrationService.processImage(tenant, group, request.getTransactionId(), request.getDroneId(), droneImage.getImageChannel(), droneImage.getBase64Image());
var oid = imageProcessingIntegrationService.processImage(tenant, group, request.getTransactionId(), request.getCameraId(), droneImage.getImageChannel(), droneImage.getBase64Image());
oids.add(oid);
});
return ResponseEntity.status(HttpStatus.CREATED).body(ImageProcessingResponse.builder()
.oids(oids)
.build());
}

/**
* Processes one or multiple images from the mica sense camera.
*
* @return HTTP status 200 if image was processed successfully.
*/
@Operation(
operationId = "images.process-stationary-image",
description = "Processes one or multiple stationary images from the mica sense camera.",
tags = BaseMappings.IMAGE_PROCESSING
)
@ApiResponse(
responseCode = "201",
description = "Images were processed successfully.",
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = ImageProcessingResponse.class)
)
)
@ApiResponse(
responseCode = "400",
description = "The request is invalid.",
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = Response.class)
)
)
@PostMapping(value = "/stationary", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<? extends Response> processImage(@Valid @RequestBody @Parameter(description = "The image processing request.", required = true) StationaryImageProcessingRequest request, Principal principal) {
var tenant = validateTenant(tenantService, principal);
var group = groupService.getOrDefault(tenant, request.getGroupId());
var oids = new ArrayList<String>();
request.getImages().forEach(droneImage -> {
var oid = imageProcessingIntegrationService.processStationaryImage(tenant, group, request.getCameraId(), droneImage.getImageChannel(), droneImage.getBase64Image());
oids.add(oid);
});
return ResponseEntity.status(HttpStatus.CREATED).body(ImageProcessingResponse.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@
* Represents a MicaSense image.
*/
@Slf4j
public record MicaSenseImage(
public record CameraImage(
String id,
String type,
Attribute group,
Attribute oid,
Attribute droneId,
Attribute cameraId,
Attribute transactionId,
Attribute imageChannel,
Attribute base64encodedImage,
Expand All @@ -33,7 +33,7 @@ public String asJson() {
" \"type\":\"" + type.trim() + "\"," +
" \"customGroup\":" + group.asJson().trim() + "," +
" \"oid\":" + oid.asJson().trim() + "," +
" \"droneId\":" + droneId.asJson().trim() + "," +
" \"cameraId\":" + cameraId.asJson().trim() + "," +
" \"transactionId\":" + transactionId.asJson().trim() + "," +
" \"imageChannel\":" + imageChannel.asJson().trim() + "," +
" \"base64encodedImage\":" + base64encodedImage.asJson().trim() + "," +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@

import de.app.fivegla.api.enums.EntityType;
import de.app.fivegla.integration.fiware.FiwareEntityIntegrationService;
import de.app.fivegla.integration.fiware.model.MicaSenseImage;
import de.app.fivegla.integration.fiware.model.CameraImage;
import de.app.fivegla.integration.fiware.model.internal.TextAttribute;
import de.app.fivegla.persistence.entity.Group;
import de.app.fivegla.persistence.entity.Image;
import de.app.fivegla.persistence.entity.StationaryImage;
import de.app.fivegla.persistence.entity.Tenant;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -26,18 +27,18 @@ public class ImageProcessingFiwareIntegrationServiceWrapper {
private String imagePathBaseUrl;

/**
* Create a new drone device measurement in FIWARE.
* Create a new image device measurement in FIWARE.
*
* @param image the image to create the measurement for
* @param transactionId the transaction id
*/
public void createDroneDeviceMeasurement(Tenant tenant, Group group, String droneId, Image image, String transactionId) {
var deviceMeasurement = new MicaSenseImage(
tenant.getFiwarePrefix() + droneId,
public void createCameraImage(Tenant tenant, Group group, String cameraId, Image image, String transactionId) {
var deviceMeasurement = new CameraImage(
tenant.getFiwarePrefix() + cameraId,
EntityType.MICASENSE_IMAGE.getKey(),
new TextAttribute(group.getOid()),
new TextAttribute(image.getOid()),
new TextAttribute(droneId),
new TextAttribute(cameraId),
new TextAttribute(image.getTransactionId()),
new TextAttribute(image.getChannel().name()),
new TextAttribute(image.getBase64encodedImage()),
Expand All @@ -48,4 +49,12 @@ public void createDroneDeviceMeasurement(Tenant tenant, Group group, String dron
fiwareEntityIntegrationService.persist(tenant, group, deviceMeasurement);
}

/**
* Create a new stationary image device measurement in FIWARE.
*
* @param micaSenseImage the image to create the measurement for
*/
public void createStationaryCameraImage(Tenant tenant, Group group, String cameraId, StationaryImage micaSenseImage) {
// FIXME: Save stationary image in FIWARE
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import de.app.fivegla.api.dto.SortableImageOids;
import de.app.fivegla.integration.imageprocessing.dto.TransactionIdWithTheFirstImageTimestamp;
import de.app.fivegla.persistence.ImageRepository;
import de.app.fivegla.persistence.StationaryImageRepository;
import de.app.fivegla.persistence.entity.Group;
import de.app.fivegla.persistence.entity.Image;
import de.app.fivegla.persistence.entity.StationaryImage;
import de.app.fivegla.persistence.entity.Tenant;
import de.app.fivegla.persistence.entity.enums.ImageChannel;
import lombok.RequiredArgsConstructor;
Expand All @@ -26,25 +28,26 @@ public class ImageProcessingIntegrationService {
private final ImageProcessingFiwareIntegrationServiceWrapper fiwareIntegrationServiceWrapper;
private final PersistentStorageIntegrationService persistentStorageIntegrationService;
private final ImageRepository imageRepository;
private final StationaryImageRepository stationaryImageRepository;

/**
* Processes an image from the mica sense camera.
*
* @param transactionId The transaction id.
* @param group The group.
* @param droneId The id of the drone.
* @param cameraId The id of the drone.
* @param imageChannel The channel the image was taken with.
* @param base64Image The base64 encoded tiff image.
*/
public String processImage(Tenant tenant, Group group, String transactionId, String droneId, ImageChannel imageChannel, String base64Image) {
public String processImage(Tenant tenant, Group group, String transactionId, String cameraId, ImageChannel imageChannel, String base64Image) {
var decodedImage = Base64.getDecoder().decode(base64Image);
log.debug("Channel for the decodedImage: {}.", imageChannel);
var point = exifDataIntegrationService.readLocation(decodedImage);
var image = new Image();
image.setOid(UUID.randomUUID().toString());
image.setGroup(group);
image.setTenant(tenant);
image.setDroneId(droneId);
image.setCameraId(cameraId);
image.setTransactionId(transactionId);
image.setChannel(imageChannel);
image.setLongitude(point.getX());
Expand All @@ -53,7 +56,7 @@ public String processImage(Tenant tenant, Group group, String transactionId, Str
image.setBase64encodedImage(base64Image);
var micaSenseImage = imageRepository.save(image);
log.debug("Image with oid {} added to the application data.", micaSenseImage.getOid());
fiwareIntegrationServiceWrapper.createDroneDeviceMeasurement(tenant, group, droneId, micaSenseImage, transactionId);
fiwareIntegrationServiceWrapper.createCameraImage(tenant, group, cameraId, micaSenseImage, transactionId);
persistentStorageIntegrationService.storeImage(transactionId, micaSenseImage);
return micaSenseImage.getOid();
}
Expand Down Expand Up @@ -113,4 +116,24 @@ public List<TransactionIdWithTheFirstImageTimestamp> listAllTransactionsForTenan
return allTransactionsForTenant;
}

public String processStationaryImage(Tenant tenant, Group group, String cameraId, ImageChannel imageChannel, String base64Image) {
var decodedImage = Base64.getDecoder().decode(base64Image);
log.debug("Channel for the decodedImage: {}.", imageChannel);
var point = exifDataIntegrationService.readLocation(decodedImage);
var image = new StationaryImage();
image.setOid(UUID.randomUUID().toString());
image.setGroup(group);
image.setTenant(tenant);
image.setCameraId(cameraId);
image.setChannel(imageChannel);
image.setLongitude(point.getX());
image.setLatitude(point.getY());
image.setMeasuredAt((Date.from(exifDataIntegrationService.readMeasuredAt(decodedImage))));
image.setBase64encodedImage(base64Image);
var micaSenseImage = stationaryImageRepository.save(image);
log.debug("Image with oid {} added to the application data.", micaSenseImage.getOid());
fiwareIntegrationServiceWrapper.createStationaryCameraImage(tenant, group, cameraId, micaSenseImage);
// FIXME Store image in persistent storage
return micaSenseImage.getOid();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package de.app.fivegla.persistence;

import de.app.fivegla.persistence.entity.StationaryImage;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
* Repository for the stationary image entity.
*/
@Repository
public interface StationaryImageRepository extends JpaRepository<StationaryImage, Long> {
}
8 changes: 4 additions & 4 deletions src/main/java/de/app/fivegla/persistence/entity/Image.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ public class Image extends BaseEntity {
private String oid;

/**
* The id of the drone.
* The id of the camera.
*/
@Column(name = "drone_id", nullable = false)
private String droneId;
@Column(name = "camera_id", nullable = false)
private String cameraId;

/**
* The transaction id.
Expand Down Expand Up @@ -97,6 +97,6 @@ public byte[] getImageAsRawData() {
* @return the name of the image
*/
public String getFullFilename(Tenant tenant, String transactionId) {
return tenant.getTenantId() + "/" + transactionId + "/" + droneId + "_" + channel.name() + "_" + measuredAt.toInstant().getEpochSecond() + ".tiff";
return tenant.getTenantId() + "/" + transactionId + "/" + cameraId + "_" + channel.name() + "_" + measuredAt.toInstant().getEpochSecond() + ".tiff";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package de.app.fivegla.persistence.entity;

import de.app.fivegla.persistence.entity.enums.ImageChannel;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.util.Date;

/**
* Image.
*/
@Entity
@Getter
@Setter
@Table(name = "image")
public class StationaryImage extends BaseEntity {

/**
* The oid of the image.
*/
@Column(name = "oid", nullable = false, unique = true)
private String oid;

/**
* The id of the camera.
*/
@Column(name = "camera_id", nullable = false)
private String cameraId;

/**
* The channel of the image since the value cannot be read from the EXIF.
*/
@Column(name = "image_channel", nullable = false)
@Enumerated(EnumType.STRING)
private ImageChannel channel;

/**
* The base64 encoded tiff image.
*/
@Lob
@Column(name = "base64_encoded_image", nullable = false)
private String base64encodedImage;

/**
* The time the image was taken.
*/
@Column(name = "measured_at", nullable = false)
@Temporal(TemporalType.TIMESTAMP)
private Date measuredAt;

/**
* The location of the image.
*/
@Column(name = "longitude", nullable = false)
private double longitude;

/**
* The location of the image.
*/
@Column(name = "latitude", nullable = false)
private double latitude;

/**
* The group of the image.
*/
@ManyToOne
@JoinColumn(name = "group_id", nullable = false)
private Group group;

/**
* The tenant of the image.
*/
@ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER, optional = false)
@JoinColumn(name = "tenant_id", nullable = false)
private Tenant tenant;

/**
* Returns the name of the image.
*
* @return the name of the image
*/
public String getFullFilename(Tenant tenant) {
return tenant.getTenantId() + "/" + cameraId + "/" + channel.name() + "/" + measuredAt.toInstant().getEpochSecond() + ".tiff";
}
}
Loading

0 comments on commit 246564c

Please sign in to comment.