현재 진행 중인 빈빈 프로젝트에서는 CCTV나 카메라 영상에서 캡쳐된 이미지를 기반으로 객체를 검출하여
"사람"이 몇 명 있는지를 높은 정확도로 보여줄 수 있어야 한다.
뿐만 아니라 검출된 객체들의 개별 위치를 2차원 좌표값으로 가져와서 매장 도면에 표시할 수 있어야 한다.
위와 같은 기능을 개발하기 위해 AWS Rekognition과 Google Vision API 같은 AI 비전 인식 플랫폼을 활용하기로 하였다.
일차적인 단계로는 저 둘을 비교해보고 우리 프로젝트에 더 적합한 플랫폼을 찾기로 하였다.
AWS Rekognition
1. 일반 객체 검출 기능
DetectLabels - Amazon Rekognition
DetectLabels - Amazon Rekognition
DetectLabels Detects instances of real-world entities within an image (JPEG or PNG) provided as input. This includes objects like flower, tree, and table; events like wedding, graduation, and birthday party; and concepts like landscape, evening, and nature
docs.aws.amazon.com
Rekognition 자체 사전 학습 모델 이용 (S3 버킷에 있는 이미지 불러와서 객체 검출 가능)
데모 사이트는 다음과 같다. 아래 사이트에서 이미지를 등록하여 본격적으로 사용하기 전 어떤 식으로 객체가 검출되는지를 손쉽게 확인할 수 있다.
ap-northeast-2.console.aws.amazon.com
https://ap-northeast-2.console.aws.amazon.com/rekognition/home?region=ap-northeast-2#/label-detection
ap-northeast-2.console.aws.amazon.com
2. 사용자 정의 레이블 기능
Getting started with Amazon Rekognition Custom Labels - Rekognition
Getting started with Amazon Rekognition Custom Labels - Rekognition
Thanks for letting us know this page needs work. We're sorry we let you down. If you've got a moment, please tell us how we can make the documentation better.
docs.aws.amazon.com
사전 학습된 모델이 아닌, 사용자가 직접 라벨링한 이미지 데이터셋을 기반으로 학습시킨 모델을 Rekognition에서 사용할 수 있게 해주는 기능이다.
만약 일반 Rekognition, Google Vision API 둘 다 책상, 의자, 사람 객체를 동시에 검출하지 못한다면 위와 같은 사용자 라벨링 데이터셋을 만들어서 하는 방법으로 진행해야할 듯 하다.
의존성 설정
implementation 'software.amazon.awssdk:rekognition:2.31.31'
AwsRekognitionConfiguration
AWS Rekognition을 사용하기 위한 설정 클래스이다. RekognitionClient 빈을 생성하여 AWS와의 이미지 분석 API 통신을 할 수 있도록 한다.
@Configuration
public class AwsRekognitionConfiguration {
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;
@Bean
public RekognitionClient amazonRekognition() {
AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey);
return RekognitionClient.builder()
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.region(Region.AP_NORTHEAST_2)
.build();
}
}
RekognitionService
RekognitionClient 를 통한 이미지 객체 검출 예시
BoundingBox 객체의 width, height, left, top 을 가공하여 픽셀 좌표 x, y로 변환하고, 이를 별도의 객체로 만들어 반환한다.
@Service
public class RekognitionService {
private static final Logger log = LoggerFactory.getLogger(RekognitionService.class);
private final RekognitionClient client;
public RekognitionService(RekognitionClient client) {
this.client = client;
}
public List<DetectedItem> detectLabels(MultipartFile file) throws IOException {
try {
Image awsImage = Image.builder()
.bytes(SdkBytes.fromByteArray(file.getBytes()))
.build();
DetectLabelsRequest request = DetectLabelsRequest.builder()
.image(awsImage)
.maxLabels(30)
.minConfidence(70F) // 신뢰도 70% 이상만 필터
.build();
DetectLabelsResponse response = client.detectLabels(request);
List<DetectedItem> list = new ArrayList<>();
for (Label label : response.labels()) {
DetectedItem item = new DetectedItem();
item.setKey(label.name());
item.setConfidence(label.confidence());
List<Instance> instances = label.instances();
int count = instances != null ? instances.size() : 0;
item.setValue(count);
if (instances != null && !instances.isEmpty()) {
List<PersonDto> persons = instances.stream()
.map(instance -> {
BoundingBox box = instance.boundingBox();
return PersonDto.create(box);
})
.toList();
item.setpositions(persons);
}
list.add(item);
}
return list;
} catch (RekognitionException e) {
e.printStackTrace();
log.error("Rekognition Exception: ", e.getMessage());
}
return null;
}
}
DetectionRestController
일반적으로는 s3 버킷에서 이미지를 불러오는 방식으로 쓰일테지만 빠른 테스트를 위해 직접 이미지 파일을 보내는 방식으로 만들었다.
@PostMapping(value = "/aws", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
public ResponseEntity<List<DetectedItem>> detectAWS(@RequestPart(value = "image") MultipartFile image) throws IOException {
var response = rekognitionService.detectLabels(image);
return ResponseEntity.status(HttpStatus.OK).body(response);
}
테스트 예시
아래는 위 이미지에 대한 객체 검출 응답이다.
사람 명 수는 99% 이상의 confidence를 보이는 반면 Table이나 Chair 같은 객체들의 검출 정확도가 다소 떨어지는 편이다.
ex) 의자에 사람이 앉아있다. ⇒ 의자나 테이블이 사람에 가려지기 때문에 “Person” 객체와 “Chair/Table” 객체의 정확한 검출이 이루어지지 않을 수 있다.
(위에 링크된 2번 방식의 Custom Detect Labels로 사전 학습 모델 만들어 사용자 지정 레이블을 검출하는 방법이 필요)
응답 예시
[
{
"key": "Cafeteria",
"value": 0,
"confidence": 100.0,
"positions": null
},
{
"key": "Indoors",
"value": 0,
"confidence": 100.0,
"positions": null
},
{
"key": "Restaurant",
"value": 0,
"confidence": 100.0,
"positions": null
},
{
"key": "Cafe",
"value": 0,
"confidence": 99.9985,
"positions": null
},
{
"key": "Person",
"value": 19,
"confidence": 99.92577,
"positions": [
{
"x": 0.03723147138953209,
"y": 0.1312280148267746
},
{
"x": 0.06628157943487167,
"y": 0.20732468366622925
},
{
"x": 0.010972012765705585,
"y": 0.2056771218776703
}
... (길어서 줄임)
]
}
{
"key": "Chair",
"value": 2,
"confidence": 87.05063,
"positions": [
{
"x": 0.013605069369077682,
"y": 0.1912941038608551
},
{
"x": 0.10664529353380203,
"y": 0.15508025884628296
}
]
},
{
"key": "Laptop",
"value": 1,
"confidence": 76.45852,
"positions": [
{
"x": 0.010020907036960125,
"y": 0.03286810591816902
}
]
}
]
...
Google Vision API
구글에서 제공하는 머신러닝 기반 이미지 분석 API 이다.
AWS 에서와 같이 데모 사이트를 제공해준다.
Vision AI: Image and visual AI tools
https://cloud.google.com/vision#demo
cloud.google.com
여러 객체 감지 | Cloud Vision API | Google Cloud
여러 객체 감지 | Cloud Vision API | Google Cloud
의견 보내기 여러 객체 감지 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 참고: Vision API는 현재 모든 기능에 오프라인 비동기 배치 이미지 주석을 지원합
cloud.google.com
의존성 설정
implementation 'com.google.cloud:google-cloud-vision:3.58.0'
implementation 'com.google.protobuf:protobuf-java:3.25.1'
VisionService
@Service
public class VisionService {
public VisionService() {}
public List<DetectedItem> detectObjects(MultipartFile file) throws IOException {
List<AnnotateImageRequest> requests = new ArrayList<>();
ByteString imgBytes = ByteString.readFrom(file.getInputStream());
Image image = Image.newBuilder().setContent(imgBytes).build();
Feature feature = Feature.newBuilder()
.setType(Feature.Type.OBJECT_LOCALIZATION)
.build();
AnnotateImageRequest request = AnnotateImageRequest.newBuilder()
.setImage(image)
.addFeatures(feature)
.build();
requests.add(request);
List<DetectedItem> list = new ArrayList<>();
try (ImageAnnotatorClient client = ImageAnnotatorClient.create()) {
BatchAnnotateImagesResponse response = client.batchAnnotateImages(requests);
List<AnnotateImageResponse> responses = response.getResponsesList();
for (AnnotateImageResponse res : responses) {
for (LocalizedObjectAnnotation entity : res.getLocalizedObjectAnnotationsList()) {
entity
.getBoundingPoly()
.getNormalizedVerticesList()
.forEach(vertex -> System.out.format("- (%s, %s)%n", vertex.getX(), vertex.getY()));
DetectedItem item = new DetectedItem();
item.setKey(entity.getName());
item.setConfidence(entity.getScore());
// 기본적으로 객체 하나당 1개 인식해서 그냥 1로 고정
item.setValue(1);
BoundingPoly box = entity.getBoundingPoly();
if (box.getNormalizedVerticesCount() > 0) {
List<PositionDto> positions = box.getNormalizedVerticesList().stream()
.map(PositionDto::from)
.toList();
item.setPositions(positions);
}
list.add(item);
}
}
}
return list;
}
}
테스트 예시
Rekognition 테스트 시에 사용했던 같은 이미지로 테스트를 진행했다.
AWS Rekognition 과의 응답 차이점은 객체 하나당 1개로 인식한다는 부분이다.
예를 들어 Rekognition에서는 “Person”이라는 객체 하나당 인스턴스가 여러 개 있는데 비해, Vision API에서는 “Person”이라는 객체가 여러 개로 나뉘어져 보여진다.
Google Vision API는 객체마다 별도의 인스턴스로 리턴하기 때문에 "Person"이라는 객체 이름을 기준으로 "사람 수"를 측정할 수 있다.
사람 수의 검출 정확도를 확인해보았을 때, Rekognition에서는 19명으로 검출되었으나 Vision API에서는 10명으로 검출되었다.
응답 예시
[
{
"key": "Person",
"value": 1,
"confidence": 0.75235236,
"positions": [
{
"x": 0.7890625,
"y": 0.56640625
},
{
"x": 0.90625,
"y": 0.56640625
},
{
"x": 0.90625,
"y": 0.8125
},
{
"x": 0.7890625,
"y": 0.8125
}
]
},
{
"key": "Person",
"value": 1,
"confidence": 0.742182,
"positions": [
{
"x": 0.0712890625,
"y": 0.5859375
},
{
"x": 0.212890625,
"y": 0.5859375
},
{
"x": 0.212890625,
"y": 0.9765625
},
{
"x": 0.0712890625,
"y": 0.9765625
}
]
},
{
"key": "Clothing",
"value": 1,
"confidence": 0.72819823,
"positions": [
{
"x": 0.330078125,
"y": 0.796875
},
{
"x": 0.466796875,
"y": 0.796875
},
{
"x": 0.466796875,
"y": 0.9921875
},
{
"x": 0.330078125,
"y": 0.9921875
}
]
},
{
"key": "Clothing",
"value": 1,
"confidence": 0.726897,
"positions": [
{
"x": 0.09375,
"y": 0.640625
},
{
"x": 0.2138671875,
"y": 0.640625
},
{
"x": 0.2138671875,
"y": 0.8125
},
{
"x": 0.09375,
"y": 0.8125
}
]
},
{
"key": "Person",
"value": 1,
"confidence": 0.7221366,
"positions": [
{
"x": 0.33203125,
"y": 0.56640625
},
{
"x": 0.439453125,
"y": 0.56640625
},
{
"x": 0.439453125,
"y": 0.796875
},
{
"x": 0.33203125,
"y": 0.796875
}
]
},
{
"key": "Person",
"value": 1,
"confidence": 0.7203831,
"positions": [
{
"x": 0.7890625,
"y": 0.56640625
},
{
"x": 0.90625,
"y": 0.56640625
},
{
"x": 0.90625,
"y": 0.8125
},
{
"x": 0.7890625,
"y": 0.8125
}
]
},
{
"key": "Person",
"value": 1,
"confidence": 0.7201729,
"positions": [
{
"x": 0.0712890625,
"y": 0.5859375
},
{
"x": 0.212890625,
"y": 0.5859375
},
{
"x": 0.212890625,
"y": 0.9765625
},
{
"x": 0.0712890625,
"y": 0.9765625
}
]
},
{
"key": "Person",
"value": 1,
"confidence": 0.72008365,
"positions": [
{
"x": 0.421875,
"y": 0.48046875
},
{
"x": 0.458984375,
"y": 0.48046875
},
{
"x": 0.458984375,
"y": 0.67578125
},
{
"x": 0.421875,
"y": 0.67578125
}
]
},
{
"key": "Person",
"value": 1,
"confidence": 0.6994652,
"positions": [
{
"x": 0.33203125,
"y": 0.56640625
},
{
"x": 0.439453125,
"y": 0.56640625
},
{
"x": 0.439453125,
"y": 0.796875
},
{
"x": 0.33203125,
"y": 0.796875
}
]
},
{
"key": "Top",
"value": 1,
"confidence": 0.69552207,
"positions": [
{
"x": 0.63671875,
"y": 0.6484375
},
{
"x": 0.7109375,
"y": 0.6484375
},
{
"x": 0.7109375,
"y": 0.75
},
{
"x": 0.63671875,
"y": 0.75
}
]
},
{
"key": "Person",
"value": 1,
"confidence": 0.6918984,
"positions": [
{
"x": 0.337890625,
"y": 0.69140625
},
{
"x": 0.53125,
"y": 0.69140625
},
{
"x": 0.53125,
"y": 0.984375
},
{
"x": 0.337890625,
"y": 0.984375
}
]
},
{
"key": "Clothing",
"value": 1,
"confidence": 0.69086725,
"positions": [
{
"x": 0.337890625,
"y": 0.59765625
},
{
"x": 0.439453125,
"y": 0.59765625
},
{
"x": 0.439453125,
"y": 0.796875
},
{
"x": 0.337890625,
"y": 0.796875
}
]
},
{
"key": "Clothing",
"value": 1,
"confidence": 0.6888664,
"positions": [
{
"x": 0.54296875,
"y": 0.64453125
},
{
"x": 0.609375,
"y": 0.64453125
},
{
"x": 0.609375,
"y": 0.7578125
},
{
"x": 0.54296875,
"y": 0.7578125
}
]
},
{
"key": "Person",
"value": 1,
"confidence": 0.6766228,
"positions": [
{
"x": 0.60546875,
"y": 0.6015625
},
{
"x": 0.7109375,
"y": 0.6015625
},
{
"x": 0.7109375,
"y": 0.91796875
},
{
"x": 0.60546875,
"y": 0.91796875
}
]
},
{
"key": "Clothing",
"value": 1,
"confidence": 0.6763222,
"positions": [
{
"x": 0.421875,
"y": 0.48046875
},
{
"x": 0.458984375,
"y": 0.48046875
},
{
"x": 0.458984375,
"y": 0.67578125
},
{
"x": 0.421875,
"y": 0.67578125
}
]
},
{
"key": "Houseplant",
"value": 1,
"confidence": 0.63724834,
"positions": [
{
"x": 0.8984375,
"y": 0.64453125
},
{
"x": 0.98828125,
"y": 0.64453125
},
{
"x": 0.98828125,
"y": 0.79296875
},
{
"x": 0.8984375,
"y": 0.79296875
}
]
},
{
"key": "Top",
"value": 1,
"confidence": 0.6274083,
"positions": [
{
"x": 0.79296875,
"y": 0.62109375
},
{
"x": 0.859375,
"y": 0.62109375
},
{
"x": 0.859375,
"y": 0.79296875
},
{
"x": 0.79296875,
"y": 0.79296875
}
]
},
{
"key": "Chair",
"value": 1,
"confidence": 0.61188215,
"positions": [
{
"x": 0.6796875,
"y": 0.8203125
},
{
"x": 0.83203125,
"y": 0.8203125
},
{
"x": 0.83203125,
"y": 0.9921875
},
{
"x": 0.6796875,
"y": 0.9921875
}
]
},
{
"key": "Table top",
"value": 1,
"confidence": 0.5777681,
"positions": [
{
"x": 0.75,
"y": 0.7265625
},
{
"x": 0.99609375,
"y": 0.7265625
},
{
"x": 0.99609375,
"y": 0.98046875
},
{
"x": 0.75,
"y": 0.98046875
}
]
},
{
"key": "Top",
"value": 1,
"confidence": 0.556311,
"positions": [
{
"x": 0.09375,
"y": 0.640625
},
{
"x": 0.2138671875,
"y": 0.640625
},
{
"x": 0.2138671875,
"y": 0.8125
},
{
"x": 0.09375,
"y": 0.8125
}
]
},
{
"key": "Person",
"value": 1,
"confidence": 0.5285061,
"positions": [
{
"x": 0.337890625,
"y": 0.69140625
},
{
"x": 0.53125,
"y": 0.69140625
},
{
"x": 0.53125,
"y": 0.984375
},
{
"x": 0.337890625,
"y": 0.984375
}
]
},
{
"key": "Top",
"value": 1,
"confidence": 0.52386,
"positions": [
{
"x": 0.33203125,
"y": 0.609375
},
{
"x": 0.44140625,
"y": 0.609375
},
{
"x": 0.44140625,
"y": 0.6953125
},
{
"x": 0.33203125,
"y": 0.6953125
}
]
},
{
"key": "Houseplant",
"value": 1,
"confidence": 0.5183098,
"positions": [
{
"x": 0.46875,
"y": 0.69140625
},
{
"x": 0.58203125,
"y": 0.69140625
},
{
"x": 0.58203125,
"y": 0.828125
},
{
"x": 0.46875,
"y": 0.828125
}
]
}
]
차이점
Vision API의 Object Localization은 객체마다 instances 필드가 따로 없기 때문에, 감지된 객체 하나당 하나의 바운딩 박스를 반환하는 방식이다.
=> 객체당 하나의 bounding box만 반환하므로, 하나의 객체가 여러 개 있는 경우에도 각각 구분되지 않고 개별적으로 처리됨
(AWS Rekognition처럼 instance 여러 개를 구분하고 싶을 때는 별도 처리가 필요할 것으로 보임)
마무리
각각의 서비스 제공 방식과 성능을 비교해보면서 프로젝트에서 사용할 API를 결정해보는 과정을 정리해보았다.
OCR같은 경우에는 Google Vision API를 많이들 사용하는 것 같은데 이미지 기반 객체 검출 면에서는
개인적으로 Rekognition이 좀 더 현재 진행하는 프로젝트에 적합하지 않을까 싶다.
이미지 출처