졸업작품

[졸업작품] 'OCR & NFC 기반 시각 장애인 사물 인식 보조기기' 제작

내가 진짜 유일한 2024. 11. 16. 23:45

2023년 대학교 3학년부터 2024년 4학년까지, 졸업 작품 개발을 팀장으로써 진행했다.

이번 포스팅은 '성장기'팀의 1년 동안 생각하고 개발한 졸업작품에 대해 작성하고자 한다.


 

한 학기 동안 아이디어를 선정하고 담당 교수님과 팀원들이 전부 마음에 든 아이디어를 채택해서 개발을 시작했다.

그렇다면, 우리 팀의 아이디어 선정을 어떻게 하게 되었는 지 얘기해보겠다.

1. 졸업작품 아이디어 선정 - NFC 스티커와 OCR 기술을 활용

우리는 먼저 대주제를 잡고 자료조사를 했다.

먼저, 대주제는 홀로 생활하는 장애인의 불편함 개선이라는 주제를 잡고 자료 조사를 하기 시작했다.

한국보건사회연구 통계를 토대로 한 그래프 (좌) 장애인 인구수 증가 (우) 혼자 생활하는 장애인의 비율 증가

 

사실 누군가를 대상으로 어떤 서비스를 개발할 때는,

대상 집단에게 설문조사를 진행하고 설문조사를 바탕으로 개발할 서비스를 정하는 게 일반적이다.

 

우리 팀은 대상이 장애인이었고, 그 대상에게 설문조사를 했어야 했지만, 우리는 대체안으로 인터뷰 영상을 찾기로 했다.

 

그래서 YouTube에서 시각장애인 대상으로 인터뷰한 영상을 바탕으로 불편함을 간접적으로 조사하였다.

그 중에서 'TV러셀'의 9급 공무원이 된 시각장애인 인터뷰 영상을 봤다.

여기에서 홀로 생활하고 있는 시각장애인이 느끼는 불폄함에 대해 정말 많이 알 수 있었고, 대표적으로 몇가지를 보겠다.

자료 출처: TV러셀 - 시각을 읽고 귀로 공부해서 서울에서 9급 공무원이 된 27살 영상 중 https://youtu.be/gsOiUZefeuY?si=oEAHn9Jf2AkijUPs

 

시각장애인이 혼자 생활하면서 많은 불편함이 있었고, 우리 팀은 점자스티커와 사물 인식에 집중했다.

시각장애인은 물건을 사용할 때, 사전에 비장애인에게 도움을 받아 물건에 점자스티커를 부착해서 물건을 인식하고 있었다.

 

하지만, 매번 비장애인에게 도움을 받아 점자스티커를 부착하기에 어려움이 있을 것이라고 생각했고,

점자스티커 특성상 공간이 많이 필요하고 긴 내용을 담기에 공간이 부족할 수 있기 때문에, 이를 해소하고자 아이디어를 제시했다.

 

그래서, 카메라로 물건과 텍스트를 인식해서 사용자에게 들려주고, 사용자는 이를 바탕으로 음성 녹음을 해서 NFC 스티커에 담을 수 있게 하면, 후에 사용자가 물건을 구분하기 위해 NFC 스티커에 태그하면 음성 녹음을 들려주도록 하는 서비스를 생각했다.

2. 졸업작품 설계서 작성

설계서에 들어갈 주요 내용으로 아이디어 시나리오, 시장 조사, 요구사항 정의, 기능 설계 등이 있다.

아이디어 시나리오

우리 팀의 아이디어 시나리오를 그림으로 표현한 것이다.

 

시각장애인이 요리를 한다고 가정해보자,

여러가지 어려움이 있겠지만, 설탕과 소금의 구별, 뜯지 않은 밀키트나 조리팩의 구별이 어려울 것이다.

매번, 시각을 제외한 나머지 감각기관(후각, 미각, 촉각)으로 이것들을 구분하기엔 많은 스트레스를 받을 것이라고 생각했다.

 

그래서, 장치로 이게 어떤 물건인지 알려주면 사용자가 녹음을 해서 NFC 스티커에 저장하고 부착해서 들을 수 있게 하는 것이다.

프로토타입 모델링

프로토타입 3D 모델링

프로토타입을 3D 모델링으로 제작해봤다.

