diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/alarm/AlarmService.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/alarm/AlarmService.java index 96f65cf1..08ddf545 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/alarm/AlarmService.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/alarm/AlarmService.java @@ -5,8 +5,8 @@ import com.wakeUpTogetUp.togetUp.api.alarm.dto.response.AlarmSimpleRes; import com.wakeUpTogetUp.togetUp.api.alarm.dto.response.AlarmTimeLineRes; import com.wakeUpTogetUp.togetUp.api.alarm.model.Alarm; -import com.wakeUpTogetUp.togetUp.api.mission.MissionObjectRepository; -import com.wakeUpTogetUp.togetUp.api.mission.MissionRepository; +import com.wakeUpTogetUp.togetUp.api.mission.repository.MissionObjectRepository; +import com.wakeUpTogetUp.togetUp.api.mission.repository.MissionRepository; import com.wakeUpTogetUp.togetUp.api.mission.model.Mission; import com.wakeUpTogetUp.togetUp.api.mission.model.MissionObject; import com.wakeUpTogetUp.togetUp.api.users.UserRepository; @@ -47,7 +47,7 @@ public Alarm createAlarmDeprecated(Integer userId, PostAlarmReq postAlarmReq) { if (postAlarmReq.getMissionObjectId() != null) { missionObject = missionObjectRepository.findById(postAlarmReq.getMissionObjectId()) - .orElseThrow(() -> new BaseException(Status.OBJECT_NOT_FOUND)); + .orElseThrow(() -> new BaseException(Status.MISSION_OBJECT_NOT_FOUND)); if (missionObject.getMission().getId() != mission.getId()) { throw new BaseException(Status.MISSION_ID_NOT_MATCH); diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/MissionController.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/MissionController.java index 5abe4231..0ba3542f 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/MissionController.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/MissionController.java @@ -1,19 +1,16 @@ package com.wakeUpTogetUp.togetUp.api.mission; -import com.google.cloud.vision.v1.FaceAnnotation; import com.wakeUpTogetUp.togetUp.api.auth.AuthUser; -import com.wakeUpTogetUp.togetUp.api.mission.model.CustomAnalysisEntity; -import com.wakeUpTogetUp.togetUp.infra.aws.s3.FileService; import com.wakeUpTogetUp.togetUp.api.mission.dto.request.MissionCompleteReq; import com.wakeUpTogetUp.togetUp.api.mission.dto.response.GetMissionLogRes; import com.wakeUpTogetUp.togetUp.api.mission.dto.response.GetMissionWithObjectListRes; import com.wakeUpTogetUp.togetUp.api.mission.dto.response.MissionCompleteRes; import com.wakeUpTogetUp.togetUp.api.mission.dto.response.MissionPerfomRes; -import com.wakeUpTogetUp.togetUp.api.mission.service.MissionImageService; +import com.wakeUpTogetUp.togetUp.api.mission.service.MissionProvider; +import com.wakeUpTogetUp.togetUp.api.mission.service.MissionService; import com.wakeUpTogetUp.togetUp.common.Status; import com.wakeUpTogetUp.togetUp.common.annotation.validator.ImageFile; import com.wakeUpTogetUp.togetUp.common.dto.BaseResponse; -import com.wakeUpTogetUp.togetUp.infra.aws.s3.model.CustomFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -32,6 +29,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @@ -44,10 +42,9 @@ @RequestMapping("/app/mission") public class MissionController { + private final MissionFacade missionFacade; private final MissionProvider missionProvider; private final MissionService missionService; - private final MissionImageService missionImageService; - private final FileService fileService; @Operation(summary = "미션 목록 가져오기") @GetMapping("/{missionId}") @@ -63,10 +60,11 @@ BaseResponse getObjectDetectionMissions( return new BaseResponse<>(Status.SUCCESS, missionProvider.getMission(missionId)); } - // TODO: 객체 탐지, 표정 인식 통합 - // TODO: 객체 이름이 아닌 알람 ID를 요청값으로 받게 리팩토링하기 + 그냥 리팩토링 - @Operation(summary = "객체 탐지 미션") - @PostMapping(value = "/object-detection/{object}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "미션 수행 결과 불러오기", + description = "미션 이름을 기반으로 미션을 수행합니다.\n" + + "- 사용 가능 미션 이름: `direct-registration`/`object-detection`/`expression-recognition`\n" + + "- 직접 촬영 미션(direct-registration)의 경우 object 필드가 필수적이지 않습니다.") + @PostMapping(value = "/{missionName}/result", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.CREATED) @ApiResponses(value = { @ApiResponse( @@ -79,37 +77,11 @@ BaseResponse getObjectDetectionMissions( public BaseResponse recognizeObject( @Parameter(hidden = true) @AuthUser Integer userId, @Parameter(required = true, description = "미션 수행 사진") @RequestPart @ImageFile MultipartFile missionImage, - @Parameter(required = true, description = "탐지할 객체") @PathVariable String object - ) throws Exception { - List detectedObjects = missionService.getDetectionResult(object, missionImage); - CustomFile file = missionImageService.processODResultImage(missionImage, detectedObjects); - String imageUrl = fileService.uploadFile(file, "mission"); - - return new BaseResponse<>(Status.MISSION_SUCCESS, new MissionPerfomRes(imageUrl)); - } - - @Operation(summary = "표정 인식 미션") - @PostMapping(value = "/face-recognition/{object}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - @ResponseStatus(HttpStatus.CREATED) - @ApiResponses(value = { - @ApiResponse( - responseCode = "201", - description = "미션을 성공하였습니다.", - content = @Content(schema = @Schema(implementation = MissionPerfomRes.class))), - @ApiResponse(responseCode = "200", description = "탐지된 객체가 없습니다."), - @ApiResponse(responseCode = "200", description = "미션을 성공하지 못했습니다."), - @ApiResponse(responseCode = "500", description = "예상치 못한 서버 에러입니다. 제보 부탁드립니다.") - }) - public BaseResponse recognizeFaceExpression( - @Parameter(hidden = true) @AuthUser Integer userId, - @Parameter(required = true, description = "미션 수행 사진") @RequestPart @ImageFile MultipartFile missionImage, - @Parameter(required = true, description = "탐지할 표정(`joy`/`sorrow`/`anger`/`surprise`)") @PathVariable String object - ) throws Exception { - List faceAnnotations = missionService.getFaceRecognitionResult(object, missionImage); - CustomFile file = missionImageService.processFRResultImage(missionImage, faceAnnotations, object); - String imageUrl = fileService.uploadFile(file, "mission"); - - return new BaseResponse<>(Status.MISSION_SUCCESS, new MissionPerfomRes(imageUrl)); + @Parameter(description = "미션 이름") @PathVariable String missionName, + @Parameter(description = "탐지할 미션 객체") @RequestParam(required = false) String object + ) { + MissionPerfomRes response = missionFacade.performMission(missionImage, missionName, object); + return new BaseResponse<>(Status.MISSION_SUCCESS, response); } @Operation(summary = "미션 완료 - 수행 기록 생성 및 경험치, 레벨 정산 및 경우에 따라 아바타 해금") diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/MissionFacade.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/MissionFacade.java new file mode 100644 index 00000000..12e973fa --- /dev/null +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/MissionFacade.java @@ -0,0 +1,30 @@ +package com.wakeUpTogetUp.togetUp.api.mission; + +import com.wakeUpTogetUp.togetUp.api.mission.dto.response.MissionPerfomRes; +import com.wakeUpTogetUp.togetUp.api.mission.strategy.MissionImageStrategyFactory; +import com.wakeUpTogetUp.togetUp.api.mission.model.MissionType; +import com.wakeUpTogetUp.togetUp.infra.aws.s3.FileService; +import com.wakeUpTogetUp.togetUp.infra.aws.s3.model.CustomFile; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +@Component +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class MissionFacade { + + private final MissionImageStrategyFactory missionImageStrategyFactory; + private final FileService fileService; + + public MissionPerfomRes performMission(MultipartFile missionImage, String missionName, String object) { + MissionType type = MissionType.getByName(missionName); + + CustomFile file = missionImageStrategyFactory + .getStrategy(type) + .execute(missionImage, type, object); + String imageUrl = fileService.uploadFile(file, "mission"); + + return new MissionPerfomRes(imageUrl); + } +} diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/CustomAnalysisEntity.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/CustomAnalysisEntity.java new file mode 100644 index 00000000..7645cd4a --- /dev/null +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/CustomAnalysisEntity.java @@ -0,0 +1,19 @@ +package com.wakeUpTogetUp.togetUp.api.mission.domain; + +import com.wakeUpTogetUp.togetUp.api.mission.model.BoundingBox; +import lombok.Getter; + +@Getter +public abstract class CustomAnalysisEntity { + + private final double confidence; + + private final BoundingBox box; + + protected CustomAnalysisEntity(double confidence, BoundingBox box) { + this.confidence = confidence; + this.box = box; + } + + protected abstract boolean isMatchEntity(Object target); +} diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/CustomDetectedFaceAnnotation.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/CustomDetectedFaceAnnotation.java new file mode 100644 index 00000000..c1a9e21b --- /dev/null +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/CustomDetectedFaceAnnotation.java @@ -0,0 +1,47 @@ +package com.wakeUpTogetUp.togetUp.api.mission.domain; + +import com.wakeUpTogetUp.togetUp.api.mission.model.BoundingBox; +import com.wakeUpTogetUp.togetUp.api.mission.model.Expression; +import com.wakeUpTogetUp.togetUp.common.Status; +import com.wakeUpTogetUp.togetUp.exception.BaseException; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class CustomDetectedFaceAnnotation extends CustomAnalysisEntity { + + private final int joyLikelihood; + private final int sorrowLikelihood; + private final int angerLikelihood; + private final int surpriseLikelihood; + + @Builder + private CustomDetectedFaceAnnotation(double confidence, BoundingBox box, int joyLikelihood, + int sorrowLikelihood, int angerLikelihood, int surpriseLikelihood) { + super(confidence, box); + this.joyLikelihood = joyLikelihood; + this.sorrowLikelihood = sorrowLikelihood; + this.angerLikelihood = angerLikelihood; + this.surpriseLikelihood = surpriseLikelihood; + } + + @Override + protected boolean isMatchEntity(Object target) { + return getLikelihood((Expression) target) >= 3; + } + + private int getLikelihood(Expression expression) { + switch (expression) { + case JOY: + return joyLikelihood; + case SORROW: + return sorrowLikelihood; + case ANGER: + return angerLikelihood; + case SURPRISE: + return surpriseLikelihood; + default: + throw new BaseException(Status.INVALID_OBJECT_NAME); + } + } +} diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/CustomDetectedObject.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/CustomDetectedObject.java new file mode 100644 index 00000000..e466bd1e --- /dev/null +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/CustomDetectedObject.java @@ -0,0 +1,29 @@ +package com.wakeUpTogetUp.togetUp.api.mission.domain; + +import com.wakeUpTogetUp.togetUp.api.mission.model.BoundingBox; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class CustomDetectedObject extends CustomAnalysisEntity { + + private final String name; + private final String parent; + + @Builder + private CustomDetectedObject(String name, String parent, double confidence, BoundingBox box) { + super(confidence, box); + this.name = name; + this.parent = parent; + } + + @Override + public boolean isMatchEntity(Object target) { + return concatObjectAndParent() + .contains(target.toString().toLowerCase()); + } + + public String concatObjectAndParent() { + return (name + parent).toLowerCase(); + } +} diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/CustomDetectedTag.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/CustomDetectedTag.java new file mode 100644 index 00000000..3aca2b05 --- /dev/null +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/CustomDetectedTag.java @@ -0,0 +1,22 @@ +package com.wakeUpTogetUp.togetUp.api.mission.domain; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class CustomDetectedTag extends CustomAnalysisEntity { + + private final String name; + + @Builder + public CustomDetectedTag(String name, double confidence) { + super(confidence, null); + this.name = name; + } + + @Override + protected boolean isMatchEntity(Object target) { + return name.toLowerCase() + .contains(target.toString().toLowerCase()); + } +} diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/FaceAnnotationRecognitionResult.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/FaceAnnotationRecognitionResult.java new file mode 100644 index 00000000..643d0e56 --- /dev/null +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/FaceAnnotationRecognitionResult.java @@ -0,0 +1,56 @@ +package com.wakeUpTogetUp.togetUp.api.mission.domain; + +import com.wakeUpTogetUp.togetUp.api.mission.model.Expression; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; +import lombok.Builder; + +public class FaceAnnotationRecognitionResult extends VisionAnalysisResult { + + private final Expression expression; + private final List faceAnnotations; + + @Builder + public FaceAnnotationRecognitionResult(String target, List faceAnnotations) { + super(target); + this.expression = Expression.fromName(targetName); + this.faceAnnotations = faceAnnotations; + } + + @Override + public boolean isFail() { + return faceAnnotations.stream() + .noneMatch(faceAnnotation -> faceAnnotation.isMatchEntity(this.expression)); + } + + @Override + public List getEntities() { + return faceAnnotations.stream() + .map(CustomAnalysisEntity.class::cast) + .collect(Collectors.toList()); + } + + @Override + public List getMatches(int size) { + return faceAnnotations.stream() + .filter(faceAnnotation -> faceAnnotation.isMatchEntity(this.expression)) + .sorted(Comparator.comparing(CustomDetectedFaceAnnotation::getConfidence).reversed()) + .limit(size) + .collect(Collectors.toList()); + } + + @Override + public void print() { + System.out.println("\n[FACE ANNOTATION]"); + System.out.println("emotions.size() = " + faceAnnotations.size()); + + faceAnnotations.forEach(faceAnnotation -> { + System.out.println("JOY = " + faceAnnotation.getJoyLikelihood()); + System.out.println("SORROW = " + faceAnnotation.getSorrowLikelihood()); + System.out.println("ANGER = " + faceAnnotation.getAngerLikelihood()); + System.out.println("SURPRISE = " + faceAnnotation.getSurpriseLikelihood()); + System.out.println(); + }); + } +} diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/Letterbox.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/Letterbox.java deleted file mode 100644 index b5071a2e..00000000 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/Letterbox.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.wakeUpTogetUp.togetUp.api.mission.domain; - -import org.opencv.core.Core; -import org.opencv.core.Mat; -import org.opencv.core.Size; -import org.opencv.imgproc.Imgproc; -import org.springframework.stereotype.Component; - -@Component -public class Letterbox { - - private Size newShape = new Size(1280, 1280); - private double[] color = new double[]{114, 114, 114}; - private Boolean auto = false; - private Boolean scaleUp = false; - private Integer stride = 32; - - private double ratio; - private double dw; - private double dh; - - public double getRatio() { - return ratio; - } - - public double getDw() { - return dw; - } - - public Integer getWidth() { - return (int) this.newShape.width; - } - - public Integer getHeight() { - return (int) this.newShape.height; - } - - public double getDh() { - return dh; - } - - public void setNewShape(Size newShape) { - this.newShape = newShape; - } - - public void setStride(Integer stride) { - this.stride = stride; - } - - public Mat letterbox(Mat image) { // 단계 제약 조건을 충족하도록 이미지 크기 조정 및 채우기, 매개 변수 기록 - int[] shape = {image.rows(), image.cols()}; // 현재 모양 [height, width] - // Scale ratio (new / old) - double r = Math.min(this.newShape.height / shape[0], this.newShape.width / shape[1]); - - if (!this.scaleUp) { // 축소만, 확대는 하지 않음(일단 mAP를 위해서) - r = Math.min(r, 1.0); - } - - // Compute padding - Size newUnpad = new Size(Math.round(shape[1] * r), Math.round(shape[0] * r)); - double dw = this.newShape.width - newUnpad.width, dh = this.newShape.height - newUnpad.height; - if (this.auto) { // 최소 직사각형 - dw = dw % this.stride; - dh = dh % this.stride; - } - dw /= 2; // 채울 때 양쪽을 절반씩 채워 그림을 중심에 놓으십시오 - dh /= 2; - - // resize - if (shape[1] != newUnpad.width || shape[0] != newUnpad.height) { - Imgproc.resize(image, image, newUnpad, 0, 0, Imgproc.INTER_LINEAR); - } - - int top = (int) Math.round(dh - 0.1), bottom = (int) Math.round(dh + 0.1); - int left = (int) Math.round(dw - 0.1), right = (int) Math.round(dw + 0.1); - - // 그림을 정사각형으로 채우기 - Core.copyMakeBorder(image, image, top, bottom, left, right, Core.BORDER_CONSTANT, new org.opencv.core.Scalar(this.color)); - - this.ratio = r; - this.dh = dh; - this.dw = dw; - return image; - } -} \ No newline at end of file diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/MissionPerformResult.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/MissionPerformResult.java deleted file mode 100644 index a8f9bbec..00000000 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/MissionPerformResult.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.wakeUpTogetUp.togetUp.api.mission.domain; - -import com.wakeUpTogetUp.togetUp.api.mission.model.CustomAnalysisEntity; -import java.util.List; -import lombok.Builder; -import lombok.Getter; -public class MissionPerformResult { - private final int MAX_MATCHES_LIMIT = 3; - private final String targetObject; - - @Getter - private final VisionAnalysisResult analysisResult; - - @Builder - private MissionPerformResult(String object, VisionAnalysisResult analysisResult) { - this.targetObject = object; - this.analysisResult = analysisResult; - } - - public boolean isFail() { - return analysisResult.isFail(targetObject); - } - - public List getMatches() { - return analysisResult.getMatches(targetObject, MAX_MATCHES_LIMIT); - } -} diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/ODResult.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/ODResult.java deleted file mode 100644 index a77ad331..00000000 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/ODResult.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.wakeUpTogetUp.togetUp.api.mission.domain; - -import java.text.DecimalFormat; - -public class ODResult { - private final Integer batchId; - private final Float x0; - private final Float y0; - private final Float x1; - private final Float y1; - private final Integer clsId; - private final Float score; - - public ODResult(float[] x) { - this.batchId = (int) x[0]; - this.x0 = x[1]; - this.y0 = x[2]; - this.x1 = x[3]; - this.y1 = x[4]; - this.clsId = (int) x[5]; - this.score = x[6]; - } - - public Integer getBatchId() { - return batchId; - } - - public Float getX0() { - return x0; - } - - public Float getY0() { - return y0; - } - - public Float getX1() { - return x1; - } - - public Float getY1() { - return y1; - } - - public Integer getClsId() { - return clsId; - } - - public String getScore() { - DecimalFormat df = new DecimalFormat("0.00%"); - return df.format(this.score); - } - - @Override - public String toString() { - return "Object: " + - " \t batchId=" + batchId + - " \t x0=" + x0 + - " \t y0=" + y0 + - " \t x1=" + x1 + - " \t y1=" + y1 + - " \t clsId=" + clsId + - " \t score=" + getScore() + - " \t ;"; - } -} \ No newline at end of file diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/ObjectDetectionModel.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/ObjectDetectionModel.java deleted file mode 100644 index 16245a9e..00000000 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/ObjectDetectionModel.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.wakeUpTogetUp.togetUp.api.mission.domain; - -import ai.onnxruntime.OrtEnvironment; -import ai.onnxruntime.OrtException; -import ai.onnxruntime.OrtSession; - -import javax.annotation.PostConstruct; -import lombok.Getter; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -@Component -@Getter -public class ObjectDetectionModel { - private OrtSession session; - private OrtEnvironment environment; - private OrtSession.SessionOptions sessionOptions; - - @Value("${my.path.model-path}") - private String modelPath; - -} \ No newline at end of file diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/ObjectDetectionResult.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/ObjectDetectionResult.java index 45bfe498..081e9d71 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/ObjectDetectionResult.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/ObjectDetectionResult.java @@ -1,51 +1,53 @@ package com.wakeUpTogetUp.togetUp.api.mission.domain; -import com.wakeUpTogetUp.togetUp.api.mission.model.CustomAnalysisEntity; -import com.wakeUpTogetUp.togetUp.api.mission.model.CustomDetectedObject; -import com.wakeUpTogetUp.togetUp.api.mission.model.CustomDetectedTag; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; import lombok.Builder; -import lombok.Getter; -@Getter public class ObjectDetectionResult extends VisionAnalysisResult { private final List objects; - private final List tags; @Builder - private ObjectDetectionResult(List objects, List tags) { + private ObjectDetectionResult(String target, List objects, List tags) { + super(target); this.objects = objects; this.tags = tags; } @Override - public List getEntities() { + public boolean isFail() { + return !hasAnyMatchTag() && !hasAnyMatchObject(); + } + + private boolean hasAnyMatchTag() { + return tags.stream() + .anyMatch(tag -> tag.isMatchEntity(targetName)); + } + + private boolean hasAnyMatchObject() { return objects.stream() - .map(CustomAnalysisEntity.class::cast) - .collect(Collectors.toList()); + .anyMatch(object -> object.isMatchEntity(targetName)); } @Override - public boolean isFail(String targetObject) { - return tags.stream() - .noneMatch(tag -> tag.isMatchEntity(targetObject)); + public List getEntities() { + return objects.stream() + .map(CustomAnalysisEntity.class::cast) + .collect(Collectors.toList()); } @Override - public List getMatches(String targetObject, int size) { + public List getMatches(int size) { return objects.stream() - .filter(object -> object.isMatchEntity(targetObject)) + .filter(object -> object.isMatchEntity(targetName)) .sorted(Comparator.comparing(CustomDetectedObject::getConfidence).reversed()) .limit(size) .collect(Collectors.toList()); } - - // TODO: 디버그용 검증 후 삭제요망 @Override public void print() { printDetectedObjects(); @@ -53,15 +55,18 @@ public void print() { } private void printDetectedObjects() { - System.out.println("[OBJECT]"); + System.out.println("\n[OBJECT]"); System.out.println("objects.size() = " + objects.size()); - objects.forEach(object -> System.out.println(object.getName())); - objects.forEach(object -> System.out.println(object.getName())); + objects.forEach(object -> { + System.out.println(); + System.out.println("object = " + object.getName()); + System.out.println("parent = " + object.getParent());}); } private void printDetectedTags() { - System.out.println("[TAG]"); + System.out.println("\n[TAG]"); System.out.println("tags.size() = " + tags.size()); + System.out.println(); tags.forEach(tag -> System.out.println(tag.getName())); } } diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/VisionAnalysisResult.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/VisionAnalysisResult.java index 0ced9b49..d162f0af 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/VisionAnalysisResult.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/domain/VisionAnalysisResult.java @@ -1,16 +1,19 @@ package com.wakeUpTogetUp.togetUp.api.mission.domain; -import com.wakeUpTogetUp.togetUp.api.mission.model.CustomAnalysisEntity; import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +@AllArgsConstructor(access = AccessLevel.PROTECTED) public abstract class VisionAnalysisResult { - public abstract List getEntities(); + protected final String targetName; + + public abstract boolean isFail(); - protected abstract boolean isFail(String object); + public abstract List getEntities(); - protected abstract List getMatches(String object, int size); + public abstract List getMatches(int size); - // TODO: 디버그용 삭제요망 public abstract void print(); } diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/dto/response/MissionPerfomRes.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/dto/response/MissionPerfomRes.java index d04577b3..6bf630d3 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/dto/response/MissionPerfomRes.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/dto/response/MissionPerfomRes.java @@ -2,12 +2,8 @@ import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; @Getter -@Setter -@NoArgsConstructor @AllArgsConstructor public class MissionPerfomRes { private String filePath; diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/BoundingBox.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/BoundingBox.java index 8691e837..64a3a9c6 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/BoundingBox.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/BoundingBox.java @@ -8,14 +8,21 @@ @Getter @Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.MODULE) @AllArgsConstructor(access = AccessLevel.PRIVATE) public class BoundingBox { - private int x; - private int y; + private Coord coord; private int w; private int h; + + public int getX() { + return coord.getX(); + } + + public int getY() { + return coord.getY(); + } } diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/Coord.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/Coord.java new file mode 100644 index 00000000..947472d4 --- /dev/null +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/Coord.java @@ -0,0 +1,13 @@ +package com.wakeUpTogetUp.togetUp.api.mission.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class Coord { + + private final int x; + + private final int y; +} diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/CustomAnalysisEntity.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/CustomAnalysisEntity.java deleted file mode 100644 index e9fab596..00000000 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/CustomAnalysisEntity.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.wakeUpTogetUp.togetUp.api.mission.model; - -import lombok.Getter; - -@Getter -public abstract class CustomAnalysisEntity { - - protected final String name; - - private final double confidence; - - private final BoundingBox box; - - public CustomAnalysisEntity(String name, double confidence, BoundingBox box) { - this.name = name; - this.confidence = confidence; - this.box = box; - } - - public abstract boolean isMatchEntity(String target); -} diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/CustomDetectedObject.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/CustomDetectedObject.java deleted file mode 100644 index 20ac51d3..00000000 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/CustomDetectedObject.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.wakeUpTogetUp.togetUp.api.mission.model; - -import lombok.Builder; -import lombok.Getter; - -@Getter -public class CustomDetectedObject extends CustomAnalysisEntity { - - private String parent; - - @Builder - private CustomDetectedObject(String object, String parent, double confidence, BoundingBox box) { - super(object, confidence, box); - this.parent = parent; - } - - @Override - public boolean isMatchEntity(String targetObject) { - return concatObjectAndParent().toLowerCase().contains(targetObject.toLowerCase()); - } - - public String concatObjectAndParent() { - return name + parent; - } - -} diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/CustomDetectedTag.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/CustomDetectedTag.java deleted file mode 100644 index a1f2d17d..00000000 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/CustomDetectedTag.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.wakeUpTogetUp.togetUp.api.mission.model; - -import lombok.Builder; -import lombok.Getter; - -@Getter -public class CustomDetectedTag extends CustomAnalysisEntity { - @Builder - public CustomDetectedTag(String name, double confidence) { - super(name, confidence, new BoundingBox()); - } - - @Override - public boolean isMatchEntity(String target) { - return name.toLowerCase().contains(target.toLowerCase()); - } -} diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/Emotion.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/Expression.java similarity index 52% rename from src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/Emotion.java rename to src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/Expression.java index 43ffc308..d1c3c367 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/Emotion.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/Expression.java @@ -3,17 +3,17 @@ import com.wakeUpTogetUp.togetUp.common.Status; import com.wakeUpTogetUp.togetUp.exception.BaseException; -public enum Emotion { +public enum Expression { JOY, SORROW, ANGER, SURPRISE; - public static Emotion fromName(String object) { + public static Expression fromName(String name) { try { - return Emotion.valueOf(object.toUpperCase()); + return Expression.valueOf(name.toUpperCase()); } catch (IllegalArgumentException exception) { - throw new BaseException(Status.BAD_REQUEST_PARAM); + throw new BaseException("해당하는 감정 열거형 값이 없습니다.", exception, Status.BAD_REQUEST_PARAM); } } } diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/MissionObject.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/MissionObject.java index a31241a0..88d9ebcd 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/MissionObject.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/MissionObject.java @@ -21,9 +21,11 @@ public class MissionObject { @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(columnDefinition = "INT UNSIGNED") private Integer id; - // todo : enum 처리하기 + private String name; + private String kr; + private String icon; @ManyToOne(fetch = FetchType.LAZY) diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/MissionType.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/MissionType.java new file mode 100644 index 00000000..d17c92d6 --- /dev/null +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/model/MissionType.java @@ -0,0 +1,27 @@ +package com.wakeUpTogetUp.togetUp.api.mission.model; + +import com.wakeUpTogetUp.togetUp.common.Status; +import com.wakeUpTogetUp.togetUp.exception.BaseException; +import java.util.Arrays; +import lombok.Getter; + +public enum MissionType { + + DIRECT_REGISTRATION("direct-registration"), + OBJECT_DETECTION("object-detection"), + EXPRESSION_RECOGNITION("expression-recognition"); + + @Getter + private final String name; + + MissionType(String name) { + this.name = name; + } + + public static MissionType getByName(String name) { + return Arrays.stream(MissionType.values()) + .filter(type -> type.getName().equals(name)) + .findAny() + .orElseThrow(() -> new BaseException(Status.MISSION_NOT_FOUND)); + } +} diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/MissionLogRepository.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/repository/MissionLogRepository.java similarity index 92% rename from src/main/java/com/wakeUpTogetUp/togetUp/api/mission/MissionLogRepository.java rename to src/main/java/com/wakeUpTogetUp/togetUp/api/mission/repository/MissionLogRepository.java index 41fa820d..7d5c86ec 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/MissionLogRepository.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/repository/MissionLogRepository.java @@ -1,4 +1,4 @@ -package com.wakeUpTogetUp.togetUp.api.mission; +package com.wakeUpTogetUp.togetUp.api.mission.repository; import com.wakeUpTogetUp.togetUp.api.mission.model.MissionLog; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/MissionObjectRepository.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/repository/MissionObjectRepository.java similarity index 68% rename from src/main/java/com/wakeUpTogetUp/togetUp/api/mission/MissionObjectRepository.java rename to src/main/java/com/wakeUpTogetUp/togetUp/api/mission/repository/MissionObjectRepository.java index 1ec4be00..5fedc5e6 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/MissionObjectRepository.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/repository/MissionObjectRepository.java @@ -1,4 +1,4 @@ -package com.wakeUpTogetUp.togetUp.api.mission; +package com.wakeUpTogetUp.togetUp.api.mission.repository; import com.wakeUpTogetUp.togetUp.api.mission.model.MissionObject; import org.springframework.data.jpa.repository.JpaRepository; @@ -7,4 +7,5 @@ @Repository public interface MissionObjectRepository extends JpaRepository { + boolean existsByMission_NameAndName(String missionName, String name); } \ No newline at end of file diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/MissionRepository.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/repository/MissionRepository.java similarity index 91% rename from src/main/java/com/wakeUpTogetUp/togetUp/api/mission/MissionRepository.java rename to src/main/java/com/wakeUpTogetUp/togetUp/api/mission/repository/MissionRepository.java index c9b80e61..23605d0f 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/MissionRepository.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/repository/MissionRepository.java @@ -1,4 +1,4 @@ -package com.wakeUpTogetUp.togetUp.api.mission; +package com.wakeUpTogetUp.togetUp.api.mission.repository; import com.wakeUpTogetUp.togetUp.api.mission.model.Mission; import java.util.List; diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/MissionImageService.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/MissionImageService.java index 7e7c2950..2fe8b013 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/MissionImageService.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/MissionImageService.java @@ -1,42 +1,30 @@ package com.wakeUpTogetUp.togetUp.api.mission.service; -import com.google.cloud.vision.v1.FaceAnnotation; -import com.wakeUpTogetUp.togetUp.api.mission.model.CustomAnalysisEntity; -import com.wakeUpTogetUp.togetUp.utils.ImageProcessor; +import com.wakeUpTogetUp.togetUp.api.mission.domain.CustomAnalysisEntity; +import com.wakeUpTogetUp.togetUp.utils.file.FileUtil; +import com.wakeUpTogetUp.togetUp.utils.image.ImageDrawer; import com.wakeUpTogetUp.togetUp.infra.aws.s3.model.CustomFile; -import java.io.IOException; import java.util.List; import lombok.RequiredArgsConstructor; -import org.apache.commons.imaging.ImageReadException; -import org.apache.commons.imaging.formats.tiff.TiffImageMetadata; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @Service @RequiredArgsConstructor public class MissionImageService { - private final ImageProcessor imageProcessor; - public CustomFile processODResultImage( - MultipartFile file, List entities) throws IOException, ImageReadException { + private final ImageDrawer imageDrawer; + + public CustomFile processResultImage( + MultipartFile file, + List entities, + String object + ) { if (entities.isEmpty()) { - return CustomFile.fromProcessedFile(file, file.getBytes()); + return CustomFile.fromProcessedFile(file, FileUtil.getBytes(file)); } - // TODO : 파일 품질 손상 고치기 - TiffImageMetadata metadata = imageProcessor.getImageMetadata(file); - byte[] drawnImageBytes = imageProcessor.drawODResultOnImage(file, entities); - byte[] orientedImageBytes = imageProcessor.orientImage(drawnImageBytes, metadata); - - return CustomFile.fromProcessedFile(file, orientedImageBytes); - } - - public CustomFile processFRResultImage( - MultipartFile file, List faceAnnotations, String object) throws Exception { - TiffImageMetadata metadata = imageProcessor.getImageMetadata(file); - byte[] drawnImageBytes = imageProcessor.drawFRResultOnImage(file, faceAnnotations, object); - byte[] orientedImageBytes = imageProcessor.orientImage(drawnImageBytes, metadata); - - return CustomFile.fromProcessedFile(file, orientedImageBytes); + byte[] drawnImageBytes = imageDrawer.drawResultOnImage(file, entities, object); + return CustomFile.fromProcessedFile(file, drawnImageBytes); } } \ No newline at end of file diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/MissionProvider.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/MissionProvider.java similarity index 64% rename from src/main/java/com/wakeUpTogetUp/togetUp/api/mission/MissionProvider.java rename to src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/MissionProvider.java index 2d8b990d..3ecfaf91 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/MissionProvider.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/MissionProvider.java @@ -1,9 +1,13 @@ -package com.wakeUpTogetUp.togetUp.api.mission; +package com.wakeUpTogetUp.togetUp.api.mission.service; import com.wakeUpTogetUp.togetUp.api.mission.dto.response.GetMissionLogRes; import com.wakeUpTogetUp.togetUp.api.mission.dto.response.GetMissionWithObjectListRes; import com.wakeUpTogetUp.togetUp.api.mission.model.Mission; import com.wakeUpTogetUp.togetUp.api.mission.model.MissionLog; +import com.wakeUpTogetUp.togetUp.api.mission.model.MissionType; +import com.wakeUpTogetUp.togetUp.api.mission.repository.MissionLogRepository; +import com.wakeUpTogetUp.togetUp.api.mission.repository.MissionObjectRepository; +import com.wakeUpTogetUp.togetUp.api.mission.repository.MissionRepository; import com.wakeUpTogetUp.togetUp.common.Status; import com.wakeUpTogetUp.togetUp.exception.BaseException; import com.wakeUpTogetUp.togetUp.utils.mapper.EntityDtoMapper; @@ -14,7 +18,9 @@ @Service @RequiredArgsConstructor public class MissionProvider { + private final MissionRepository missionRepository; + private final MissionObjectRepository missionObjectRepository; private final MissionLogRepository missionLogRepository; public GetMissionWithObjectListRes getMission(int missionId) { @@ -23,7 +29,6 @@ public GetMissionWithObjectListRes getMission(int missionId) { return EntityDtoMapper.INSTANCE.toGetMissionRes(mission); } - public GetMissionLogRes getMissionLog(Integer missionCompleteLogId) { MissionLog missionLog = missionLogRepository.findById(missionCompleteLogId) .orElseThrow( @@ -37,4 +42,18 @@ public List getMissionCompleteLogsByUserId(Integer userId) { return EntityDtoMapper.INSTANCE.toMissionLogResList(missionLogList); } + + public void validateMissionObject(MissionType type, String object) { + if (object == null) { + throw new BaseException(Status.BAD_REQUEST_PARAM); + } + + if (isNotExistMissionObjectName(type.getName(), object)) { + throw new BaseException(Status.MISSION_NOT_FOUND); + } + } + + private boolean isNotExistMissionObjectName(String missionName, String object) { + return !missionObjectRepository.existsByMission_NameAndName(missionName, object); + } } diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/MissionService.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/MissionService.java similarity index 50% rename from src/main/java/com/wakeUpTogetUp/togetUp/api/mission/MissionService.java rename to src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/MissionService.java index 3a9c764f..caf283ef 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/MissionService.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/MissionService.java @@ -1,19 +1,14 @@ -package com.wakeUpTogetUp.togetUp.api.mission; +package com.wakeUpTogetUp.togetUp.api.mission.service; -import com.google.cloud.vision.v1.FaceAnnotation; -import com.google.cloud.vision.v1.Likelihood; import com.wakeUpTogetUp.togetUp.api.alarm.AlarmRepository; import com.wakeUpTogetUp.togetUp.api.alarm.model.Alarm; +import com.wakeUpTogetUp.togetUp.api.mission.domain.VisionAnalysisResult; import com.wakeUpTogetUp.togetUp.api.mission.dto.request.MissionCompleteReq; import com.wakeUpTogetUp.togetUp.api.mission.dto.response.MissionCompleteRes; -import com.wakeUpTogetUp.togetUp.api.mission.model.CustomAnalysisEntity; -import com.wakeUpTogetUp.togetUp.api.mission.model.Emotion; +import com.wakeUpTogetUp.togetUp.api.mission.domain.CustomAnalysisEntity; import com.wakeUpTogetUp.togetUp.api.mission.model.MissionLog; -import com.wakeUpTogetUp.togetUp.api.mission.domain.MissionPerformResult; -import com.wakeUpTogetUp.togetUp.infra.azure.vision.AzureVision32Service; -import com.wakeUpTogetUp.togetUp.infra.azure.vision.AzureVision40Service; -import com.wakeUpTogetUp.togetUp.infra.google.vision.GoogleVisionService; -import com.wakeUpTogetUp.togetUp.api.mission.service.ObjectDetectionService; +import com.wakeUpTogetUp.togetUp.api.mission.model.MissionType; +import com.wakeUpTogetUp.togetUp.api.mission.repository.MissionLogRepository; import com.wakeUpTogetUp.togetUp.api.notification.NotificationService; import com.wakeUpTogetUp.togetUp.api.users.UserAvatarService; import com.wakeUpTogetUp.togetUp.api.users.UserRepository; @@ -24,9 +19,7 @@ import com.wakeUpTogetUp.togetUp.common.Status; import com.wakeUpTogetUp.togetUp.exception.BaseException; -import java.util.Comparator; import java.util.List; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -41,10 +34,9 @@ @Slf4j public class MissionService { - private final ObjectDetectionService objectDetectionService; - private final AzureVision32Service azureVision32Service; - private final AzureVision40Service azureVision40Service; - private final GoogleVisionService googleVisionService; + private final int MAX_MATCHES_LIMIT = 3; + + private final VisionServiceFactory visionServiceFactory; private final AlarmRepository alarmRepository; private final MissionLogRepository missionLogRepository; private final UserService userService; @@ -52,70 +44,18 @@ public class MissionService { private final UserRepository userRepository; private final NotificationService notificationService; - public List getDetectionResult(String object, MultipartFile missionImage) - throws Exception { - MissionPerformResult result = MissionPerformResult.builder() - .object(object) - .analysisResult(azureVision40Service.detect(missionImage)) - .build(); + public List getMissionResult(MissionType type, String object, MultipartFile missionImage) { + VisionAnalysisResult result = visionServiceFactory + .getVisionService(type) + .getResult(missionImage, object); - // TODO: 디버그용 메서드 검증 후 삭제요망 - result.getAnalysisResult().print(); + result.print(); if (result.isFail()) { throw new BaseException(Status.MISSION_FAILURE); } - return result.getMatches(); - } - - public List getFaceRecognitionResult(String object, MultipartFile missionImage) { - Emotion emotion = Emotion.fromName(object); - List faceAnnotations = googleVisionService.getFaceRecognitionResult(missionImage); - - if (faceAnnotations.isEmpty()) { - throw new BaseException(Status.NO_DETECTED_OBJECT); - } - - List highestConfidenceFaces = faceAnnotations.stream() - .filter(face -> getLikelihood(emotion, face).getNumber() >= 3) - .sorted(Comparator.comparing(FaceAnnotation::getDetectionConfidence).reversed()) - .limit(3) - .collect(Collectors.toList()); - - if (highestConfidenceFaces.isEmpty()) { - throw new BaseException(Status.MISSION_FAILURE); - } - - return highestConfidenceFaces; - } - - private Likelihood getLikelihood(Emotion emotion, FaceAnnotation face) { - switch (emotion) { - case JOY: - return face.getJoyLikelihood(); - case SORROW: - return face.getSorrowLikelihood(); - case ANGER: - return face.getAngerLikelihood(); - case SURPRISE: - return face.getSurpriseLikelihood(); - default: - throw new BaseException(Status.INVALID_OBJECT_NAME); - } - } - - // 모델로 객체 인식 - public void recognizeObjectByModel(String object, MultipartFile missionImage) { - for (String objectDetected : objectDetectionService.detectObject(missionImage)) { - log.info("objectDetected = " + objectDetected); - - if (objectDetected.equals(object)) { - return; - } - } - - throw new BaseException(Status.MISSION_FAILURE); + return result.getMatches(MAX_MATCHES_LIMIT); } @Transactional @@ -143,8 +83,6 @@ public MissionCompleteRes afterMissionComplete(int userId, MissionCompleteReq re private void sendNotificationIfRoomExists(Alarm alarm, User user) { if (alarm.isRoomAlarm()) { - // 미션 성공 후 처리 로직과 알림 보내기 로직을 독립적으로 분리 - // 알림을 보내는데 실패해도 모든 과정이 롤백되지 않음 try { notificationService.sendNotificationToUsersInRoom(alarm.getId(), user.getId()); } catch (BaseException e) { @@ -166,5 +104,4 @@ private void createMissionLog(User user, MissionCompleteReq req) { missionLogRepository.save(missionLog); } -} - +} \ No newline at end of file diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/ObjectDetectionService.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/ObjectDetectionService.java deleted file mode 100644 index 8cf3b875..00000000 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/ObjectDetectionService.java +++ /dev/null @@ -1,152 +0,0 @@ -package com.wakeUpTogetUp.togetUp.api.mission.service; - -import ai.onnxruntime.OnnxTensor; -import ai.onnxruntime.OrtException; -import ai.onnxruntime.OrtSession; -import com.wakeUpTogetUp.togetUp.api.mission.domain.Letterbox; -import com.wakeUpTogetUp.togetUp.api.mission.domain.ODResult; -import com.wakeUpTogetUp.togetUp.api.mission.domain.ObjectDetectionModel; -import com.wakeUpTogetUp.togetUp.common.Status; -import com.wakeUpTogetUp.togetUp.config.ODConfig; -import com.wakeUpTogetUp.togetUp.exception.BaseException; -import com.wakeUpTogetUp.togetUp.utils.ImageProcessor; -import java.nio.FloatBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import lombok.RequiredArgsConstructor; -import org.opencv.core.Mat; -import org.opencv.core.MatOfByte; -import org.opencv.core.Point; -import org.opencv.core.Scalar; -import org.opencv.imgcodecs.Imgcodecs; -import org.opencv.imgproc.Imgproc; -import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; - -@Service -@RequiredArgsConstructor -public class ObjectDetectionService { - - private final ObjectDetectionModel odm; - private final ODConfig odConfig; - private final Letterbox letterbox; - private final ImageProcessor imageProcessor; - -// @Value("${my.path.save-pic-path}") -// private String savePicPath; - - public ArrayList detectObject(MultipartFile missionImage) { - long startTime = System.currentTimeMillis(); - - try { - // 기본 정보 출력 - odm.getSession().getInputInfo().keySet().forEach(x -> { - try { - System.out.println("input name = " + x); - System.out.println(odm.getSession().getInputInfo().get(x).getInfo().toString()); - } catch (OrtException e) { - throw new RuntimeException(e); - } - }); - } catch (Exception e) { - e.printStackTrace(); - throw new BaseException(Status.LOAD_MODEL_FAILURE); - } - - try { - // image 읽기 - Mat img = Imgcodecs.imdecode(new MatOfByte(missionImage.getBytes()), - Imgcodecs.IMREAD_COLOR); -// Mat img = Imgcodecs.imdecode( -// new MatOfByte(imageProcessor.compress(missionImage, 0.5f)), -// Imgcodecs.IMREAD_COLOR); - Imgproc.cvtColor(img, img, Imgproc.COLOR_BGR2RGB); - Mat image = img.clone(); - - // 아래 상자의 굵기, 글자의 크기, 글자의 종류, 글자의 색을 먼저 정의합니다. (비례에 따라 굵기를 설정하는 것이 좋습니다.) - int minDwDh = Math.min(img.width(), img.height()); - int thickness = minDwDh / odConfig.getLineThicknessRatio(); - double fontSize = minDwDh / odConfig.getFontSizeRatio(); - int fontFace = Imgproc.FONT_HERSHEY_SIMPLEX; - Scalar fontColor = new Scalar(0, 0, 0); - - // image 크기 변경 - image = letterbox.letterbox(image); - double ratio = letterbox.getRatio(); - double dw = letterbox.getDw(); - double dh = letterbox.getDh(); - int rows = letterbox.getHeight(); - int cols = letterbox.getWidth(); - int channels = image.channels(); - - // Mat 객체의 픽셀 값을 Float[] 객체에 할당합니다. - float[] pixels = new float[channels * rows * cols]; - for (int i = 0; i < rows; i++) { - for (int j = 0; j < cols; j++) { - double[] pixel = image.get(j, i); - for (int k = 0; k < channels; k++) { - // 이러한 설정은 image.transpose (2, 0, 1) 작업을 동시에 수행하는 것과 같습니다 - pixels[rows * cols * k + j * cols + i] = (float) pixel[k] / 255.0f; - } - } - } - - // OnnxTensor 개체 만들기 - long[] shape = {1L, (long) channels, (long) rows, (long) cols}; - OnnxTensor tensor = OnnxTensor.createTensor(odm.getEnvironment(), - FloatBuffer.wrap(pixels), shape); - HashMap stringOnnxTensorHashMap = new HashMap<>(); - stringOnnxTensorHashMap.put(odm.getSession().getInputInfo().keySet().iterator().next(), - tensor); - - // 모델 실행 - OrtSession.Result output = odm.getSession().run(stringOnnxTensorHashMap); - - // 결과를 얻기 - // 디버깅 - System.out.println("info = " + output.get(0).getInfo().toString()); - float[][] outputData = (float[][]) output.get(0).getValue(); - - ArrayList detectedObejectList = new ArrayList(); - Arrays.stream(outputData).iterator().forEachRemaining(x -> { - ODResult odResult = new ODResult(x); - System.out.println("odResult : " + odResult); - -// // 그림틀 - Point topLeft = new Point((odResult.getX0() - dw) / ratio, - (odResult.getY0() - dh) / ratio); - Point bottomRight = new Point((odResult.getX1() - dw) / ratio, - (odResult.getY1() - dh) / ratio); - Scalar color = new Scalar(odConfig.getColor(odResult.getClsId())); - Imgproc.rectangle(img, topLeft, bottomRight, color, thickness); - - // 틀에 글을 쓰다 - String boxName = odConfig.getName(odResult.getClsId()) + ": " + odResult.getScore(); - Point boxNameLoc = new Point((odResult.getX0() - dw) / ratio, - (odResult.getY0() - dh) / ratio - 3); - - Imgproc.putText(img, boxName, boxNameLoc, fontFace, fontSize, fontColor, thickness); - - // 감지된 클래스 리스트 생성 - detectedObejectList.add(odConfig.getName(odResult.getClsId())); - }); - -// // 그림 저장 - Imgproc.cvtColor(img, img, Imgproc.COLOR_RGB2BGR); -// Imgcodecs.imwrite(savePicPath, img); - - // 걸린 시간 계산 - long endTime = System.currentTimeMillis(); - - long timeElapsed = endTime - startTime; - System.out.println("Execution time in milliseconds: " + timeElapsed); - - return detectedObejectList; - - } catch (Exception e) { - e.printStackTrace(); - throw new BaseException(Status.NO_DETECTED_OBJECT); - } - } -} diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/VisionService.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/VisionService.java new file mode 100644 index 00000000..f46c6fb8 --- /dev/null +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/VisionService.java @@ -0,0 +1,9 @@ +package com.wakeUpTogetUp.togetUp.api.mission.service; + +import com.wakeUpTogetUp.togetUp.api.mission.domain.VisionAnalysisResult; +import org.springframework.web.multipart.MultipartFile; + +public interface VisionService { + + VisionAnalysisResult getResult(MultipartFile file, String target); +} diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/VisionServiceFactory.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/VisionServiceFactory.java new file mode 100644 index 00000000..6fa77f93 --- /dev/null +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/VisionServiceFactory.java @@ -0,0 +1,33 @@ +package com.wakeUpTogetUp.togetUp.api.mission.service; + +import com.wakeUpTogetUp.togetUp.api.mission.model.MissionType; +import com.wakeUpTogetUp.togetUp.common.Status; +import com.wakeUpTogetUp.togetUp.exception.BaseException; +import com.wakeUpTogetUp.togetUp.infra.azure.vision.AzureVision32Service; +import com.wakeUpTogetUp.togetUp.infra.azure.vision.AzureVision40Service; +import com.wakeUpTogetUp.togetUp.infra.google.vision.GoogleVisionService; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class VisionServiceFactory { + + private final AzureVision32Service azureVision32Service; + private final AzureVision40Service azureVision40Service; + private final GoogleVisionService googleVisionService; + + public VisionService getVisionService(MissionType missionType) { + switch (missionType) { + case OBJECT_DETECTION: + return azureVision40Service; + + case EXPRESSION_RECOGNITION: + return googleVisionService; + + default: + throw new BaseException(Status.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/strategy/AnalysisConversionStrategy.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/strategy/AnalysisConversionStrategy.java new file mode 100644 index 00000000..434f7c50 --- /dev/null +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/strategy/AnalysisConversionStrategy.java @@ -0,0 +1,30 @@ +package com.wakeUpTogetUp.togetUp.api.mission.strategy; + +import com.wakeUpTogetUp.togetUp.api.mission.service.MissionImageService; +import com.wakeUpTogetUp.togetUp.api.mission.service.MissionProvider; +import com.wakeUpTogetUp.togetUp.api.mission.service.MissionService; +import com.wakeUpTogetUp.togetUp.api.mission.domain.CustomAnalysisEntity; +import com.wakeUpTogetUp.togetUp.api.mission.model.MissionType; +import com.wakeUpTogetUp.togetUp.infra.aws.s3.model.CustomFile; +import java.util.List; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +@Component +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class AnalysisConversionStrategy implements MissionImageStrategy { + + private final MissionService missionService; + private final MissionImageService missionImageService; + private final MissionProvider missionProvider; + + @Override + public CustomFile execute(MultipartFile missionImage, MissionType type, String object) { + missionProvider.validateMissionObject(type, object); + + List detectedObjects = missionService.getMissionResult(type, object, missionImage); + return missionImageService.processResultImage(missionImage, detectedObjects, object); + } +} diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/strategy/MissionImageStrategy.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/strategy/MissionImageStrategy.java new file mode 100644 index 00000000..fcfec742 --- /dev/null +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/strategy/MissionImageStrategy.java @@ -0,0 +1,9 @@ +package com.wakeUpTogetUp.togetUp.api.mission.strategy; + +import com.wakeUpTogetUp.togetUp.api.mission.model.MissionType; +import com.wakeUpTogetUp.togetUp.infra.aws.s3.model.CustomFile; +import org.springframework.web.multipart.MultipartFile; + +public interface MissionImageStrategy { + CustomFile execute(MultipartFile missionImage, MissionType type, String object); +} diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/strategy/MissionImageStrategyFactory.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/strategy/MissionImageStrategyFactory.java new file mode 100644 index 00000000..45a1c95a --- /dev/null +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/strategy/MissionImageStrategyFactory.java @@ -0,0 +1,24 @@ +package com.wakeUpTogetUp.togetUp.api.mission.strategy; + +import com.wakeUpTogetUp.togetUp.api.mission.model.MissionType; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class MissionImageStrategyFactory { + + private final SimpleConversionStrategy simpleConversionStrategy; + private final AnalysisConversionStrategy imageProcessingStrategy; + + public MissionImageStrategy getStrategy(MissionType missionType) { + switch (missionType) { + case DIRECT_REGISTRATION: + return simpleConversionStrategy; + + default: + return imageProcessingStrategy; + } + } +} diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/strategy/SimpleConversionStrategy.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/strategy/SimpleConversionStrategy.java new file mode 100644 index 00000000..c90bd6fe --- /dev/null +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/strategy/SimpleConversionStrategy.java @@ -0,0 +1,16 @@ +package com.wakeUpTogetUp.togetUp.api.mission.strategy; + +import com.wakeUpTogetUp.togetUp.api.mission.model.MissionType; +import com.wakeUpTogetUp.togetUp.infra.aws.s3.model.CustomFile; +import com.wakeUpTogetUp.togetUp.utils.file.FileUtil; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +@Component +public class SimpleConversionStrategy implements MissionImageStrategy { + + @Override + public CustomFile execute(MultipartFile missionImage, MissionType type, String object) { + return CustomFile.fromProcessedFile(missionImage, FileUtil.getBytes(missionImage)); + } +} diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/room/RoomController.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/room/RoomController.java index fd29d61b..968c4609 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/room/RoomController.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/room/RoomController.java @@ -2,7 +2,7 @@ import com.wakeUpTogetUp.togetUp.api.alarm.dto.request.AlarmCreateReq; import com.wakeUpTogetUp.togetUp.api.auth.AuthUser; -import com.wakeUpTogetUp.togetUp.api.mission.MissionLogRepository; +import com.wakeUpTogetUp.togetUp.api.mission.repository.MissionLogRepository; import com.wakeUpTogetUp.togetUp.api.room.dto.request.RoomReq; import com.wakeUpTogetUp.togetUp.api.room.dto.response.RoomDetailRes; import com.wakeUpTogetUp.togetUp.api.room.dto.response.RoomInfoRes; diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/room/RoomService.java b/src/main/java/com/wakeUpTogetUp/togetUp/api/room/RoomService.java index 6a611ba1..13640151 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/room/RoomService.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/api/room/RoomService.java @@ -5,7 +5,7 @@ import com.wakeUpTogetUp.togetUp.api.alarm.dto.request.AlarmCreateReq; import com.wakeUpTogetUp.togetUp.api.alarm.dto.request.PostAlarmReq; import com.wakeUpTogetUp.togetUp.api.alarm.model.Alarm; -import com.wakeUpTogetUp.togetUp.api.mission.MissionLogRepository; +import com.wakeUpTogetUp.togetUp.api.mission.repository.MissionLogRepository; import com.wakeUpTogetUp.togetUp.api.mission.model.MissionLog; import com.wakeUpTogetUp.togetUp.api.mission.model.MissionObject; import com.wakeUpTogetUp.togetUp.api.room.dto.request.RoomReq; @@ -31,7 +31,6 @@ import java.time.LocalTime; import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/common/Status.java b/src/main/java/com/wakeUpTogetUp/togetUp/common/Status.java index e8409ee2..dc451e71 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/common/Status.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/common/Status.java @@ -61,7 +61,7 @@ public enum Status { ALARM_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 알람 입니다."), MISSION_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 미션 입니다."), AVATAR_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 아바타 입니다."), - OBJECT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 미션 객체입니다."), + MISSION_OBJECT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 미션 객체입니다."), FILE_NOT_FOUND(HttpStatus.NOT_FOUND, "파일을 찾을 수 없습니다."), ACCOUNT_DOESNT_EXISTS(HttpStatus.NOT_FOUND, "계정이 존재하지 않습니다."), ROOM_USER_NOT_FOUND(HttpStatus.NOT_FOUND, "그룹의 해당 멤버가 없습니다."), diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/common/annotation/validator/ImageFileValidator.java b/src/main/java/com/wakeUpTogetUp/togetUp/common/annotation/validator/ImageFileValidator.java index 24e2b9f9..22c3810b 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/common/annotation/validator/ImageFileValidator.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/common/annotation/validator/ImageFileValidator.java @@ -1,6 +1,6 @@ package com.wakeUpTogetUp.togetUp.common.annotation.validator; -import com.wakeUpTogetUp.togetUp.utils.FileValidator; +import com.wakeUpTogetUp.togetUp.utils.file.FileValidator; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import org.springframework.web.multipart.MultipartFile; diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/config/ODConfig.java b/src/main/java/com/wakeUpTogetUp/togetUp/config/ODConfig.java deleted file mode 100644 index b2e0d7ea..00000000 --- a/src/main/java/com/wakeUpTogetUp/togetUp/config/ODConfig.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.wakeUpTogetUp.togetUp.config; - -import lombok.Getter; - -import java.util.Random; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.HashMap; -import org.springframework.stereotype.Component; - -@Component -@Getter -public class ODConfig { - - private final Integer lineThicknessRatio = 333; - private final Double fontSizeRatio = 1145.14; - private final List names = new ArrayList<>(Arrays.asList( - "person", "bicycle", "car", "motorcycle", "airplane", "bus", - "train", "boat", "traffic light", "bird", "cat", "dog", - "horse", "backpack", "umbrella", "handbag", "tie", "suitcase", - "frisbee", "skis", "surfboard", "snowboard", "sports ball", - "baseball bat", "baseball glove", "skateboard", "tennis racket", - "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", - "banana", "apple", "sandwich", "orange", "broccoli", "carrot", - "hot dog", "pizza", "donut", "cake", "chair", "couch", - "potted plant", "bed", "tv", "laptop", "mouse", "keyboard", - "cell phone", "oven", "toaster", "refrigerator", "book", "clock", - "vase", "scissors", "teddy bear", "hair drier", "toothbrush")); - - private Map colors; - - public ODConfig() { - this.colors = new HashMap<>(); - names.forEach(name->{ - Random random = new Random(); - double[] color = {random.nextDouble()*256, random.nextDouble()*256, random.nextDouble()*256}; - colors.put(name, color); - }); - } - - public String getName(int clsId) { - return names.get(clsId); - } - - public double[] getColor(int clsId) { - return colors.get(getName(clsId)); - } -} \ No newline at end of file diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/exception/BaseException.java b/src/main/java/com/wakeUpTogetUp/togetUp/exception/BaseException.java index 8ee7ba3a..83b74f4a 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/exception/BaseException.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/exception/BaseException.java @@ -11,4 +11,14 @@ @AllArgsConstructor public class BaseException extends RuntimeException { private Status status; + + public BaseException(String message, Throwable cause, Status status) { + super(message, cause); + this.status = status; + } + + public BaseException(Throwable cause, Status status) { + super(cause); + this.status = status; + } } diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/infra/aws/s3/FileService.java b/src/main/java/com/wakeUpTogetUp/togetUp/infra/aws/s3/FileService.java index b55e0fbb..fa5fd73c 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/infra/aws/s3/FileService.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/infra/aws/s3/FileService.java @@ -5,7 +5,7 @@ import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; import com.wakeUpTogetUp.togetUp.infra.aws.s3.model.CustomFile; -import com.wakeUpTogetUp.togetUp.utils.FileUtil; +import com.wakeUpTogetUp.togetUp.utils.file.FileUtil; import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/infra/aws/s3/model/ImageContentType.java b/src/main/java/com/wakeUpTogetUp/togetUp/infra/aws/s3/model/ImageContentType.java index 52d682c3..d48d6679 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/infra/aws/s3/model/ImageContentType.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/infra/aws/s3/model/ImageContentType.java @@ -1,13 +1,15 @@ package com.wakeUpTogetUp.togetUp.infra.aws.s3.model; +import com.wakeUpTogetUp.togetUp.common.Status; +import com.wakeUpTogetUp.togetUp.exception.BaseException; import java.util.Arrays; public enum ImageContentType { JPG("image/jpeg", "jpg"), - JPEG("image/jpeg", "jpeg"), - PNG("image/png", "png"), - WEBP("image/webp", "webp"), - HEIF("application/octet-stream", "heic"); + JPEG("image/jpeg", "jpeg"); +// PNG("image/png", "png"), +// WEBP("image/webp", "webp"), +// HEIF("application/octet-stream", "heic"); private final String contentType; private final String extension; @@ -25,6 +27,13 @@ public String getExtension() { return extension; } + public static ImageContentType getByContentType(String contentType) { + return Arrays.stream(ImageContentType.values()) + .filter(type -> type.contentType.equals(contentType)) + .findAny() + .orElseThrow(() -> new BaseException(Status.INVALID_FILE_CONTENT_TYPE_EXCEPTION)); + } + public static boolean exists(String contentType, String extension) { return Arrays.stream(ImageContentType.values()) .anyMatch(type -> type.fieldsMatches(contentType, extension)); diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/infra/azure/vision/AzureVision32Service.java b/src/main/java/com/wakeUpTogetUp/togetUp/infra/azure/vision/AzureVision32Service.java index 24190fb6..97d171c1 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/infra/azure/vision/AzureVision32Service.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/infra/azure/vision/AzureVision32Service.java @@ -3,11 +3,14 @@ import com.microsoft.azure.cognitiveservices.vision.computervision.ComputerVisionClient; import com.microsoft.azure.cognitiveservices.vision.computervision.models.ImageAnalysis; import com.microsoft.azure.cognitiveservices.vision.computervision.models.VisualFeatureTypes; -import com.wakeUpTogetUp.togetUp.api.mission.model.CustomDetectedObject; -import com.wakeUpTogetUp.togetUp.api.mission.service.mapper.ObjectDetectedV32Mapper; +import com.wakeUpTogetUp.togetUp.api.mission.domain.ObjectDetectionResult; +import com.wakeUpTogetUp.togetUp.api.mission.domain.VisionAnalysisResult; +import com.wakeUpTogetUp.togetUp.api.mission.service.VisionService; +import com.wakeUpTogetUp.togetUp.infra.azure.vision.mapper.ObjectDetectedV32Mapper; import com.wakeUpTogetUp.togetUp.common.Status; import com.wakeUpTogetUp.togetUp.exception.BaseException; -import java.io.IOException; +import com.wakeUpTogetUp.togetUp.infra.azure.vision.mapper.TagDetectedV32Mapper; +import com.wakeUpTogetUp.togetUp.utils.image.ImageProcessor; import java.util.List; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; @@ -16,28 +19,45 @@ @Service @RequiredArgsConstructor(access = AccessLevel.PRIVATE) -public class AzureVision32Service { +public class AzureVision32Service implements VisionService { - private final ComputerVisionClient client; - private final ObjectDetectedV32Mapper mapper; + private final int IMAGE_SIZE_LIMIT = 4; - public List detectObjects(MultipartFile file) { - List features = List.of(VisualFeatureTypes.OBJECTS); + private final ComputerVisionClient client; + private final ObjectDetectedV32Mapper objectDetectedV32Mapper; + private final TagDetectedV32Mapper tagDetectedV32Mapper; + public VisionAnalysisResult getResult(MultipartFile file, String target) { try { - ImageAnalysis analysis = client.computerVision() - .analyzeImageInStream() - .withImage(file.getBytes()) - .withVisualFeatures(features) - .execute(); - - if (analysis.objects() == null) { - throw new BaseException(Status.NO_DETECTED_OBJECT); - } - - return mapper.toCustomDetectedObjects(analysis.objects()); - } catch (IOException e) { - throw new BaseException(Status.INVALID_IMAGE); + byte[] data = ImageProcessor.compressUntil(file, IMAGE_SIZE_LIMIT); + + ImageAnalysis analysis = getAnalysisResult(data); + validateNotDetected(analysis); + + return ObjectDetectionResult.builder() + .target(target) + .objects(objectDetectedV32Mapper.toCustomDetectedObjects(analysis.objects())) + .tags(tagDetectedV32Mapper.toCustomDetectedTags(analysis.tags())) + .build(); + } catch (Exception e) { + throw new BaseException(Status.INTERNAL_SERVER_ERROR); + } + } + + private ImageAnalysis getAnalysisResult(byte[] data) { + List features = + List.of(VisualFeatureTypes.OBJECTS, VisualFeatureTypes.TAGS); + + return client.computerVision() + .analyzeImageInStream() + .withImage(data) + .withVisualFeatures(features) + .execute(); + } + + private void validateNotDetected(ImageAnalysis analysis) { + if (analysis.objects() == null && analysis.tags() == null) { + throw new BaseException(Status.NO_DETECTED_OBJECT); } } } \ No newline at end of file diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/infra/azure/vision/AzureVision40Service.java b/src/main/java/com/wakeUpTogetUp/togetUp/infra/azure/vision/AzureVision40Service.java index 88ccc323..c51e0690 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/infra/azure/vision/AzureVision40Service.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/infra/azure/vision/AzureVision40Service.java @@ -13,10 +13,13 @@ import com.azure.ai.vision.imageanalysis.ImageAnalysisResultReason; import com.azure.ai.vision.imageanalysis.ImageAnalyzer; import com.wakeUpTogetUp.togetUp.api.mission.domain.ObjectDetectionResult; -import com.wakeUpTogetUp.togetUp.api.mission.service.mapper.ObjectDetectedV40Mapper; -import com.wakeUpTogetUp.togetUp.api.mission.service.mapper.TagDetectedV40Mapper; +import com.wakeUpTogetUp.togetUp.api.mission.domain.VisionAnalysisResult; +import com.wakeUpTogetUp.togetUp.api.mission.service.VisionService; +import com.wakeUpTogetUp.togetUp.infra.azure.vision.mapper.ObjectDetectedV40Mapper; +import com.wakeUpTogetUp.togetUp.infra.azure.vision.mapper.TagDetectedV40Mapper; import com.wakeUpTogetUp.togetUp.common.Status; import com.wakeUpTogetUp.togetUp.exception.BaseException; +import com.wakeUpTogetUp.togetUp.utils.image.ImageProcessor; import java.io.IOException; import java.nio.ByteBuffer; import lombok.AccessLevel; @@ -26,46 +29,61 @@ @Service @RequiredArgsConstructor(access = AccessLevel.PRIVATE) -public class AzureVision40Service { +public class AzureVision40Service implements VisionService { + + private final int IMAGE_SIZE_LIMIT = 10; private final VisionServiceOptions serviceOptions; private final ImageAnalysisOptions analysisOptions; private final ObjectDetectedV40Mapper objectDetectedV40Mapper; private final TagDetectedV40Mapper tagDetectedV40Mapper; - public ObjectDetectionResult detect(MultipartFile file) throws Exception { - VisionSource imageSource = getVisionSource(file); + public VisionAnalysisResult getResult(MultipartFile file, String target) { + byte[] data = ImageProcessor.compressUntil(file, IMAGE_SIZE_LIMIT); - try (ImageAnalyzer analyzer = new ImageAnalyzer(serviceOptions, imageSource, analysisOptions); + try (ImageAnalyzer analyzer = new ImageAnalyzer(serviceOptions, getVisionSource(data), + analysisOptions); ImageAnalysisResult result = analyzer.analyze()) { - if (result.getReason() != ImageAnalysisResultReason.ANALYZED) { - ImageAnalysisErrorDetails errorDetails = ImageAnalysisErrorDetails.fromResult(result); - printErrorDetails(errorDetails); - - throw new BaseException(Status.IMAGE_ANALYSIS_FAIL); - } - + catchNotAnalyzedError(result); printDetectionResult(result); printResultDetails(ImageAnalysisResultDetails.fromResult(result)); return ObjectDetectionResult.builder() + .target(target) .objects(objectDetectedV40Mapper.toCustomDetectedObjects(result.getObjects())) .tags(tagDetectedV40Mapper.customDetectedTags(result.getTags())) .build(); + } catch (IOException e) { + throw new BaseException(Status.INVALID_IMAGE); + } catch (Exception e) { + throw new BaseException(Status.INTERNAL_SERVER_ERROR); } } - private VisionSource getVisionSource(MultipartFile file) throws IOException { + private VisionSource getVisionSource(byte[] data) { ImageSourceBuffer imageSourceBuffer = new ImageSourceBuffer(); try (ImageWriter imageWriter = imageSourceBuffer.getWriter()) { - imageWriter.write(ByteBuffer.wrap(file.getBytes())); + imageWriter.write(ByteBuffer.wrap(data)); } return VisionSource.fromImageSourceBuffer(imageSourceBuffer); } + private void catchNotAnalyzedError(ImageAnalysisResult result) { + if (isNotAnalyzed(result)) { + ImageAnalysisErrorDetails errorDetails = ImageAnalysisErrorDetails.fromResult(result); + printErrorDetails(errorDetails); + + throw new BaseException(Status.IMAGE_ANALYSIS_FAIL); + } + } + + private boolean isNotAnalyzed(ImageAnalysisResult result) { + return result.getReason() != ImageAnalysisResultReason.ANALYZED; + } + private void printDetectionResult(ImageAnalysisResult result) { System.out.println(" Image height = " + result.getImageHeight()); System.out.println(" Image width = " + result.getImageWidth()); diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/mapper/ObjectDetectedV32Mapper.java b/src/main/java/com/wakeUpTogetUp/togetUp/infra/azure/vision/mapper/ObjectDetectedV32Mapper.java similarity index 73% rename from src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/mapper/ObjectDetectedV32Mapper.java rename to src/main/java/com/wakeUpTogetUp/togetUp/infra/azure/vision/mapper/ObjectDetectedV32Mapper.java index 1e3c3493..437456a1 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/mapper/ObjectDetectedV32Mapper.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/infra/azure/vision/mapper/ObjectDetectedV32Mapper.java @@ -1,8 +1,9 @@ -package com.wakeUpTogetUp.togetUp.api.mission.service.mapper; +package com.wakeUpTogetUp.togetUp.infra.azure.vision.mapper; import com.microsoft.azure.cognitiveservices.vision.computervision.models.DetectedObject; import com.wakeUpTogetUp.togetUp.api.mission.model.BoundingBox; -import com.wakeUpTogetUp.togetUp.api.mission.model.CustomDetectedObject; +import com.wakeUpTogetUp.togetUp.api.mission.domain.CustomDetectedObject; +import com.wakeUpTogetUp.togetUp.api.mission.model.Coord; import java.util.List; import java.util.stream.Collectors; import org.springframework.stereotype.Component; @@ -20,14 +21,16 @@ private CustomDetectedObject toCustomDetectedObject(DetectedObject detectedObjec String parent = detectedObject.parent() != null ? detectedObject.parent().objectProperty() : null; + Coord coord = new Coord( + detectedObject.rectangle().x(), + detectedObject.rectangle().y()); return CustomDetectedObject.builder() - .object(detectedObject.objectProperty()) + .name(detectedObject.objectProperty()) .parent(parent) .confidence(detectedObject.confidence()) .box(BoundingBox.builder() - .x(detectedObject.rectangle().x()) - .y(detectedObject.rectangle().y()) + .coord(coord) .w(detectedObject.rectangle().w()) .h(detectedObject.rectangle().h()) .build()) diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/mapper/ObjectDetectedV40Mapper.java b/src/main/java/com/wakeUpTogetUp/togetUp/infra/azure/vision/mapper/ObjectDetectedV40Mapper.java similarity index 69% rename from src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/mapper/ObjectDetectedV40Mapper.java rename to src/main/java/com/wakeUpTogetUp/togetUp/infra/azure/vision/mapper/ObjectDetectedV40Mapper.java index b6886820..27b5f593 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/mapper/ObjectDetectedV40Mapper.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/infra/azure/vision/mapper/ObjectDetectedV40Mapper.java @@ -1,8 +1,9 @@ -package com.wakeUpTogetUp.togetUp.api.mission.service.mapper; +package com.wakeUpTogetUp.togetUp.infra.azure.vision.mapper; import com.azure.ai.vision.imageanalysis.DetectedObject; import com.wakeUpTogetUp.togetUp.api.mission.model.BoundingBox; -import com.wakeUpTogetUp.togetUp.api.mission.model.CustomDetectedObject; +import com.wakeUpTogetUp.togetUp.api.mission.domain.CustomDetectedObject; +import com.wakeUpTogetUp.togetUp.api.mission.model.Coord; import java.util.List; import java.util.stream.Collectors; import org.springframework.stereotype.Component; @@ -16,13 +17,16 @@ public List toCustomDetectedObjects(List o } private CustomDetectedObject toCustomDetectedObject(DetectedObject detectedObject) { + Coord coord = new Coord( + detectedObject.getBoundingBox().getX(), + detectedObject.getBoundingBox().getY()); + return CustomDetectedObject.builder() - .object(detectedObject.getName()) + .name(detectedObject.getName()) .parent(null) .confidence(detectedObject.getConfidence()) .box(BoundingBox.builder() - .x(detectedObject.getBoundingBox().getX()) - .y(detectedObject.getBoundingBox().getY()) + .coord(coord) .w(detectedObject.getBoundingBox().getW()) .h(detectedObject.getBoundingBox().getH()) .build()) diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/infra/azure/vision/mapper/TagDetectedV32Mapper.java b/src/main/java/com/wakeUpTogetUp/togetUp/infra/azure/vision/mapper/TagDetectedV32Mapper.java new file mode 100644 index 00000000..7c4ad792 --- /dev/null +++ b/src/main/java/com/wakeUpTogetUp/togetUp/infra/azure/vision/mapper/TagDetectedV32Mapper.java @@ -0,0 +1,24 @@ +package com.wakeUpTogetUp.togetUp.infra.azure.vision.mapper; + +import com.microsoft.azure.cognitiveservices.vision.computervision.models.ImageTag; +import com.wakeUpTogetUp.togetUp.api.mission.domain.CustomDetectedTag; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.stereotype.Component; + +@Component +public class TagDetectedV32Mapper { + + public List toCustomDetectedTags(List tags) { + return tags.stream() + .map(this::toCustomDetectedTag) + .collect(Collectors.toList()); + } + + private CustomDetectedTag toCustomDetectedTag(ImageTag tag) { + return CustomDetectedTag.builder() + .name(tag.name()) + .confidence(tag.confidence()) + .build(); + } +} diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/mapper/TagDetectedV40Mapper.java b/src/main/java/com/wakeUpTogetUp/togetUp/infra/azure/vision/mapper/TagDetectedV40Mapper.java similarity index 83% rename from src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/mapper/TagDetectedV40Mapper.java rename to src/main/java/com/wakeUpTogetUp/togetUp/infra/azure/vision/mapper/TagDetectedV40Mapper.java index dc24a733..a6da101b 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/api/mission/service/mapper/TagDetectedV40Mapper.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/infra/azure/vision/mapper/TagDetectedV40Mapper.java @@ -1,7 +1,7 @@ -package com.wakeUpTogetUp.togetUp.api.mission.service.mapper; +package com.wakeUpTogetUp.togetUp.infra.azure.vision.mapper; import com.azure.ai.vision.imageanalysis.ContentTag; -import com.wakeUpTogetUp.togetUp.api.mission.model.CustomDetectedTag; +import com.wakeUpTogetUp.togetUp.api.mission.domain.CustomDetectedTag; import java.util.List; import java.util.stream.Collectors; import org.springframework.stereotype.Component; diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/infra/google/vision/GoogleVisionService.java b/src/main/java/com/wakeUpTogetUp/togetUp/infra/google/vision/GoogleVisionService.java index abdaf9a9..0291266c 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/infra/google/vision/GoogleVisionService.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/infra/google/vision/GoogleVisionService.java @@ -1,9 +1,13 @@ package com.wakeUpTogetUp.togetUp.infra.google.vision; import com.google.cloud.vision.v1.AnnotateImageResponse; -import com.google.cloud.vision.v1.FaceAnnotation; import com.google.cloud.vision.v1.Feature.Type; -import java.util.List; +import com.wakeUpTogetUp.togetUp.api.mission.domain.FaceAnnotationRecognitionResult; +import com.wakeUpTogetUp.togetUp.api.mission.domain.VisionAnalysisResult; +import com.wakeUpTogetUp.togetUp.api.mission.service.VisionService; +import com.wakeUpTogetUp.togetUp.common.Status; +import com.wakeUpTogetUp.togetUp.exception.BaseException; +import com.wakeUpTogetUp.togetUp.infra.google.vision.mapper.GoogleVisionMapper; import lombok.RequiredArgsConstructor; import org.springframework.cloud.gcp.vision.CloudVisionTemplate; import org.springframework.stereotype.Service; @@ -11,13 +15,24 @@ @Service @RequiredArgsConstructor -public class GoogleVisionService { +public class GoogleVisionService implements VisionService { + private final CloudVisionTemplate cloudVisionTemplate; - public List getFaceRecognitionResult(MultipartFile file) { + private final GoogleVisionMapper googleVisionMapper; + + @Override + public VisionAnalysisResult getResult(MultipartFile file, String target) { AnnotateImageResponse response = - this.cloudVisionTemplate.analyzeImage(file.getResource(), Type.FACE_DETECTION); + cloudVisionTemplate.analyzeImage(file.getResource(), Type.FACE_DETECTION); + + if (response.getFaceAnnotationsList().isEmpty()) { + throw new BaseException(Status.NO_DETECTED_OBJECT); + } - return response.getFaceAnnotationsList(); + return FaceAnnotationRecognitionResult.builder() + .target(target) + .faceAnnotations(googleVisionMapper.toCustomRecognizedEmotions(response.getFaceAnnotationsList())) + .build(); } } diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/infra/google/vision/mapper/GoogleVisionMapper.java b/src/main/java/com/wakeUpTogetUp/togetUp/infra/google/vision/mapper/GoogleVisionMapper.java new file mode 100644 index 00000000..0988d9c5 --- /dev/null +++ b/src/main/java/com/wakeUpTogetUp/togetUp/infra/google/vision/mapper/GoogleVisionMapper.java @@ -0,0 +1,48 @@ +package com.wakeUpTogetUp.togetUp.infra.google.vision.mapper; + +import com.google.cloud.vision.v1.FaceAnnotation; +import com.google.cloud.vision.v1.Vertex; +import com.wakeUpTogetUp.togetUp.api.mission.domain.CustomDetectedFaceAnnotation; +import com.wakeUpTogetUp.togetUp.api.mission.model.BoundingBox; +import com.wakeUpTogetUp.togetUp.api.mission.model.Coord; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.stereotype.Component; + +@Component +public class GoogleVisionMapper { + + public List toCustomRecognizedEmotions(List faceAnnotations) { + return faceAnnotations.stream() + .map(this::toCustomRecognizedEmotion) + .collect(Collectors.toList()); + } + + private CustomDetectedFaceAnnotation toCustomRecognizedEmotion(FaceAnnotation faceAnnotation) { + return CustomDetectedFaceAnnotation.builder() + .joyLikelihood(faceAnnotation.getJoyLikelihoodValue()) + .sorrowLikelihood(faceAnnotation.getSorrowLikelihoodValue()) + .angerLikelihood(faceAnnotation.getAngerLikelihoodValue()) + .surpriseLikelihood(faceAnnotation.getSurpriseLikelihoodValue()) + .confidence(faceAnnotation.getDetectionConfidence()) + .box(toBoundingBox(faceAnnotation.getBoundingPoly().getVerticesList())) + .build(); + } + + private BoundingBox toBoundingBox(List vertexs) { + int x1 = vertexs.get(0).getX(); + int y1 = vertexs.get(0).getY(); + int x2 = vertexs.get(2).getX(); + int y2 = vertexs.get(2).getY(); + + Coord coord = new Coord(x1, y1); + int width = x2 - x1; + int height = y2 - y1; + + return BoundingBox.builder() + .coord(coord) + .w(width) + .h(height) + .build(); + } +} diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/utils/ImageProcessor.java b/src/main/java/com/wakeUpTogetUp/togetUp/utils/ImageProcessor.java deleted file mode 100644 index dc2adbaf..00000000 --- a/src/main/java/com/wakeUpTogetUp/togetUp/utils/ImageProcessor.java +++ /dev/null @@ -1,265 +0,0 @@ -package com.wakeUpTogetUp.togetUp.utils; - -import static org.apache.commons.imaging.Imaging.getMetadata; - -import com.google.cloud.vision.v1.FaceAnnotation; -import com.google.cloud.vision.v1.Vertex; -import com.wakeUpTogetUp.togetUp.api.mission.model.BoundingBox; -import com.wakeUpTogetUp.togetUp.api.mission.model.CustomAnalysisEntity; -import java.awt.BasicStroke; -import java.awt.Color; -import java.awt.Font; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.Image; -import java.awt.geom.AffineTransform; -import java.awt.geom.Rectangle2D; -import java.awt.image.AffineTransformOp; -import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.Iterator; -import java.util.List; -import java.util.Random; -import javax.imageio.IIOImage; -import javax.imageio.ImageIO; -import javax.imageio.ImageWriteParam; -import javax.imageio.ImageWriter; -import org.apache.commons.imaging.ImageReadException; -import org.apache.commons.imaging.common.ImageMetadata; -import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata; -import org.apache.commons.imaging.formats.tiff.TiffField; -import org.apache.commons.imaging.formats.tiff.TiffImageMetadata; -import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants; -import org.springframework.stereotype.Component; -import org.springframework.web.multipart.MultipartFile; - -@Component -public class ImageProcessor { - - public byte[] compress(byte[] imageToByte, float quality) throws IOException { - BufferedImage originalImage = readImage(imageToByte); - - ByteArrayOutputStream compressedImageStream = new ByteArrayOutputStream(); - Iterator writers = ImageIO.getImageWritersByFormatName("jpg"); - - if (writers.hasNext()) { - ImageWriter writer = writers.next(); - ImageWriteParam param = writer.getDefaultWriteParam(); - - if (param.canWriteCompressed()) { - param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); - param.setCompressionQuality(quality); // 0.0 to 1.0 - } - - writer.setOutput(ImageIO.createImageOutputStream(compressedImageStream)); - writer.write(null, new IIOImage(originalImage, null, null), param); - writer.dispose(); - } else { - throw new IllegalStateException("No writers found"); - } - compressedImageStream.close(); - - return compressedImageStream.toByteArray(); - } - - public byte[] resize(byte[] imageToByte, double ratio) throws IOException { - BufferedImage originalImage = readImage(imageToByte); - - int scaledWidth = (int) (originalImage.getWidth() * ratio); - int scaledHeight = (int) (originalImage.getHeight() * ratio); - - Image resizeImage = originalImage - .getScaledInstance(scaledWidth, scaledHeight, Image.SCALE_FAST); - BufferedImage newImage = - new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_RGB); - - Graphics gp = newImage.getGraphics(); - gp.drawImage(resizeImage, 0, 0, null); - gp.dispose(); - - ByteArrayOutputStream compressedImageStream = new ByteArrayOutputStream(); - ImageIO.write(newImage, "jpg", compressedImageStream); - compressedImageStream.close(); - - return compressedImageStream.toByteArray(); - } - - public byte[] rotate(byte[] imageToByte, int degrees) throws IOException { - BufferedImage originalImage = readImage(imageToByte); - - AffineTransform transform = new AffineTransform(); - transform.rotate(Math.toRadians(degrees), - (double) originalImage.getWidth() / 2, - (double) originalImage.getHeight() / 2); - AffineTransformOp op = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR); - - BufferedImage rotatedImage = op.filter(originalImage, null); - - ByteArrayOutputStream rotatedImageStream = new ByteArrayOutputStream(); - ImageIO.write(rotatedImage, "jpg", rotatedImageStream); - rotatedImageStream.close(); - - return rotatedImageStream.toByteArray(); - } - - public byte[] orientImage(byte[] imageToByte, TiffImageMetadata metadata) - throws IOException, ImageReadException { - TiffField orientationField = metadata != null - ? metadata.findField(TiffTagConstants.TIFF_TAG_ORIENTATION) - : null; - int orientation = orientationField != null ? orientationField.getIntValue() : 1; - - BufferedImage originalImage = readImage(imageToByte); - AffineTransform affineTransform = new AffineTransform(); - - switch (orientation) { - case 1: - break; - case 2: // Flip horizontally - affineTransform.scale(-1.0, 1.0); - affineTransform.translate(-originalImage.getWidth(), 0); - break; - case 3: // Rotate 180 degrees - affineTransform.translate(originalImage.getWidth(), originalImage.getHeight()); - affineTransform.rotate(Math.PI); - break; - case 4: // Flip vertically - affineTransform.scale(1.0, -1.0); - affineTransform.translate(0, -originalImage.getHeight()); - break; - case 5: // Flip vertically and rotate 90 degrees - affineTransform.scale(1.0, -1.0); - affineTransform.translate(0, -originalImage.getHeight()); - affineTransform.translate(originalImage.getHeight(), 0); - affineTransform.rotate(Math.PI / 2); - break; - case 6: // Rotate 90 degrees - affineTransform.translate(originalImage.getHeight(), 0); - affineTransform.rotate(Math.PI / 2); - break; - case 7: // Flip horizontally and rotate 90 degrees - affineTransform.scale(-1.0, 1.0); - affineTransform.translate(-originalImage.getHeight(), 0); - affineTransform.translate(originalImage.getHeight(), 0); - affineTransform.rotate(Math.PI / 2); - break; - case 8: // Rotate 270 degrees - affineTransform.translate(0, originalImage.getWidth()); - affineTransform.rotate(3 * Math.PI / 2); - break; - } - - AffineTransformOp op = - new AffineTransformOp(affineTransform, AffineTransformOp.TYPE_BILINEAR); - BufferedImage rotatedImage = op.filter(originalImage, null); - - // AffineTransformOp에 의해 반환된 BufferedImage의 타입이 원본과 다를 수 있어 때로 약간의 색상 변화나 문제가 발생할 수 있다. - // 새로운 BufferedImage를 생성하고 그 위에 결과 이미지를 그린다. - BufferedImage newImage = - new BufferedImage(rotatedImage.getWidth(), rotatedImage.getHeight(), BufferedImage.TYPE_INT_RGB); - - Graphics2D g = newImage.createGraphics(); - g.drawImage(rotatedImage, 0, 0, null); - g.dispose(); - - ByteArrayOutputStream rotatedImageStream = new ByteArrayOutputStream(); - ImageIO.write(newImage, "jpg", rotatedImageStream); - rotatedImageStream.close(); - - return rotatedImageStream.toByteArray(); - } - - - // TODO : 인식 결과 그림 그리기 하나로 통일하기 - public byte[] drawODResultOnImage(MultipartFile file, List entities) - throws IOException { - BufferedImage originalImage = readImage(file.getBytes()); - Graphics2D g2d = originalImage.createGraphics(); - - for (CustomAnalysisEntity entity : entities) { - BoundingBox box = entity.getBox(); - - int minDwDh = Math.min(originalImage.getWidth(), originalImage.getHeight()); - - int thickness = minDwDh / 150; - Random rand = new Random(); - g2d.setColor(new Color(rand.nextInt(256), rand.nextInt(256), rand.nextInt(256))); - g2d.setStroke(new BasicStroke(thickness)); - g2d.draw(new Rectangle2D.Float(box.getX(), box.getY(), box.getW(), box.getH())); - - int fontSize = minDwDh / 25; - g2d.setFont(new Font("Arial", Font.PLAIN, fontSize)); - g2d.drawString( - entity.getName() + " : " + String.format("%.3f", entity.getConfidence()), - box.getX(), box.getY() - (float) (originalImage.getHeight() / 100)); - } - g2d.dispose(); - - var outputImageStream = new ByteArrayOutputStream(); - ImageIO.write(originalImage, "jpg", outputImageStream); - outputImageStream.close(); - - return outputImageStream.toByteArray(); - } - - public byte[] drawFRResultOnImage(MultipartFile file, List faceAnnotations, String object) - throws IOException { - BufferedImage originalImage = readImage(file.getBytes()); - - Graphics2D g2d = originalImage.createGraphics(); - - for (FaceAnnotation face : faceAnnotations) { - List coord = face.getBoundingPoly().getVerticesList(); - - int x1 = coord.get(0).getX(); - int y1 = coord.get(0).getY(); - int x2 = coord.get(2).getX(); - int y2 = coord.get(2).getY(); - - int width = x2 - x1; - int height = y2 - y1; - - int minDwDh = Math.min(originalImage.getWidth(), originalImage.getHeight()); - int thickness = minDwDh / 150; - - Random rand = new Random(); - g2d.setColor(new Color(rand.nextInt(256), rand.nextInt(256), rand.nextInt(256))); - g2d.setStroke(new BasicStroke(thickness)); - g2d.draw(new Rectangle2D.Float(x1, y1, width, height)); - - int fontSize = minDwDh / 25; - g2d.setFont(new Font("Arial", Font.PLAIN, fontSize)); - g2d.drawString( - object, - x1, y1 - (float) (originalImage.getHeight() / 100)); - } - g2d.dispose(); - - var outputImageStream = new ByteArrayOutputStream(); - ImageIO.write(originalImage, "jpg", outputImageStream); - outputImageStream.close(); - - return outputImageStream.toByteArray(); - } - - private BufferedImage readImage(byte[] data) throws IOException { - return ImageIO.read(new ByteArrayInputStream(data)); - } - - public TiffImageMetadata getImageMetadata(MultipartFile file) - throws IOException, ImageReadException { - return readExifMetadata(file.getBytes()); - } - - private TiffImageMetadata readExifMetadata(byte[] jpegData) - throws IOException, ImageReadException { - ImageMetadata imageMetadata = getMetadata(jpegData); - if (imageMetadata == null) { - return null; - } - JpegImageMetadata jpegMetadata = (JpegImageMetadata) imageMetadata; - return jpegMetadata.getExif(); - } -} \ No newline at end of file diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/utils/FileUtil.java b/src/main/java/com/wakeUpTogetUp/togetUp/utils/file/FileUtil.java similarity index 63% rename from src/main/java/com/wakeUpTogetUp/togetUp/utils/FileUtil.java rename to src/main/java/com/wakeUpTogetUp/togetUp/utils/file/FileUtil.java index 88f4dba3..d206576f 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/utils/FileUtil.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/utils/file/FileUtil.java @@ -1,9 +1,14 @@ -package com.wakeUpTogetUp.togetUp.utils; +package com.wakeUpTogetUp.togetUp.utils.file; +import com.wakeUpTogetUp.togetUp.common.Status; +import com.wakeUpTogetUp.togetUp.exception.BaseException; +import com.wakeUpTogetUp.togetUp.utils.DateTimeProvider; +import java.io.IOException; import java.util.Locale; import java.util.UUID; import lombok.AccessLevel; import lombok.NoArgsConstructor; +import org.springframework.web.multipart.MultipartFile; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class FileUtil { @@ -11,6 +16,14 @@ public class FileUtil { private static final String FILE_NAME_FORMAT = "%s/%s/%s"; private static final String FILE_DATE_TIME_FORMAT = "yyyy/MM/dd"; + public static byte[] getBytes(MultipartFile file) { + try { + return file.getBytes(); + } catch (IOException e) { + throw new BaseException("파일 읽기를 실패했습니다.", e, Status.INVALID_IMAGE); + } + } + public static String extractExtension(String fileName) { FileValidator.validateFileName(fileName); diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/utils/FileValidator.java b/src/main/java/com/wakeUpTogetUp/togetUp/utils/file/FileValidator.java similarity index 97% rename from src/main/java/com/wakeUpTogetUp/togetUp/utils/FileValidator.java rename to src/main/java/com/wakeUpTogetUp/togetUp/utils/file/FileValidator.java index 0629aed0..36da6121 100644 --- a/src/main/java/com/wakeUpTogetUp/togetUp/utils/FileValidator.java +++ b/src/main/java/com/wakeUpTogetUp/togetUp/utils/file/FileValidator.java @@ -1,4 +1,4 @@ -package com.wakeUpTogetUp.togetUp.utils; +package com.wakeUpTogetUp.togetUp.utils.file; import com.wakeUpTogetUp.togetUp.infra.aws.s3.model.ImageContentType; import com.wakeUpTogetUp.togetUp.common.Status; diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/utils/image/ImageDrawer.java b/src/main/java/com/wakeUpTogetUp/togetUp/utils/image/ImageDrawer.java new file mode 100644 index 00000000..d7106c59 --- /dev/null +++ b/src/main/java/com/wakeUpTogetUp/togetUp/utils/image/ImageDrawer.java @@ -0,0 +1,102 @@ +package com.wakeUpTogetUp.togetUp.utils.image; + +import com.wakeUpTogetUp.togetUp.api.mission.domain.CustomAnalysisEntity; +import com.wakeUpTogetUp.togetUp.api.mission.model.BoundingBox; +import com.wakeUpTogetUp.togetUp.api.mission.model.Coord; +import com.wakeUpTogetUp.togetUp.common.Status; +import com.wakeUpTogetUp.togetUp.exception.BaseException; +import com.wakeUpTogetUp.togetUp.infra.aws.s3.model.ImageContentType; +import com.wakeUpTogetUp.togetUp.utils.file.FileUtil; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.Random; +import javax.imageio.ImageIO; +import org.apache.commons.imaging.formats.tiff.TiffImageMetadata; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +@Component +public class ImageDrawer extends ImageUtil { + + private final Random rand = new Random(); + private BufferedImage image; + private Color color; + private int thickness; + private int fontSize; + + private void setup(MultipartFile file) { + setOrientedImage(file); + + this.color = new Color(rand.nextInt(256), rand.nextInt(256), rand.nextInt(256)); + int minWH = Math.min(image.getWidth(), image.getHeight()); + this.thickness = minWH / 150; + this.fontSize = minWH / 23; + } + + private void setOrientedImage(MultipartFile file) { + this.image = readImage(FileUtil.getBytes(file)); + TiffImageMetadata metadata = getImageMetadata(file); + image = ImageProcessor.orientImage(image, metadata); + } + + public byte[] drawResultOnImage(MultipartFile file, List entities, String object) { + setup(file); + + drawEntityRectangles(entities); + drawEntityLabel(entities, object); + + String format = ImageContentType + .getByContentType(file.getContentType()) + .getExtension(); + + try (ByteArrayOutputStream outputImageStream = new ByteArrayOutputStream()) { + ImageIO.write(image, format, outputImageStream); + return outputImageStream.toByteArray(); + } catch (IOException e) { + throw new BaseException(IMAGE_WRITE_ERROR_MESSAGE, e, Status.INVALID_IMAGE); + } + } + + private void drawEntityRectangles(List entities) { + Graphics2D g2d = image.createGraphics(); + + for (CustomAnalysisEntity entity : entities) { + BoundingBox box = entity.getBox(); + + drawRectangle(g2d, box); + } + g2d.dispose(); + } + + private void drawEntityLabel(List entities, String object) { + Graphics2D g2d = image.createGraphics(); + + for (CustomAnalysisEntity entity : entities) { + BoundingBox box = entity.getBox(); + Coord coord = new Coord(box.getX(), box.getY() - (image.getHeight() / 100)); + String entityLabel = String.format("%s : %.3f", object, entity.getConfidence()); + + drawString(g2d, coord, entityLabel); + } + g2d.dispose(); + } + + private void drawRectangle(Graphics2D g2d, BoundingBox box) { + g2d.setColor(color); + g2d.setStroke(new BasicStroke(thickness)); + g2d.draw(new Rectangle2D.Float(box.getX(), box.getY(), box.getW(), box.getH())); + } + + private void drawString(Graphics2D g2d, Coord coord, String content) { + g2d.setColor(color); + g2d.setFont(new Font("Arial", Font.PLAIN, fontSize)); + g2d.drawString(content, coord.getX(), coord.getY()); + } +} diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/utils/image/ImageProcessor.java b/src/main/java/com/wakeUpTogetUp/togetUp/utils/image/ImageProcessor.java new file mode 100644 index 00000000..6c0cc47f --- /dev/null +++ b/src/main/java/com/wakeUpTogetUp/togetUp/utils/image/ImageProcessor.java @@ -0,0 +1,218 @@ +package com.wakeUpTogetUp.togetUp.utils.image; + +import com.wakeUpTogetUp.togetUp.common.Status; +import com.wakeUpTogetUp.togetUp.exception.BaseException; +import com.wakeUpTogetUp.togetUp.infra.aws.s3.model.ImageContentType; +import com.wakeUpTogetUp.togetUp.utils.file.FileUtil; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.geom.AffineTransform; +import java.awt.image.AffineTransformOp; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Iterator; +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.formats.tiff.TiffField; +import org.apache.commons.imaging.formats.tiff.TiffImageMetadata; +import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants; +import org.springframework.web.multipart.MultipartFile; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ImageProcessor extends ImageUtil { + + public static byte[] compressUntil(MultipartFile file, int sizeOfMB) { + ImageContentType contentType = ImageContentType.getByContentType(file.getContentType()); + byte[] data = FileUtil.getBytes(file); + int sizeLimit = 1024 * 1024 * sizeOfMB; + + if (data.length < sizeLimit) { + return data; + } + + // TODO: 디버그용 삭제 + System.out.println("[압축]"); + System.out.println("original size: " + data.length); + int cnt = 1; + while (data.length >= sizeLimit && cnt++ <= 3) { + data = compress(data, 0.5f, contentType); + System.out.println(data.length); + } + + TiffImageMetadata metadata = getImageMetadata(file); + return orientByteImage(data, metadata, contentType); + } + + private static byte[] compress(byte[] imageByte, float quality, ImageContentType contentType) { + BufferedImage originalImage = readImage(imageByte); + + try (ByteArrayOutputStream compressedImageStream = new ByteArrayOutputStream()) { + Iterator writers = + ImageIO.getImageWritersByFormatName(contentType.getExtension()); + + if (writers.hasNext()) { + ImageWriter writer = writers.next(); + ImageWriteParam param = writer.getDefaultWriteParam(); + setCompressImageWriterParam(param, quality); + + writer.setOutput(ImageIO.createImageOutputStream(compressedImageStream)); + writer.write(null, new IIOImage(originalImage, null, null), param); + writer.dispose(); + } else { + throw new IllegalStateException("No writers found"); + } + + return compressedImageStream.toByteArray(); + } catch (IOException e) { + throw new BaseException(IMAGE_WRITE_ERROR_MESSAGE, e, Status.INVALID_IMAGE); + } + } + + private static void setCompressImageWriterParam(ImageWriteParam param, float quality) { + if (param.canWriteCompressed()) { + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionQuality(quality); + } + } + + public static byte[] resize(byte[] imageByte, double ratio) throws IOException { + BufferedImage originalImage = readImage(imageByte); + + int scaledWidth = (int) (originalImage.getWidth() * ratio); + int scaledHeight = (int) (originalImage.getHeight() * ratio); + + Image resizeImage = originalImage + .getScaledInstance(scaledWidth, scaledHeight, Image.SCALE_FAST); + BufferedImage newImage = + new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_RGB); + + Graphics gp = newImage.getGraphics(); + gp.drawImage(resizeImage, 0, 0, null); + gp.dispose(); + + ByteArrayOutputStream compressedImageStream = new ByteArrayOutputStream(); + ImageIO.write(newImage, "jpg", compressedImageStream); + compressedImageStream.close(); + + return compressedImageStream.toByteArray(); + } + + public static byte[] rotate(byte[] imageByte, int degrees) throws IOException { + BufferedImage originalImage = readImage(imageByte); + + AffineTransform transform = new AffineTransform(); + transform.rotate(Math.toRadians(degrees), + (double) originalImage.getWidth() / 2, + (double) originalImage.getHeight() / 2); + AffineTransformOp op = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR); + + BufferedImage rotatedImage = op.filter(originalImage, null); + + ByteArrayOutputStream rotatedImageStream = new ByteArrayOutputStream(); + ImageIO.write(rotatedImage, "jpg", rotatedImageStream); + rotatedImageStream.close(); + + return rotatedImageStream.toByteArray(); + } + + protected static byte[] orientByteImage(byte[] imageByte, TiffImageMetadata metadata, ImageContentType contentType) { + int orientation = getOrientation(metadata); + BufferedImage originalImage = readImage(imageByte); + BufferedImage rotatedImage = transformImage(originalImage, orientation); + + BufferedImage newImage = new BufferedImage( + rotatedImage.getWidth(), + rotatedImage.getHeight(), + BufferedImage.TYPE_INT_RGB); + + Graphics2D g = newImage.createGraphics(); + g.drawImage(rotatedImage, 0, 0, null); + g.dispose(); + + try (ByteArrayOutputStream rotatedImageStream = new ByteArrayOutputStream();) { + ImageIO.write(newImage, contentType.getExtension(), rotatedImageStream); + return rotatedImageStream.toByteArray(); + } catch (IOException e) { + throw new BaseException(IMAGE_WRITE_ERROR_MESSAGE, e, Status.INVALID_IMAGE); + } + } + + protected static BufferedImage orientImage(BufferedImage image, TiffImageMetadata metadata) { + int orientation = getOrientation(metadata); + image = transformImage(image, orientation); + + BufferedImage newImage = new BufferedImage( + image.getWidth(), + image.getHeight(), + BufferedImage.TYPE_INT_RGB); + + Graphics2D g = newImage.createGraphics(); + g.drawImage(image, 0, 0, null); + + return newImage; + } + + private static int getOrientation(TiffImageMetadata metadata) { + try { + TiffField tiffField = metadata != null + ? metadata.findField(TiffTagConstants.TIFF_TAG_ORIENTATION) + : null; + + return tiffField != null ? tiffField.getIntValue() : 1; + } catch (ImageReadException e) { + throw new BaseException(e, Status.INVALID_IMAGE); + } + } + + private static BufferedImage transformImage(BufferedImage originalImage, int orientation) { + AffineTransform affineTransform = new AffineTransform(); + + switch (orientation) { + case 1: + break; + case 2: // Flip horizontally + affineTransform.scale(-1.0, 1.0); + affineTransform.translate(-originalImage.getWidth(), 0); + break; + case 3: // Rotate 180 degrees + affineTransform.translate(originalImage.getWidth(), originalImage.getHeight()); + affineTransform.rotate(Math.PI); + break; + case 4: // Flip vertically + affineTransform.scale(1.0, -1.0); + affineTransform.translate(0, -originalImage.getHeight()); + break; + case 5: // Flip vertically and rotate 90 degrees + affineTransform.scale(1.0, -1.0); + affineTransform.translate(0, -originalImage.getHeight()); + affineTransform.translate(originalImage.getHeight(), 0); + affineTransform.rotate(Math.PI / 2); + break; + case 6: // Rotate 90 degrees + affineTransform.translate(originalImage.getHeight(), 0); + affineTransform.rotate(Math.PI / 2); + break; + case 7: // Flip horizontally and rotate 90 degrees + affineTransform.scale(-1.0, 1.0); + affineTransform.translate(-originalImage.getHeight(), 0); + affineTransform.translate(originalImage.getHeight(), 0); + affineTransform.rotate(Math.PI / 2); + break; + case 8: // Rotate 270 degrees + affineTransform.translate(0, originalImage.getWidth()); + affineTransform.rotate(3 * Math.PI / 2); + break; + } + + AffineTransformOp op = new AffineTransformOp(affineTransform, AffineTransformOp.TYPE_BILINEAR); + + return op.filter(originalImage, null); + } +} \ No newline at end of file diff --git a/src/main/java/com/wakeUpTogetUp/togetUp/utils/image/ImageUtil.java b/src/main/java/com/wakeUpTogetUp/togetUp/utils/image/ImageUtil.java new file mode 100644 index 00000000..452c9662 --- /dev/null +++ b/src/main/java/com/wakeUpTogetUp/togetUp/utils/image/ImageUtil.java @@ -0,0 +1,51 @@ +package com.wakeUpTogetUp.togetUp.utils.image; + +import static org.apache.commons.imaging.Imaging.getMetadata; + +import com.wakeUpTogetUp.togetUp.common.Status; +import com.wakeUpTogetUp.togetUp.exception.BaseException; +import com.wakeUpTogetUp.togetUp.utils.file.FileUtil; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import javax.imageio.ImageIO; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.commons.imaging.common.ImageMetadata; +import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata; +import org.apache.commons.imaging.formats.tiff.TiffImageMetadata; +import org.springframework.web.multipart.MultipartFile; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ImageUtil { + + protected static final String IMAGE_IO_ERROR_MESSAGE = "이미지 입출력을 실패했습니다"; + protected static final String IMAGE_WRITE_ERROR_MESSAGE = "이미지 쓰기에 실패했습니다."; + private static final String EXTRACT_METADATA_ERROR_MESSAGE = "파일 메타데이터 추출에 실패했습니다."; + + protected static BufferedImage readImage(byte[] data) { + try { + return ImageIO.read(new ByteArrayInputStream(data)); + } catch (IOException e) { + throw new BaseException(IMAGE_IO_ERROR_MESSAGE, e, Status.INVALID_IMAGE); + } + } + + protected static TiffImageMetadata getImageMetadata(MultipartFile file) { + return readExifMetadata(FileUtil.getBytes(file)); + } + + // TODO: 다른 파일 형식 메타 데이터도 가져오게 + private static TiffImageMetadata readExifMetadata(byte[] data) { + try{ + ImageMetadata imageMetadata = getMetadata(data); + if (imageMetadata == null) { + return null; + } + JpegImageMetadata jpegMetadata = (JpegImageMetadata) imageMetadata; + return jpegMetadata.getExif(); + } catch (Exception e) { + throw new BaseException(EXTRACT_METADATA_ERROR_MESSAGE, e, Status.INVALID_IMAGE); + } + } +} \ No newline at end of file