Donggukthon Google Maps, Google Vision API 적용기 (+ 회고)

김민성·2024년 1월 29일
6

회고

목록 보기
8/9
post-thumbnail

Donggukthon Google Maps, Google Vision API 적용기 (+ 회고)

구름톤을 진행하면서 해커톤에 참여에 흥미를 느끼게 되었다.
그렇게 해커톤 공고를 찾아본 결과 동국대학교 학생을 대상으로 한 "동국톤"이 진행된다는 공고를 찾게 되었고, 선착순 참여라기에 신청 폼이 오픈되자마자 곧바로 신청을 해 참여할 기회를 얻을 수 있었다!

팀은 개개인의 해커톤, 프로젝트 경험을 시드로 삼아 자동 매칭이 되었다. 나는 다른 참가자들에 비해 많은 편이었는지, 나를 제외한 팀원들은 이러한 프로젝트 경험이 처음인 사람들과 함께 매칭이 되었다.

1. 동국톤 개발 주제

1. 겨울철 이벤트성으로 활용 가능한 서비스
2. 다양한 사회문제를 해결하기 위한 서비스

주제는 위의 두 가지 중 한 가지를 선택해 진행하는 것이었고, 꽤나 많은 팀에서 겨울철 이벤트성으로 활용가능한 서비스를 만들겠다고 예상했고, 무엇보다도, 사람들에게 즐거움을 줄 수 있는 서비스를 넘어 사회적 문제를 해결하는 데에 도움을 줄 수 있는 서비스를 만들고 싶다는 생각이 컸었다.

위와 같은 이유를 갖고 잘 어필했더니 팀원들과도 의견이 잘 맞게 되어 다양한 사회문제를 해결하기 위한 서비스를 개발하기로 결정했다.

이 주제를 통해 가장 크게 고민했던 점은 “어떻게 해야 사람들이 사회에 조금씩 기여하는 데에 동참하도록 유도할 수 있을까?” 이다.

요즘 길거리가 많이 더러워지고 있고, 나 또한 쓰레기를 손에 쥐고 있을 때 당장 주변에 쓰레기통이 없어 들고다니기 난감할 때가 많았다. 그래서 유저가 쓰레기통을 등록하여 이를 바탕으로 쓰레기통의 위치를 알려주고, 버려진 쓰레기도 등록해 유저들이 쓰레기를 치우도록 유도하게끔 쓰레기의 위치도 지도에 마커로 표시를 해주는 서비스를 개발하기로 결정했다.

이를 통해 쓰레기통 위치 알리미 “CleanCity”라는 서비스를 만들 수 있었다. Google Vision API을 활용해 카메라를 통해 촬영한 쓰레기통을 Detection할 수 있는 기능을 넣어 유저들이 보다 신뢰성있는 쓰레기통 위치를 Google Maps 상에 등록, 파악할 수 있게 하였고, 길거리에 버려진 쓰레기 또한 지도 상에 표시했다. 보다 깨끗한 길거리 환경 조성을 위해 버려진 쓰레기 또한 Detection 기능을 통해 해당 위치의 쓰레기를 치웠는지 파악했다.

2. Google Vision API

Google Vision API

Detection 기능을 넣은 이유는, 사용자 제보 기반으로 지도에 띄워주기 때문에 쓰레기통, 쓰레기인지 정확한 검증이 필요하다고 생각했기에 Google Vision API의 Label Detection을 활용해 사진 내 객체에 대한 String 값을 추출해 내가 코드 상에 쓰레기, 쓰레기통과 관련된 String 배열 내의 값과 일치하게 되면 검증을 통과하게끔 로직을 구성했다.