우리 팀은 라즈베리파이, 허스키렌즈, 스피커, 마이크등을 사용하려고 했지만, 기기의 크기가 생각 이상으로 커졌다(거의 텀블러 크기)

 

프로토타입을 모델링하고 나니까, 아이디어를 어느정도 뒤집을 필요가 있다고 생각했다.

현실적으로 이런 크기의 기기와 배터리까지 따로 들고다니면, 불편함을 해소하기보다 불편함을 더 들고다니는 것과 다르지 않다.

 

그래서, 우리 팀은 이 기기의 모든 기능을 갖고 있으면서, 작고 가볍기 까지 하면서 항상 들고 다니는 스마트폰으로 대체하기로 했다.

스마트폰에는 필요한 모든 장치를 갖고 있고, 항상 들고 다니기 때문에 이러한 문제를 해결할 수 있다고 생각해서 아이디어를 변경했다.

 

만약 모델링을 하지 않았다면, 개발이 시작되고 나서, 이러한 문제를 발견하고 뒤늦게 뒤집었을 것이다.

이런 과정을 통해 프로토타입과 모델링의 중요성을 알 수 있었다.

3.  졸업작품 개발

개발은 하드웨어 파트, 앱 파트, 서버 파트로 나눠 개발했다.

A. 서비스 플로우

사용자가 NFC 스티커에 음성녹음을 담는 과정

NFC 스티커에 음성 녹음을 담는 과정 설명

  1. 사용자가 앱을 통해 사물 위에 있는 텍스트를 인식한다
  2. 인식한 텍스트를 사용자에게 들려주고, 사용자는 이를 바탕으로 음성 녹음을 한다
  3. 음성 녹음 파일을 서버에 올리고, 올린 음성 녹음파일의 위치가 포함된 서버 주소를 반환한다
  4. 음성 녹음 파일이 포함된 서버 주소를 TCP 통신으로 라즈베리파이에 전송한다
  5. NFC 모듈을 통해 NFC 스티커에 서버 주소를 담는다

사용자가 NFC 스티커에서 음성 녹음을 듣는 과정

NFC 스티커에서 음성 녹음을 재생하는 과정

  1. 사물에 있는 NFC 스티커를 NFC 모듈로 태그해서, NFC 스티커에 있는 서버 주소를 가져온다
  2. 라즈베리파이에서 앱으로 서버 주소를 TCP 통신으로 전송한다
  3. 앱에서 음성 녹음 파일이 포함된 서버 주소로 다운로드 요청을 해서 음성 녹음파일을 받아온다
  4. 유저에게 음성 녹음 파일을 들려준다

B. 하드웨어 파트 - 모델링 및 구성

일단 하드웨어에는 라즈베리파이와 NFC 모듈인 PN532 모듈이 들어간다.

기본적으로 스마트폰에 NFC 기능이 탑재된 기기가 많지만,

아이폰6 이전 모델이나, 중국 저가형 모델에는 NFC 기능이 없는 모델이 있기 때문에, 따로 NFC 태그 역할을 해주는 기기를 제작했다.

하드웨어 단면 및 구성

모델링 부분에서 특별하게 신경 쓴 부분은 시각장애인이 사용하는 장치이기 때문에,

점자를 추가해서 어떤 부분이 NFC 모듈 부분인지 알려주도록 모델링을 하였다.

 

실제 모델링을 3D 프린팅으로 출력한 후에 라즈베리파이와 NFC 모듈을 넣은 모습이다.

하드웨어 구성을 보면, 라즈베리파이와 NFC 모듈이 배치되어 있고, NFC 태그를 할 수 있게, 모듈 앞에는 구성이 뚫려있다.

B. 하드웨어 파트 - 라즈베리파이 통신 코드

라즈베리파이에서 해야하는 역할은

  1. 플러터 앱에서 서버 주소를 전송 받으면 NFC 스티커에 저장
  2. NFC 스티커를 태그하면 NFC 스티커에 있는 서버 주소를 읽어서 플러터 앱에 전송

이렇게 2가지만 수행하면 되었지만, 문제가 발생했다.

라즈베리파이 통신 알고리즘 문제점

바로 언제 1번을 실행하고, 2번을 실행 해야하는 지, 타이밍을 결정하는 게 문제였다.

 

