주소를 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만줄이 넘는 파일인데 중간에 “폐지”라고 쓰여져 있는 부분은 사용안하는 부분 입니다.

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

PNU 코드를 이용해서 토지이용계획 확인하기

지번주소를 PNU 코드로 바꾸는 것에 관한 내용은 아래 링크 참고해주세요.

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

오늘은 PNU 코드를 이용해서 국토교통부 토지이용계획정보서비스 API에 요청을 보내고 해당 필지의 토지이용계획정보를 얻어와 보겠습니다.

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

토지이용계획은 굉장히 다양한 정보를 담고 있습니다. 예를들어 해당 필지가 상업지역이면서 대공방어협조구역 일 수도 있고 재정비촉진지구 일 수 도 있어요. 즉, API 정보를 보내면 필드가 1개만 있는게 아니라 배열로 반환된다는 점 입니다.

아래는 저희 동네에 있는 어떤 시설의 결과 값을 API로 요청해서 받은것 입니다. JSON 내용을 봐보면 배열에 담겨 있는 것을 확인할 수 있죠? 이점 참고해서 데이터를 가공하거나 사용하시면 되겠습니다.

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."
{
  "landUses": {
    "field": [
      {
        "regstrSeCode": "1",
        "pnu": "1126010200100830008",
        "lastUpdtDt": "2022-07-14",
        "manageNo": "15000001126020000000UQA01X0001001",
        "ldCode": "1126010200",
        "ldCodeNm": "서울특별시 중랑구 상봉동",
        "mnnmSlno": "83-8",
        "regstrSeCodeNm": "토지대장",
        "cnflcAt": "1",
        "cnflcAtNm": "포함",
        "prposAreaDstrcCode": "UQA01X",
        "prposAreaDstrcCodeNm": "도시지역",
        "registDt": "2017-09-05"
      },
      {
        "regstrSeCode": "1",
        "pnu": "1126010200100830008",
        "lastUpdtDt": "2022-07-14",
        "manageNo": "15000001126020000000UQA2200017017",
        "ldCode": "1126010200",
        "ldCodeNm": "서울특별시 중랑구 상봉동",
        "mnnmSlno": "83-8",
        "regstrSeCodeNm": "토지대장",
        "cnflcAt": "1",
        "cnflcAtNm": "포함",
        "prposAreaDstrcCode": "UQA220",
        "prposAreaDstrcCodeNm": "일반상업지역",
        "registDt": "2017-09-05"
      },
      {
        "regstrSeCode": "1",
        "pnu": "1126010200100830008",
        "lastUpdtDt": "2022-07-14",
        "manageNo": "15000001126020090001UBA1000001001",
        "ldCode": "1126010200",
        "ldCodeNm": "서울특별시 중랑구 상봉동",
        "mnnmSlno": "83-8",
        "regstrSeCodeNm": "토지대장",
        "cnflcAt": "1",
        "cnflcAtNm": "포함",
        "prposAreaDstrcCode": "UBA100",
        "prposAreaDstrcCodeNm": "과밀억제권역",
        "registDt": "2017-09-05"
      },
      {
        "regstrSeCode": "1",
        "pnu": "1126010200100830008",
        "lastUpdtDt": "2022-07-14",
        "manageNo": "15800001126020192019UNE2000001003",
        "ldCode": "1126010200",
        "ldCodeNm": "서울특별시 중랑구 상봉동",
        "mnnmSlno": "83-8",
        "regstrSeCodeNm": "토지대장",
        "cnflcAt": "1",
        "cnflcAtNm": "포함",
        "prposAreaDstrcCode": "UNE200",
        "prposAreaDstrcCodeNm": "대공방어협조구역",
        "registDt": "2019-07-01"
      },
      {
        "regstrSeCode": "1",
        "pnu": "1126010200100830008",
        "lastUpdtDt": "2022-07-14",
        "manageNo": "30600001126020110009UMZ1000001001",
        "ldCode": "1126010200",
        "ldCodeNm": "서울특별시 중랑구 상봉동",
        "mnnmSlno": "83-8",
        "regstrSeCodeNm": "토지대장",
        "cnflcAt": "1",
        "cnflcAtNm": "포함",
        "prposAreaDstrcCode": "UMZ100",
        "prposAreaDstrcCodeNm": "가축사육제한구역",
        "registDt": "2017-09-05"
      },
      {
        "regstrSeCode": "1",
        "pnu": "1126010200100830008",
        "lastUpdtDt": "2022-07-14",
        "manageNo": "30600001126020190027ZA00130001001",
        "ldCode": "1126010200",
        "ldCodeNm": "서울특별시 중랑구 상봉동",
        "mnnmSlno": "83-8",
        "regstrSeCodeNm": "토지대장",
        "cnflcAt": "2",
        "cnflcAtNm": "저촉",
        "prposAreaDstrcCode": "ZA0013",
        "prposAreaDstrcCodeNm": "건축선",
        "registDt": "2019-11-21"
      },
      {
        "regstrSeCode": "1",
        "pnu": "1126010200100830008",
        "lastUpdtDt": "2022-07-14",
        "manageNo": "61100001126020150099UDA1000003001",
        "ldCode": "1126010200",
        "ldCodeNm": "서울특별시 중랑구 상봉동",
        "mnnmSlno": "83-8",
        "regstrSeCodeNm": "토지대장",
        "cnflcAt": "1",
        "cnflcAtNm": "포함",
        "prposAreaDstrcCode": "UDA100",
        "prposAreaDstrcCodeNm": "재정비촉진지구",
        "registDt": "2017-09-05"
      },
      {
        "regstrSeCode": "1",
        "pnu": "1126010200100830008",
        "lastUpdtDt": "2022-07-14",
        "manageNo": "61100001126020170174UQS1160001001",
        "ldCode": "1126010200",
        "ldCodeNm": "서울특별시 중랑구 상봉동",
        "mnnmSlno": "83-8",
        "regstrSeCodeNm": "토지대장",
        "cnflcAt": "2",
        "cnflcAtNm": "저촉",
        "prposAreaDstrcCode": "UQS116",
        "prposAreaDstrcCodeNm": "대로3류(폭 25m~30m)",
        "registDt": "2017-09-05"
      },
      {
        "regstrSeCode": "1",
        "pnu": "1126010200100830008",
        "lastUpdtDt": "2022-07-14",
        "manageNo": "61100001126020170402UDA1000001001",
        "ldCode": "1126010200",
        "ldCodeNm": "서울특별시 중랑구 상봉동",
        "mnnmSlno": "83-8",
        "regstrSeCodeNm": "토지대장",
        "cnflcAt": "1",
        "cnflcAtNm": "포함",
        "prposAreaDstrcCode": "UDA100",
        "prposAreaDstrcCodeNm": "재정비촉진지구",
        "registDt": "2017-12-06"
      },
      {
        "regstrSeCode": "1",
        "pnu": "1126010200100830008",
        "lastUpdtDt": "2022-07-14",
        "manageNo": "61100001126020170402UQQ3000001001",
        "ldCode": "1126010200",
        "ldCodeNm": "서울특별시 중랑구 상봉동",
        "mnnmSlno": "83-8",
        "regstrSeCodeNm": "토지대장",
        "cnflcAt": "1",
        "cnflcAtNm": "포함",
        "prposAreaDstrcCode": "UQQ300",
        "prposAreaDstrcCodeNm": "지구단위계획구역",
        "registDt": "2017-12-06"
      }
    ],
    "totalCount": "10",
    "numOfRows": "10",
    "pageNo": "1",
    "resultCode": null,
    "resultMsg": null
  }
}

