이미지를 업로드 하는 방법에 대한 고민을 해결하였으니, 이제 실제 이미지를 서버에 업로드 하면서 추가적으로 구현하고 싶은 목표를 이루도록 해보자.
목적에 맞는 의류 이미지를 업로드 할 수 있도록 가이드 하는 방법
이번 프로젝트에서 이미지는 중요한 역할을 한다고 생각한다. 실제 유저가 어떤 의류가 저장됬는지 가장 한눈에 살펴볼 수 있는 것이 이미지이고, 그렇기 때문에 의류를 저장하기 전에 Preview 를 보여주는것은 기본이며, 개인적으로는 혹여나 실수로 전혀 관련없는 이미지를 제출해버리는 것을 방지해주는 것도 좋지 않을까 생각했다.
의도는 좋으나 방법이 떠오르질 않았는데, 이미지를 파악해서 컴퓨터가 알아서 이미지를 분석해서 이 이미지는 의류인지 아닌지를 파악하는것을 지금 내가 직접 코드를 작성하기에는 무리라고 생각했다. 이런 기능을 해주는 기능은 뭔가 인공지능이 해주는 것이 아닐까 싶었고, 그래서 처음에는 그냥 preview 만을 보여주는 것에 만족하려고 하였었다.
그러다 조금 우연하게도 어떠한 이미지를 분석해주는 AI 가 있다는 사실을 알게 되었고, 그것이 Google Vision Ai 라는 사실을 알게 되었다.
한번도 사용해본적은 없으나, 별다른 어려움 없이 제공하는 API 를 통해 사용이 가능할 것으로 파악하였고, 실제 홈페이지에서 시연을 해볼 수 있었는데 생각보다 만족스러운 결과를 얻을 수 있어서 이를 활용하는 방향으로 계획을 수정하였다.
Vision AI 는 여러 기능을 제공해주는데, 여러기능 중 내가 사용하려 했던 것은 '여러 객체 감지' 를 사용하고자 하였다. 실제 이미지 내 객체들을 파악하고, 사진에서 가장 우세한 객체를 파악할 수 있다는 점에서 의류 이미지라는 것을 감지하는데 충족할 것 같았다. 즉, 이미지를 업로드 했을 시 의류가 포함된 이미지라도, 의류가 차지하는 비중이 적다면 적합한 의류 이미지가 아니라고 사용자에게 알려주는 것이다!
계획이 수정되었으니 이제 실제로 적용하는 일만 남았다.
적용해보기
API 를 사용하기 전에 먼저 해야할 일이 있는데, 우선은 google Cloud 에 프로젝트를 생성해야 한다.
자신이 사용하고자 하는 프로젝트를 생성하자. 지금의 나는 Closet 을 사용하니 그대로 생성해주었다.
API 를 다루기 전에 먼저 클라우드의 결제 설정을 해주어야 하는데, 결제 사용 설정이 되어있어야 한다. 설정을 하는 방법은 아래 링크를 참고해서 진행해주면 된다. 참고로 3개월은 무료로 사용이가능하고, 이후부터는 어떠한 기능을 1000번 이하로 사용할 때는 무료이며 그 이후부터는 요금이 들어가게 된다.
https://cloud.google.com/billing/docs/how-to/verify-billing-enabled?hl=ko
프로젝트의 결제 상태 확인 | Cloud Billing | Google Cloud
의견 보내기 프로젝트의 결제 상태 확인 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Cloud Billing 계정은 특정 리소스 집합의 비용을 누가 지불할지 정의하
cloud.google.com
이후 API 설정을 한뒤(뒤에 링크를 추가할 테니 그 순서대로 해주면 된다), 서비스 계정을 만들어야 한다.
우선 프로젝트를 선택하고, 이후 서비스 계정 이름과 설명을 적는다. 다음 Project Owner 를 설정해준 다음 완료를 누르면 생성이 된다.
이제 키를 생성해야 하는데, 키는 만들어진 서비스 계정 이메일을 클릭한 다음 키를 클릭하자. 키 추가를 누르고 새 키추가하기 를 하면 JSON 파일이 생성이 되고 다운이 자동으로 될 것이다. (이 파일을 잊어버리지말고 가지고 있어야 한다!)
여기까지 진행했으면, 기본적으로 사용 조건 설정이 끝났다. 글로 설명하느라 이해가 안될 수 있는데 아래 링크를 차례대로 따라가면 쉽게 등록할 수 있다.
https://cloud.google.com/vision/docs/before-you-begin?hl=ko
시작하기 전에 | Cloud Vision API | Google Cloud
의견 보내기 시작하기 전에 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Cloud Vision API를 사용하려면 우선 프로젝트에서 사용 설정해야 합니다. Google Cloud
cloud.google.com
이제 node.js 서버에서 vision AI 를 설정해주도록 하자. 우선은 vision Ai 를 설치해야한다
npm install @google-cloud/vision
설치 후 아까 다운받았던 JSON 파일 기억하는가? 이 파일을 이제 가져와서 레포에 넣어주어야 한다.
const express = require('express');
const vision = require("@google-cloud/vision");
const dotenv = require("dotenv");
dotenv.config();
// 환경변수에 이미지의 path 를 지정해주어야 한다.
const client = new vision.ImageAnnotatorClient({
keyFilename: process.env.GOOGLE_APPLICATION_CREDENTIALS,
});
Vision AI 는 이미지를 분석해야하기에 우선 이미지를 가져와야 한다. 그렇기에 이미지가 저장되어있는 Path 를 환경변수에 저장해준 다음, keyFilename 에 지정해주어야 한다.
이렇게 하면 우선적으로 기본 셋팅은 마무리되었고, 이제 이미지를 Vision Ai 가 가져와서 분석을 해야한다. 그 말은 서버나 추후 배포시 S3 같은 곳에 이미지가 저장되어있어야 하며 그 이미지를 불러와야 한다는 점이다. 잠깐 다시 이미지 업로드 쪽으로 넘어가보자
잠깐 돌아와서 이미지를 전송하는 방법에 대해 살펴보자
계속 Vision AI 를 다루기 전에 먼저 이미지가 실제 저장될 장소를 어떻게 설정할 것인지에 대해 먼저 살펴보고 가자.
이전에 이미지파일을 폼데이터 형식으로 서버로 전송하려 했다는 것을 기억해보자. 보통 JSON 형식으로 데이터를 전송할 경우 쉽게 AJAX 를 통해서 처리할 수 있는데, 이미지의 경우는 조금 경우가 다르다. 왜냐하면 이미지는 바이너리 데이터이기 때문이다.
기본적으로 HTTP 프로토콜 전송 방식은 텍스트 기반이다. 당장 request 의 header 나 body 부분을 봐도 아스키기반이거나 그 확장기반 문자열로 이루어져있음을 알 수 있다. 이렇기 때문에 기본적으로 JSON 파일에 대한 전송은 그냥 aplication/* 타입으로 전송해도 크게 문제가 되지 않는다.
반면 이미지의 경우 바이너리 데이터로 이루어져있으며, 이를 기본적인 타입의 HTTP 프로토콜로 전송하기 위해서는 이 데이터를 문자열로 인코딩하는 과정이 필요하다. 전송하는 이미지의 크기가 크다면, 당연하게도 인코딩 역시 비효율적일 수밖에 없다. 이러한 이유만으로도 multipart/form-data 형식으로 이미지를 전송하는 것이 합당하다 생각할 수 있지만, 보통 이미지를 전송할 때 이미지만을 전송하지 않고 이미지에 대한 설명이나 혹은 다른 텍스트 데이터를 같이 전송하곤 한다. 회원가입의 경우를 생각해보면 프로필 사진과 함께 닉네임 자기소개 등등을 같이 전송하게 된다.
이렇게 여러 타입의 데이터를 한꺼번에 전송하게 될 경우에 multipart/form-data 형식이 적합하다. 우선 데이터 각각 그 타입에 맞게 식별이 된다는 점에서 유리하다. 이미지는 바이너리 데이터 형식으로 전달이 되며, 텍스트 형식은 텍스트에 맞게 전달이 된다.
MIME 타입
MIME 타입이란 Multipurpose Internet Mail Extensions 의 약자로서, 클라이언트에게 전송된 문서의 다양성을 알려주기 위한 메커니즘이라 할 수 있다. MIME 타입은 정확하게 설정해주는것이 중요한데, 서버는 이러한 MIME 타입을 보고 문서의 타입에 따른 행동을 할 수 있기 때문이다.
보통 형식의 경우 type/subtype 으로 표현되며, 예를 들자면 개별 타입으로는 text/plain, aplication/json, image/jpeg 등등으로 표현된다. 반면 멀티파트 타입은 multipart/form-data 와 같이 여러 개별 타입들의 집합이라고 생각하면 된다.
보통 웹 개발을 하는 경우 많이 마주하게 될 타입들은 크게,
aplication/octet-stream, text/plain, text/css, text/html, image/*, audio/*, multipart/form-data 정도가 있다.
기본적으로 multipart/form-data 로 데이터를 전송할 때의 request body 는 다음과 같다.
POST / HTTP/1.1
Host: localhost:8000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=---------------------------8721656041911415653955004498
Content-Length: 465
-----------------------------8721656041911415653955004498
Content-Disposition: form-data; name="myTextField"
Test
-----------------------------8721656041911415653955004498
Content-Disposition: form-data; name="myCheckBox"
on
-----------------------------8721656041911415653955004498
Content-Disposition: form-data; name="myFile"; filename="test.txt"
Content-Type: text/plain
Simple file.
-----------------------------8721656041911415653955004498--
보면 boundary 가 존재하는데, 이걸 기준으로 개별의 데이터 형식이 구별되며 전송된다.
이러한 이유로 기존 포스팅에서 이미지를 업로드 할 때 Form Data 클래스를 활용해서 이미지 파일을 append 하여, 폼 데이터를 서버에 전송한 것이었다.
Multer
기본적으로 이미지를 서버에 전송하는 방향에는, multipart/form-data 형식을 통해 다른 입력값들과 함께 전송하는 방향이 있고, 이미지 파일을 먼저 전송하고, 이후 나머지 입력값들을 json 파일로 전송하는 방향이 있다. 현 프로젝트의 경우 두번째 방향인 이미지 파일을 먼저 업로드 하는 쪽으로 가기로 했다.
한번에 전달할 경우 전달되는 이미지와 그 이미지와 연관된 텍스트 데이터를 form data 에서는 연결시켜주지 않기 때문에, 따로 서버에서 이를 처리해주어야 한다는 단점이 있다. 다만 이러한 단점때문에 이미지를 먼저 전송하려고 한 것은 아니다. 이미지를 먼저 전송해야지 이후 Vision AI 가 이에 대해 이미지 속 객체를 분석하고 그 결과값을 받아야 의류의 저장 여부를 결정할 수 있기 때문이다.
이러한 이유로 인해 먼저 이미지를 드래그나 버튼을 통해 업로드 할 시, 서버로 이미지를 전송하도록 코드를 구상하였다.
이미지가 서버로 왔을 때, 서버에서는 이미지를 저장하여야 하고, 이렇게 form-data 형식의 이미지를 저장하는 것은 express 에서 쉽게 작업하게 해주는 라이브러리가 있는데 바로 multer 이다. multer 를 통해서 이미지가 저장될 디렉토리를 정해줄 수 있고, 선택옵션으로 파일의 제한용량을 설정해줄 수도 있다. 우선 설치는 npm i multer 를 통해 해주면 된다.
multer 를 사용하기 전, multer 에는 이미지가 저장될 목적지 폴더가 필요한데, 이 부분을 먼저 express 에서 설정해주자
// app.js
const path = require("path");
const fs = require("fs");
app.use("/", express.static(path.join(__dirname, "uploads")));
// post.js
const path = require("path");
const fs = require("fs");
try {
fs.accessSync("uploads");
} catch (err) {
console.log("upload 폴더가 없으니 생산합니다");
fs.mkdirSync("uploads");
}
express.static 을 통해서 생성할 upload 폴더에 대한 절대경로를 설정해줄 수 있다. 여기서 _dirname 의 경우 app.js 이전까지의 경로를 나타내며, 여기에 join 으로 uploads 폴더 경로를 합쳐 '/' 의 경로로 표현하겠다는 의미이다.
예를 들자면 /ubuntu/Closet/back/uploads 가 uploads 까지의 경로라면 이 경로가 '/' 로 표현되는 것이다. 이렇게 되면 추후에 클라이언트에서 서버의 이미지에 접근하기가 편해진다. 왜냐하면 '서버의 url주소'/'이미지.jpg' 이런식으로만 이미지의 src 가 표현되어도 알아서 이미지의 경로를 찾아서 가져올 수 있기 때문이다.
fs 를 활용해서 만일 uploads 폴더가 없다면 생성하고, 있다면 연결시키는 코드까지 작성하면 준비는 끝났다.
const upload = multer({
storage: multer.diskStorage({
// 목적지를 설정해줄 수 있다. 여기선 uploads 로 설정되었다.
destination(req, file, done) {
done(null, "uploads");
},
// 저장할 파일 이름을 설정해줄 수 있다. 중복이름이 나오지 않게 조금 조치가 필요하다.
filename(req, file, done) {
// 원래 파일에서 원익.jpg 로 오게 되면
const ext = path.extname(file.originalname); // .jpg
const basename = path.basename(file.originalname, ext); // 원익
done(null, basename + "_" + new Date().getTime() + ext); // 원익23123.jpg
},
}),
// 선택옵션으로 용량제한을 걸어줄 수 있다.
limits: { fileSize: 20 * 1024 * 1024 }, // 20mb
});
위 코드는 multer 를 통해 전달되는 이미지를 uploads 폴더에 저장하는 과정을 구현하였다. 코드를 보면 결국 목적지와 파일이름을 설정할 수 있는데, 파일 이름의 경우 이름이 중복될 수 있으니, 매 사진을 저장할 때 마다 그때의 시간을 파일 이름에 붙여서 중복을 피해주자. (위 코드는 서버내 로컬 스토리지에 저장하는 과정을 나타냈으며, 만일 S3 같이 외부 스토리지에 저장할 경우 multer-s3 를 활용한다. 기회가 되면 다루겠다)
이제 이미지를 업로드 하면 실제 uploads 폴더에 이미지가 저장됨을 확인할 수 있다. 참고로 multer 버전이 1.4.4 보다 높다면 이미지가 한글 이름으로 되어있다면 이 한글이 깨져서 저장되는 오류가 발생할 수 있다. 만일 깨져서 저장이 된다면 multer 의 버전을 낮춰보자.
다시 Vision AI 로 돌아와서..
이미지를 uploads 폴더에 업로드 할 수 있게 되었으니, 이제 preview 를 띄우기 위해서는 파일 이름을 클라이언트에 전송해주어야 한다. 그냥 preview 만 하는 경우라면 그냥 전달을 해주면 되는데, 우리는 이 이미지의 객체를 파악하여 그 결과값을 같이 전송해야 한다.
코드로 한번에 살펴보자.
const express = require('express');
const vision = require("@google-cloud/vision");
const dotenv = require("dotenv");
dotenv.config();
// 환경변수에 이미지의 path 를 지정해주어야 한다.
const client = new vision.ImageAnnotatorClient({
keyFilename: process.env.GOOGLE_APPLICATION_CREDENTIALS,
});
// 생략
router.post("/images", isLoggedIn, upload.single("image"), async (req, res, next) => {
// POST /post/images 파일 한개씩 업로드
// upload.single 은 파일을 한개씩 저장하겠다는 의미이고, 여기에 저장된 파일은 req.file 로 접근이 가능하다.
try {
// 우선 uploads 에 저장된 이미지를 가져와야한다. path를 정확히 입력해야한다.
const filename = path.resolve(__dirname, `../uploads/${req.file.filename}`);
// fs.readFileSync 를 통해 이미지파일의 데이터형식을 Buffer 로 가져올 수 있다.
const request = {
image: { content: fs.readFileSync(filename) },
};
// vision ai 에서 이미지 속 객체를 분석하게 된다.
const [result] = await client.objectLocalization(request);
const objects = result.localizedObjectAnnotations;
// objects 는 객체의 이름과 그 객체가 이미지에서 차지하는 비율인 score 로 이루어진 객체들의 모인 배열이다
const resArray = [];
objects.forEach((object) => {
let obj = { name: object.name, confidence: object.score };
resArray.push(obj);
});
// objects 에서는 객체의 사진속 위치도 표현되어있어서 필요한 데이터만 다시 정리해서 resultObject 에 담는다
const resultObject = {
src: req.file.filename,
visionSearch: resArray,
};
res.status(200).json(resultObject);
} catch (err) {
console.error(err);
next(err);
}
});
우선 주석부분을 천천히 읽어보길 바란다.
과정은 간단하다. uploads 폴더에서 현재 저장한 파일을 req.file 을 통해 가져올 수 있다. 참고로 upload.array('image') 로 미들웨어를 설정할 경우 req.files 로 접근이 가능하다. 또한 'image' 는 앞서서 클라이언트에서 form data 로 이미지를 append 할 때의 키값이다.
여하튼 이렇게 이미지를 가져오고, 이 이미지를 Buffer 형식으로 변환하여 vision AI 에 분석하도록 전달해주게 된다.
Buffer
순수 자바스크립트에서는 이진 데이터를 다룰 수 없는데, Buffer 를 통해서 이를 해결 가능한데, 원하는 크기의 데이터 공간을 할당하여 데이터를 저장하는 클래스라고 할 수 있다. 이때 데이터는 1byte 단위로 저장이 된다는 점에 유의하자.
예를 들어 동영상이나 이미지를 Buffer 로 변경하여 전달한다고 가정하면, 우리가 할당한 데이터 공간만큼 1byte 식 데이터를 채워가면서 다 채워지면 채워진 부분을 전송하고, 공간을 비워준다. 그 다음 다시 할당한 공간만큼 데이터를 채우고 이를 전달하면서 비우는 과정을 반복하여, 전체 동영상, 이미지의 데이터를 전송하는 방식이다.
이때 할당된 데이터 공간에 쪼개진 동영상이나 이미지 데이터를 채우는 과정을 Buffering 이라고 한다. 흔히 우리가 말하는 버퍼링과 같은 개념이다. 버퍼링 중이라는 것은 할당 공간에 데이터를 계속 채우고 있다는 중이다.
Buffer 의 데이터 형식을 콘솔로 찍어서 보게 되면 <Buffer 61 84 A2 ... > 이런식으로 표현됨을 확인할 수 있다.
1byte 는 8bit 이며 00000000 단위만큼 데이터를 표현할 수 있는데, 이를 4bit 로 쪼개서 0000 0000 으로 나눈 뒤, 각각 16진수로 변경을 해준다. 즉 61 이라는 숫자는 0110 0001 에서 오게 된 것이다.
보통 Buffer 를 인코딩할 때는 .toString() 으로 변환할 수 있다. 아무론 조건을 달아주지 않으면 utf8 로 변환이 된다.
이미지는 cilent.objectLocalization 을 통해서 분석이 시작되고, 최종적으로 반환된 결과데이터는 아래와 같은 형식을 띈다
{
"responses": [
{
"localizedObjectAnnotations": [
{
"mid": "/m/01bqk0",
"name": "Bicycle wheel",
"score": 0.9423431,
"boundingPoly": {
"normalizedVertices": [
{
"x": 0.31524897,
"y": 0.78658724
},
{
"x": 0.44186485,
"y": 0.78658724
},
{
"x": 0.44186485,
"y": 0.9692919
},
{
"x": 0.31524897,
"y": 0.9692919
}
]
}
},
{
"mid": "/m/01bqk0",
"name": "Bicycle wheel",
"score": 0.9337022,
"boundingPoly": {
"normalizedVertices": [
{
"x": 0.50342137,
"y": 0.7553652
},
{
"x": 0.6289583,
"y": 0.7553652
},
{
"x": 0.6289583,
"y": 0.9428141
},
{
"x": 0.50342137,
"y": 0.9428141
}
]
}
},
// 생략
]
}
}
이 데이터중에서 필요한 부분은 name 과 score 부분이기에 두 데이터를 뽑아서 resArray 에 기입해준다.
이제 이미지의 주소와 더불어 resArray 를 클라이언트에게 전달한다. 이로서 클라이언트는 업로드한 이미지에 대한 AI 의 분석결과를 받아올 수 있다.
AI 분석 결과를 토대로 Preview Card 를 작성하기
Vision AI 를 통해 이미지의 객체를 분석한 결과를 받았으니, 이제 이를 바탕으로 Preview 를 구현해보자.
전달받은 분석 결과를 통해서 유저에게 어떤 정보를 제공할 지에 대해 고민하였고, 이에 3가지 정도를 제공하는게 좋겠다고 판단했다
- 업로드 한 사진은 의류 사진에 적합한 가?
- 이미지 객체 분석 결과에서 name 들에 단 하나라도 '의류' 관련 name 이 존재하는가?
- 업로드 한 사진은 현재 설정해둔 '카테고리' 에 적합한 가?
- 카테고리가 pants 인데 이에 관련한 객체 분석 name 이 존재하는가?
- 업로드 한 사진이 현재 설정해둔 '카테고리' 객체가 절대적으로 차지하고 있는가?
- 카테고리가 pants 일 때 실젝 객체 분석의 가장 높은 score 를 가지는 name 역시 pants 와 관련되어있는가?
전달받은 name 과 score 의 배열데이터를 통해 위 3가지 부분을 판단할 수 있을 것이라 생각하였고, 물론 정말 정밀한 판단까지는 아니지만 사용자에게 좀 더 깔끔하고 목적에 맞는 사진을 업로드 하도록 유도하고 싶었다.
우선적으로 의류 사진이 맞다면 업로드를 의류 데이터를 저장할 수 있도록 제출 버튼의 disabled 를 false 로 할 생각이었고, 나머지 부분에 대해서는 경고 수준을 목표로 하고 있다. 정말 옷과 관련없는 사진일 경우만 제한을 하는것이 사진의 자유도를 어느정도 유지할 수 있는 방향이라고 생각한다.
위 3가지를 적용할 때 가장 문제가 되었던 점은, 실제 vision AI 에서 제공하는 의류의 name 에 대한 전체 데이터를 구할 수가 없었다는 점이다. 세상에는 수만가지 객체들이 존재하고, 코드를 작성하는 입장에서 AI 가 의류들을 어떠한 name 으로 분석해서 전달해 주는지를 알 수가 없었다.
공식 문서도 찾아봤는데 아직까지도 발견을 하질 못해서, 우선 해결하기 위해서는 결국 여러 이미지를 AI 가 분석하게 하여 그 키워드들을 수집하는 방법밖에는 없는것 같았다... 노가다라서 싫었지만 다른 방법을 찾을 수 없었다.
interface CategoriObject {
[key: string]: string[];
}
export const visionAI = ['Outerwear', 'Coat', 'Shoe', 'Footwear', 'Sunglasses', 'Top', 'Belt', 'Luggage & Bag', 'Jeans', 'Scarf', 'Tie', 'Shirt', 'Pants', 'Hat'];
export const categoriToVisionAI: CategoriObject = {
Outer: ['Outerwear', 'Coat'],
Top: ['Top'],
Pant: ['Pants', 'Jeans'],
Shirt: ['Shirt'],
Shoe: ['Shoe'],
Muffler: ['Scarf'],
};
export const visionAICardData = [
{
intro: '의류 사진여부 판단',
good: '#52c41a',
bad: '#E7373C',
goodExplain: '적절한 사진입니다.',
badExplain: '의류 사진을 넣어주세요',
},
{
intro: '카테고리 적합성',
good: '#52c41a',
bad: '#F4A100',
goodExplain: '카테고리에 적합한 의류입니다',
badExplain: '저장하실 순 있지만 적합의류는 아닙니다.',
},
{
intro: '사진 내 카테고리 이미지 차지 비율',
good: '#52c41a',
bad: '#F4A100',
goodExplain: '의류 비중이 적합합니다.',
badExplain: '좀 더 적합의류의 비중이 높은 사진을 올려주세요',
},
];
최대한 사진을 분석하게 한 뒤, 얻은 name 값들을 visionAI 배열에 기입하였고, 이렇게 얻은 name 들 중에서 현재 데이터베이스에 나눠서 저장할 카테고리들과 연관된 name 들을 연결시킨 categoriToVisionAI 배열을 구현하였다.
3가지의 판단에 대한 boolean 값에 대해서 판독 결과를 유저에게 알려주기 위해 visionAICardData 를 생성하였고, 위 데이터들이 종합적으로 Preview Card 에 적용이 될 것이다.
이제 실제 받아오는 name 과 score, 그리고 위 데이터들을 통해 실제 이미지에 대한 객체 평가를 해주는 preview card 코드는 다음과 같다.
// ItemForm.tsx
return (
// 생략
<PreviewSection>
// 로딩중이라면 스켈레톤 UI 를 보여준다
{imageUploadLoding ? <VisionAICard imageUploadLoding={true} src={'src'} index={1} isClothes={true} isCategori={true} confidence={true} /> : null};
// 이미지가 존재한다면,
{imagePath.length > 0 &&
imagePath.map((v, i) => {
// 현재 카테고리를 관찰한다
let cate = watch('categori');
// 의류인지를 확인하는 boolean. 의류 name 데이터 목록에 v.name 이 포함되는가?
let isClothes = v.visionSearch.some(v => visionAI.includes(v.name));
// 카테고리에 적합한 의류인지. 단 하나라도 v.name 이 현 카테코리의 name 에 포함되는가?
let isCategori = v.visionSearch.map(v => v.name).some(item => categoriToVisionAI[cate]?.includes(item));
// 현 카테고리 name 의 score 가 가장 높은가? 우선 이미지 객체를 분석했다는 가정 하에, 내림차순으로 정렬된 v.visionSearch의 맨 처음 값이 카테고리에 포함되는가?
let confidence = v.visionSearch.length > 0 && categoriToVisionAI[cate]?.includes(v.visionSearch[0].name);
return (
<>
<VisionAICard imageUploadLoding={imageUploadLoding} src={v.src} index={i} isClothes={isClothes} isCategori={isCategori} confidence={confidence} />
</>
);
})}
</PreviewSection>
// 생략
)
3가지 조건들의 boolean 값을 VisonAiCard 에 props 로 전달해준다음, 이 정보를 받아 VisionAiCard 가 렌더가 된다.
렌더된 모습은 다음과 같다.
그리고 설명하다 보니 위 저장하기 버튼의 disabled 여부를 어떤식으로 조정하는지를 코드로 설명하지 못하였는데, useEffect 를 활용해서 상태값을 설정하였다
// ItemForm.tsx
useEffect(() => {
// 이미지가 없으면 return
if (imagePath.length === 0) {
return;
}
// 의류에 적합한지에 대한 판별식은 동일하다.
let visionSearch = imagePath.map(v => v.visionSearch?.some(i => visionAI.includes(i.name)));
// boolean 값이 true 라면
if (visionSearch.every(bool => bool === true)) {
setIsClothes(true);
} else {
setIsClothes(false);
}
}, [imagePath]);
useEffect 를 통해서, isClothes 상태값에 따라 disabled 값을 조정해주었다. 여기서 every 를 통해 전체 true 여부를 확인해주는 과정은 추후 기존 저장한 의류를 수정할 때 이미지도 가져와야 하기에 설정한 것이며, 나중에 수정 파트에서 다시 다루도록 하겠다.
한계점이 있지만..
Vision AI 를 활용하는 나의 코드에도 한계점이 있고, Vision AI 자체에도 한계점은 존재한다.
내 코드에서라면 Vision AI 가 제공해주는 의류 관련 name 을 전부 가져올 수는 없었다는 점이 있고, 실제 uploads 폴더나 추후 S3 외부 스토리지에 이미지를 업로드를 하게 될 것인데, 이미지를 업로드를 하였다가 다시 제거할 때, 화면상에서만 삭제가 될 뿐 실제 저장소에서는 삭제가 되질 않는다. 이 부분에 관해서도 좀 더 탐색해보고 조정할 필요가 있어보인다
Vision AI 역시 한계점을 가지고 있다. 실제로 분명 옷이 있는 사진임에도 없다고 분석 결과를 보낼 때도 존재했었기에 (물론 제대로 정면에서 옷을 찍은 이미지를 올리면 다 의류로 판단한다) 이 부분에 대해서도 좀 아쉬운 느낌이 있다.
그럼에도 이번 프로젝트에서 AI 를 활용해본 경험은 앞으로 다른 프로젝트를 진행할 때에 큰 디딤돌같은 경험이 되지 않을까 생각한다. 세상에는 정말로 훌륭한 API 들이 존재하고, 이제는 이를 잘 활용할 줄 알아야 더 좋은 서비스를 제공할 수 있기에, 앞으로도 기회가 된다면 다른 API 들도 활용하면서 웹사이트를 개발하고 싶다.
이번 포스팅은 설명해야할 부분이 많이 있어서 길어졌는데, 다음 포스팅은 이제 마지막인 실제 데이터를 수정할 경우 기존 입력값을 표시해주는 과정에 대해서 포스팅 하고자 한다.
'Practice' 카테고리의 다른 글
[Closet]Sequelize custom method 을 활용한 데이터 수정 (0) | 2023.04.30 |
---|---|
[Closet]React-Hook-Form의 Reset (1) | 2023.04.30 |
[Closet] 이미지 업로드 시 Drag and Drop을 활용해보기 (0) | 2023.04.29 |
[Closet]React-hook-form을 통해 입력폼 구성하기 (0) | 2023.04.29 |
페이지마다 다른 레이아웃을 적용하기(Next.js) (0) | 2023.04.27 |