생각해보면, 라즈베리파이는 항상 코드를 무한 반복하고 있어야 하고,

그 와중에 사용자가 언제 NFC 스티커에 서버 주소를 입력할 지, NFC 스티커에서 서버 주소를 읽어올 지 마음을 읽는 게 아닌 이상

 

라즈베리파이 자체적으로 판단하는 게 어려웠다.

 

그래서 생각해낸 방법이 몇 가지가 있었고,

 

예를 들어, 5초 동안 NFC 스티커를 찾지 못 하면 1번을 수행하고, 그게 아니라면 2번을 수행하는 방식이 있었는데

이건 최악의 경우 사용자가 5초 동안 스티커에 NFC 모듈을 계속 태그하고 있어야 하고, 제대로 인식하지 못 하는 경우가 발생해서

 

결국, 다른 방법을 생각해야했다.

문제 해결 방법: Write, Read 모드 추가

팀장인 내가 생각한 문제 해결 방법은 '모드를 추가하는 것' 이었다.

여기에서 어떤 모드가 하는 역할은 Write 모드와 Read 모드를 통해 언제 1번, 2번을 수행할 지 구분을 앱에서 해주는 것이다.

 

Flutter 앱에서 Write 모드와 Read 모드를 먼저 tcp 통신으로 전송해준다.

라즈베리파이는 'Write' 모드를 수신하면 tcp 통신으로 서버 주소를 받아서, NFC 모듈을 통해 NFC 스티커에 수신한 서버 주소를 쓴다.

'Read' 모드를 수신하면, 라즈베리파이에서 NFC 스티커에서 서버 주소를 받을 준비를 하고 서버 주소를 읽으면 flutter 앱에 전송한다.

 

이렇게, 모드를 추가하고, flutter 앱에서 어떤 동작을 수행할 지 보내주는 방법을 통해 문제를 해결했다.