Method Channel을 이용해서 플러터에서 OpenCV Car Detection 구현하기(1)

Flutter에서 Dart 언어 이용해서 OPENCV를 쓸 수 있는 플러그인이 있긴 합니다.

OPENCV for FLutter

그런데 제가 구현해 보고 싶은 사진속 자동차 인식은 CascadeClassifier 함수를 사용해야 하거든요. 위 플러그인은 그게 지원이 안되더라구요. 제가 구현하고 싶은건 아래와 같은 내용을 플러터앱으로 시도해 보는 것 이라서요.

Vehicle Detection and Counting System using OpenCV

그래서 어쩔수 없이 Kotlin에서 OpenCV를 이용하고 Method Channel을 통해서 그 결과를 받는 쪽으로 구현을 해봤습니다. 먼저 android/app/build.gradle 파일에 Kotlin에서 사용할 OpenCV 의존성을 추가해 주셔야 합니다.

build.gradle 일부 코드

dependencies {
    // .. 원래 있던 것은 놔두시고 아래 내용 추가해주세요. 버젼은 매번 달라질 수 있습니다. ..
    implementation 'com.quickbirdstudios:opencv:4.5.2'
}

android/app/src/main에 assets 폴더를 만드시고 car.xml을 저장해두세요. 저는 아래 링크에서 구했어요. 이게 맞는지는 확인을 해봐야 합니다만… car.xml