const trashRelatedKeywords = [
  'trash', 'garbage', 'waste', 'litter', 'rubbish', 'debris', 'refuse', 'pollution', 'dust',
  'plastic', 'transparency', 'plastic bag', 'waste container', 'bin bag', 'pollution'
];
const validPredictions = [
  'ashcan', 'trash can', 'garbage can', 'wastebin', 'ash bin',
  'ash-bin', 'ashbin', 'dustbin', 'trash barrel', 'trash bin',
  'trash-can', 'garbage-can', 'waste container', 'waste-container',
  'waste-can', "waste containment",  "waste-containment"
];

위의 배열들은 임의로 수십개의 쓰레기, 쓰레기통 이미지를 Google Vision API에 넣고 받아온 String 값들 중 유의미한 값들을 담은 배열이다.

ImageAnalyzer.tsx

import React, { useEffect, useState } from 'react';
import { analyzeImage } from './GoogleVisionAPI';

export const ImageAnalyzer = ({ imageUrl, onDetectionResult }) => {
    const [analysisResult, setAnalysisResult] = useState(null);

    useEffect(() => {
        const convertBlobUrlToBase64 = (blobUrl) => {
            return fetch(blobUrl)
                .then(response => response.blob())
                .then(blob => new Promise((resolve, reject) => {
                    const reader = new FileReader();
                    reader.onloadend = () => resolve(reader.result);
                    reader.onerror = reject;
                    reader.readAsDataURL(blob);
                }));
        };

        const analyze = async () => {
            try {
                const base64Image = await convertBlobUrlToBase64(imageUrl);
                const result = await analyzeImage(base64Image);
                const labels = result.responses[0].labelAnnotations.map(label => label.description);
                setAnalysisResult(labels);
                onDetectionResult(labels);
            } catch (err) {
                //alert(`분석 중 에러가 발생했습니다: ${err.message}`);
            }
        };

        if (imageUrl) {
            analyze();
        }
    }, [imageUrl]);

    if (!analysisResult) {
        return <>분석 중...</>;
    }
};

analysisResult는 이미지 분석 결과를 저장하는 상태이다.

useEffect 훅을 이용해 imageUrl이 변경될 때마다 실행되게 한다. 이미지 URL이 존재하면 analyze 함수를 호출하여 이미지 분석을 시작한다.

분석 결과가 없으면 "분석 중..."을 화면에 표시한다. 분석이 완료되면 analysisResult 상태가 업데이트되고, onDetectionResult 콜백을 통해 부모 컴포넌트에 결과가 전달된다.

convertBlobUrlToBase64

const convertBlobUrlToBase64 = (blobUrl) => {
    return fetch(blobUrl)
        .then(response => response.blob())
        .then(blob => new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onloadend = () => resolve(reader.result);
            reader.onerror = reject;
            reader.readAsDataURL(blob);
        }));
};

이 함수는 주어진 Blob URL을 Base64 형식의 문자열로 변환한다. fetch를 사용해 Blob 데이터를 받아오고, FileReader를 통해 Base64 문자열로 변환한다.

analyze

const analyze = async () => {
    try {
        const base64Image = await convertBlobUrlToBase64(imageUrl);
        const result = await analyzeImage(base64Image);
        // ... 결과 처리 ...
    } catch (err) {
        // ... 에러 처리 ...
    }
};

if (imageUrl) {
    analyze();
}

convertBlobUrlToBase64 함수를 사용해 이미지 URL을 Base64로 변환한다.
변환된 이미지를 analyzeImage 함수에 전달하여 Google Vision API를 통한 이미지 분석을 수행한다.

analyzeImage.tsx

import axios from 'axios';

const GOOGLE_VISION_API_URL = 'https://vision.googleapis.com/v1/images:annotate';

export const analyzeImage = async (base64Image) => {
  // base64 인코딩된 이미지에서 'data:image/jpeg;base64,' 접두사 제거
  const formattedImage = base64Image.replace(/^data:image\/(png|jpg|jpeg);base64,/, '');

  const requestBody = {
    requests: [
      {
        image: {
          content: formattedImage
        },
        features: [
            { 
                type: 'LABEL_DETECTION',
                maxResults: 5,
            }
        ]
      }
    ]
  };

  try {
    const response = await axios.post(`${GOOGLE_VISION_API_URL}?key=${import.process.env.GOOGLE_VISION_API_KEY}`, requestBody, {
      headers: {
        'Content-Type': 'application/json'
      }
    });
    return response.data;
  } catch (error) {
    console.error('Google Vision API Error:', error);
    throw error;
  }
};