while True:
    print(f"Server is waiting for connections at {PORT}...")

    connection, client_address = server_socket.accept()
    print(f"Connected by {client_address}")
    mode = connection.recv(1024).decode().strip()

    if mode == "write":
        # 첫 번째 케이스: 플러터에서 TCP 통신으로 "write" 모드가 수신된 경우
        print("----wirte case---- Mode received: write")

        url = connection.recv(1024).decode().strip()
        print(f"----wirte case---- Received data: {url}")

        # NFC 카드가 감지될 때까지 대기
        if wait_for_nfc_card():
            if write_string_to_blocks(url, 4):
                response = "----wirte case---- URL successfully written to the NFC card."
                print("----wirte case---- URL successfully written to the NFC card.")
                
                # NFC 카드에서 문자열 읽기
                read_url = read_string_from_blocks(4, (len(url) + 3) // 4)
                print("----wirte case---- Read data from card:", read_url)
                response += f": {read_url}"
            else:
                response = "----wirte case---- Failed to write URL to the NFC card."
                print("----wirte case---- Failed to write URL to the NFC card.")
        else:
            response = "----wirte case---- No NFC card detected."
            print("----wirte case---- No NFC card detected.")

        connection.sendall(response.encode())
        print("----wirte case---- First case finished")
        connection.close()


    elif mode == "read":
        # 두 번째 케이스: 플러터에서 TCP 통신으로 "read" 모드가 수신된 경우
        print("----read case---- Mode received: read")
        
        if wait_for_nfc_card():
            # NFC 카드에서 문자열 데이터 읽기
            read_mode_url = read_string_from_blocks(4, 20)  # 예시로 최대 20개의 블록까지 읽도록 설정
            print("----read case---- Read data from card:", read_mode_url)
            connection.sendall(read_mode_url.encode())
        else:
            print("----read case---- No NFC card detected within 10 seconds.")
            connection.sendall(b"No NFC card detected within 10 seconds.")

        print("----read case---- Second case finished")
        connection.close()

    else:
        print("Invalid mode received")
        connection.sendall(b"Invalid mode")

 

 

라즈베리파이가 실행하는 코드의 일부이다.

이 코드가 가장 중요한 부분이며, 자세하게 보면,

 

우선, 한번 코드를 실행하면 계속 동작 가능하게 while True를 통해 무한 반복하고 있다.

그리고 가장 먼저 mode를 tcp 통신으로 받아 오고 있다.

 

만약에 "write"라면, 한번 더 TCP 통신으로 URL (서버 주소)를 받아오고, 그 후에 받아온 URL를 write_string_to_block()을 통해

NFC 스티커에 URL(서버 주소)를 쓴다.

 

만약에 "read"라면, NFC 스티커에서 read_mode_url(스티커에서 받아온 서버주소)를 읽을 준비를 하고,

읽어온 서버 주소를 Flutter 앱에 tcp 통신으로 넘겨준다.

라즈베리파이 로그

이러한 과정을 거쳐 문제를 해결했다.

C. 앱 파트

앱은 Flutter로 안드로이드와 ios 모두 사용할 수 있는 크로스 플랫폼이라서 flutter를 선택했다.

시각장애인이 스마트폰을 사용할 수 있을지 조사해봤고, TalkBack, VoiceOver 기능을 통해

시각장애인 또한 스마트폰을 사용할 수 있고, 어렵지 않게 사용한다는 것을 확인했다.

그래서, TalkBack 기능을 고려하여 앱을 제작했다.

앱 구조 설명 이미지

 

앱 파트에서 중요한 부분은 OCR(Optical Character Recognition) 기능이다.

결국 앱에서 OCR 기능을 제대로 구현하지 못 한다면, 이 서비스 자체를 수행하지 못 하기 때문에, OCR 기능 구현을 다뤄보겠다.

OCR은 Flutter에서 지원하는 Google ML kit를 사용해서 기능을 사용했다.

https://pub.dev/packages/google_ml_kit

 

google_ml_kit | Flutter package

A Flutter plugin to use all APIs from Google's standalone ML Kit for mobile platforms.

pub.dev

<앱 코드 부분>

촬영한 이미지에서 OCR을 통해 텍스트를 추출했는데,

테스트를 했을 때 예상하지 못 한 문제가 발생했다.

Flutter OCR 문제점

촬영한 이미지에 있는 모든 텍스트를 인식해서, 인식을 원하는 사물 외에 모든 텍스트를 읽는 문제가 발생했다.

 

시각장애인이 사용하는 특성상 카메라를 정확히 원하는 사물의 텍스트를 촬영하게 조절하기에는 어려움이 있다고 생각하여,

해결 방법으로 객체 탐지(Object Detection) 기능을 추가해서 문제를 해결하기로 했다.

문제 해결 방법 1: Object Dection 기능 추가

Object Dection 기능을 추가함으로써, 사물 위에 있는 텍스트만을 인식하도록 개선하였다.

추가적으로, 다수의 사물을 촬영했을 때, 다수의 사물에 있는 텍스트를 인식해서 읽어주는 문제가 있어서 이를 해결할 필요가 있었다.

void _recognizeTextInBoundingBox(XFile imageFile, Rect boundingBox) async {
    final inputImage = InputImage.fromFilePath(imageFile.path);
    final textRecognizer = GoogleMlKit.vision.textRecognizer(script: TextRecognitionScript.korean);
    try {
      final RecognizedText recognizedText = await textRecognizer.processImage(inputImage);
      String detectedText = '';
      List<Rect> newTextBoundingBoxes = [];
      for (TextBlock block in recognizedText.blocks) {
        final Rect rect = block.boundingBox;
        if (boundingBox.overlaps(rect)) {
          detectedText += block.text + ' ';
          newTextBoundingBoxes.add(rect);
        }
      }

      if (detectedText.isEmpty) {
        _speak("글씨를 인식할 수 없습니다. 다시 촬영해주세요.");
      } else {
        setState(() {
          textBoundingBoxes = newTextBoundingBoxes;
        });
        _speak(detectedText);
      }
    } catch (e) {
      print('Error recognizing text: $e');
    } finally {
      textRecognizer.close();
    }
  }

문제 해결 방법 2: Object Dection의 bounding box 활용

bounding box의 width와 height를 통해, 가장 큰 Object를 선택하고

가장 큰 Object 위에 있는 텍스트만 인식하도록 알고리즘을 개선했다.

void _detectObjectsAndRecognizeText(XFile imageFile) async {
    final inputImage = InputImage.fromFilePath(imageFile.path);
    final options = ObjectDetectorOptions(
      mode: DetectionMode.single,
      classifyObjects: true,
      multipleObjects: true,
    );
    final objectDetector = ObjectDetector(options: options);

    try {
      final List<DetectedObject> objects = await objectDetector.processImage(inputImage);
      if (objects.isNotEmpty) {
        DetectedObject largestObject = objects[0];
        for (DetectedObject object in objects) {
          if (object.boundingBox.width * object.boundingBox.height >
              largestObject.boundingBox.width * largestObject.boundingBox.height) {
            largestObject = object;
          }
        }

        final File imageFileObj = File(imageFile.path);
        final img.Image? image = img.decodeImage(imageFileObj.readAsBytesSync());
        if (image == null || !_isObjectProperlyCaptured(largestObject.boundingBox, Size(image.width.toDouble(), image.height.toDouble()))) {
          setState(() {
            objectBoundingBoxes = [];
            objectLabels = [];
            textBoundingBoxes = [];
          });
          return;
        }

        setState(() {
          objectBoundingBoxes = [largestObject.boundingBox];
          objectLabels = largestObject.labels.map((label) => label.text).toList();
          textBoundingBoxes = [];
        });

        _recognizeTextInBoundingBox(imageFile, largestObject.boundingBox);
      } else {
        setState(() {
          objectBoundingBoxes = [];
          objectLabels = [];
          textBoundingBoxes = [];
        });
        _speak("사물이 탐지되지 않았습니다. 다시 촬영해주세요.");
      }
    } catch (e) {
      print('Error occurred while detecting objects: $e');
    } finally {
      objectDetector.close();
    }
  }

이렇게 개선한 이유는 사용자인 시각장애인이 텍스트를 인식하고 싶은 사물을 카메라에 가깝게 두고 촬영할 것이라고 가정했기 때문에,

이러한 가정을 바탕으로 개선하였다.

OCR과 Object Dectection 기능 추가 이미지

기존에는 모든 사물 위에 텍스트를 인식했지만, 개선된 버전은 가장 큰 사물만 인식하도록 수정하였다.

 

이렇게 개선하고 시연을 해보니까 새로운 문제가 발생했다.

사실, 가장 근본적인 문제인 시각장애인이 어떻게 카메라를 잘 사용하게 할 수 있을까? 였다.

시각장애인이 주요 사용자인 이상, 카메라 사용에 있어 가이드를 제시해야한다고 생각했고,

 

그래서 bouding box를 활용해서 카메라의 가장자리를 넘어서 box가 있다면 카메라 위치를 옮길 수 있도록 알고리즘을 추가했다.

  bool _isObjectProperlyCaptured(Rect boundingBox, Size imageSize) {
    const double margin = 5.0;
    const double marginRight = 10.0;
    if (boundingBox.top < margin) {
      _speak("사물이 너무 상단에 위치해 있습니다. 카메라를 올려주세요.");
      return false;
    }
    if (boundingBox.right > imageSize.width - marginRight) {
      _speak("사물이 너무 오른쪽에 위치해 있습니다. 카메라를 오른쪽으로 옮겨주세요.");
      return false;
    }
    if (boundingBox.bottom > imageSize.height - margin) {
      _speak("사물이 너무 하단에 위치해 있습니다. 카메라를 내려주세요.");
      return false;
    }
    if (boundingBox.left < margin) {
      _speak("사물이 너무 왼쪽에 위치해 있습니다. 카메라를 왼쪽으로 옮겨주세요.");
      return false;
    }
    return true;
  }

결과는 밑에 시연 영상의 마지막 부분에서 확인할 수 있다.

D. 백엔드 파트

백엔드 파트에서 해야하는 역할은 Get, Post api를 만들어 Flutter 앱과 http api로 음성녹음 파일을 전송 및 저장하는 역할이다.

  1. Post 요청이 오면 음성녹음 파일을 저장하고, 음성녹음파일이 저장된 위치와 파일 이름을 서버 주소를 포함해서 전달
  2. Get 요청이 서버 주소와 녹음 파일의 경로와 이름이 포함된 파라미터를 통해 요청되면, 그 녹음파일을 전송

백엔드는 Spring 프레임워크를 통해 개발 했고, 간단한 역할만 수행하면 되었기에 간단하게 개발하였다.

백엔드 파트에서 중요한 Controller 코드를 첨부하겠다.

@PostMapping("/upload")
    public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
        try {
            String fileName = voiceService.storeFile(file);
            String localIpAddress = NetworkUtil.getLocalIpAddress();
            String fileDownloadUri = "http://" + localIpAddress + ":8080/" + fileName;
            System.out.println("post method : upload, file name:" + fileName);
            return ResponseEntity.ok(fileDownloadUri);
        } catch (IOException e) {
            logger.error("Could not upload the file: ", e);
            System.out.println("post method : IOException");
            return ResponseEntity.status(500).body("Could not upload the file: " + e.getMessage());
        }
    }

    @GetMapping("/{fileName:.+}")
    public ResponseEntity<Resource> downloadFile(@PathVariable("fileName") String fileName) {
        try {
            Path filePath = voiceService.loadFile(fileName);
            Resource resource = new UrlResource(filePath.toUri());

            if (resource.exists() || resource.isReadable()) {
                System.out.println("get method : download" + fileName);
                return ResponseEntity.ok()
                        .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
                        .body(resource);
            } else {
                return ResponseEntity.status(404).body(null);
            }
        } catch (MalformedURLException e) {
            logger.error("Error occurred while trying to load the file: ", e);
            return ResponseEntity.status(500).body(null);
        }
    }