MainActivity.kt 주요 코드

  // import는 알아서 해주세요~
  class MainActivity: FlutterActivity() {
      private val CHANNEL = "패키지 이름/채널 이름";
  
      override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
          super.configureFlutterEngine(flutterEngine)
          OpenCVLoader.initDebug();
          MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler{call, result ->
              // dart 코드에서 Method Channel 호출 할 때 filepath라는 변수에 이미지 절대주소를 넣어서 보냅니다. 
              var filepath: String? = call.argument("filepath")
              if (call.method == "findEdge") {
                  val srcMat: Mat = Imgcodecs.imread(filepath)
                  // 그레이 스케일  
                  val graySrc = Mat()
                  Imgproc.cvtColor(srcMat, graySrc, Imgproc.COLOR_BGR2GRAY)
                  // 가우시안 블러
                  val gausSrc = Mat()
                  Imgproc.GaussianBlur(graySrc, gausSrc, Size(5.0, 5.0), 0.0)
                  // 적절한 단어가 뭔지 모르겠으나, 팽창?
                  val diSrc = Mat()
                  val kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, Size(3.0, 3.0))
                  Imgproc.dilate(gausSrc, diSrc, kernel)
                  // 이 부분도 팽창의 일환인것 같습니다. 모폴로지 연산이라고 하네요.
                  val morSrc = Mat()
                  val kernel2 = Imgproc.getStructuringElement(Imgproc.MORPH_ELLIPSE, Size(2.0,2.0))
                  Imgproc.morphologyEx(diSrc, morSrc, Imgproc.MORPH_CLOSE, kernel2)
                  val rects = MatOfRect()
                  val assetManager = resources.assets
                  // assets에 넣어놓은 car.xml을 불러옵니다.
                  val inputStream = assetManager.open("car.xml")
                  val cdir = getDir("cascade", Context.MODE_PRIVATE)
                  val mfile = File(cdir, "car.xml")
                  mfile.writeBytes(inputStream.readBytes())
                  inputStream.close()
                  // CascadeClassifier 호출합니다
                  val car = CascadeClassifier(mfile.getAbsolutePath())
                  car.detectMultiScale(morSrc, rects, 1.1, 1)
                  var ret: String = ""
                  val pts:Array<Rect> = rects.toArray()
                  // 리턴값 준비
                  for (j in 0 until pts.size) {
                      ret = ret + pts[j].x.toString() + "," +
                              pts[j].y.toString() + "," + pts[j].width.toString() + "," + pts[j].height.toString() + "\n"
                      Log.d("Contour", pts[j].x.toString() +
                              "," + pts[j].y.toString() + "," + pts[j].width.toString() + "," + pts[j].height.toString())
                  }
                  result.success(ret)
              }
          }
      }
  }

플러터 쪽 코드는 method_channel과 image_picker 그리고 CustomPainer를 이용해서 구현했습니다. 이건 그렇게 어렵지 않으니 스스로 검색해서 만들어 보세요~

테스트 결과

  • 시료1
    • 이미지1
    • 인터넷에서 구한 네비게이션 화면 입니다. 저는 네비게이션 화면을 이용해서 자동차 디텍팅하는 앱을 생각하고 있어서 이런 유형의 시료가 제일 중요한데 정적 이미지에서는 정확도가 그리 높지 않아 보이네요.
  • 시료2
    • 이미지2
    • 제가 직접 찍은건데요, 이것도 그다지 높아보이진 않습니다만 앞에 것보다는 좀 쓸모 있어 보이기도 합니다.
  • 시료3
    • 이미지3
    • 위에 있는 링크에서 사용한 시료 인데요. 이건 해당 포스팅에 있는것과 거의 비슷하게 나왔네요. 아마도 촬영각도가 높으면 높을 수록 결과가 좋게 나오는거 아닌가 생각이 듭니다.
  • 시료4
    • 이미지4
    • 사람은 어떨까? 하는 궁금증이 들어서 광고에 나왔던 CCTV 화면을 한번 시도해봤습니다. 사람용은 아닌듯 하네요.

추가적으로 검색을 해본 결과, 정적이미지의 결과 만으로는 쓸만한 값이 나오기 어렵다고 하네요. 동영상에서 일정 간격으로 이미지를 추출해서 변화값을 함께 이용하는 방식을 채택해야 실사용 할 수 있는 결과가 나온다고 합니다. 그것도 시간이 나는데로 구현해 보겠습니다.