Base64 인코딩된 이미지를 Google Vision API에 전송한다.
LABEL_DETECTION 기능을 사용하여 이미지에 있는 레이블을 감지한다.

결과

3. Google Maps

Google Maps Platform

const [map, setMap] = useState<google.maps.Map | null>(null);
const [center, setCenter] = useState<google.maps.LatLngLiteral | null>(null);
const [map, setMap] = useState<google.maps.Map | null>(null);

위와 같이 지도와 관련된 상태들을 관리한다.

주소 가져오는 함수 (getAddress)

const getAddress = useCallback((location) => {
  geocoder.geocode({ location }, (results, status) => {
    // ... 결과 처리 로직 ...
  });
}, [setUserLocationInfo]);

geocode 메소드를 사용하여 주어진 위치의 주소를 조회한다.
조회된 주소는 setUserLocationInfo를 통해 Recoil 상태에 저장한다. (나는 상태관리 라이브러리로 Recoil을 활용했다.)

장소 자동완성 설정

 useEffect(() => {
    if (!map) return;
    
    // Autocomplete 객체 생성 및 입력 필드에 연결
    const autocomplete = new window.google.maps.places.Autocomplete(inputRef.current);
    autocomplete.bindTo('bounds', map);
  
    // place_changed 이벤트 리스너 설정
    autocomplete.addListener('place_changed', () => {
      const place = autocomplete.getPlace();
      if (!place.geometry) {
        // 장소가 선택되지 않았을 경우
        console.log("No details available for input: '" + place.name + "'");
        return;
      }
  
      if (place.geometry.viewport) {
        map.fitBounds(place.geometry.viewport);
      } else {
        map.setCenter(place.geometry.location);
        map.setZoom(17);  // 상세 확대
      }
    });
  }, [map]);

GoogleMap 인스턴스가 생성된 후, Google Places Autocomplete 기능을 설정한다.
사용자가 검색 필드에 입력할 때 자동으로 장소를 제안한다.

사용자 위치 설정

  useEffect(() => {
    if ("geolocation" in navigator) {
      navigator.geolocation.getCurrentPosition(
        (position) => {
          const newPos = {
            lat: position.coords.latitude,
            lng: position.coords.longitude
          };
          
          setCenter(newPos);
        },
        (error) => {
          console.error("Error getting location", error);
        }
      );
    }
  }, [map]);

브라우저의 Geolocation API를 사용해 사용자의 현재 위치를 가져온다.
가져온 위치는 center 상태에 저장되어 지도의 중심으로 사용한다.

맵 로드 및 언마운트 핸들러

  const onLoad = React.useCallback((map: google.maps.Map) => {
    if (center) {
      const bounds = new window.google.maps.LatLngBounds(center);
      map.fitBounds(bounds);
    }
    setMap(map);
  }, [center]);

  const onUnmount = React.useCallback(() => {
    setMap(null);
  }, []);

onLoad 콜백에서는 맵 인스턴스를 map 상태에 저장한다.
onUnmount 콜백은 컴포넌트가 언마운트될 때 맵 인스턴스를 정리한다.

Fallback UI

  // 위치 정보가 로드되기 전까지 표시할 Fallback UI
  if (!center) {
    return <Loading/>;
  }

Google Maps를 불러오는 데에 로딩 시간이 소요되기에 Loading 컴포넌트를 표시하여 유저에게 대기 상태를 알렸다.

profile
다양한 활동을 통해 인사이트를 얻는 것을 즐깁니다. 저 또한 인사이트를 주는 사람이 되고자 합니다.

0개의 댓글