동영상의 특정 영역안에서 움직이는 물체가 있는 구간찾기

최근에 출시되는 CCTV 관제 프로그램에는 움직임을 감지해서, 해당 구간만 조회할 수 있는 기능이 있습니다. 그런데 구형 CCTV 시스템이거나, 원본 동영상 파일만 가지고 있을 때 이런 기능을 구현하려면 별도의 프로그램이 있어야 합니다. 게다가 특정 영역안에서만 움직임을 필터링하려면 몇 가지 조건을 더 추가해 주어야 하지요. 오늘은 이러한 프로그램의 핵심기술이 될 수 있는 루틴을 공유해 보고자 합니다.

def make_frames_and_trace(self, filename):
    # 비디오 캡쳐할 파일을 지정하고 각종 변수 가져오기
    cpt = cv2.VideoCapture(filename)
    frame_count = int(cpt.get(cv2.CAP_PROP_FRAME_COUNT))
    fps = cpt.get(cv2.CAP_PROP_FPS)
    f_width = cpt.get(cv2.CAP_PROP_FRAME_WIDTH)
    f_height = cpt.get(cv2.CAP_PROP_FRAME_HEIGHT)

    # 첫번째 프레임 이미지 얻기
    cpt.set(cv2.CAP_PROP_POS_FRAMES, 0)
    _, frame1 = cpt.read()
    on_blank = True
    prev_second = 0.0

    # 1초 단위로 움직임을 체크하려고 합니다
    fps_i = int(fps)

    has_track = None

    for i in range(1, frame_count - 1, fps_i):
        if i + fps < frame_count:
            # 비교할 프레임 이미지 얻기
            cpt.set(cv2.CAP_PROP_POS_FRAMES, i+fps_i)
            _, frame2 = cpt.read()
            preview = frame1
            dst1 = cv2.cvtColor(frame1, cv2.COLOR_BGR2GRAY)
            dst2 = cv2.cvtColor(frame2, cv2.COLOR_BGR2GRAY)
            diff = cv2.absdiff(dst1, dst2)
            ret_thr, thr = cv2.threshold(
                diff, 30, 255, cv2.THRESH_BINARY)
            dilate = cv2.dilate(thr, self.kernel)
            contours, hierarchy = cv2.findContours(
                dilate, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
            has_it = False
            for ct in range(len(contours)):
                # 내가 지정한 영역 rect1과 윤곽선추출된 최소영역인 rect2가 겹치는지를 점검합니다
                area_size = cv2.contourArea(contours[ct])
                min_rect = cv2.minAreaRect(contours[ct])
                rect1 = [self.rect.left(), self.rect.top(),
                         self.rect.right(), self.rect.bottom()]
                rect2 = [min_rect[0][0] / f_width, min_rect[0][1] / f_height,
                         (min_rect[0][0] + min_rect[1][0]) / f_width,
                         (min_rect[0][1] + min_rect[1][1]) / f_height]
                if area_size > 100 and rect2[2] - rect2[0] > 0.01 and rect2[3] - rect2[1] > 0.01 and self.check_overlap(rect1, rect2):
                    x,y,w,h = cv2.boundingRect(contours[ct])
                    # 미리보기 화면 그리기
                    cv2.rectangle(preview, (x,y), (x+w,y+h), (255, 0, 0), 5)
                    has_it = True

            current_second = int((i-1) / fps)

            # 변화가 감지되었나?
            if has_it:
                # 기존에 공백구간이었나?
                if on_blank:
                    on_blank = False
                    prev_second = current_second
                    has_track = TrackItem(filename, current_second, current_second)
                else:
                    prev_second = current_second
                    if has_track is not None:
                        has_track.end = current_second
            else:
                # 10초 이상 감지되지 않았는가? 10초 이상 변화가 없으면 새로운 구간을 만들어도 됩니다
                if current_second - prev_second > 10:
                    on_blank = True
                    if has_track is not None:
                        # 감지된 구간의 길이가 3초 이상일 때만 추가
                        if has_track.end - has_track.start >= 3:
                            self.trackAdded.emit(has_track)
                        has_track = None
            # 감지를 원하는 영역을 미리보기에 그려주기
            cv2.rectangle(preview,
                          (int(self.rect.left() * f_width), int(self.rect.top() * f_height)),
                          (int(self.rect.right() * f_width), int(self.rect.bottom() * f_height)),
                          (0,255,0), 5)
            frame1 = frame2
    if has_track is not None:
        self.trackAdded.emit(has_track)

# arr1과 arr2 구간이 겹치는지 체크
def check_overlap(self, arr1, arr2):
    if arr1[0] == arr1[2] or arr1[1] == arr1[3] or arr2[0] == arr2[2] or arr2[1] == arr2[3]:
        return False
    if arr1[0] > arr2[2] or arr2[0] > arr1[2]:
        return False
    if arr1[1] > arr2[3] or arr2[1] > arr1[3]:
        return False
    return True

그리하여 아래와 같은 응용프로그램을 만들어 볼 수 있었는데요, 테스트가 완료되어서 최소한의 안정화 버전이 갖춰지는대로 github에 공개하고 여기에도 공유해 보겠습니다. 이미지1

Flutter에서 MiBand4의 현재걸음수를 가져오기

써드파티 앱에서 샤오미 미밴드(MiBand4)에 집계된 현재걸음수를 알아내려면 어떻게 해야 할까요? 오늘은 그 방법에 대해 정리해보고자 합니다. 방법은 간단합니다. 일단 flutter_blue_plus 라이브러리를 dependencies에 정의해주세요.

그런 다음 flutter_blue_plus 인스턴스와 변수들을 초기화 합니다.

static const String MI_BAND_NAME = "Mi Smart Band 4";
static const String UUID_SERVICE_MIBAND =
      "0000fee0-0000-1000-8000-00805f9b34fb";
static const String UUID_CHARACTERISTIC_REALTIME_STEPS =
      "00000007-0000-3512-2118-0009af100700";
FlutterBluePlus flutterBlue = FlutterBluePlus.instance;
BluetoothDevice? miDevice;
BluetoothCharacteristic? stepChr;

그리고 나서 연결하려는 블루투스 디바이스를 찾습니다. 저 같은 경우엔 디바이스 네임을 특정해서 하드코딩된 방식으로 찾았지만, 보통의 경우 별도의 선택 UI를 만들어서 연결합니다.

// 이미 연결되어 있으면 먼저 연결을 해제합니다
List<BluetoothDevice> devices = await flutterBlue.connectedDevices;
if (devices.length > 0) {
  for (BluetoothDevice d in devices) {
    if (d.name == MI_BAND_NAME) {
      d.disconnect();
    }
  }
}
// 이제 다시 연결합니다. 
flutterBlue.startScan(timeout: Duration(seconds: 4));
var subs = flutterBlue.scanResults.listen((results) async {
  for (ScanResult r in results) {
    if (r.device.name == MI_BAND_NAME) {
      miDevice = r.device;
      flutterBlue.stopScan();
      await miDevice!.connect(timeout: Duration(seconds: 4), autoConnect: false);
    }
  }
});

이제 현재걸음수를 얻기 위해 필요한 블루투스특성을 얻어옵니다.

List<BluetoothService> services = await miDevice!.discoverServices();
for (BluetoothService s in services) {
    if (s.uuid.toString() == UUID_SERVICE_MIBAND) {
        for (BluetoothCharacteristic c in basicService!.characteristics) {
            if (c.uuid.toString() == UUID_CHARACTERISTIC_REALTIME_STEPS) {
              stepChr = c;
            }
        }
    }
}

마지막으로 해당특성을 이용해서 현재 걸음수를 얻어와 봅시다. 여기서 중요한 점은 돌아오는 바이트스트림의 1,2번째에 숫자가 패키징되어서 저장된다는 점 입니다.

await stepChr!.setNotifyValue(true);
stepChr!.value.listen((data) {
    if (data.length > 2) {
        int steps = ((((data[1] & 255) | ((data[2] & 255) << 8))));
        print("Current Step: ${steps}");
    }      
});

심박수 정보도 얻어오고 싶은데 아직은 연구가 필요한 상황입니다. 추후에 정리되면 게시하겠습니다. 그 이후에 음악재생하는 방법도 게시해보고자 합니다.

주소를 PNU 코드로 변환해봅시다

PNU(필지고유번호)라는 것이 있습니다. 우리가 살고 있는 곳의 주소를 숫자로만 이루어진 코드로 표현하는 방식 인데요. 정부에서 제공하는 GIS OPEN API를 사용하다보면 이 코드를 이용해야 하는 경우가 많습니다.

예를들어 아래와 같은 데이터가 그러한데요.

국토교통부_토지이용계획정보서비스

내가 살고 있는 곳이 주거지역인지 상업지역인지 등의 토지용도를 알아내려고 할 때 요청값으로 PNU코드를 사용해야 합니다. 위 링크의 REST Request Parameter를 봐보시면 알겠지만, 요청변수에 지번주소는 존재하지 않아요. 지번주소를 PNU 코드로 변환해서 보내줘야 결과를 얻을 수 있습니다.

그런데 이 PNU 코드는 아래와 같은 규칙으로 정해집니다.

광역시도코드(2자리) + 시/군/구 코드(3자리) + 읍/면/동 코드(3자리) + 
리 코드(2자리) + 토지/임야 코드(1자리) + 
본번 코드(4자리) + 부번 코드(4자리)

예를들어 서울특별시 중랑구 상봉동 126-39을 PNU 코드로 바꿔보면요

  • 광역시도 코드는 서울이니까 : 11
  • 시군구 코드는 중랑구니까 : 260
  • 읍면동 코드는 상봉동이니까 : 102
  • 리 코드는 없으니까 : 00
  • 토지니까 : 1 (‘산’ 이 붙는 주소의 경우 임야라서 2가 붙습니다)
  • 본번은 4자리여야 하니까 앞에 0을 붙여서 : 0126
  • 부번도 4자리여야 하니까 앞에 00을 붙여서 : 0039
  • 이렇게 해서 전체코드는 : 1126010200101260039 입니다.

주소를 PNU 코드로 입력하려면 적어도 리까지는 맵타입이던지 Nested Object 데이터가 있어야 하겠죠? 그때 쓰면 좋을 시료 파일을 공유해봅니다. 2022년 4월 버젼이에요. 4만줄이 넘는 파일인데 중간에 “폐지”라고 쓰여져 있는 부분은 사용안하는 부분 입니다.

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."