data-return/quexian.py

716 lines
30 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import re
import sys
import sqlite3
import subprocess
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QFileDialog, QTableWidget,
QTableWidgetItem, QComboBox, QMessageBox, QHeaderView, QMenu,
QDateEdit, QGroupBox, QRadioButton, QButtonGroup, QSplitter)
from PyQt5.QtCore import Qt, QDate, QThread, pyqtSignal
from PyQt5.QtGui import QIcon, QPixmap
class DefectPhotoScanner(QThread):
"""缺陷照片扫描线程"""
scan_finished = pyqtSignal(str, bool, str) # 参数: root_dir, success, message
def __init__(self, root_dir, db_conn):
super().__init__()
self.root_dir = root_dir
self.db_conn = db_conn
def run(self):
try:
cursor = self.db_conn.cursor()
# 清理当前项目路径下的旧数据
print(f"[DEBUG] 清理数据库中的项目路径: {self.root_dir}")
cursor.execute("DELETE FROM defect_photos WHERE project_path = ?", (self.root_dir,))
deleted_rows = cursor.rowcount
print(f"[DEBUG] 已删除 {deleted_rows} 条旧记录")
self.db_conn.commit()
if not os.path.exists(self.root_dir):
self.scan_finished.emit(self.root_dir, False, f"目录不存在: {self.root_dir}")
return
# 收集所有缺陷照片文件
defect_files = [f for f in os.listdir(self.root_dir)
if os.path.isfile(os.path.join(self.root_dir, f)) and
f.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp'))]
print(f"[DEBUG] 找到 {len(defect_files)} 个缺陷照片文件")
if not defect_files:
self.scan_finished.emit(self.root_dir, False, "未找到任何缺陷照片文件")
return
total_photos = 0
for file in defect_files:
file_path = os.path.join(self.root_dir, file)
# 解析文件名中的信息
defect_info = self.parse_defect_info(file)
if not defect_info:
print(f"[DEBUG] 跳过无法解析的文件: {file}")
continue
# 提取拍摄时间
capture_time = self.parse_capture_time(file)
if not capture_time:
print(f"[DEBUG] 跳过无法解析时间的文件: {file}")
continue
# 插入数据库
cursor.execute("""
INSERT OR IGNORE INTO defect_photos (
project_path, unit_id, blade_number, defect_type,
defect_description, axial_length, chord_length,
photo_path, capture_time, file_name
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
self.root_dir, defect_info['unit_id'], defect_info['blade_number'],
defect_info['defect_type'], defect_info['description'],
defect_info['axial_length'], defect_info['chord_length'],
file_path, capture_time, file
))
if cursor.rowcount > 0:
total_photos += 1
self.db_conn.commit()
print(f"[DEBUG] 扫描完成,共找到 {total_photos} 张缺陷照片")
self.scan_finished.emit(self.root_dir, True, f"缺陷目录扫描完成,共找到 {total_photos} 张照片")
except Exception as e:
error_msg = f"扫描缺陷目录时出错: {str(e)}"
print(f"[ERROR] {error_msg}")
self.scan_finished.emit(self.root_dir, False, error_msg)
def parse_defect_info(self, filename):
"""解析缺陷照片文件名中的信息"""
# 初始化默认值
unit_id = ""
blade_number = ""
description = ""
defect_type = ""
axial_length = 0
chord_length = 0
try:
# 规则1提取机组号从开头到"机组"前的所有字符)
parts = re.split(r'机组|_|#| ', filename, maxsplit=1)
if len(parts) > 1:
unit_id = parts[0].strip()
remaining = parts[1]
else:
return None # 不符合命名规范
# 规则2提取叶片编号从"机组"后到"叶片"前的数字)
parts = re.split(r'叶片|_|#| ', remaining, maxsplit=1)
if len(parts) > 1:
blade_number_match = re.search(r'\d+', parts[0])
if blade_number_match:
blade_number = blade_number_match.group()
remaining = parts[1]
else:
blade_number = "" # 可能没有明确的叶片编号
# 优化点1去除多余的"叶片"字样
remaining = re.sub(r'叶片,?叶片', '叶片', remaining)
# 预先检查复合缺陷类型
compound_defects = ["轴向裂纹", "弦向裂纹", "玻璃钢壳体裸露"]
defect_pos = -1
defect_to_find = None
# 查找第一个出现的复合缺陷类型
for defect in compound_defects:
pos = remaining.find(defect)
if pos != -1 and (defect_pos == -1 or pos < defect_pos):
defect_pos = pos
defect_to_find = defect
# 规则3提取缺陷描述
if defect_to_find:
# 如果有复合缺陷类型,描述到缺陷类型开始处
description = remaining[:defect_pos].strip(" ,。.")
defect_type = defect_to_find
remaining = remaining[defect_pos + len(defect_to_find):]
else:
# 否则按原有逻辑处理
axial_pos = remaining.find("轴向")
chord_pos = remaining.find("弦向")
# 确定描述结束位置
if axial_pos != -1 and chord_pos != -1:
desc_end = min(axial_pos, chord_pos)
elif axial_pos != -1:
desc_end = axial_pos
elif chord_pos != -1:
desc_end = chord_pos
else:
desc_end = len(remaining)
description = remaining[:desc_end].strip(" ,。.")
remaining = remaining[desc_end:]
# 规则4从描述中提取缺陷类型优化分类逻辑
defect_type_map = {
"玻璃钢壳体裸露": "涂层损伤",
"玻璃钢壳体": "涂层损伤",
"玻璃钢": "涂层损伤",
"龟裂": "涂层损伤",
"涂层龟裂": "涂层损伤",
"雷击损伤": "雷击损伤",
"涂层损伤": "涂层损伤",
"涂层裂纹": "涂层损伤",
"弦向裂纹": "裂纹",
"轴向裂纹": "裂纹",
"裂纹": "裂纹"
}
# 按优先级匹配缺陷类型
for keyword, dtype in defect_type_map.items():
if keyword in description:
defect_type = dtype
break
# 规则5提取缺陷尺寸
# 提取轴向尺寸(支持多种格式)
axial_match = re.search(r'轴向[:]?\s*(\d+)mm', remaining)
if axial_match:
axial_length = int(axial_match.group(1))
# 提取弦向尺寸(支持多种格式)
chord_match = re.search(r'弦向[:]?\s*(\d+)mm', remaining)
if chord_match:
chord_length = int(chord_match.group(1))
# 处理复合尺寸(如"弦向80mm弦向50mm"
if "弦向(上)" in remaining and "弦向(下)" in remaining:
upper_match = re.search(r'弦向(上)[:]?\s*(\d+)mm', remaining)
lower_match = re.search(r'弦向(下)[:]?\s*(\d+)mm', remaining)
if upper_match and lower_match:
chord_length = f"{upper_match.group(1)}/{lower_match.group(1)}"
return {
'unit_id': unit_id,
'blade_number': blade_number,
'defect_type': defect_type,
'description': description,
'axial_length': axial_length,
'chord_length': chord_length
}
except Exception as e:
print(f"解析文件名出错: {filename}, 错误: {str(e)}")
return None
def parse_capture_time(self, filename):
"""从文件名解析拍摄时间"""
# 尝试第一种格式DJI_YYYYMMDDHHMMSS_xxx_Z.JPG
match = re.search(r"DJI_(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})", filename)
if match:
year, month, day, hour, minute, second = match.groups()
try:
return f"{year}-{month}-{day} {hour}:{minute}:{second}"
except:
return None
# 尝试第二种格式YYYYMMDD_HHMMSS.jpg
match = re.search(r"(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})", filename)
if match:
year, month, day, hour, minute, second = match.groups()
try:
return f"{year}-{month}-{day} {hour}:{minute}:{second}"
except:
return None
return None
class DefectPhotoManager(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("风电叶片缺陷照片管理系统")
self.setWindowIcon(QIcon("defect.ico"))
self.setGeometry(100, 100, 1400, 800)
# 使用独立的缺陷检查数据库
self.db_conn = sqlite3.connect("defect_inspection.db",
check_same_thread=False)
self.create_db_table()
# 初始化UI组件
self.init_ui_components()
# 加载数据
self.load_units_to_combo()
self.show()
def create_db_table(self):
"""创建独立的缺陷检查数据库表结构"""
cursor = self.db_conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS defect_photos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_path TEXT NOT NULL,
unit_id TEXT NOT NULL,
blade_number TEXT NOT NULL,
defect_type TEXT NOT NULL,
defect_description TEXT NOT NULL,
axial_length INTEGER NOT NULL DEFAULT 0,
chord_length INTEGER NOT NULL DEFAULT 0,
photo_path TEXT NOT NULL UNIQUE,
capture_time TEXT NOT NULL,
file_name TEXT NOT NULL
)
""")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_def_project_path ON defect_photos(project_path)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_def_unit_id ON defect_photos(unit_id)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_def_blade_number ON defect_photos(blade_number)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_def_capture_time ON defect_photos(capture_time)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_def_defect_type ON defect_photos(defect_type)")
self.db_conn.commit()
def init_ui_components(self):
"""初始化缺陷检查专用UI"""
main_widget = QWidget()
main_layout = QVBoxLayout()
# 目录选择区域
dir_layout = QHBoxLayout()
self.dir_label = QLabel("缺陷目录:")
self.dir_input = QLineEdit()
self.dir_input.setPlaceholderText("请选择缺陷照片目录")
self.browse_btn = QPushButton("浏览...")
self.browse_btn.clicked.connect(self.browse_directory)
self.scan_btn = QPushButton("扫描目录")
self.scan_btn.clicked.connect(self.start_scan_directory)
dir_layout.addWidget(self.dir_label)
dir_layout.addWidget(self.dir_input, stretch=1)
dir_layout.addWidget(self.browse_btn)
dir_layout.addWidget(self.scan_btn)
# 查询条件区域
query_layout = QHBoxLayout()
# 机组编号查询
unit_layout = QVBoxLayout()
unit_layout.addWidget(QLabel("机组编号"))
self.unit_combo = QComboBox()
self.unit_combo.addItem("所有机组", "")
unit_layout.addWidget(self.unit_combo)
# 叶片编号选择框
blade_layout = QVBoxLayout()
blade_layout.addWidget(QLabel("叶片编号"))
self.blade_combo = QComboBox()
self.blade_combo.addItem("所有叶片", "")
blade_layout.addWidget(self.blade_combo)
# 缺陷类型选择
type_layout = QVBoxLayout()
type_layout.addWidget(QLabel("缺陷类型"))
self.type_combo = QComboBox()
self.type_combo.addItem("所有类型", "")
self.type_combo.addItem("涂层损伤", "涂层损伤")
self.type_combo.addItem("裂纹", "裂纹")
self.type_combo.addItem("雷击损伤", "雷击损伤")
self.type_combo.addItem("玻璃钢壳体裸露", "玻璃钢壳体裸露")
type_layout.addWidget(self.type_combo)
# 日期范围查询
date_group = QGroupBox("日期范围")
date_layout = QHBoxLayout()
self.start_date_edit = QDateEdit()
self.start_date_edit.setCalendarPopup(True)
self.start_date_edit.setDisplayFormat("yyyy-MM-dd")
self.start_date_edit.setDate(QDate.currentDate().addDays(-30))
self.end_date_edit = QDateEdit()
self.end_date_edit.setCalendarPopup(True)
self.end_date_edit.setDisplayFormat("yyyy-MM-dd")
self.end_date_edit.setDate(QDate.currentDate())
date_layout.addWidget(self.start_date_edit)
date_layout.addWidget(QLabel(""))
date_layout.addWidget(self.end_date_edit)
date_group.setLayout(date_layout)
# 尺寸筛选
size_group = QGroupBox("缺陷尺寸(mm)")
size_layout = QHBoxLayout()
self.min_size_label = QLabel("最小轴向:")
self.min_size_input = QLineEdit()
self.min_size_input.setPlaceholderText("0")
self.max_size_label = QLabel("最大轴向:")
self.max_size_input = QLineEdit()
self.max_size_input.setPlaceholderText("不限")
size_layout.addWidget(self.min_size_label)
size_layout.addWidget(self.min_size_input)
size_layout.addWidget(self.max_size_label)
size_layout.addWidget(self.max_size_input)
size_group.setLayout(size_layout)
query_layout.addLayout(unit_layout)
query_layout.addLayout(blade_layout)
query_layout.addLayout(type_layout)
query_layout.addWidget(date_group)
query_layout.addWidget(size_group)
# 查询按钮
self.query_btn = QPushButton("查询")
self.query_btn.clicked.connect(self.query_defects)
query_layout.addWidget(self.query_btn)
# 主内容区域 - 使用分割器确保预览区域可见
splitter = QSplitter(Qt.Horizontal)
# 结果显示区域
self.result_table = QTableWidget()
self.result_table.setColumnCount(9)
headers = ["机组编号", "叶片编号", "缺陷类型", "缺陷描述", "轴向(mm)", "弦向(mm)", "拍摄时间", "文件名", "路径"]
self.result_table.setHorizontalHeaderLabels(headers)
self.result_table.setSelectionBehavior(QTableWidget.SelectRows)
self.result_table.setEditTriggers(QTableWidget.NoEditTriggers)
self.result_table.doubleClicked.connect(self.open_selected_photo)
# 设置列宽策略
header = self.result_table.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeToContents) # 机组
header.setSectionResizeMode(1, QHeaderView.ResizeToContents) # 叶片
header.setSectionResizeMode(2, QHeaderView.ResizeToContents) # 类型
header.setSectionResizeMode(3, QHeaderView.Stretch) # 描述
header.setSectionResizeMode(4, QHeaderView.ResizeToContents) # 轴向
header.setSectionResizeMode(5, QHeaderView.ResizeToContents) # 弦向
header.setSectionResizeMode(6, QHeaderView.ResizeToContents) # 时间
header.setSectionResizeMode(7, QHeaderView.ResizeToContents) # 文件名
header.setSectionResizeMode(8, QHeaderView.Stretch) # 路径
# 预览区域
preview_widget = QWidget()
preview_layout = QVBoxLayout()
preview_layout.addWidget(QLabel("缺陷照片预览"))
self.preview_label = QLabel()
self.preview_label.setAlignment(Qt.AlignCenter)
self.preview_label.setMinimumSize(400, 400)
self.preview_label.setText("照片预览区域")
self.preview_label.setStyleSheet("""
QLabel {
border: 1px solid gray;
background-color: #f0f0f0;
qproperty-alignment: AlignCenter;
}
""")
# 缺陷信息显示
self.defect_info_label = QLabel()
self.defect_info_label.setWordWrap(True)
self.defect_info_label.setStyleSheet("""
QLabel {
border: 1px solid #ccc;
padding: 5px;
background-color: white;
}
""")
preview_layout.addWidget(self.preview_label)
preview_layout.addWidget(QLabel("缺陷详情:"))
preview_layout.addWidget(self.defect_info_label)
preview_widget.setLayout(preview_layout)
# 将表格和预览区域添加到分割器
splitter.addWidget(self.result_table)
splitter.addWidget(preview_widget)
splitter.setStretchFactor(0, 3) # 表格占3份空间
splitter.setStretchFactor(1, 1) # 预览占1份空间
# 连接信号
self.unit_combo.currentIndexChanged.connect(self.update_blade_combo)
# 连接选择变化信号
self.result_table.selectionModel().selectionChanged.connect(self.update_preview)
# 状态栏
self.statusBar().showMessage("就绪")
# 布局组装
main_layout.addLayout(dir_layout)
main_layout.addLayout(query_layout)
main_layout.addWidget(splitter, stretch=1) # 使用分割器作为主要内容区域
main_widget.setLayout(main_layout)
self.setCentralWidget(main_widget)
def browse_directory(self):
dir_path = QFileDialog.getExistingDirectory(self, "选择缺陷照片目录")
if dir_path:
self.dir_input.setText(dir_path)
def update_blade_combo(self):
"""更新叶片编号下拉框"""
unit_id = self.unit_combo.currentData()
project_path = self.dir_input.text()
if not project_path:
return
self.blade_combo.clear()
self.blade_combo.addItem("所有叶片", "")
cursor = self.db_conn.cursor()
query = """
SELECT DISTINCT blade_number
FROM defect_photos
WHERE project_path = ?
"""
params = [project_path]
if unit_id:
query += " AND unit_id = ?"
params.append(unit_id)
query += " ORDER BY blade_number"
cursor.execute(query, params)
blades = [row[0] for row in cursor.fetchall()]
for blade in blades:
self.blade_combo.addItem(blade, blade)
def start_scan_directory(self):
"""启动目录扫描线程"""
root_dir = self.dir_input.text()
if not root_dir or not os.path.exists(root_dir):
QMessageBox.warning(self, "警告", "请选择有效的目录路径")
return
self.scan_btn.setEnabled(False)
self.statusBar().showMessage("正在扫描缺陷目录,请稍候...")
self.scanner = DefectPhotoScanner(root_dir, self.db_conn)
self.scanner.scan_finished.connect(self.on_scan_finished)
self.scanner.start()
def on_scan_finished(self, root_dir, success, message):
"""扫描完成后的回调"""
self.scan_btn.setEnabled(True)
if success:
self.load_units_to_combo(root_dir)
self.query_defects()
self.statusBar().showMessage(f"缺陷目录扫描完成: {root_dir}", 5000)
QMessageBox.information(self, "完成", message)
else:
error_msg = f"扫描缺陷目录时出错:\n\n{message}\n\n请检查:\n1. 文件名是否符合规范\n2. 是否有访问权限"
QMessageBox.critical(self, "扫描错误", error_msg)
self.statusBar().showMessage(f"扫描失败: {message}", 5000)
def load_units_to_combo(self, project_path=None):
"""加载机组编号到下拉框"""
cursor = self.db_conn.cursor()
# 如果没有传入project_path尝试使用当前目录输入框的值
if project_path is None:
project_path = self.dir_input.text() if self.dir_input.text() else None
if project_path:
cursor.execute("""
SELECT DISTINCT unit_id FROM defect_photos
WHERE unit_id != '' AND project_path = ?
ORDER BY unit_id
""", (project_path,))
else:
cursor.execute("""
SELECT DISTINCT unit_id FROM defect_photos
WHERE unit_id != ''
ORDER BY unit_id
""")
units = [row[0] for row in cursor.fetchall()]
self.unit_combo.clear()
self.unit_combo.addItem("所有机组", "")
for unit in units:
self.unit_combo.addItem(unit, unit)
def query_defects(self):
"""查询缺陷数据"""
# 获取查询条件
unit_id = self.unit_combo.currentData() or None
blade_number = self.blade_combo.currentData() or None
defect_type = self.type_combo.currentData() or None
start_date = self.start_date_edit.date().toString("yyyy-MM-dd")
end_date = self.end_date_edit.date().toString("yyyy-MM-dd")
project_path = self.dir_input.text()
# 获取尺寸筛选条件
min_size = self.min_size_input.text().strip()
max_size = self.max_size_input.text().strip()
try:
min_size = int(min_size) if min_size else 0
max_size = int(max_size) if max_size else 99999
except ValueError:
QMessageBox.warning(self, "输入错误", "尺寸必须为整数")
return
if not project_path:
QMessageBox.warning(self, "警告", "请先选择缺陷目录")
return
# 构建查询
query = """
SELECT
unit_id, blade_number, defect_type, defect_description,
axial_length, chord_length, capture_time, file_name, photo_path
FROM defect_photos
WHERE date(capture_time) BETWEEN ? AND ?
AND project_path = ?
AND axial_length BETWEEN ? AND ?
"""
params = [start_date, end_date, project_path, min_size, max_size]
# 添加机组条件
if unit_id:
query += " AND unit_id = ?"
params.append(unit_id)
# 添加叶片编号条件
if blade_number:
query += " AND blade_number = ?"
params.append(blade_number)
# 添加缺陷类型条件
if defect_type:
query += " AND defect_type = ?"
params.append(defect_type)
query += " ORDER BY unit_id, blade_number, capture_time"
# 执行查询
cursor = self.db_conn.cursor()
try:
cursor.execute(query, params)
results = cursor.fetchall()
# 显示结果
self.result_table.setRowCount(len(results))
for row_idx, row in enumerate(results):
for col_idx, col in enumerate(row):
item = QTableWidgetItem(str(col) if col is not None else "")
self.result_table.setItem(row_idx, col_idx, item)
self.statusBar().showMessage(f"找到 {len(results)} 条缺陷记录", 5000)
# 如果有结果,自动选择第一行并更新预览
if results:
self.result_table.selectRow(0)
self.update_preview()
else:
self.preview_label.setText("无照片可预览")
self.preview_label.setPixmap(QPixmap())
self.defect_info_label.setText("")
except sqlite3.Error as e:
QMessageBox.critical(self, "数据库错误", f"查询失败: {str(e)}")
def open_selected_photo(self, index):
"""双击打开选中的照片"""
selected_row = index.row()
photo_path = self.result_table.item(selected_row, 8).text() # 路径在第8列
if not photo_path:
QMessageBox.warning(self, "警告", "未找到文件路径")
return
photo_path = os.path.normpath(photo_path)
if not os.path.exists(photo_path):
QMessageBox.warning(self, "文件不存在", f"无法找到文件:\n{photo_path}")
return
try:
if os.name == 'nt':
os.startfile(photo_path)
elif sys.platform == 'darwin':
subprocess.call(['open', photo_path])
else:
subprocess.call(['xdg-open', photo_path])
self.statusBar().showMessage(f"正在打开: {os.path.basename(photo_path)}", 3000)
except Exception as e:
QMessageBox.critical(self, "错误", f"无法打开文件:\n路径: {photo_path}\n错误: {str(e)}")
def update_preview(self):
"""更新照片预览和缺陷信息"""
selected_rows = self.result_table.selectionModel().selectedRows()
if selected_rows:
selected_row = selected_rows[0].row()
# 获取照片路径(第8列)
photo_path = self.result_table.item(selected_row, 8).text()
# 获取缺陷信息
unit_id = self.result_table.item(selected_row, 0).text()
blade_number = self.result_table.item(selected_row, 1).text()
defect_type = self.result_table.item(selected_row, 2).text()
description = self.result_table.item(selected_row, 3).text()
axial_length = self.result_table.item(selected_row, 4).text()
chord_length = self.result_table.item(selected_row, 5).text()
capture_time = self.result_table.item(selected_row, 6).text()
# 更新预览图片
if photo_path and os.path.exists(photo_path):
try:
pixmap = QPixmap(photo_path)
if not pixmap.isNull():
# 保持宽高比缩放图片
scaled_pixmap = pixmap.scaled(
self.preview_label.width(),
self.preview_label.height(),
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
self.preview_label.setPixmap(scaled_pixmap)
self.preview_label.setText("")
else:
self.preview_label.setText("无法加载预览")
self.preview_label.setPixmap(QPixmap())
except Exception as e:
print(f"预览错误: {str(e)}")
self.preview_label.setText("预览错误")
self.preview_label.setPixmap(QPixmap())
else:
self.preview_label.setText("文件不存在")
self.preview_label.setPixmap(QPixmap())
# 更新缺陷信息
info_text = f"""
<b>机组编号:</b> {unit_id}<br>
<b>叶片编号:</b> {blade_number}<br>
<b>缺陷类型:</b> {defect_type}<br>
<b>缺陷描述:</b> {description}<br>
<b>缺陷尺寸:</b> 轴向 {axial_length}mm × 弦向 {chord_length}mm<br>
<b>拍摄时间:</b> {capture_time}
"""
self.defect_info_label.setText(info_text)
def closeEvent(self, event):
self.db_conn.close()
event.accept()
if __name__ == "__main__":
app = QApplication(sys.argv)
manager = DefectPhotoManager()
sys.exit(app.exec_())