4. 'NFC & OCR 기반 시각장애인 사물 인식 서비스' 시연

시연 영상

제일 중요한 시연 영상이다.

앞에서 길게 어떤 서비스인지 말했지만, 시연 영상을 보지 않으면 이해하기 어렵기 때문에, 시연 영상을 첨부해했다.

앱 시연 영상의 마지막 부분에서 Object Detection의 bouding box를 통해 사물이 카메라의 가장자리에서 벗어나 있다면,

알려줘서 사용자가 카메라 위치를 수정할 수 있게 한 부분과 여러 Object가 있을 때, 가장 큰 Object만을 선택한 시연 영상을 볼 수 있다.

5. 2024년 학술제 졸업작품 참가

2024년 11월에 순천향대학교에서 진행하는 학술제에 'NFC를 이용한 OCR 기반 시각장애인 사물 인식 서비스' 졸업작품으로 참가하였다.

먼저 포스터를 만들어서 참가하고, 본선에 진출하면 다른 학과와 같이 전시를 하면서, 다수의 심사위원에게 심사를 받아 입상이 결정된다.

포스터 전시 및 심사 결과

포스터 제작 및 전시 사진

우리 팀은 본선에 진출하게 되어, 다른 학과들과 같이 전시를 하게 되었고, 심사를 받았다.

