commit 37267985b4a3f094b94c0ebdb564e49594181771 Author: Young Hoon Lee Date: Tue Sep 23 20:59:17 2025 +0900 First commit diff --git a/ExtEXIFData.py b/ExtEXIFData.py new file mode 100644 index 0000000..62220b8 --- /dev/null +++ b/ExtEXIFData.py @@ -0,0 +1,192 @@ +import os + +from PIL import Image +from PIL.ExifTags import TAGS, GPSTAGS + +import UtilPack as util + +# EXIF 데이터에서 사진이 찍힌 날자와 시간을 가져온다 +def GetDateInfo(ImgPath): + img = _imageOpen(ImgPath) + if None == img: + return "", 0.0, 0.0 + + dataEXIF = _getEXIFData(img) + date = _getDateEXIF(dataEXIF) + dataGPS = _parseGPSInfo(dataEXIF) + lat, lon = _getlatlon(dataGPS) + + return date, lat, lon + +def ProcessImg(imgAbsPath, fRatio, nQlt = 85, bOrientApy = True): + # + dirSave = "Thumb" + dirpath, filename = os.path.split(imgAbsPath) + parenDir = os.path.dirname(dirpath) + dirThumb = os.path.join(parenDir, dirSave) + finalPath = os.path.join(dirThumb, filename) + + # 설정은 상관없이 이전에 리사이즈 해 둔 것이 있으면 경로 반환 + if True == os.path.exists(finalPath): + #print(f"[] -> {finalPath}") + return finalPath + + img = _imageOpen(imgAbsPath) + if None == img: + return imgAbsPath + + nRotate = 0 + if True == bOrientApy: + dataEXIF = _getEXIFData(img) + nRotate = _getOrientationEXIF(dataEXIF) + + img = _rotateImg(img, nRotate) + img = _resizeImg(img, fRatio) + + if False == os.path.exists(dirThumb): + os.makedirs(dirThumb, 0o777) + print(f"Thumb folder Created : {dirThumb}") + + img.save(finalPath, optimize=True, quality=nQlt) + + #print(f"{imgAbsPath} -> {finalPath}") + + return finalPath + +def _rotateImg(imgSrc, nRotate): + if None == imgSrc: + return imgSrc + + if not isinstance(imgSrc, Image.Image): + #raise TypeError("Input must be a PIL Image object") + print("Type Error") + return imgSrc + + if 0 >= nRotate or 360 <= nRotate: + return imgSrc + + imgRet = imgSrc.rotate(nRotate, expand=True) + + bbox = imgRet.getbbox() + imgRet = imgRet.crop(bbox) + + return imgRet + +def _resizeImg(imgSrc, fRatio ): + if None == imgSrc: + return imgSrc + + if not isinstance(imgSrc, Image.Image): + #raise TypeError("Input must be a PIL Image object") + return imgSrc + + if 0 >= fRatio or 1.0 < fRatio: + return imgSrc + + imgRet = imgSrc.resize((int(imgSrc.width * fRatio), int(imgSrc.height * fRatio))) + return imgRet + +def _imageOpen(ImgPath): + imgRet = None + try: + # 이미지 열기 시도 + imgRet = Image.open(ImgPath) + imgRet.load() # 이미지 파일을 실제로 로드하여 확인 + + util.DbgOut("Image open successful") + # 이미지 처리 코드 추가 + except FileNotFoundError: + util.DbgOut(f"can't find Image': {ImgPath}") + except OSError: + util.DbgOut(f"Image broken or unsupported MIME: {ImgPath}") + except Exception as e: + util.DbgOut(f"unexpected error: {e}") + + return imgRet + +# 이미지에서 EXIF 데이터를 추출하여 딕셔너리 형태로 반환 +def _getEXIFData(image): + exif_data = {} + try: + info = image._getexif() + if info: + for tag, value in info.items(): + decoded = TAGS.get(tag, tag) + exif_data[decoded] = value + except AttributeError: + pass + + return exif_data + +def _getDateEXIF(exif_data): + date_taken = exif_data.get('DateTimeOriginal') + return date_taken + +# 0 : NoData +# 1: "Landscape (normal) -> 0" +# 3: "Landscape (upside down) -> 180" +# 6: "Portrait (rotated 90° CW) -> 270" +# 8: "Portrait (rotated 90° CCW) -> 90" +def _getOrientationEXIF(exif_data): + if exif_data is None: + return 0 + + retValue = 0 + # EXIF 태그에서 Orientation 추출 + for tag, value in exif_data.items(): + tag_name = TAGS.get(tag, tag) + if tag_name == 'Orientation': + if value == 3: + retValue = 180 + elif value == 6: + retValue = 270 + elif value == 8: + retValue = 90 + else: # value == 1: + retValue = 0 + break + + return retValue + +# EXIF 데이터에서 GPS 정보를 파싱하여 딕셔너리 형태로 반환 +def _parseGPSInfo(exif_data): + gps_info = {} + if 'GPSInfo' in exif_data: + for key in exif_data['GPSInfo'].keys(): + decode = GPSTAGS.get(key, key) + gps_info[decode] = exif_data['GPSInfo'][key] + + return gps_info + +# 도, 분, 초 형식의 GPS 데이터를 도 형식으로 변환 +def _convertToDegrees(value): + + d = 0.0 + if 0 < value[0]: + d = float(value[0]) + + m = 0.0 + if 0 < value[1]: + m = float(value[1]) / 60.0 + + s = 0.0 + if 0 < value[2]: + s = float(value[2]) / 3600.0 + + return d + m + s + +# GPS 정보를 사용하여 위도와 경도를 도 형식으로 반환 +def _getlatlon(gps_info): + lat,lon = 0.0, 0.0 + if 'GPSLatitude' in gps_info and 'GPSLatitudeRef' in gps_info: + lat = _convertToDegrees(gps_info['GPSLatitude']) + if gps_info['GPSLatitudeRef'] != 'N': + lat = -lat + + if 'GPSLongitude' in gps_info and 'GPSLongitudeRef' in gps_info: + lon = _convertToDegrees(gps_info['GPSLongitude']) + if gps_info['GPSLongitudeRef'] != 'E': + lon = -lon + + return lat, lon + diff --git a/JusoMngr.py b/JusoMngr.py new file mode 100644 index 0000000..7677ad4 --- /dev/null +++ b/JusoMngr.py @@ -0,0 +1,51 @@ +import csv + +class JusoDB: + def __init__(self, path_csv): + self.path = path_csv + self.dict_juso = [] + + self.ReadCSV(path_csv) + + def ReadCSV(self, path): + # CSV 파일 읽기 + with open(path, mode='r', newline='', encoding='utf-8') as file: + reader = csv.DictReader(file) + # 헤더 읽기 + header = next(reader) + + # 각 행을 딕셔너리로 저장 + for row in reader: + self.dict_juso.append(row) + + print(f"Juso Count : {len(self.dict_juso)}") + + def CompareAndFindValue(self, src, trg, value): + retValue = "" + for row in self.dict_juso: + temp1 = value.replace(" ", "").strip() + temp2 = row[src].replace(" ","").strip() + if temp1 == temp2: + retValue = row[trg] + break; + + return retValue + + def ConvJusoToRoad(self, juso): + return self.CompareAndFindValue("jibun", "road", juso) + + def ConvRoadToJibun(self, juso): + return self.CompareAndFindValue("road", "jibun", juso) + + def GetRoadArea(self, road): + if road == "": + return "" + + #서울시 용산구 무시무시로20길 99-99 <- 현재까지 가진 데이터는 전부 이런 형식 + words = road.split() + if len(words) < 3: + return "" + + return words[2] + + \ No newline at end of file diff --git a/Main.py b/Main.py new file mode 100644 index 0000000..33231be --- /dev/null +++ b/Main.py @@ -0,0 +1,26 @@ +import os +import sys + +import MgrUtilityPoleUI + +from PyQt5.QtWidgets import QApplication + + +def main(argc, argv): + app = QApplication(argv) + + app.setQuitOnLastWindowClosed(True) + main_window = MgrUtilityPoleUI.MyApp() + main_window.show() + + sys.exit(app.exec_()) + + +# For Main Loop +if __name__ == '__main__': + argc = len(sys.argv) + argv = sys.argv + main(argc, argv) + + + diff --git a/MgrUtilityPoleConverter.py b/MgrUtilityPoleConverter.py new file mode 100644 index 0000000..b6a5e42 --- /dev/null +++ b/MgrUtilityPoleConverter.py @@ -0,0 +1,303 @@ +import os +import sys + +import UtilPack as util +import PoleXLS as myxl +import ExtEXIFData as exEXIF +import VWorldAPIs as vwapi +import JusoMngr + +mCurPath = "" # 스크립트가 실행되는 경로 +mArcPath = "Zips" # 압축파일이 있는 폴더, 하위를 훑는다. +mXlsPath = "" # 엑셀 파일이 있는 경로, 틀리면 안됨. +mImgPath = "/Volumes/ExSSD/Working/Images" # 이미지가 들어있는 폴더, 이 아래에 A,B,Pole 등이 있어야 한다. + +# 분류작업을 위해 임시로 생성 +mTempPath = "/Volumes/ExSSD/Working/TmpImgs" + +#def main(argc, argv): +# if argc != 2: +# printUsage() +# return + +# 기준 경로, 사용할 경로 +# 사용할 경로가 상대경로면 기준 경로를 이용해 계산, +# 사용할 경로가 절대경로만 경로가 유효한지 확인해서 그대로 반환, 아니라면 기준경로 반환 +# 기준경로는 따로 입력되는게 없으면 실행 경로 +def GetAbsPath(PathCri, PathTrg): + if True == util.IsEmptyStr(PathCri): + PathCri = os.getcwd() + + PathRet = PathCri + if True == os.path.isabs(PathTrg): + PathRet = os.path.join(PathCri, PathTrg) + else: + PathRet = PathTrg + + if False == os.path.exists(PathTrg): + PathRet = PathCri + + return PathTrg + +def ExtractArchives(pathArc, pathTrg): + if False == os.path.exists(pathArc): + return [] + + ZIPList = os.listdir(pathArc) + + retUNZipList = [] + for zipFile in ZIPList: + zipName, zipExt = os.path.splitext(zipFile) + trgPath = os.path.join(pathTrg, zipName) + + # 압축을 푼 폴더가 존재한다면 넘어간다. 이후 보강해야 함 + if True == os.path.exists(trgPath): + continue + + zipPath = os.path.join(pathArc, zipFile) + util.ExtractZIP(zipPath, trgPath) + + retUNZipList.append(trgPath) + + return retUNZipList + + +def GetPoleInfos(xls, nRow): + if None == xls or False == isinstance(xls, myxl.PoleXLS): + return [] + + retList = xls.GetAllRowValue("PoleInfo", nRow, 5) + return retList + + +def GetJPGFileList(listUNZip, pathImage): + trgDirList = [] + # 압축 푼 폴더가 있으면 그것만, 아니면 이미지 폴더 전부를 다 + if len(listUNZip) > 0: + trgDirList = listUNZip + else: + trgDirList = util.GetSubDirectories(pathImage) + + retImgList = [] + for dirName in trgDirList: + imgSubDirPath = os.path.join(pathImage, dirName) + contents = os.listdir(imgSubDirPath) + + for item in contents: + name, ext = os.path.splitext(item) + if ext.lower() == '.jpg': + imgPath = os.path.join(imgSubDirPath, item) + retImgList.append(imgPath) + + return retImgList + +# 필요한 이미지 정보를 모아 한 시트에 전부 넣는다. +def CollectImageData(xls, sheetTrg, listImg, pathImgDir): + if None == xls or False == isinstance(xls, myxl.PoleXLS) or True == util.IsEmptyStr(sheetTrg): + return -1 + + pathcsv = os.path.join(mCurPath, "JusoDB.csv") + jusomgr = JusoMngr.JusoDB(pathcsv) + + nIdx = 1 + for item in listImg: + # 이미지가 삽입되었는지 여부 확인 + if 0 < xls.FindInsertedImgPath(pathImgDir, item): + nIdx = xls.FindLastRow(sheetTrg) + 1 + continue + + all_date, lat, lon = exEXIF.GetDateInfo(item) + pathRel = os.path.relpath( item, pathImgDir ) + level = util.GetParentDirName(item, 1) + if level == "Thumb": + continue + + # 0 : 지번, 1 : 구역, 2 : 도로명, 3 : 도로명 구역 + juso = vwapi.GetJusofromGPS(lat, lon) + + # 주소 조회가 안되는 현상이 있다. 재시도... + if lat != 0.0 and lon != 0.0 and juso[2] == "": + juso = vwapi.GetJusofromGPS(lat, lon) + + # 도로명 주소를 못 끌어오면, 저장해 둔 목록에서 한번 더 조회 + if juso[0] != "" and juso[2] == "": + juso[2] = jusomgr.ConvJusoToRoad(juso[0]) + juso[3] = jusomgr.GetRoadArea(juso[2]) + + xls.SetCellValueStr(sheetTrg, 1, nIdx, nIdx) + xls.SetCellValueStr(sheetTrg, 2, nIdx, "") # 이미지가 들어갈 시트 이름. 넣을 떄 넣자. + xls.SetCellValueStr(sheetTrg, 3, nIdx, all_date) + xls.SetCellValueStr(sheetTrg, 4, nIdx, level) + xls.SetCellValueStr(sheetTrg, 5, nIdx, pathRel) + xls.SetCellValueStr(sheetTrg, 6, nIdx, juso[3]) + xls.SetCellValueStr(sheetTrg, 7, nIdx, juso[2]) + xls.SetCellValueStr(sheetTrg, 8, nIdx, f"{lat},{lon}") + xls.SetCellValueStr(sheetTrg, 9, nIdx, juso[1]) + xls.SetCellValueStr(sheetTrg, 10, nIdx, juso[0]) + + # VWorld 좌표로 변환하여 저장 + lat_Vworld, lon_Vworld = 0.0, 0.0 + if lat != 0.0 and lon != 0.0 : + lat_Vworld, lon_Vworld = vwapi.Transform4326to3857(lat, lon) + + xls.SetCellValueStr(sheetTrg, 11, nIdx, f"{lat_Vworld},{lon_Vworld}") + + nIdx += 1 + + return nIdx + +# DB 시트에 있는 파일을 골라내서 PoleInfo 시트에 넣는다. +# PoleInfo : 번호, GPS 좌표, 구역, 주소, 사진 +def CollectPoleInfoData(xls, TrgSheetName, DBSheetName, pathImgDir): + if None == xls: + return -1 + + nLastDBRow = xls.FindLastRow( DBSheetName ) + if 0 >= nLastDBRow: + print(f"{DBSheetName} Sheet is Empty.") + return -1 + + # 헤더가 있는 시트는 헤더가 1, 그래서 2부터 시작 + nTrgRow = xls.FindLastRow(TrgSheetName) + 1 + for nDBRow in range(1, nLastDBRow + 1): + listValues = xls.GetAllRowValue(DBSheetName, nDBRow, 8) + # 0: Index, 1: Sheetname, 2:Date, 3:Level, 4:Rel Path, 5:Area, 6:Juso, 7:GPS(Lat,Lon) + if None == listValues: + continue + + if listValues[1] == TrgSheetName: + continue + + if listValues[3] != "Pole" : + continue + + if listValues[7] == "" or listValues[7] == "0.0,0.0": + continue + + print( listValues ) + + xls.SetCellValueStr(TrgSheetName, 1, nTrgRow, str(nTrgRow -1)) + xls.SetCellValueStr(TrgSheetName, 2, nTrgRow, listValues[7]) + xls.SetCellValueStr(TrgSheetName, 3, nTrgRow, listValues[5]) + xls.SetCellValueStr(TrgSheetName, 4, nTrgRow, listValues[6]) + + imgPath = os.path.join(pathImgDir, listValues[4]) + imgPath = exEXIF.ProcessImg(imgPath, 0.1, 80, True) + xls.SetCellValueImg(TrgSheetName, 5, nTrgRow, imgPath) + + xls.SetCellValueStr(TrgSheetName, 6, nTrgRow, listValues[0]) + + # 목표 시트에 넣었으니 시트 이름을 DB 시트에 적어 넣는다. + xls.SetCellValueStr(DBSheetName, 2, nDBRow, TrgSheetName) + + nTrgRow += 1 + + return nTrgRow + +# DB 시트와 점봇대 정보 시트로 정보를 찾고 계산한다. +# Pole : 연번, 점검일자, 구역, 가까운 전신주 번호, 가까운 전신주 거리, 심각, 양호, 등급, 비고 +def CollectPoleData( xls, TrgSheetName, InfoSheetName, DBSheetName, pathImgDir ): + if None == xls: + return -1 + + nLastDBRow = xls.FindLastRow( DBSheetName ) + if 0 >= nLastDBRow: + print(f"{DBSheetName} Sheet is Empty.") + return -1 + + # 헤더가 있는 시트는 헤더가 1, 그래서 2부터 시작 + nTrgRow = xls.FindLastRow(TrgSheetName) + 1 + for nDBRow in range(1, nLastDBRow + 1): + listValues = xls.GetAllRowValue(DBSheetName, nDBRow, 8) + + # 0: Index, 1: Sheetname, 2:Date, 3:Level, 4:Rel Path, 5:Area, 6:Juso, 7:GPS(Lat,Lon) + if None == listValues: + continue + + if listValues[1] == TrgSheetName: + continue + + if listValues[3] != "A" and listValues[3] != "B" : + continue + + if listValues[7] == "" or listValues[7] == "0.0,0.0": + continue + + # 날짜만 골라낸다. + strDate = "" + if listValues[2] != "": + strDate, strTime = listValues[2] .split(' ') + strDate = strDate.replace(':','/') + + xls.SetCellValueStr(TrgSheetName, 1, nTrgRow, str(nTrgRow -1)) + xls.SetCellValueStr(TrgSheetName, 2, nTrgRow, strDate) + + # 가장 가까운 점봇대를 찾는다. 인덱스를 얻어서... + strLat, strLon = listValues[7] .split(',') + nPoleIdx, nDistance = xls.FindCloestPole(float(strLat), float(strLon)) + + # 배열은 0부터, Pole 인덱스는 1부터 시작하고, 셀 번호는 헤더를 포함해서 2 부터 시작한다. + # 셀 번호를 넣어서 해당하는 정보를 가져온다. + # 0:번호, 1:GPS 좌표, 2:구역, 3:주소, 4:사진 + #print( f"{nDBRow} : {nTrgRow} : {nPoleIdx}") + retList = xls.GetAllRowValue(InfoSheetName, nPoleIdx, 5) + + xls.SetCellValueStr(TrgSheetName, 3, nTrgRow, retList[2]) + xls.SetCellValueStr(TrgSheetName, 4, nTrgRow, nPoleIdx) + xls.SetCellValueStr(TrgSheetName, 5, nTrgRow, nDistance) + + ColIdx = 6 + if listValues[3] == "B" : + ColIdx = 7 + + imgAbsPath = os.path.join(pathImgDir, listValues[4]) + imgpath = exEXIF.ProcessImg(imgAbsPath, 0.1, 80, True) + xls.SetCellValueImg(TrgSheetName, ColIdx, nTrgRow, imgpath) + + xls.SetCellValueStr(TrgSheetName, 8, nTrgRow, listValues[3]) + xls.SetCellValueStr(TrgSheetName, 9, nTrgRow, nDBRow) + + # 목표 시트에 넣었으니 시트 이름을 DB 시트에 적어 넣는다. + xls.SetCellValueStr(DBSheetName, 2, nDBRow, TrgSheetName) + + nTrgRow += 1 + + return nTrgRow + +def main(): + # 경로를 정리하여 절대 경로로 변환해서 저장한다. + CurPath = GetAbsPath(mCurPath, "") + ArcDirPath = GetAbsPath(CurPath, mArcPath) + XlsDirPath = GetAbsPath(CurPath, mXlsPath) + ImgDirPath = GetAbsPath(CurPath, mImgPath) + + # 압축해제 + listUNZip = ExtractArchives(ArcDirPath, ImgDirPath) + + # 이미지 파일 목록 작성 + trgImgList = GetJPGFileList(listUNZip, ImgDirPath) + + # 엑셀을 연다. + XLSPath = os.path.join(XlsDirPath, "EPoleDB.xlsx") + tempxls = myxl.PoleXLS(XLSPath) + tempxls.DBXLSOpen() + + # 이미지 정보를 전부 모아서 한 시트에 저장 + CollectImageData(tempxls, "ImgInfo", trgImgList, ImgDirPath) + # 모아 놓은 이미지 정보에서 점봇대 정보를 골라서 저장 + CollectPoleInfoData(tempxls, "PoleInfo", "ImgInfo", ImgDirPath) + # 점봇대 정보 시트를 바탕으로 제보받은 이미지를 판별, 계산하여 저장 + CollectPoleData(tempxls, "Poles", "PoleInfo", "ImgInfo", ImgDirPath) + + tempxls.DBXLSClose() + + +def printUsage(): + print("Usage : python main.py ") + +# For Main Loop +if __name__ == '__main__': +# argc = len(sys.argv) +# argv = sys.argv +# main(argc, argv) + main() \ No newline at end of file diff --git a/MgrUtilityPoleOCR.py b/MgrUtilityPoleOCR.py new file mode 100644 index 0000000..7da73f5 --- /dev/null +++ b/MgrUtilityPoleOCR.py @@ -0,0 +1,46 @@ +import cv2 +import pytesseract +from pytesseract import Output + +img_path = '/Volumes/ExSSD/Working/용공추 사진/2,3월 데이터/Pole/20250307_153821.jpg' + +# 이미지 경로 +img = cv2.imread(img_path) + +# 전처리 +gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) +blur = cv2.GaussianBlur(gray, (3, 3), 0) +thresh = cv2.adaptiveThreshold( + blur, 255, + cv2.ADAPTIVE_THRESH_MEAN_C, + cv2.THRESH_BINARY_INV, + 11, 2 +) + +# OCR +custom_config = r'--oem 3 --psm 6' +data = pytesseract.image_to_data( + thresh, lang='kor+eng', + config=custom_config, + output_type=Output.DICT +) + +# 보안등 아래 숫자 찾기 +target_text = '보안등' +number_below = None + +for i, word in enumerate(data['text']): + if word.strip() == target_text: + for j in range(i + 1, len(data['text'])): + if data['text'][j].strip().isdigit() and data['top'][j] > data['top'][i]: + number_below = data['text'][j].strip() + break + break + +print(f"보안등 아래 숫자: {number_below}") + +# 디버깅용: 전체 텍스트 출력 +print("\n전체 인식 텍스트:") +for word in data['text']: + if word.strip(): + print(word.strip()) \ No newline at end of file diff --git a/MgrUtilityPoleUI.py b/MgrUtilityPoleUI.py new file mode 100644 index 0000000..23935f5 --- /dev/null +++ b/MgrUtilityPoleUI.py @@ -0,0 +1,167 @@ +import sys +import os +import shutil +import UtilPack as util + + +from PyQt5.QtCore import Qt, QSettings, QRect +from PyQt5.QtWidgets import QApplication, QWidget, QMainWindow, QPushButton, QVBoxLayout, QLineEdit, \ + QHBoxLayout, QTableWidget, QTableWidgetItem, QAbstractItemView, QHeaderView, QFileDialog, \ + QListWidget, QListWidgetItem, QMessageBox +from PyQt5.QtGui import QResizeEvent, QCloseEvent, QColor + +class MyApp(QMainWindow): + + def __init__(self): + super().__init__() + self.initUI() + self.loadINI() + self.initDB() + + # + def closeEvent(self, a0: 'QCloseEvent | None'): + filenameLog = f"{util.GetCurrentTime()}_DbgLog.txt" + pathDbgLog = os.path.join( self.pathLog, filenameLog) + util.SaveDbgMessages(pathDbgLog) + self.saveINI() + super().closeEvent(a0) + + # + def resizeEvent(self, a0: 'QResizeEvent | None'): + pass + + # + def loadINI(self): + pass + + # + def saveINI(self): + pass + + # + def initDB(self): + pass + + # + def MakeUI_Left(self): + layout = QVBoxLayout() + + layout_top = QHBoxLayout() + self.listWidget_Folders = QListWidget() + self.listWidget_Folders.setFixedHeight(100) + + layout_Btns = QVBoxLayout() + btn_FolderAdd = QPushButton("폴더 추가") + btn_FolderAdd.clicked.connect(self.on_btnFolderAdd_clicked) + btn_FolderDel = QPushButton("폴더 삭제") + btn_FolderDel.clicked.connect(self.on_btnFolderDel_clicked) + btn_FolderParss = QPushButton("폴더 파싱") + btn_FolderParss.clicked.connect(self.on_btnFolderParse_clicked) + + layout_Btns.addWidget(btn_FolderAdd) + layout_Btns.addWidget(btn_FolderDel) + layout_Btns.addWidget(btn_FolderParss) + + layout_top.addWidget(self.listWidget_Folders) + layout_top.addLayout(layout_Btns) + + self.tableWidget_Src = QTableWidget() + self.tableWidget_Src.verticalHeader().setVisible(False) + self.tableWidget_Src.setEditTriggers(QAbstractItemView.NoEditTriggers) + self.tableWidget_Src.setSortingEnabled(True) + self.tableWidget_Src.setColumnCount(5) + self.tableWidget_Src.setHorizontalHeaderLabels(["Path", "Title", "H.ID", "Img Cnt", "Dw Cnt"]) + self.tableWidget_Src.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + self.tableWidget_Src.horizontalHeader().sectionClicked.connect(self.on_tableWidget_Src_headerClicked) + self.tableWidget_Src.setSelectionBehavior(QAbstractItemView.SelectRows) + self.tableWidget_Src.setSelectionMode(QAbstractItemView.SingleSelection) + self.tableWidget_Src.itemSelectionChanged.connect(self.on_tableWidget_Src_itemSelectionChanged) + + layout.addLayout(layout_top) + layout.addWidget(self.tableWidget_Src) + + return layout + + # + def MakeUI_Center(self): + layout = QVBoxLayout() + + btn_Emptyfolder = QPushButton("빈 폴더 이동") + btn_Emptyfolder.clicked.connect(self.on_btn_Emptyfolder_clicked) + btn_ChkDuplicate = QPushButton("중복 검사 및 제거") + btn_ChkDuplicate.clicked.connect(self.on_btn_ChkDuplicate_clicked) + btn_Archive = QPushButton("압축 및 데이터 저장") + btn_Archive.clicked.connect(self.on_btn_Archive_clicked) + btn_EnterCalibre = QPushButton("컬리버에 삽입") + btn_EnterCalibre.clicked.connect(self.on_btn_EnterCalibre_clicked) + + layout.addWidget(btn_Emptyfolder) + layout.addWidget(btn_ChkDuplicate) + layout.addWidget(btn_Archive) + layout.addWidget(btn_EnterCalibre) + + return layout + + # + def MakeUI_Right(self): + layout_top = QHBoxLayout() + self.edit_DB = QLineEdit(self) + self.edit_DB.setReadOnly(True) + self.edit_DB.setText("...") + btn_DB = QPushButton("...") + btn_DB.setFixedWidth(50) + btn_DB.clicked.connect(self.on_btnDB_clicked) + layout_top.addWidget(self.edit_DB) + layout_top.addWidget(btn_DB) + + self.tableWidget_DB = QTableWidget() + self.tableWidget_DB.verticalHeader().setVisible(False) + self.tableWidget_DB.setColumnCount(5) + self.tableWidget_DB.setHorizontalHeaderLabels(["Title", "Author", "Cover", "Exts", "ID"]) + self.tableWidget_DB.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + self.tableWidget_DB.setSelectionBehavior(QAbstractItemView.SelectRows) + self.tableWidget_DB.setSelectionMode(QAbstractItemView.SingleSelection) + self.tableWidget_DB.itemSelectionChanged.connect(self.on_tableWidget_DB_itemSelectionChanged) + + layout = QVBoxLayout() + layout.addLayout(layout_top) + layout.addWidget(self.tableWidget_DB) + + return layout + + # + def MakeUI(self): + layout_L = self.MakeUI_Left() + layout_C = self.MakeUI_Center() + layout_R = self.MakeUI_Right() + + # 레이아웃 설정 + layout = QHBoxLayout() + layout.addLayout(layout_L, stretch = 10) + layout.addLayout(layout_C, stretch = 1) + layout.addLayout(layout_R, stretch = 10) + + return layout + + # + def initUI(self): + layout = self.MakeUI() + + # 레이아웃을 윈도우에 적용 + central_widget = QWidget() + central_widget.setLayout(layout) + self.setCentralWidget(central_widget) + self.setWindowTitle('Manga Database') + self.resize(800, 600) + self.show() + + # 테이블의 특정 행에 배경색을 설정한다 + # nRow: 배경색을 설정할 행 번호, color: 배경색 (Qt.GlobalColor) + def SrcTableRowBgColor(self, nRow:int, color:Qt.GlobalColor) -> None: + for col in range(self.tableWidget_Src.columnCount()): + item = self.tableWidget_Src.item(nRow, col) + if item: + item.setBackground(Qt.GlobalColor(color)) + + + diff --git a/PoleXLS.py b/PoleXLS.py new file mode 100644 index 0000000..ffab361 --- /dev/null +++ b/PoleXLS.py @@ -0,0 +1,173 @@ +from StoreXLS import DBXLStorage +import os +import UtilPack as util + +class PoleXLS(DBXLStorage): + m_dictSheetInfo = {} + + # 여기서부터 이 프로젝트에 특화된 함수 + def checkSheetsInfo(self): + sheet = self.getSheet(self.xls_infosheet) + if sheet is None: + return + + # 3번째 줄부터 읽는다. + for row in sheet.iter_rows(min_row=3, values_only=True): + sheetName = row[0] + idxList = range(0, len(row)) + listRecp = [] + for colIdx, item in zip(idxList, row): + # 시트 이름은 넘어가자. + if 0 == colIdx: + continue + + self.getSheet(sheetName, True) + + title, recp = util.GetSheetInfoValue(item) + self.SetCellValueStr(sheetName, colIdx, 1, title) + listRecp.append(recp) + + self.m_dictSheetInfo[sheetName] = listRecp + + #print(self.m_dictSheetInfo) + + + def FindCloestPole(self, srcLat, srcLon): + sheet = self.getSheet("PoleInfo") + if sheet is None: + return -1, 0 + + if None == srcLat or None == srcLon: + return -1, 0 + + # 헤더 다음 줄부터 읽는다. + listDistance = [] + for row in sheet.iter_rows(min_row=2, values_only=True): + temp = row[1] + + if "None" in temp: + continue + + dist = 0 + if temp != "": + parts = temp.split(',') + + trgLat = float(parts[0]) + trgLon = float(parts[1]) + dist = util.GetDistanceGPS(srcLat, srcLon, trgLat, trgLon) + + listDistance.append(dist) + + if 0 >= len(listDistance): + return -1, 0 + + min_value = min(listDistance) + retIdx = listDistance.index(min_value) + 1 + + return retIdx, round( min_value, 0 ) + + + def GetPoleInfoAll(self, nIdx): + sheet = self.getSheet("PoleInfo") + if sheet is None: + return None + + retList = [] + retList.append(sheet.cell(row=nIdx,column=1).value) + retList.append(sheet.cell(row=nIdx,column=2).value) + retList.append(sheet.cell(row=nIdx,column=3).value) + retList.append(sheet.cell(row=nIdx,column=4).value) + retList.append(sheet.cell(row=nIdx,column=5).value) + retList.append(sheet.cell(row=nIdx,column=6).value) + + return retList + + + def GetPoleInfo(self, nIdx, colNum): + sheet = self.getSheet("PoleInfo") + if sheet is None: + return None + + if 0 >= nIdx or 0 >= colNum: + return None + + return sheet.cell(row=nIdx,column=colNum).value + + + def AddImgInfo(self, TrgSheetName, EXIF_Date, EXIF_GPS, Path): + sheet = self.getSheet("ImgInfo") + if sheet is None: + return -1 + + # 마지막 행 찾기 + last_row = self.FindLastRow("ImgInfo") + if 0 > last_row: + return -1 + + TrgRow = last_row + 1 + + sheet.cell(row=TrgRow,column=1,value=TrgSheetName) + sheet.cell(row=TrgRow,column=2,value=EXIF_Date) + sheet.cell(row=TrgRow,column=3,value=EXIF_GPS) + sheet.cell(row=TrgRow,column=4,value=Path) + + return TrgRow + + + def FindRowCompValue(self, sheetNameTrg, nTrgCol, valueSrc, bCmpFile = False, pathBase = ""): + sheet = self.getSheet(sheetNameTrg) + if sheet is None: + return -1 + + value = valueSrc + if True == bCmpFile: + if False == os.path.isabs(valueSrc): + value = os.path.join(pathBase, valueSrc) + + retIdx = 0 + trgIdx = 0 + for row in sheet.iter_rows(min_row=2, values_only=True): + valueTrg = row[nTrgCol] + + if True == bCmpFile: + # 상대경로로 저장했지만 혹시 절대경로 일 수도 있으니... + if False == os.path.isabs(valueTrg): + valueTrg = os.path.join(pathBase, valueTrg) + + if True == os.path.samefile(value, valueTrg): + retIdx = trgIdx + 1 + break; + else: + if value == valueTrg: + retIdx = trgIdx + 1 + break; + + trgIdx += 1 + + return retIdx + + + def FindInsertedImgPath(self, pathBase, pathImg): + retIdx = self.FindRowCompValue("ImgInfo", 4, pathImg, True, pathBase ) + return retIdx + + # + def GetAllRowValue(self, SheetName, nRow, nColCount): + sheet = self.getSheet(SheetName) + if sheet is None: + return None + + if 0 >= nColCount: + return None + + retList = [] + # 엑셀의 컬럼은 1부터 시작하고, 입력받는 값은 컬럼의 개수다. 조심! + for nCol in range(1, nColCount + 1): + retList.append(sheet.cell(row=nRow,column=nCol).value) + + return retList + + + + + diff --git a/Pole_serial_sorter.py b/Pole_serial_sorter.py new file mode 100644 index 0000000..de88522 --- /dev/null +++ b/Pole_serial_sorter.py @@ -0,0 +1,108 @@ +import os +import shutil +import pytesseract +from PIL import Image +import re +from pytesseract import Output +import cv2 +import numpy as np + +# --- 사용자 설정 --- +# 원본 이미지 폴더 경로 +TOP_SOURCE_DIR = '/Volumes/ExSSD/Working/용공추 사진/' +SOURCE_DIR = '/Volumes/ExSSD/Working/용공추 사진/Raw_Data_4' + +# 결과 저장 폴더 +SERIAL_FOLDER = os.path.join(TOP_SOURCE_DIR, '일련번호_사진') +NON_SERIAL_FOLDER = os.path.join(TOP_SOURCE_DIR, '일반_사진') + +# 일련번호 정규표현식 (기본: 5~6자리 숫자) +#SERIAL_PATTERN = r'\b\d{5,6}\b' +SERIAL_PATTERN = r' ' + +def find_number_below_security_light_tesseract(data): + texts = data['text'] + tops = data['top'] + heights = data['height'] + + print(texts) + + # "보안등" 위치 찾기 + for i, text in enumerate(texts): + if '보안등' in text: + base_y = tops[i] + heights[i] # 아래 기준점 + + candidates = [] + for j, candidate_text in enumerate(texts): + if re.fullmatch(r'\d+', candidate_text): # 숫자인 경우 + if tops[j] > base_y + 5: # '보안등' 아래쪽에 있는지 확인 + candidates.append((tops[j], candidate_text)) + + if not candidates: + return None + + # y값이 가장 가까운(위에 있는) 숫자 반환 + candidates.sort(key=lambda x: x[0]) + return candidates[0][1] + + return None # "보안등"이 없으면 None + + +def extract_serial_number(text): + matches = re.findall(SERIAL_PATTERN, text) + return matches[0] if matches else None + + +def classify_and_extract(): + if not os.path.exists(SERIAL_FOLDER): + os.makedirs(SERIAL_FOLDER) + if not os.path.exists(NON_SERIAL_FOLDER): + os.makedirs(NON_SERIAL_FOLDER) + + for root, _, files in os.walk(SOURCE_DIR): + for file in files: + if file.lower().endswith(('.jpg', '.jpeg', '.png')): + img_path = os.path.join(root, file) + + try: + img = Image.open(img_path) + text = pytesseract.image_to_string(img, lang='eng+kor') # 한국어+영어 혼합 OCR + serial = extract_serial_number(text) + + if serial: + dest_path = os.path.join(SERIAL_FOLDER, file) + shutil.copy2(img_path, dest_path) + print(f"[✓] {file} → 일련번호: {serial}") + else: + dest_path = os.path.join(NON_SERIAL_FOLDER, file) + shutil.copy2(img_path, dest_path) + print(f"[ ] {file} → 일반 사진") + except Exception as e: + print(f"[!] 오류: {file} → {e}") + + +if __name__ == '__main__': +# classify_and_extract() + + #img_path = "/Volumes/ExSSD/Working/용공추 사진/2,3월 데이터/Pole/20250218_114838.jpg" + img_path = '/Volumes/ExSSD/Working/용공추 사진/2,3월 데이터/Pole/20250307_153821.jpg' + #img_path ="/Volumes/ExSSD/Working/용공추 사진/2,3월 데이터/Pole/20250303_121704.jpg" + + image = Image.open(img_path) + + + # image = cv2.imread(img_path, cv2.IMREAD_COLOR) + # gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + # _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) + # kernel = np.ones((1, 1), np.uint8) + # denoised = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) + + data = pytesseract.image_to_data(image, output_type=Output.DICT, lang='eng+kor') + + number = find_number_below_security_light_tesseract(data) + print("보안등 아래 숫자:", number) + + custom_config = r'--oem 3 --psm 6' + text = pytesseract.image_to_string(image, lang='kor+eng', config=custom_config) + + print(text) \ No newline at end of file diff --git a/StoreXLS.py b/StoreXLS.py new file mode 100644 index 0000000..5a28537 --- /dev/null +++ b/StoreXLS.py @@ -0,0 +1,186 @@ +import os +import openpyxl as Workbook + +from openpyxl.utils import get_column_letter +from openpyxl.drawing.image import Image + +import UtilPack as util + +from abc import ABC, abstractmethod + +class DBXLStorage(ABC): + xls_name = "EPoleDB.xlsx" + xls_infosheet = "DBInfo" + + m_wb = None + m_openedXLS = "" + m_defaultImgW = 140 + + def __init__(self, path): + self.path = path + + #def __enter__(self): + #self.DBXLSOpen(self.path) + + def __exit__(self, ex_type, ex_value, traceback): + self.DBXLSClose() + + def DBXLSOpen(self): + xls_path = self.GetXLSPath(self.path) + util.DbgOut(xls_path, True) + + try: + self.m_wb = Workbook.load_workbook(xls_path) + util.DbgOut("xls Open Successed") + except FileNotFoundError: + self.m_wb = Workbook.Workbook() + + ws = self.m_wb.active + ws.title = self.xls_infosheet #DBInfo + ws.cell(row=1,column=1,value="PoleDB_XLS") + + self.m_wb.save(xls_path) + util.DbgOut(f"{xls_path} Created", True) + + self.m_openedXLS = xls_path + + if self.m_wb is None: + util.DbgOut("XLS Open Something Wrong...", True) + self.m_openedXLS = "" + self.m_wb = None + return + + self.checkSheetsInfo() + + + def DBXLSClose(self): + if self.m_wb is None or self.m_openedXLS is None: + util.DbgOut("XLS Close something wrong...", True) + return + + self.m_wb.save(self.m_openedXLS) + self.m_wb.close() + util.DbgOut(f"Close : {self.m_openedXLS} Saved") + + self.m_wb = None + + + def GetCellValueStr(self, sheetName, nCol, nRow): + sheet = self.getSheet(sheetName); + if sheet is None: + return "" + + return sheet.cell(row=nRow, column=nCol).value.str + + + def SetCellValueStr(self, sheetName, nCol, nRow, ValueStr): + sheet = self.getSheet(sheetName); + if sheet is None: + return + + sheet.cell(row=nRow,column=nCol,value=ValueStr) + + + def SetCellValueImg(self, sheetName, nCol, nRow, ImagePath): + sheet = self.getSheet(sheetName); + if sheet is None: + return + + if False == os.path.exists(ImagePath): + return + + xlImg = Image(ImagePath) + nNewH = util.GetRSzHeight(xlImg.width, + xlImg.height, self.m_defaultImgW ) + xlImg.height = nNewH + xlImg.width = self.m_defaultImgW + + ColLetter = get_column_letter(nCol) + TrgCell = f"{ColLetter}{nRow}" + + cellW, cellH = util.GetCellSize(xlImg.width, xlImg.height) + + sheet.row_dimensions[nRow].height = cellH + sheet.column_dimensions[ColLetter].width = cellW + + sheet.add_image(xlImg, TrgCell) + + + def GetCellValue(self, sheetName, nCol, nRow): + sheet = self.getSheet(sheetName); + if sheet is None: + return "" + + return sheet.cell(nRow, nCol).value + + + # 시트를 가져온다. + # 엑셀 파일이 안 열려 있으면 None + def getSheet(self, sheetName, bNew = False): + retSheet = None + + if None == sheetName or "" == sheetName: + return None + + if self.m_wb is None: + util.DbgOut("XLS not opened", True) + return None + + try: + retSheet = self.m_wb[sheetName] + except KeyError: + if True == bNew: + retSheet = self.m_wb.create_sheet(title=sheetName) + util.DbgOut(f"GetSheet : {sheetName} is Created", True) + + return retSheet + + + # 데이터베이스용 엑셀 파일의 경로를 얻어온다. + # 폴더만 입력되었으면 파일이름을 삽입하고, 완전한 경로라면 패스 + def GetXLSPath(self, pathSrc): + retPath = self.path + + if os.path.isdir(pathSrc): + retPath = os.path.join(pathSrc, self.xls_name) + + return retPath + + def FindLastRow(self, sheetName): + sheet = self.getSheet(sheetName) + if sheet is None: + return -1 + + # 마지막 행 찾기 + last_row = sheet.max_row + + # 빈 셀이 있는지 확인하고, 실제로 값이 있는 마지막 행 찾기 + while last_row > 0: + row = sheet[last_row] # 현재 행을 가져옴 + + # 현재 행의 모든 셀을 검사하여 값이 있는지 확인 + is_empty = True + for cell in row: + if cell.value is not None: + is_empty = False + break + + # 값이 있는 행을 찾았으면 종료 + if not is_empty: + break + + # 행 번호를 하나씩 감소시켜 이전 행을 검사 + last_row -= 1 + + return last_row + + + @abstractmethod + def checkSheetsInfo(self): + pass + + + + + + diff --git a/UI.py b/UI.py new file mode 100644 index 0000000..caa7154 --- /dev/null +++ b/UI.py @@ -0,0 +1,246 @@ +import os +import sys +import UtilPack as util +import VWorldAPIs as vw +import ExtEXIFData as exif + +from PyQt5.QtCore import Qt, QUrl +from PyQt5.QtWebEngineWidgets import QWebEngineView +from PyQt5.QtWidgets import QApplication, QWidget, QMainWindow, QPushButton, QVBoxLayout, QLineEdit, QHBoxLayout, QListWidget, QTableWidget, QRadioButton, QLabel, QFileDialog, QListWidgetItem +from PyQt5.QtGui import QPixmap, QTransform + +QApplication.setAttribute(Qt.AA_ShareOpenGLContexts) + +class MyApp(QMainWindow): + + def __init__(self): + super().__init__() + self.initUI() + self.loadINI() + + def closeEvent(self, event): + event.accept() + + def initUI(self): + layout = self.MakeUI() + + # 레이아웃을 윈도우에 적용 + central_widget = QWidget() + central_widget.setLayout(layout) + self.setCentralWidget(central_widget) + self.setWindowTitle('Poles Database') + self.move(100, 300) + self.show() + + def loadINI(self): + print("Load INI") + + def saveINI(self): + print("Save INI") + + def MakeUI_Left_Top(self): + label_SrcPath = QLabel("Source Path") + self.edit_SrcPath = QLineEdit("", self) + self.edit_SrcPath.setReadOnly(True) + btn_SrcPath_OK = QPushButton("...", self) + btn_SrcPath_OK.clicked.connect(self.on_click_SourcePath) + self.edit_SrcPath.setEnabled(False) + btn_SrcPath_OK.setEnabled(False) + + layout01 = QHBoxLayout() + layout01.addWidget(label_SrcPath) + layout01.addWidget(self.edit_SrcPath) + layout01.addWidget(btn_SrcPath_OK) + + label_ImgPath = QLabel("Image Path") + self.edit_ImgPath = QLineEdit("", self) + self.edit_ImgPath.setReadOnly(True) + btn_ImgPath_OK = QPushButton("...", self) + btn_ImgPath_OK.clicked.connect(self.on_click_ImagePath) + + layout02 = QHBoxLayout() + layout02.addWidget(label_ImgPath) + layout02.addWidget(self.edit_ImgPath) + layout02.addWidget(btn_ImgPath_OK) + + self.btn_ParseImages = QPushButton("Parse!!", self) + self.btn_ParseImages.clicked.connect(self.on_click_btn_ParseImages) + self.btn_ParseImages.setMaximumWidth(410) + + layout = QVBoxLayout() + layout.addLayout(layout01) + layout.addLayout(layout02) + layout.addWidget(self.btn_ParseImages) + + return layout + + def MakeUI_Left_Mid(self): + # 이미지 목록 + self.list_Images = QListWidget(self) + self.list_Images.itemSelectionChanged.connect(self.on_SelChanged_ImageList) + self.list_Images.setMinimumWidth(100) + self.list_Images.setMaximumWidth(200) + + # 이미지 정보 테이블 + self.list_Info = QListWidget(self) + self.list_Info.setMinimumWidth(100) + self.list_Info.setMaximumWidth(200) + + layout_r = QVBoxLayout() + layout_r.addWidget(self.list_Info) + + layout = QHBoxLayout() + layout.addWidget(self.list_Images) + layout.addLayout(layout_r) + + return layout + + def MakeUI_left_Down(self): + label_ExlPath = QLabel("DB(xlsx) Path") + self.edit_ExlPath = QLineEdit("", self) + self.edit_ExlPath.setReadOnly(True) + btn_ExlPath_OK = QPushButton("OK", self) + layout01 = QHBoxLayout() + layout01.addWidget(label_ExlPath) + layout01.addWidget(self.edit_ExlPath) + layout01.addWidget(btn_ExlPath_OK) + + btn_Exl_Export = QPushButton("Export to EXCEL", self) + btn_Exl_Export.clicked.connect(self.on_click_btn_ExportToExcel) + + layout = QVBoxLayout() + layout.addLayout(layout01) + layout.addWidget(btn_Exl_Export) + + return layout + + def MakeUI_Left(self): + lsyout_lefttop = self.MakeUI_Left_Top() + layout_leftmid = self.MakeUI_Left_Mid() + layout_leftDwn = self.MakeUI_left_Down() + + layout = QVBoxLayout() + layout.addLayout(lsyout_lefttop) + layout.addLayout(layout_leftmid) + layout.addLayout(layout_leftDwn) + + return layout + + def MakeUI(self): + layout_L = self.MakeUI_Left() + + # show Image in middle Layout + self.label_Image = QLabel(self) + + # Load Default Image + pixmap = QPixmap("layoutImg.jpeg") + scaled_Pix = pixmap.scaledToWidth(450) + self.label_Image.setPixmap(scaled_Pix) + self.label_Image.setMaximumWidth(450) + + # Show GPS Mapped map in Right Layout + self.webview_map = QWebEngineView() + self.webview_map.setFixedWidth(450) + self.webview_map.setUrl(QUrl("file:///Users/minarinari/Workspace/Python/UtilityPole_Info/sample_Event.html")) + + # 레이아웃 설정 + layout = QHBoxLayout() + layout.addLayout(layout_L) + layout.addWidget(self.label_Image) + layout.addWidget(self.webview_map) + + return layout + + def on_click_SourcePath(self): + folder_path = QFileDialog.getExistingDirectory(self, '폴더 선택', '') + self.edit_SrcPath.setText(folder_path) + + def on_click_ImagePath(self): + folder_path = QFileDialog.getExistingDirectory(self, '폴더 선택', '') + self.edit_ImgPath.setText(folder_path) + + def on_click_btn_ParseImages(self): + self.btn_ParseImages.setEnabled(False) + pathImage= self.edit_ImgPath.text() + + trgDirList = util.GetSubDirectories(pathImage) + + retImgList = [] + for dirName in trgDirList: + if dirName.lower() in "thumb": + continue + + imgSubDirPath = os.path.join(pathImage, dirName) + contents = os.listdir(imgSubDirPath) + + for item in contents: + name, ext = os.path.splitext(item) + if ext.lower() == '.jpg': + imgPath = os.path.join(imgSubDirPath, item) + retImgList.append(imgPath) + + for img in retImgList: + item = QListWidgetItem(util.GetParentDirName(img, 0)) + listData = [] + listData.append(img) + + all_date, lat, lon = exif.GetDateInfo(img) + listData.append(str(all_date)) + listData.append(str(lat)) + listData.append(str(lon)) + + juso, area = vw.GetJusofromGPS(lat, lon) + listData.append(area) + listData.append(juso) + + listData.append(util.GetParentDirName(img, 0)) + listData.append(util.GetParentDirName(img, 1)) + item.setData(Qt.UserRole, listData) + self.list_Images.addItem(item) + + self.btn_ParseImages.setEnabled(True) + print("Parse END!") + + def on_click_btn_ExportToExcel(self): + print("Excel!!") + + def on_SelChanged_ImageList(self): + items = self.list_Images.selectedItems() + for item in items: + listData = item.data(Qt.UserRole) + + self.list_Info.clear() + for content in listData: + row = QListWidgetItem(content) + self.list_Info.addItem(row) + + pixmap = QPixmap(listData[0]) + + szLabel = self.label_Image.size() + + + resized_pixmap = pixmap.scaled(100, 100) + rotated_pixmap = resized_pixmap.transformed(QTransform().rotate(90)) + + self.label_Image.setPixmap(pixmap) + + + # def open_image(self): + # # 이미지 파일 열기 다이얼로그 + # file_name, _ = QFileDialog.getOpenFileName(self, "Open Image File", "", "Images (*.png *.xpm *.jpg)") + + # if file_name: + # # QPixmap을 사용해 이미지를 QLabel에 설정 + # pixmap = QPixmap(file_name) + # self.label.setPixmap(pixmap) + # self.label.setScaledContents(True) + +if __name__ == '__main__': + app = QApplication(sys.argv) + + app.setQuitOnLastWindowClosed(True) + main_window = MyApp() + #main_window.show() + + sys.exit(app.exec_()) + diff --git a/UtilPack.py b/UtilPack.py new file mode 100644 index 0000000..db4e9f2 --- /dev/null +++ b/UtilPack.py @@ -0,0 +1,298 @@ +import os +import re +import math +import time +import rarfile +import zipfile +import shutil +import difflib +import subprocess +import UtilPack as util + +m_dbgLevel = 0 +listDbgStr = [] + +# +def IsEmptyStr(string): + return 0 == len(string.strip()) + +# +def GetCurrentTime(): + # 현재 시간을 구하고 구조체로 변환 + current_time_struct = time.localtime() + + # 구조체에서 연, 월, 일, 시간, 분, 초를 추출 + year = current_time_struct.tm_year + month = current_time_struct.tm_mon + day = current_time_struct.tm_mday + hour = current_time_struct.tm_hour + minute = current_time_struct.tm_min + second = current_time_struct.tm_sec + + strRet = (f"{year}/{month}/{day}_{hour}:{minute}:{second}") + + return strRet + +#for debug +def DbgOut(strInput, bPrint = False): + strMsg = (f"{GetCurrentTime()} : {strInput}") + listDbgStr.append(strMsg) + + if True == bPrint: + print(strMsg) + +def printDbgMessages(): + for line in listDbgStr: + print(line) + +# 입력된 패스에서 부모 폴더를 찾는다. 0 은 자기자신 1은 부모, 숫자대로 위로. +def GetParentDirName(FullPath, nUp): + parts = FullPath.split(os.sep) + + nTrgIdx = 0 + if nUp < len(parts): + nTrgIdx = len(parts) -nUp -1 + elif nUp < 0: + nTrgIdx = len(parts) - 1 + else: + nTrgIdx = 0 + + return parts[nTrgIdx] + +# os 모듈을 사용하여 자식 폴더를 반환 +def GetSubDirectories(folder_path): + subdirectories = [ + d for d in os.listdir(folder_path) + if os.path.isdir(os.path.join(folder_path, d))] + + return subdirectories + +# 입력된 경로의 자식 폴더를 찾아 반환한다. +# 반환하는 리스트는 리커시브 - 손자, 증손자 폴더까지 전부 포함한다 +def GetAllSubDirectories(root_dir): + subdirectories = [] + + # root_dir에서 하위 디렉토리 및 파일 목록을 얻음 + for dirpath, dirnames, filenames in os.walk(root_dir): + # 하위 디렉토리 목록을 반복하며 하위 디렉토리만 추출 + for dirname in dirnames: + path = os.path.join(dirpath, dirname) + + if True == IsFinalFolder(path): + subdirectories.append(path) + + return subdirectories + +def ListFileExtRcr(pathTrg, strExt): + listRet= [] + + # pathTrg의 하위 디렉토리 및 파일 목록을 얻음 + for dirpath, dirnames, filenames in os.walk(pathTrg): + for file in filenames: + extTmp = GetExtStr(file, False) + if extTmp.lower() == strExt and file.startswith('.'): + listRet.append(os.path.join(dirpath, file)) + + return listRet + +# 입력된 경로가 자식 폴더를 가지고 있는지 판단한다.- 최종 폴더인지 여부 +# 자식이 없으면 True, 자식이 있으면 False +def IsFinalFolder(path): + bRet = True + + contents = os.listdir(path) + for item in contents: + if True == os.path.isdir(item): + bRet = False + break + + return bRet; + +# 어떤 경로 안에서 특정 확장자의 파일을 뽑아내어 그 리스트를 반환한다. +def FindFileFromExt(path, ext): + bDot = False + if 0 <= ext.find('.'): + bDot = True + + listRet = [] + if False == os.path.exists(path): + return listRet + + contents = os.listdir(path) + for item in contents: + if True == os.path.isdir(item): + continue + + extItem = GetExtStr(item, bDot) + if extItem.lower() == ext.lower(): + listRet.append(item) + + return listRet + + +# 파일 이름에서 확장자를 뽑아낸다. True : '.' 을 포함한다. +def GetExtStr(file_path, bDot = True): + retStr = "" + # 파일 경로에서 마지막 점을 찾아 확장자를 추출 + last_dot_index = file_path.rfind('.') + if last_dot_index == -1: + retStr = "" # 점이 없는 경우 확장자가 없음 + else: + if True == bDot: + retStr = file_path[last_dot_index:] + else: + retStr = file_path[last_dot_index+1:] + + return retStr + + +# 문자열에 포함된 단어를 지운다. +def RmvSubString(mainString, subString): + # 문자열에서 부분 문자열의 인덱스를 찾습니다. + strIdx = mainString.find(subString) + if strIdx == -1: # 부분 문자열이 존재하지 않으면 그대로 반환합니다. + return mainString + + endIdx = strIdx + len(subString) + + # 부분 문자열을 제거하고 새로운 문자열을 반환합니다. + return mainString[:strIdx] + mainString[endIdx:] + +def ExtractZIP(zip_file, extract_to): + with zipfile.ZipFile(zip_file, 'r') as zf: + zf.extractall(extract_to) + +# +def CreateZIP(output_zip, *files): + with zipfile.ZipFile(output_zip, 'w') as zf: + for file in files: + pathTemp = os.path.join('root', os.path.basename(file)) + zf.write(file, pathTemp) + + bRet = False + if os.path.exists(output_zip): + bRet = True + + return bRet + +# 파일 리스트에 들어있는 파일만 골라서 압축을 합니다. 상대경로를 제거하는게 기본값. +def CreateZIPShell(zipName, *files, bRmvRPath = True): + command = "zip " + + if True == bRmvRPath: + command += "-j " + + command += f"\"{zipName}\" " + + # 이중 리스트인 이유를 모르겠다. + for file in files: + strTemp = "" + if isinstance(file, list): + strTemp = ' '.join(file) + else: + strTemp = f"\"{file}\" " + + command += strTemp + + # for item in file: + # command += f"\"{item}\" " + result = subprocess.run(command, shell=True, + capture_output=True, text=True) + + bRet = False + if 0 == result.returncode: + bRet = True + + return bRet + +# 특정 확장자만 쉘을 이용해서 압축한다 +def CreateZIPShExt(zipName, TrgExt): + command = f"zip -j {zipName} *.{TrgExt}" + + result = subprocess.run(command, shell=True, capture_output=True, text=True) + + bRet = False + if 0 == result.returncode: + bRet = True + + return bRet + +# JSON 을 트리 구조로 출력한다. +def PrintJSONTree(data, indent=0): + if isinstance(data, dict): + for key, value in data.items(): + print(' ' * indent + str(key)) + PrintJSONTree(value, indent + 1) + elif isinstance(data, list): + for item in data: + PrintJSONTree(item, indent) + else: + print(' ' * indent + str(data)) + +# 세로 크기를 가로 비율대로 줄여서 반환 (가로를 기준으로 세로 길이) +def GetRSzHeight(nWidth, nHeight, nTrgW): + if nWidth <= 0 or nTrgW <= 0: + return 0; + + fRatio = nTrgW / nWidth + nTrgH = round(nHeight * fRatio) + + return nTrgH + +# 가로 크기를 세로 비율대로 줄여서 반환 (세로를 기준으로 가로 길이) +def GetRSzWidth(nWidth, nHeight, nTrgH): + if nHeight <= 0 or nTrgH <= 0: + return 0; + + fRatio = nTrgH / nHeight + nTrgW = round(nWidth * fRatio) + + return nTrgW + +# 엑셀 열 너비는 약 7.5픽셀 단위 +# 엑셀 행 높이는 약 25픽셀 단위 +# 이대로는 사이즈가 안 맞아서 적당히 곱해줌 +def GetCellSize(nImgW, nImgH, DPI = 96): + column_width = (nImgW / DPI) * 7.5 * 2 + row_height = (nImgH / DPI) * 25 * 3 + + return column_width, row_height + +def GetSheetInfoValue(text): + if text is None: + return "", "" + + pattern = r'<(.*?)>' + matches = re.findall(pattern, text) + cleaned = re.sub(pattern, '', text) + + return cleaned, matches + +# 두 GPS 좌표 간 거리 구하기 (미터) +def GetDistanceGPS(lat1, lon1, lat2, lon2): + # 지구의 반지름 (단위: 미터) + R = 6371008.8 + + # 위도와 경도를 라디안으로 변환 + lat1 = math.radians(lat1) + lon1 = math.radians(lon1) + lat2 = math.radians(lat2) + lon2 = math.radians(lon2) + + # 차이 계산 + dlat = lat2 - lat1 + dlon = lon2 - lon1 + + # Haversine 공식 적용 + a = math.sin(dlat / 2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2)**2 + c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) + + # 거리 계산 + distance = R * c + + return distance + + + + + \ No newline at end of file diff --git a/UtilPack2.py b/UtilPack2.py new file mode 100644 index 0000000..1dfa78b --- /dev/null +++ b/UtilPack2.py @@ -0,0 +1,346 @@ +import os +import re +import time +import uuid +import rarfile +import zipfile +import shutil +import difflib +import subprocess +import hashlib + +from pathlib import Path + +m_dbgLevel = 0 +listDbgStr: list[str] = [] + + +# +def IsEmptyStr(string: str) -> bool: + temp = f"{string}" + return 0 == len(temp.strip()) + + +# +def GetCurrentTime() -> str: + # 현재 시간을 구하고 구조체로 변환 + current_time_struct = time.localtime() + + # 구조체에서 연, 월, 일, 시간, 분, 초를 추출 + year = current_time_struct.tm_year + month = current_time_struct.tm_mon + day = current_time_struct.tm_mday + hour = current_time_struct.tm_hour + minute = current_time_struct.tm_min + second = current_time_struct.tm_sec + + strRet = (f"{year}/{month}/{day}_{hour}:{minute}:{second}") + + return strRet + + +#for debug +def DbgOut(strInput:str, bPrint:bool = False): + strMsg = (f"{GetCurrentTime()} : {strInput}") + listDbgStr.append(strMsg) + + if True == bPrint: + print(strMsg) + + +# +def printDbgMessages(): + for line in listDbgStr: + print(line) + + +# +def SaveDbgMessages(Path: str): + try: + with open(Path, 'w') as file: + for line in listDbgStr: + file.write(line + "\n") + except IOError: + DbgOut(f"Error: Could not write to the file at {Path}.", True) + + +# 입력된 경로의 자식 폴더를 찾아 반환한다. +# 반환하는 리스트는 리커시브 - 손자, 증손자 폴더까지 전부 포함한다 +def ListSubDirectories(root_dir:str)-> list[str]: + subdirectories: list[str] = [] + + # root_dir에서 하위 디렉토리 및 파일 목록을 얻음 + for dirpath, dirnames, filenames in os.walk(root_dir): + # 하위 디렉토리 목록을 반복하며 하위 디렉토리만 추출 + for dirname in dirnames: + path = os.path.join(dirpath, dirname) + + if True == IsFinalFolder(path): + subdirectories.append(path) + + return subdirectories + +# 자식 폴더를 구해온다. 직계 자식만 +def ListChildDirectories(pathDir:str, bVisibleOnly: bool = True) -> list[str]: + listRet:list[str] = [] + listTemp = os.listdir(pathDir) + for name in listTemp: + pathChild = os.path.join(pathDir, name) + if os.path.isdir(pathChild): + if True == bVisibleOnly and name.startswith('.'): + continue + + listRet.append(name) + + return listRet + +# PathDir 에 지정된 폴더의 파일목록만 구해온다. 자식 폴더에 있는건 무시. +def ListContainFiles(pathDir:str, bVisibleOnly:bool = True)-> list[str]: + listRet:list[str] = [] + listTemp = os.listdir(pathDir) + for name in listTemp: + pathChild = os.path.join(pathDir, name) + if os.path.isfile(pathChild): + if True == bVisibleOnly and name.startswith('.'): + continue + + listRet.append(name) + + return listRet + +# 리스트에 담긴 확장자와 같은 확장자를 가진 파일을 찾아서 리스트로 반환 +def ListFileExtRcr(pathTrg: str, listExt: list[str]) -> list[str]: + listRet:list[str] = [] + + # pathTrg의 하위 디렉토리 및 파일 목록을 얻음 + for dirpath, dirnames, filenames in os.walk(pathTrg): + for file in filenames: + extTmp = GetExtStr(file) + if extTmp.lower() in listExt and not file.startswith('.'): + listRet.append(os.path.join(dirpath, file)) + + return listRet + +# 입력된 경로에서 부모 폴더를 찾는다. 0 은 자기자신 1은 부모, 숫자대로 위로. +def GetParentDirName(FullPath : str, nUp : int)->str: + parts = FullPath.split(os.sep) + + nTrgIdx = 0 + if nUp < len(parts): + nTrgIdx = len(parts) -nUp -1 + elif nUp < 0: + nTrgIdx = len(parts) - 1 + else: + nTrgIdx = 0 + + return parts[nTrgIdx] + +# 입력된 경로가 자식 폴더를 가지고 있는지 판단한다.- 최종 폴더인지 여부 +# 자식이 없으면 True, 자식이 있으면 False +def IsFinalFolder(path : str) -> bool: + bRet = True + + contents = os.listdir(path) + for item in contents: + if True == os.path.isdir(item): + bRet = False + break + + return bRet; + +# 어떤 경로 안에서 특정 확장자의 파일을 뽑아내어 그 리스트를 반환한다. +def FindFileFromExt(path: str, ext: str)-> list[str]: + bDot = False + if 0 <= ext.find('.'): + bDot = True + + listRet:list[str] = [] + if False == os.path.exists(path): + return listRet + + contents = os.listdir(path) + for item in contents: + if True == os.path.isdir(item): + continue + + extItem = GetExtStr(item, bDot) + if extItem.lower() == ext.lower(): + listRet.append(item) + + return listRet + +# 파일 이름에서 확장자를 뽑아낸다. True : '.' 을 포함한다. +def GetExtStr(file_path: str, bDot: bool = True)-> str: + retStr = "" + # 파일 경로에서 마지막 점을 찾아 확장자를 추출 + last_dot_index = file_path.rfind('.') + if last_dot_index == -1: + retStr = "" # 점이 없는 경우 확장자가 없음 + else: + if True == bDot: + retStr = file_path[last_dot_index:] + else: + retStr = file_path[last_dot_index+1:] + + return retStr + +# 문자열에 포함된 단어를 지운다. +def RmvSubString(mainString: str, subString: str)-> str: + # 문자열에서 부분 문자열의 인덱스를 찾습니다. + strIdx = mainString.find(subString) + if strIdx == -1: # 부분 문자열이 존재하지 않으면 그대로 반환합니다. + return mainString + + endIdx = strIdx + len(subString) + + # 부분 문자열을 제거하고 새로운 문자열을 반환합니다. + return mainString[:strIdx] + mainString[endIdx:] + +# +def ExtractZIP(zip_file: str, extract_to: str): + with zipfile.ZipFile(zip_file, 'r') as zf: + zf.extractall(extract_to) + +# +def CreateZIP(output_zip: str, files: list[str]) -> bool: + with zipfile.ZipFile(output_zip, 'w') as zf: + for file in files: + pathTemp = os.path.join('root', os.path.basename(file)) + zf.write(file, pathTemp) + + bRet = False + if os.path.exists(output_zip): + bRet = True + + return bRet + +# 파일 리스트에 들어있는 파일만 골라서 압축을 합니다. 상대경로를 제거하는게 기본값. +def CreateZIPShell(zipName: str, files: list[str], bRmvRPath: bool = True) -> bool: + command = "zip " + + if True == bRmvRPath: + command += "-j " + + command += f"\"{zipName}\" " + + # 이중 리스트인 이유를 모르겠다. + for file in files: + command += f"\"{file}\" " + + result = subprocess.run(command, shell=True, capture_output=True, text=True) + + bRet = False + if 0 == result.returncode: + bRet = True + + return bRet + +# 특정 확장자만 쉘을 이용해서 압축한다 +def CreateZIPShExt(zipName: str, TrgExt: str)-> bool: + command = f"zip -j {zipName} *.{TrgExt}" + + result = subprocess.run(command, shell=True, capture_output=True, text=True) + + bRet = False + if 0 == result.returncode: + bRet = True + + return bRet + +# 압축 파일 내의 모든 파일 및 디렉토리 목록 가져오기 +def GetZipContentList(path: str) -> list[str]: + if True == IsEmptyStr(path) or not os.path.isfile(path): + return [] + + listRet = [] + with zipfile.ZipFile( path , 'r') as zip_file: + listRet = zip_file.namelist() + + return listRet + +# +def GetZippedFileByte(pathZip: str, FileName: str) -> bytes: + retBytes:bytes = bytes() + if True == os.path.isfile(pathZip): + with zipfile.ZipFile( pathZip , 'r') as zip_file: + # 압축 파일 내의 특정 파일을 읽기 + with zip_file.open(FileName) as file: + retBytes = file.read() + + return retBytes + +# JSON 을 트리 구조로 출력한다. +def PrintJSONTree(data, indent: int=0 ) -> None: + if isinstance(data, dict): + for key, value in data.items(): + print(' ' * indent + str(key)) + PrintJSONTree(value, indent + 1) + elif isinstance(data, list): + for item in data: + PrintJSONTree(item, indent) + else: + print(' ' * indent + str(data)) + +# +def IsPathWithin(base_path: str, target_path: str) -> bool: + base = Path(base_path).resolve() + target = Path(target_path).resolve() + return target.is_relative_to(base) + +# 랜덤 UUID 생성 +def UUIDGenRandom(): + random_uuid = uuid.uuid4() + return random_uuid + +# 이름을 기반으로 UUID 생성 +def UUIDGenName(SeedName:str): + namespace_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, SeedName) + return namespace_uuid + +# +def GetTextInBrakets(text:str)-> list[str]: + return re.findall(r'\[(.*?)\]', text) + +#파일의 해시를 계산하는 함수. +#Args: +# strFilePath (str): 파일 경로 +# method (str): 사용할 해시 알고리즘 ('md5', 'sha1', 'sha256' 등) +#Returns: +# str: 계산된 해시값 (16진수 문자열) +def CalculateFileHash(strFilePath: str, method: str="sha256")-> str: + funcHash = getattr(hashlib, method)() + + with open(strFilePath, "rb") as f: + while True: + chunk = f.read(4096) + if not chunk: + break + + funcHash.update(chunk) + + return funcHash.hexdigest() + +#파일의 해시를 비교하여 무결성 검증. +#Args: +# file_path (str): 파일 경로 +# expected_hash (str): 기대하는 해시값 +# method (str): 사용할 해시 알고리즘 +#Returns: +# bool: 파일이 정상인지 여부 +def VerifyIsValidFile(strPath: str, strCompHash: str, strMethod: str="sha256")->bool: + Hash_Calcd = CalculateFileHash(strPath, strMethod) + + return strCompHash.lower() == Hash_Calcd.lower() + +""" +# 사용 예시 +if __name__ == "__main__": + file_to_check = "example.txt" + known_good_hash = "5d41402abc4b2a76b9719d911017c592" # 예시 (MD5) + + is_valid = verify_file(file_to_check, known_good_hash, method='md5') + if is_valid: + print("파일이 정상입니다!") + else: + print("파일이 손상되었거나 다릅니다!") +""" \ No newline at end of file diff --git a/VWorldAPIs.py b/VWorldAPIs.py new file mode 100644 index 0000000..feef9d8 --- /dev/null +++ b/VWorldAPIs.py @@ -0,0 +1,77 @@ +import io +import requests +import math +import numpy as np +import json +import folium + +from bs4 import BeautifulSoup +from pyproj import Transformer + +#위도(latitude), 경도(longitude) +Earth_r = 20037508.34 # meter + +def GetJusofromGPS(lat,lon): + ret_juso = ["", "", "", ""] + + if lat <= 0.0 or lon <= 0.0: + return ret_juso + + apiurl = "https://api.vworld.kr/req/address?" + params = { + "service": "address", + "request": "getaddress", + "crs": "epsg:4326", + "point": f"{lon},{lat}", + "format": "json", + "type": "BOTH", + "key": "7E59C6EC-6BB0-3ED9-ACCC-4158869D7CFD" + } + + response = requests.get(apiurl, params=params) + + data = None + if response.status_code == 200: + data = response.json() + + print(data) + + if "OK" == data["response"]["status"]: + for item in data["response"]["result"]: + type = item["type"] + + if type.lower() == "parcel": + ret_juso[0] = item["text"] + ret_juso[1] = item["structure"]["level4L"] + elif type.lower() == "road": + ret_juso[2] = item["text"] + ret_juso[3] = item["structure"]["level4L"] + + else: + print( f"{data["response"]["status"] } : {data["error"]["code"] }" ) + + return ret_juso + +# VWorld 좌표계 : EPSG:3857 +# GPS 좌표계 : EPSG:4326 +# 경도는 람다 (\lambda) 위도는 파이 (\phi) +def Transform4326to3857(srcLat, srcLon): + transformer = Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True) + + # 좌표 변환 실행 + x, y = transformer.transform(srcLon, srcLat) + + return y, x + + + + +#37.536514,126.977138 +#37.541047,126.990966 +#37.539324791666665,126.98787990833334 +#37.539325,126.987880 +#GetJusofromGPS(37.52356569972222,126.9683578) +#"14105383.450839", "3950184.1545913" +#37.36340831688752, 35.48510801650072 +#"3950184.1545913", "14105383.450839" + diff --git a/test.py b/test.py new file mode 100644 index 0000000..be934f6 --- /dev/null +++ b/test.py @@ -0,0 +1,89 @@ +import os +import shutil +import UtilPack as util +import PoleXLS as myxl +import VWorldAPIs as vwapi + + +ImgDirPath = "/Volumes/ExSSD/Working/Images" +ImgSortedPath = "/Volumes/ExSSD/Working/Sorted" +XlsDirPath = "/Users/minarinari/Workspace/Python/UtilityPole_Info" + +indexFile = ".index.maps" + +# 엑셀을 연다. +XLSPath = os.path.join(XlsDirPath, "EPoleDB.xlsx") +tempxls = myxl.PoleXLS(XLSPath) +tempxls.DBXLSOpen() + +TrgSheet = "ImgInfo" +TrgSheetLastCol = 8 +# index, sheet, date, level, relpath, area, juso, gps +nLastRow = tempxls.FindLastRow(TrgSheet) + +for nRow in range(1, nLastRow): + listValues = tempxls.GetAllRowValue(TrgSheet, nRow, TrgSheetLastCol ) + + if TrgSheetLastCol != len(listValues): + continue + + #print(listValues) + + level = listValues[3] + relpath = listValues[4] + area = listValues[5] + gps = [7] + + if None == area or True == util.IsEmptyStr(area): + continue + + pathSrcFull = os.path.join(ImgDirPath, relpath) + if False == os.path.exists(pathSrcFull): + continue + + tempPath = f"{area}_{relpath}" + pathTrgFull = os.path.join(ImgSortedPath, tempPath) + pathTrgDir = os.path.dirname(pathTrgFull) + + if False == os.path.exists(pathTrgDir): + os.makedirs(pathTrgDir) + + indexfilePath = os.path.join(pathTrgDir, indexFile) + + with open(indexfilePath, "w") as file: + file.write(f"{area} : {level} ") + + + print(f"{pathSrcFull} -> {pathTrgFull}") + shutil.copy(pathSrcFull, pathTrgFull) + + + + + + + +#a = "/Volumes/ExSSD/Working/Images" +#b = "/Volumes/ExSSD/Working/Images/a" + +#c = os.path.relpath(b, a) +#d = os.path.realpath(c) + +# 37.53245379972222,126.96456829972223 + +#juso, area = vwapi.GetJusofromGPS(37.533949916666664,126.99432473611111) + +#print(f"{area} : {juso}") + +#Pole : 37.54353059972222,126.9650134 +#Trg : 37.5439572,126.95750569972222 + +#srcLat = 37.54353059972222 +#srcLon = 126.9650134 + +#trgLat = 37.5439572 +#trgLon = 126.95750569972222 + +#dist = util.GetDistanceGPS(srcLat, srcLon, trgLat, trgLon) +#print(dist) +