First commit
This commit is contained in:
192
ExtEXIFData.py
Normal file
192
ExtEXIFData.py
Normal 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
51
JusoMngr.py
Normal 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
26
Main.py
Normal 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
303
MgrUtilityPoleConverter.py
Normal 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
46
MgrUtilityPoleOCR.py
Normal 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
167
MgrUtilityPoleUI.py
Normal 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
173
PoleXLS.py
Normal 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
108
Pole_serial_sorter.py
Normal 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
186
StoreXLS.py
Normal 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
246
UI.py
Normal 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
298
UtilPack.py
Normal 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
346
UtilPack2.py
Normal 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
77
VWorldAPIs.py
Normal 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
89
test.py
Normal 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)
|
||||||
|
|
||||||
Reference in New Issue
Block a user