일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- Git
- 파이썬
- 꿀단집
- presignedurl
- 코딩테스트준비
- Python
- SQL
- 파이썬프로그래밍기초
- 방송대
- node.js
- JavaScript
- 방송대컴퓨터과학과
- 유노코딩
- HTML
- 개발자취업
- 프로그래머스
- MySQL
- 데이터베이스시스템
- TiL
- 중간이들
- nestjs
- Cookie
- 코딩테스트
- aws
- redis
- 항해99
- 엘리스sw트랙
- 99클럽
- CSS
- 코드잇
- Today
- Total
배꼽파지 않도록 잘 개발해요
[중간이들] Google Cloud API로 NestJS에 OCR 기능 추가 - 타입 오류 해결 및 원하는 문자열 추출 본문
글은 아래와 같은 구성으로 이루어진다.
- Google Cloud API 사용을 위한 설정
- NestJS에서 OCR 기능 추가
- API 사용 중 발생한 타입 오류 해결
- 원하는 문자열만 추출하기
Google Cloud API 사용 이유
지금 특정 학교의 특정학과 학생들(재학생, 졸업생)만 이용할 수 있는 동문 커뮤니티를 만들고 있다.
사용자가 재학증명서나 졸업증명서 이미지를 업로드하고 회원가입 폼을 제출하면 서버에서 사용자의 실명만 추출해서 Users 테이블에서 userName 컬럼에 넣어주는 로직이 필요하였다.
이때 이미지에서 텍스트를 추출하는 OCR 라이브러리가 필요하였는데, 자바스크립트에서는 tesseract.js와 Google Cloud Vision의 API를 많이 활용하는 것 같았다.
어떤 분의 블로그를 보니까 tesseract.js는 상대적으로 성능이 떨어진다고 해서 Google Cloud Vision API를 사용하기로 결정하였다.
공식 홈페이지에서 API를 사용해볼 수 있다.
영어 글자가 적힌 이미지를 업로드하면 API가 텍스트를 추출해준다. 테스트용으로 하나 올려보니 잘 된다.
'Show JSON'을 클릭하면 다음과 같다.
Request와 Response가 JSON으로 변환되어 복사를 할 수 있다.
여기서 'Cloud Vision API 무료로 사용해 보기'를 클릭한다.
계정정보를 입력해준다. 복잡하지 않아서 계정 생성은 어렵지 않다.
$300 크레딧이 3개월 동안 무료로 제공되고, 이후는 사용량 만큼 유료로 지불해야한다.
이후에도 가격적인 부담이 크지 않아서 이 API를 사용하기로 하였다.
구글 클라우드 콘솔의 메인화면이다.
스크롤을 내리다보면 '이미지 속 콘텐츠 감지'가 있다. 클릭한다.
Cloud Vision API 세부정보에서 '사용'을 누른다.
이제 사용자 인증 정보를 만들어야 한다.
구글 클라우드 콘솔 화면에서 'API 및 서비스'를 클릭한다.
'사용자 인증 정보 만들기' 버튼을 누른다.
누르면 'API 키', 'OAuth 클라이언트 ID', '서비스 계정'이 있다.
처음에는 간단하게 API키를 활용하려고 해서 발급 받았는데, 잘 안되어서 다른 블로그를 참고하니까 '서비스 계정'을 발급 받는게 보안에는 좋다고 하였다. '서비스 계정'을 클릭한다.
그러면 이렇게 서비스 계정이 활성화된다.
구글 API를 많이 사용해본 사람들은 어떻게 하는지 대충 감이 올 것 같다.
계정을 생성하였으면 키를 발급해야한다.
'키 추가'를 눌러서 JSON 파일을 다운 받아야 한다.
그럼 이렇게 본인의 컴퓨터에 JSON 파일이 다운로드된다.
이걸 잘 간직하고 있어야 한다. 안 그러면 API 활용을 못한다.
NestJS에서 OCR 기능 추가
https://cloud.google.com/nodejs/docs/reference/vision/latest
Node.js client library | Google Cloud
Send feedback Stay organized with collections Save and categorize content based on your preferences. Send feedback Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution 4.0 License, and code samples are lice
cloud.google.com
https://cloud.google.com/vision/docs/ocr?hl=ko
이미지의 텍스트 감지 | Cloud Vision API | Google Cloud
Vertex AI의 최신 멀티모달 모델인 Gemini 1.5 모델을 사용해 보고 최대 2백만 개의 토큰 컨텍스트 윈도우를 사용해 무엇을 빌드할 수 있는지 확인해 보세요. Vertex AI의 최신 멀티모달 모델인 Gemini 1.5
cloud.google.com
https://cloud.google.com/vision/docs/ocr?hl=ko#vision_text_detection_gcs-nodejs
이미지의 텍스트 감지 | Cloud Vision API | Google Cloud
Vertex AI의 최신 멀티모달 모델인 Gemini 1.5 모델을 사용해 보고 최대 2백만 개의 토큰 컨텍스트 윈도우를 사용해 무엇을 빌드할 수 있는지 확인해 보세요. Vertex AI의 최신 멀티모달 모델인 Gemini 1.5
cloud.google.com
구글 클라우드 문서를 보면 Node.js에서 이 OCR API를 어떻게 활용하는지에 대한 설명이 나와있다.
개발자가 특정 Endpoint에 호출하는 부분이 없고, 구글 클라우드 비전의 클라이언트를 생성하여 진행하면 된다.
참고한 블로그 링크는 다음과 같다.
이 블로그에 Node.js에서 이미지 추출하는 방법에 대해 자세히 나와있다.
[JavaScript, Node.js] 이미지 한글 텍스트 추출하기 (tesseract.js, Google Cloud Vision API)
먼저 tesseract.js는 머신러닝 기반의 이미지(동영상) - 텍스트 검출 라이브러리이다. 한글에 대한 인식도는 80%전후 정확도를 보여줬다.tesseract.js는 CDN, Node.js를 지원한다.1\. tesseract.js 라이브러리 설
velog.io
하지만 이 블로그의 코드를 그대로 갖다 붙여 넣을 수 없었다.
우리 서버는 NestJS를 사용하고 있기 때문에 일반 Node.js환경과 달리 타입스크립트를 사용하고, 클래스 기반의 구조를 많이 사용하므로 그에 맞춰서 작성해야한다.
작성하던 중 아래와 같은 오류가 발생하였다.
읽어보니까 type 'string'이 IFeature 인터페이스에 있는 타입이 아니라는 메시지가 떴다.
Argument of type '{ requests: { features: { type: string; }[]; image: { source: { imageUri: string; }; }; imageContext: { languageHints: string[]; }; }[]; }' is not assignable to parameter of type 'IBatchAnnotateImagesRequest'.
Types of property 'requests' are incompatible.
Type '{ features: { type: string; }[]; image: { source: { imageUri: string; }; }; imageContext: { languageHints: string[]; }; }[]' is not assignable to type 'IAnnotateImageRequest[]'.
Type '{ features: { type: string; }[]; image: { source: { imageUri: string; }; }; imageContext: { languageHints: string[]; }; }' is not assignable to type 'IAnnotateImageRequest'.
Types of property 'features' are incompatible.
Type '{ type: string; }[]' is not assignable to type 'IFeature[]'.
Type '{ type: string; }' is not assignable to type 'IFeature'.
Types of property 'type' are incompatible.
Type 'string' is not assignable to type '"TEXT_DETECTION" | Type | "TYPE_UNSPECIFIED" | "FACE_DETECTION" | "LANDMARK_DETECTION" | "LOGO_DETECTION" | "LABEL_DETECTION" | "DOCUMENT_TEXT_DETECTION" | "SAFE_SEARCH_DETECTION" | ... 4 more ... | "OBJECT_LOCALIZATION"'.ts(2345)
const requestsPayload: {
requests: {
features: {
type: string;
}[];
image: {
source: {
imageUri: string;
};
};
imageContext: {
languageHints: string[];
};
}[];
}
StackOverFlow에서 request 내용을 찾아서 똑같이 구성을 해보았다.
차이점은 우리 서버에서는 createRequest 함수에서 imageUri를 입력받아 request를 동적으로 생성하고 있다는 것이다.
뭐가 문제인지 몰라서 오류문을 자세히 살펴보았다.
알고보니 request 안에 있는 features의 type이 string으로 되어 있는데, 이게 인식이 안 되는 것이 원인이었다.
requests: {
features: {
type: string;
}[];
그래서 이걸 어떻게 해결해야하나 싶어서 구글링을 시도했다.
Interface protos.google.cloud.vision.v1.IFeature (3.0.1) | Node.js client library | Google Cloud
Send feedback Interface protos.google.cloud.vision.v1.IFeature (3.0.1) Stay organized with collections Save and categorize content based on your preferences. Package @google-cloud/vision Properties maxResults maxResults?: (number|null); model type type?: (
cloud.google.com
구글이 만들어 놓은 코드를 보면 IFeature라는 인터페이스가 있었다.
그래서 IFeature 인터페이스 파일을 하나 만들어서 저기 안에 있는 타입들을 다 넣었다.
타입스크립트에서 'apple' | 'banana'와 같은 리터럴 타입은 단순한 string 타입과 다르게 분류된다.
이 인터페이스 파일을 별도로 만들고, as IFeature이라고 적어주니 해결이 되었다.
export interface IFeature {
type: 'TYPE_UNSPECIFIED' | 'FACE_DETECTION' | 'LANDMARK_DETECTION' | 'LOGO_DETECTION' | 'LABEL_DETECTION' | 'TEXT_DETECTION' | 'DOCUMENT_TEXT_DETECTION' | 'SAFE_SEARCH_DETECTION' | 'IMAGE_PROPERTIES' | 'CROP_HINTS' | 'WEB_DETECTION' | 'OBJECT_LOCALIZATION';
}
그리고 await를 안 붙여줘서 에러가 났는데, await를 붙여주니 해결이 되었다.
OcrService 코드가 완성되었다.
ocr.service.ts
import { Injectable } from "@nestjs/common";
import { ImageAnnotatorClient } from '@google-cloud/vision';
import { createRequest } from './ocr-request';
@Injectable()
export class OcrService {
private readonly visionClient: ImageAnnotatorClient;
constructor() {
this.visionClient = new ImageAnnotatorClient({
key: process.env.GOOGLE_VISION_API_KEY,
});
}
async detextTextFromImage(imageUri: string): Promise<void> {
try{
const requestsPayload = createRequest(imageUri);
const [result] = await this.visionClient.batchAnnotateImages(requestsPayload);
const detections = result.responses[0].fullTextAnnotation;
console.log(detections.text);
} catch(error) {
console.error("OCR 실행 중 에러 발생: ", error);
}
}
}
ocr.request.ts
import { IFeature } from './interfaces/feature-interface';
export const createRequest = (imageUri: string) => ({
requests: [
{
features: [
{
type: 'TEXT_DETECTION',
} as IFeature,
],
image: {
source: {
imageUri: imageUri,
},
},
imageContext: {
languageHints: ['ko'], // 한국어
},
},
],
});
ocr.controller.ts
import { Controller, Get, Body } from '@nestjs/common';
import { OcrService } from './ocr.service';
@Controller('ocr')
export class OcrController {
constructor(private readonly ocrService: OcrService) {}
@Get('detect-text')
async detectText(@Body('imageUri') imageUri: string): Promise<void> {
if (!imageUri) {
throw new Error('이미지 URI가 없습니다.');
}
await this.ocrService.detextTextFromImage(imageUri);
}
}
이 컨트롤러는 OCR이 잘 작동하는지 확인하기 위해 만든 API이다. 실제 클라이언트 측에서는 연결하지 않는다.
그런데 막상 실행해보니까 에러가 발생하였다.
알고보니 이 'GOOGLE_VISION_PATH'에는 위에서 말한 사용자 계정 키 JSON 파일의 경로를 써야한다.
본인 컴퓨터에 다운받은 JSON파일이 위치한 경로를 환경변수에 써주고 불러오면 된다.
S3 버킷에 있는 이미지 URI를 body값으로 넣고 OCR이 잘 작동하는지 테스트를 해보았다.
콘솔로 출력하니 아주 잘 나온다.
졸업증명서와 재학증명서 이미지 파일을 각각 업로드해 보았을 때 출력 결과는 다음과 같았다.
'성'과 '명' 사이에 공백이 있다보니 이렇게 출력이 되었다.
이러한 규칙을 발견했으니 이제 이걸 활용해서 회원 실명만 추출하면 된다.
재학증명서
성
명: 김개똥
졸업증명서
성
명: 김개똥
사용자 이름을 추출하는 함수를 하나 넣어주었다.
정규식은 쓸 수 없어서 GPT한테 작성해달라고 했다. 정말 정규식은 어렵다.
import { Injectable, NotFoundException } from '@nestjs/common';
import { ImageAnnotatorClient } from '@google-cloud/vision';
import { createRequest } from './ocr-request';
@Injectable()
export class OcrService {
private readonly visionClient: ImageAnnotatorClient;
constructor() {
this.visionClient = new ImageAnnotatorClient({
keyFilename: process.env.GOOGLE_VISION_PATH,
});
}
async detextTextFromImage(imageUri: string): Promise<string> {
try {
if (!imageUri) throw new Error('이미지 URI가 없습니다.');
const requestsPayload = createRequest(imageUri);
const [result] = await this.visionClient.batchAnnotateImages(requestsPayload);
const detections = result.responses[0].fullTextAnnotation;
// console.log(detections.text);
const extractedName = this.extractNameFromText(detections.text);
if (!extractedName) throw new NotFoundException('추출된 이름을 찾을 수 없습니다.');
console.log('추출된 이름', extractedName);
return extractedName;
} catch (error) {
console.error('OCR 실행 중 에러 발생: ', error);
}
}
private extractNameFromText(text: string): string | null {
const nameRegex = /성\s*[\s\S]*?명\s*[::]?\s*([\w가-힣]+)/;
const match = text.match(nameRegex);
return match ? match[1] : null;
}
}
- nameRegex : 성'과 '명' 사이의 부분에서 구분 기호 (':', ':') 뒤에 오는 이름을 추출
- match : 정규식에 일치하는 문자열을 배열로 리턴함. 첫번째는 전체, 두번째부터는 캡쳐 그룹 순서대로 반환함.
const text = "The event will be held on 2024-09-10.";
const dateRegex = /(\d{4})-(\d{2})-(\d{2})/;
const match = text.match(dateRegex);
console.log(match);
// [
// '2024-09-10',
// '2024',
// '09',
// '10',
// index: 26,
// input: 'The event will be held on 2024-09-10.',
// groups: undefined
// ]
이렇게 해서 원하는 대로 실명만 추출하는 OCR 기능이 완성되었다.
글 내용 요약
- Google Cloud API 사용을 위해서는 사용자 계정 키를 JSON 파일로 다운받아야 한다.
- NestJS에서 OCR 라이브러리를 불러올 때 이 키 파일의 경로를 환경변수로 설정해서 클라이언트를 생성해야 한다.
- 타입을 인식하지 못하는 오류가 발생할 경우 직접 타입을 만들어서 넣어준다.
- 원하는 문자열만 추출하기 위해서는 정규식을 활용할 수 있다.
'Project' 카테고리의 다른 글
[중간이들] SMS 인증번호 발송서비스 플랫폼으로 네이버 클라우드를 선택하지 않은 이유 (0) | 2024.09.10 |
---|---|
[꿀단집] 구글 OAuth 소셜 로그인: 심사 통과 후 프로덕션 환경에서 외부인 로그인 가능 (2) | 2024.09.10 |
[꿀단집] 배포환경에서 multer로 프로필 이미지 변경이 안 되는 문제 - 서버 파일 경로 구분자 문제를 path.normalize로 해결 (0) | 2024.09.10 |
[꿀단집] 구글 검색 콘솔에 사이트 등록 및 사이트맵 만들기 (0) | 2024.09.02 |
[MoneyManyBank] - TKinter로 GUI 프로그램 ① 화면 설계 (0) | 2023.12.09 |