Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#80 [REFACTOR] 표정 인식 API 리팩토링 및 미션 수행 API 통합 #110

Merged
merged 24 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a8a19b0
#80 [REFACTOR] CustomAnalysisEntity 관련 객체들 domain으로 이전
05AM Mar 26, 2024
d7264b2
#80 [REMOVE] 사용되지 않는 모델로 객체 인식 코드 삭제
05AM Mar 26, 2024
f72bae9
#80 [REFACTOR] 외부 서비스 mapper를 infra 패키지로 이전
05AM Mar 26, 2024
de5e00f
#80 [REFACTOR] 좌표 클래스 추출
05AM Mar 27, 2024
b86dcb7
#80 [REFACTOR] VisionAnalysisResult 상속관계 변경 및 필드 추가
05AM Mar 27, 2024
f76356f
#80 [REFACTOR] CustomAnalysisEntity 필드 추가
05AM Mar 27, 2024
615dfba
#80 [REFACTOR] 표정 인식 결과 클래스 구현
05AM Mar 27, 2024
7830ae3
#80 [REFACTOR] 표정 인식 결과 반환 메서드 리팩토링
05AM Mar 27, 2024
de89963
#80 [REFACTOR] ImageProcessor 클래스 책임을 여러 클래스로 분리
05AM Mar 27, 2024
b06d70a
#80 [REFACTOR] 인식 api request 조건 만족을 위해 이미지 압축 기능 추가
05AM Mar 27, 2024
e543a82
Merge branch 'main' of https://github.com/Wake-up-together-TogetUp/to…
05AM Mar 27, 2024
8cc3fa2
#80 [REFACTOR] 이미지 유틸 클래스 패키지 이전
05AM Mar 27, 2024
91a98d3
#80 [REFACTOR] file util 클래스들 패키지 이전
05AM Mar 27, 2024
ac20a50
#80 [REFACTOR] MissionImageService 패키지 이전
05AM Mar 27, 2024
4722bf9
#80 [REFACTOR] 미션 수행 결과 반환 메서드 통합
05AM Mar 27, 2024
2ac1790
#80 [REFACTOR] 미션 유형에 따라 CustomFile 반환 방식을 동적으로 선택하는 전략 패턴 구현
05AM Mar 27, 2024
22d0750
#80 [REFACTOR] 미션 수행 API 통합
05AM Mar 27, 2024
071ce10
#80 [FIX] 아직 지원하지 않는 이미지 파일 형식 주석 처리
05AM Mar 27, 2024
867e9ec
#80 [REFACTOR] 폰트 사이즈 조정 및 불필요한 코드 삭제
05AM Mar 27, 2024
bb64c34
#80 [FIX] 압축 무한 루프 버그 해결: 추후 추가 리팩토링 예정
05AM Mar 27, 2024
f28f4b2
#80 [REFACTOR] 미션 수행 API 명세서 설명 추가
05AM Mar 27, 2024
af66244
#80 [REFACTOR] mission 도메인 repository 패키지 이전
05AM Mar 27, 2024
2448356
#80 [FIX] 직접 촬영의 경우 object validation 삭제
05AM Mar 27, 2024
7ab6d3d
#80 [FIX] MissionType name 필드 오타 수정
05AM Mar 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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}")
Expand All @@ -63,10 +60,11 @@ BaseResponse<GetMissionWithObjectListRes> 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(
Expand All @@ -79,37 +77,11 @@ BaseResponse<GetMissionWithObjectListRes> getObjectDetectionMissions(
public BaseResponse<MissionPerfomRes> 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<CustomAnalysisEntity> 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<MissionPerfomRes> 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<FaceAnnotation> 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 = "미션 완료 - 수행 기록 생성 및 경험치, 레벨 정산 및 경우에 따라 아바타 해금")
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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<CustomDetectedFaceAnnotation> faceAnnotations;

@Builder
public FaceAnnotationRecognitionResult(String target, List<CustomDetectedFaceAnnotation> 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<CustomAnalysisEntity> getEntities() {
return faceAnnotations.stream()
.map(CustomAnalysisEntity.class::cast)
.collect(Collectors.toList());
}

@Override
public List<CustomAnalysisEntity> 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();
});
}
}
Loading
Loading