First commit

This commit is contained in:
2025-09-23 20:59:17 +09:00
commit 37267985b4
14 changed files with 2308 additions and 0 deletions

192
ExtEXIFData.py Normal file
View File

@@ -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

51
JusoMngr.py Normal file
View File

@@ -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]

26
Main.py Normal file
View File

@@ -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)

303
MgrUtilityPoleConverter.py Normal file
View File

@@ -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 <Image Folder Path> <Excel File Name>")
# For Main Loop
if __name__ == '__main__':
# argc = len(sys.argv)
# argv = sys.argv
# main(argc, argv)
main()

46
MgrUtilityPoleOCR.py Normal file
View File

@@ -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())

167
MgrUtilityPoleUI.py Normal file
View File

@@ -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))

173
PoleXLS.py Normal file
View File

@@ -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

108
Pole_serial_sorter.py Normal file
View File

@@ -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)

186
StoreXLS.py Normal file
View File

@@ -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

246
UI.py Normal file
View File

@@ -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_())

298
UtilPack.py Normal file
View File

@@ -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

346
UtilPack2.py Normal file
View File

@@ -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("파일이 손상되었거나 다릅니다!")
"""

77
VWorldAPIs.py Normal file
View File

@@ -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"

89
test.py Normal file
View File

@@ -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)