심사를 받을 때, 심사위원 분들의 질문에 대해 답변을 했고, 좋은 조언을 들을 수 있었다.

 

심사 결과 우리 팀은 장려상을 받을 수 있었고, 우리 팀이 열심히 준비한 졸업작품이 입상을 해서 좋았다.

마무리하며..

기존에 여러 프로젝트에 참여하고 팀장도 많이 해봤지만,

이번 졸업작품 개발을 통해 많은 것을 배웠다...

  1. 팀원들과의 협업 문제 - 다음부턴 그냥 열심히 하는 팀원만 데리고 협업하는 게 정신에 좋다, 억지로 끌고 갔다가 팀장이 너무 힘들다..
  2. 특정 사용자에 맞게 알고리즘 개선 - 시각장애인을 대상으로 하는 서비스이기 때문에, 앱 부분에서 개선을 계속 진행했다
  3. 그래도 결국 완성하면 된다 - 팀원들 중에 해야할 일을 미루고 하지 않아서, 스트레스를 많이 받았지만, 결국 완성하면 지나간 일이다

이번 졸업 작품 프로젝트는 팀원 때문에 스트레스도 많이 받았지만,

다른 믿을 수 있는 팀원 덕분에 완성할 수 있었기 때문에 좋은 결과를 얻었다고 생각한다.

 

추가적으로 개선을 하게된다면, 백엔드 코드를 AWS Lambda로 배포하는 것으로 개선할 것이다.

 

이렇게, 1년 동안 개발했던 졸업작품에 대한 포스팅을 마치도록 하겠다.