From 74883a6e9d2e3b4374f1597463dab6dd0c55b019 Mon Sep 17 00:00:00 2001
From: Oh-JongJin <5jx2oh@gmail.com>
Date: Mon, 7 Feb 2022 16:42:48 +0900
Subject: [PATCH 01/23] Modify name what ND-01 to JS-06 in README file
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 1df7b5f..79eaf0a 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-# ND-01
-The successor of JS-08. The design doal of ND-01 is to provide higher spatial resolution for each azimuthal direction.
+# JS-06
+The successor of JS-08. The design doal of JS-06 is to provide higher spatial resolution for each azimuthal direction.
## Setting Up Development Environment
- Visual Studio Code with the end of line sequence to **LF**. You can set git to resolve the issue automatically:
From ed5fdc0be1b6234b981cd5ddcc8ec235630a1548 Mon Sep 17 00:00:00 2001
From: Oh-JongJin <5jx2oh@gmail.com>
Date: Thu, 10 Feb 2022 16:57:55 +0900
Subject: [PATCH 02/23] Modify to connect functions of QSettings
---
src/nd01_settings.py | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/src/nd01_settings.py b/src/nd01_settings.py
index 9d8e2b7..1ce3f5e 100644
--- a/src/nd01_settings.py
+++ b/src/nd01_settings.py
@@ -336,9 +336,16 @@ def get_target(self):
QMessageBox.about(self, 'Error', 'no file...')
def accept_click(self):
- print('accept? yes accept~')
- # JS06Settings.set('data_csv_path', self.)
+ JS06Settings.set('data_csv_path', self.data_csv_path_textBrowser.toPlainText())
+ JS06Settings.set('target_csv_path', self.target_csv_path_textBrowser.toPlainText())
+ JS06Settings.set('image_save_path', self.image_save_path_textBrowser.toPlainText())
+ JS06Settings.set('image_size', self.image_size_comboBox.currentIndex())
+ JS06Settings.set('visibility_alert_limit', self.vis_limit_spinBox.value())
+ JS06Settings.set('login_id', self.id_lineEdit.text())
+ JS06Settings.set('login_pw', self.pw_lineEdit.text())
+
+ self.close()
if __name__ == '__main__':
From 3a8d25d9ded2fad7854931578294db5a88c926df Mon Sep 17 00:00:00 2001
From: Oh-JongJin <5jx2oh@gmail.com>
Date: Fri, 11 Feb 2022 14:01:05 +0900
Subject: [PATCH 03/23] Replace plot module to QChartView
---
src/efficiency_chart.py | 66 +++++++++
src/nd01.py | 262 ++++++++++++++++++++++++-----------
src/nd01_settings.py | 27 +++-
src/resources/main_window.ui | 23 +--
src/resources/settings.ui | 16 ---
src/video_thread_mp.py | 9 +-
6 files changed, 272 insertions(+), 131 deletions(-)
create mode 100644 src/efficiency_chart.py
diff --git a/src/efficiency_chart.py b/src/efficiency_chart.py
new file mode 100644
index 0000000..653c4f7
--- /dev/null
+++ b/src/efficiency_chart.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+#
+# Copyright 2021-2022 Sijung Co., Ltd.
+#
+# Authors:
+# cotjdals5450@gmail.com (Seong Min Chae)
+# 5jx2oh@gmail.com (Jongjin Oh)
+
+
+import os
+
+import cv2
+import pandas as pd
+from PyQt5 import uic
+from PyQt5.QtCore import (QPoint, QRect, Qt,
+ QPointF)
+from PyQt5.QtGui import (QPixmap, QPainter, QBrush,
+ QColor, QPen, QImage,
+ QIcon)
+from PyQt5.QtWidgets import (QApplication, QLabel, QInputDialog,
+ QDialog, QAbstractItemView, QVBoxLayout,
+ QGridLayout, QPushButton, QMessageBox,
+ QFileDialog, QWidget)
+from PyQt5.QtChart import (QChartView, QLegend, QLineSeries,
+ QPolarChart, QScatterSeries, QValueAxis,
+ QChart)
+from model import JS06Settings
+
+
+class EfficiencyChart(QChartView):
+
+ def __init__(self, parent: QWidget):
+ super().__init__(parent)
+ self.setMaximumSize(600, 400)
+
+ series = QLineSeries()
+
+ load = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 30]
+ eff = [0, 0.5726, 0.7321, 0.7978, 0.8398, 0.8616, 0.8798,
+ 0.8903, 0.9002, 0.9062, 0.9127, 0.9267, 0.9379, 0.9430, 0.9448]
+
+ for i, e in zip(load, eff):
+ series.append(QPointF(float(i), float(e) * 100))
+
+ chart = QChart()
+ chart.addSeries(series)
+ chart.setTitle('Efficiency vs. Load')
+ chart.setAnimationOptions(QChart.SeriesAnimations)
+
+ axisX = QValueAxis()
+ axisX.setRange(0, 30)
+ axisX.setLabelFormat("%.1f")
+ axisX.setTickCount(7)
+
+ axisY = QValueAxis()
+ axisY.setRange(0, 100)
+ axisY.setLabelFormat("%d")
+ axisY.setMinorTickCount(5)
+
+ chart.addAxis(axisX, Qt.AlignBottom)
+ chart.addAxis(axisY, Qt.AlignLeft)
+
+ series.attachAxis(axisX)
+ series.attachAxis(axisY)
+
+ chart.legend().setVisible(False)
\ No newline at end of file
diff --git a/src/nd01.py b/src/nd01.py
index 5bd57c5..695c092 100644
--- a/src/nd01.py
+++ b/src/nd01.py
@@ -5,8 +5,7 @@
# Authors:
# cotjdals5450@gmail.com (Seong Min Chae)
# 5jx2oh@gmail.com (Jongjin Oh)
-
-
+import collections
import sys
import os
import time
@@ -17,16 +16,19 @@
import multiprocessing as mp
from multiprocessing import Process, Queue
-from PyQt5.QtGui import (QPixmap, QIcon, QPainter, QColor)
+from PyQt5.QtGui import (QPixmap, QIcon, QPainter,
+ QColor)
from PyQt5.QtWidgets import (QMainWindow, QWidget, QGraphicsScene,
- QFrame, QVBoxLayout)
-from PyQt5.QtCore import (Qt, pyqtSlot, pyqtSignal, QRect,
- QTimer, QObject, QThread)
+ QFrame, QVBoxLayout, QStyle)
+from PyQt5.QtCore import (Qt, pyqtSlot, pyqtSignal,
+ QRect, QTimer, QObject,
+ QThread, QPointF, QDateTime)
from PyQt5.QtChart import (QChartView, QLegend, QLineSeries,
- QPolarChart, QScatterSeries, QValueAxis)
+ QPolarChart, QScatterSeries, QValueAxis,
+ QChart, QDateTimeAxis)
from PyQt5 import uic
-from video_thread_mp import producer, VideoThread
+from video_thread_mp import producer
from nd01_settings import ND01SettingWidget
from model import JS06Settings
from save_db import main
@@ -81,83 +83,170 @@ def tickStrings(self, values, scale, spacing):
return [time.strftime("%H:%M:%S", time.localtime(local_time)) for local_time in values]
-class PlotWidget(QWidget):
-
- def __init__(self, parent=None):
- QWidget.__init__(self, parent)
-
- self.pw = pg.PlotWidget(
- labels={'left': 'Visibility (km)'},
- axisItems={'bottom': TimeAxisItem(orientation='bottom')}
- )
-
- self.pw.showGrid(x=True, y=True)
- self.pdi = self.pw.plot(pen='w') # PlotDataItem obj 반환.
-
- self.p1 = self.pw.plotItem
-
- self.p2 = pg.ViewBox()
- self.p1.showAxis('right')
- self.p1.scene().addItem(self.p2)
- self.p1.getAxis('right').linkToView(self.p2)
- self.p2.setXLink(self.p1)
- self.p1.getAxis('right').setLabel('Axis 2', color='#ffff00')
-
- self.p2.setGeometry(self.p1.vb.sceneBoundingRect())
- # self.p2.addItem(pg.PlotCurveItem([10, 20, 40, 80, 400, 2000], pen='y'))
- # self.pdi.sigClicked.connect(self.onclick)
+# class PlotWidget(QWidget):
+#
+# def __init__(self, parent=None):
+# QWidget.__init__(self, parent)
+#
+# self.pw = pg.PlotWidget(
+# labels={'left': 'Visibility (km)'},
+# axisItems={'bottom': TimeAxisItem(orientation='bottom')}
+# )
+# # self.setMaximumSize(600, 400)
+#
+# self.pw.showGrid(x=True, y=True)
+# self.pdi = self.pw.plot(pen='w') # PlotDataItem obj 반환.
+#
+# self.p1 = self.pw.plotItem
+#
+# self.p2 = pg.ViewBox()
+# self.p1.showAxis('right')
+# self.p1.scene().addItem(self.p2)
+# self.p1.getAxis('right').linkToView(self.p2)
+# self.p2.setXLink(self.p1)
+# self.p1.getAxis('right').setLabel('Axis 2', color='#ffff00')
+#
+# self.p2.setGeometry(self.p1.vb.sceneBoundingRect())
+# # self.p2.addItem(pg.PlotCurveItem([10, 20, 40, 80, 400, 2000], pen='y'))
+# # self.pdi.sigClicked.connect(self.onclick)
+#
+# self.plotData = {'x': [], 'y': []}
+# self.pre_plotData = {'x': [], 'y': []}
+#
+# def update_plot(self, new_time_data: int):
+# self.plotData['x'].append(new_time_data)
+# self.plotData['y'].append(np.random.randint(10000, 15000))
+#
+# # 항상 x축 시간을 설정한 범위 ( -3 시간 전 ~ 10 분 후 )만 보여줌.
+# # self.pw.setXRange(new_time_data - 3600 * 3, new_time_data + 600, padding=0)
+# # self.pw.setYRange(-1, 21, padding=0)
+#
+# data = []
+# for i in self.plotData['y']:
+# i = i / 1000
+# data.append(i)
+# self.pdi.setData(self.plotData['x'], data)
+#
+# # self.pdi.setData([1643873584, 1643873585, 1643873586, 1643873587, 1643873588],
+# # [12, 11, 10, 14, 13])
+#
+# def onclick(self, plot, points):
+# for point in points:
+# print(point.pos())
- self.plotData = {'x': [], 'y': []}
- self.pre_plotData = {'x': [], 'y': []}
- def update_plot(self, new_time_data: int):
- self.plotData['x'].append(new_time_data)
- self.plotData['y'].append(np.random.randint(10000, 15000))
+# class PolarWidget(QWidget):
+#
+# def __init__(self, parent=None):
+# QWidget.__init__(self, parent)
+#
+# self.pw = pg.plot(
+# labels={'left': 'Visibility (km)'},
+# axisItems={'bottom': TimeAxisItem(orientation='bottom')}
+# )
+#
+# self.pw.showGrid(x=True, y=True)
+# self.pdi = self.pw.plot(pen='w')
+#
+# self.pw.addLine(x=0, pen=0.2)
+# self.pw.addLine(y=0, pen=0.2)
+# for r in range(2, 20, 2):
+# circle = pg.QtGui.QGraphicsEllipseItem(-r, -r, r * 2, r * 2)
+# circle.setPen(pg.mkPen(0.2))
+# self.pw.addItem(circle)
+#
+# theta = np.linspace(0, 2 * np.pi, 8)
+# radius = np.random.normal(loc=10, size=8)
+#
+# x = radius * np.cos(theta)
+# y = radius * np.sin(theta)
+# self.pw.plot(x, y)
- # 항상 x축 시간을 설정한 범위 ( -3 시간 전 ~ 10 분 후 )만 보여줌.
- # self.pw.setXRange(new_time_data - 3600 * 3, new_time_data + 600, padding=0)
- # self.pw.setYRange(-1, 21, padding=0)
- data = []
- for i in self.plotData['y']:
- i = i / 1000
- data.append(i)
- self.pdi.setData(self.plotData['x'], data)
+class VisibilityView(QChartView):
- # self.pdi.setData([1643873584, 1643873585, 1643873586, 1643873587, 1643873588],
- # [12, 11, 10, 14, 13])
+ def __init__(self, parent: QWidget, maxlen: int):
+ super().__init__(parent)
- def onclick(self, plot, points):
- for point in points:
- print(point.pos())
+ now = QDateTime.currentSecsSinceEpoch()
+ zeros = [(t * 1000, -1) for t in range(now - maxlen * 60, now, 60)]
+ self.data = collections.deque(zeros, maxlen=maxlen)
+ self.setRenderHint(QPainter.Antialiasing)
-class PolarWidget(QWidget):
+ chart = QChart()
+ chart.legend().setVisible(False)
+ self.setChart(chart)
+ self.series = QLineSeries(name='Prevailing Visibility')
+ chart.addSeries(self.series)
- def __init__(self, parent=None):
- QWidget.__init__(self, parent)
+ axis_x = QDateTimeAxis()
+ axis_x.setFormat('hh:mm')
+ axis_x.setTitleText('Time')
+ left = QDateTime.fromMSecsSinceEpoch(self.data[0][0])
+ right = QDateTime.fromMSecsSinceEpoch(self.data[-1][0])
+ axis_x.setRange(left, right)
+ chart.setAxisX(axis_x, self.series)
- self.pw = pg.plot(
- labels={'left': 'Visibility (km)'},
- axisItems={'bottom': TimeAxisItem(orientation='bottom')}
- )
+ axis_y = QValueAxis()
+ axis_y.setRange(0, 20)
+ axis_y.setLabelFormat('%d')
+ axis_y.setTitleText('Distance (km)')
+ chart.setAxisY(axis_y, self.series)
- self.pw.showGrid(x=True, y=True)
- self.pdi = self.pw.plot(pen='w')
+ data_point = [QPointF[t, v] for t, v in self.data]
+ self.series.append(data_point)
- self.pw.addLine(x=0, pen=0.2)
- self.pw.addLine(y=0, pen=0.2)
- for r in range(2, 20, 2):
- circle = pg.QtGui.QGraphicsEllipseItem(-r, -r, r * 2, r * 2)
- circle.setPen(pg.mkPen(0.2))
- self.pw.addItem(circle)
- theta = np.linspace(0, 2 * np.pi, 8)
- radius = np.random.normal(loc=10, size=8)
+class DiscernmentView(QChartView):
- x = radius * np.cos(theta)
- y = radius * np.sin(theta)
- self.pw.plot(x, y)
+ def __init__(self, parent: QWidget):
+ super().__init__(parent)
+ self.setRenderHint(QPainter.Antialiasing)
+ self.setMaximumSize(600, 400)
+
+ chart = QPolarChart(title='Discernment Visibility')
+ chart.legend().setAlignment(Qt.AlignRight)
+ chart.legend().setMarkerShape(QLegend.MarkerShapeCircle)
+ chart.legend().setColor(QColor(255, 255, 255, 255))
+ chart.setTitleBrush(QColor(255, 255, 255, 255))
+ chart.setBackgroundBrush(QColor(0, 0, 0, 255))
+ self.setChart(chart)
+
+ self.positives = QScatterSeries(name='Positive')
+ self.negatives = QScatterSeries(name='Negative')
+ self.positives.setColor(QColor('green'))
+ self.negatives.setColor(QColor('red'))
+ self.positives.setMarkerSize(10)
+ self.negatives.setMarkerSize(10)
+ chart.addSeries(self.positives)
+ chart.addSeries(self.negatives)
+
+ axis_x = QValueAxis()
+ axis_x.setTickCount(9)
+ axis_x.setLabelsColor(QColor(255, 255, 255, 255))
+ axis_x.setRange(0, 360)
+ axis_x.setLabelFormat('%d \xc2\xb0')
+ axis_x.setTitleText('Azimuth (deg)')
+ axis_x.setTitleVisible(False)
+ chart.setAxisX(axis_x, self.positives)
+ chart.setAxisX(axis_x, self.negatives)
+
+ axis_y = QValueAxis()
+ axis_y.setLabelsColor(QColor(255, 255, 255, 255))
+ axis_y.setRange(0, 20)
+ axis_y.setLabelFormat('%d')
+ axis_y.setTitleText('Distance (km)')
+ axis_y.setTitleVisible(False)
+ chart.setAxisY(axis_y, self.positives)
+ chart.setAxisY(axis_y, self.negatives)
+
+ @pyqtSlot(list, list)
+ def refresh_stats(self, positives: list, negatives: list):
+ pos_point = [QPointF(a, d) for a, d in positives]
+ self.positives.replace(pos_point)
+ neg_point = [QPointF(a, d) for a, d in negatives]
+ self.negatives.replace(neg_point)
class ThumbnailView(QMainWindow):
@@ -184,8 +273,8 @@ def __init__(self, q):
"resources/main_window.ui")
uic.loadUi(ui_path, self)
self.showFullScreen()
- self._plot = PlotWidget()
- self._polar = PolarWidget()
+ self._plot = VisibilityView(self, 1440)
+ self._polar = DiscernmentView(self)
self.view = None
self.km_mile_convert = False
self.date = None
@@ -199,17 +288,22 @@ def __init__(self, q):
self.video_horizontalLayout.addWidget(self.front_video_widget)
self.video_horizontalLayout.addWidget(self.rear_video_widget)
- self.scene = QGraphicsScene()
- self.vis_plot.setScene(self.scene)
- self.plotWidget = self._plot.pw
- self.plotWidget.resize(600, 400)
- self.scene.addWidget(self.plotWidget)
-
- self.scene_polar = QGraphicsScene()
- self.polar_plot.setScene(self.scene_polar)
- self.polarWidget = self._polar.pw
- self.polarWidget.resize(600, 400)
- self.scene_polar.addWidget(self.polarWidget)
+ # self.scene = QGraphicsScene()
+ # self.vis_plot.setScene(self.scene)
+ # self.plotWidget = self._plot.pw
+ # self.plotWidget.resize(600, 400)
+ # self.scene.addWidget(self.plotWidget)
+
+ self.graph_horizontalLayout.addWidget(self._plot)
+
+ # self.scene_polar = QGraphicsScene()
+ # self.polar_plot.setScene(self.scene_polar)
+ # self.polarWidget = self._polar.pw
+ # self.polarWidget.resize(600, 400)
+ # self.scene_polar.addWidget(self.polarWidget)
+
+ # self.discernment_widget = DiscernmentView(self)
+ self.graph_horizontalLayout.addWidget(self._polar)
self.setting_button.enterEvent = self.btn_on
self.setting_button.leaveEvent = self.btn_off
diff --git a/src/nd01_settings.py b/src/nd01_settings.py
index 1ce3f5e..5ae3f73 100644
--- a/src/nd01_settings.py
+++ b/src/nd01_settings.py
@@ -12,7 +12,8 @@
import cv2
import pandas as pd
from PyQt5 import uic
-from PyQt5.QtCore import (QPoint, QRect, Qt)
+from PyQt5.QtCore import (QPoint, QRect, Qt,
+ QPointF)
from PyQt5.QtGui import (QPixmap, QPainter, QBrush,
QColor, QPen, QImage,
QIcon)
@@ -20,7 +21,11 @@
QDialog, QAbstractItemView, QVBoxLayout,
QGridLayout, QPushButton, QMessageBox,
QFileDialog)
+from PyQt5.QtChart import (QChartView, QLegend, QLineSeries,
+ QPolarChart, QScatterSeries, QValueAxis,
+ QChart)
from model import JS06Settings
+from efficiency_chart import EfficiencyChart
class ND01SettingWidget(QDialog):
@@ -35,13 +40,19 @@ def __init__(self, *args, **kwargs):
self.begin = QPoint()
self.end = QPoint()
+
self.upper_left = ()
self.lower_right = ()
+ # self.min_xy = ()
+
+ self.target_name = []
self.left_range = []
self.right_range = []
self.distance = []
- self.target_name = []
- self.min_xy = ()
+
+ col = ['datetime', 'camera_direction',
+ 'N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW',
+ 'prevailing_visibility']
self.isDrawing = False
self.draw_flag = False
@@ -66,6 +77,9 @@ def __init__(self, *args, **kwargs):
# self.blank_lbl.mouseMoveEvent = self.lbl_mouseMoveEvent
# self.blank_lbl.mouseReleaseEvent = self.lbl_mouseReleaseEvent
+ self.efficiencyChart = EfficiencyChart(self)
+ self.value_verticalLayout.addWidget(self.efficiencyChart)
+
self.flip_button.clicked.connect(self.camera_flip)
self.flip_button.enterEvent = self.btn_on
self.flip_button.leaveEvent = self.btn_off
@@ -294,6 +308,7 @@ def save_vis(self):
'prevailing_visibility']
result = pd.DataFrame(col)
print(result)
+
# result['datetime'] =
# result['camera_direction'] =
# result['N'] =
@@ -305,8 +320,8 @@ def save_vis(self):
# result['W'] =
# result['NW'] =
# result['prevailing_visibility'] =
- # result.to_csv(f'{JS06Settings.get("data_csv_path")}/{self.current_camera}/{self.current_camera}.csv',
- # index=False)
+ result.to_csv(f'{JS06Settings.get("data_csv_path")}/{self.current_camera}/{self.current_camera}.csv',
+ index=False)
def save_target(self, camera: str):
@@ -333,7 +348,7 @@ def get_target(self):
self.right_range = self.str_to_tuple(target_df["right_range"].tolist())
self.distance = target_df["distance"].tolist()
else:
- QMessageBox.about(self, 'Error', 'no file...')
+ print('no file...')
def accept_click(self):
diff --git a/src/resources/main_window.ui b/src/resources/main_window.ui
index ca94d06..71425ec 100644
--- a/src/resources/main_window.ui
+++ b/src/resources/main_window.ui
@@ -702,28 +702,7 @@ background-color: #1b3146;
-
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
-
+
diff --git a/src/resources/settings.ui b/src/resources/settings.ui
index cd2102e..c76e08b 100644
--- a/src/resources/settings.ui
+++ b/src/resources/settings.ui
@@ -222,22 +222,6 @@ color: #ffffff;
- -
-
-
-
- 0
- 250
-
-
-
- background-color:rgb(25,39,52);
-
-
-
-
-
-
-
diff --git a/src/video_thread_mp.py b/src/video_thread_mp.py
index b58d47b..1d5a0ef 100644
--- a/src/video_thread_mp.py
+++ b/src/video_thread_mp.py
@@ -13,8 +13,6 @@
def producer(q):
- # proc = mp.current_process()
- # print(f'{proc.name} multiprocessing start.')
cap = cv2.VideoCapture("rtsp://admin:sijung5520@192.168.100.100/profile2/media.smp")
while True:
@@ -28,7 +26,12 @@ def producer(q):
os.makedirs(f'{image_save_path}/resize/{date}', exist_ok=True)
_, frame = cap.read()
- cv2.imwrite(f'{image_save_path}/vista/{date}/{epoch}.png', frame)
+ if JS06Settings.get('image_size') == 0:
+ cv2.imwrite(f'{image_save_path}/vista/{date}/{epoch}.png', frame)
+ elif JS06Settings.get('image_size') == 1:
+ frame = cv2.resize(frame, (1920, 840), interpolation=cv2.INTER_LINEAR)
+ cv2.imwrite(f'{image_save_path}/vista/{date}/{epoch}.png', frame)
+ frame = cv2.resize(frame, (315, 131), interpolation=cv2.INTER_NEAREST)
cv2.imwrite(f'{image_save_path}/resize/{date}/{epoch}.jpg', cv2.resize(frame, (315, 131)))
cv2.destroyAllWindows()
From 295d2759ae91c19d82c41d897d34a3cbb3e3d8ba Mon Sep 17 00:00:00 2001
From: Oh-JongJin <5jx2oh@gmail.com>
Date: Mon, 14 Feb 2022 10:17:47 +0900
Subject: [PATCH 04/23] Apply sprint requirements
---
src/controller.py | 22 ++++-----
src/efficiency_chart.py | 1 -
src/model.py | 18 ++++++-
src/nd01.py | 107 ++++++++++++++++++++++++++++------------
src/nd01_settings.py | 1 -
src/video_thread_mp.py | 69 ++++++++++++++------------
6 files changed, 141 insertions(+), 77 deletions(-)
diff --git a/src/controller.py b/src/controller.py
index 2372a69..ad15fe4 100644
--- a/src/controller.py
+++ b/src/controller.py
@@ -1,9 +1,9 @@
#!/usr/bin/env python3
#
-# Copyright 2020-2021 Sijung Co., Ltd.
-#
-# Authors:
-# ruddyscent@gmail.com (Kyungwon Chun)
+# Copyright 2021-2022 Sijung Co., Ltd.
+#
+# Authors:
+# cotjdals5450@gmail.com (Seong Min Chae)
# 5jx2oh@gmail.com (Jongjin Oh)
import json
@@ -20,11 +20,11 @@
from PyQt5.QtGui import QImage
from PyQt5.QtMultimedia import QVideoFrame
-# from .model import (Js08AttrModel, Js08CameraTableModel, Js08IoRunner,
-# Js08Settings, Js08SimpleTarget, Js08Wedge)
+from .model import (Js08AttrModel, , Js08IoRunner,
+ Js08Settings, Js08SimpleTarget, Js08Wedge)
-class MainCtrl(QObject):
+class JS06MainCtrl(QObject):
abnormal_shutdown = pyqtSignal()
front_camera_changed = pyqtSignal(str) # uri
rear_camera_changed = pyqtSignal(str) # uri
@@ -60,10 +60,10 @@ def __init__(self):
self.start_observation_timer()
def init_db(self):
- db_host = Js08Settings.get('db_host')
- db_port = Js08Settings.get('db_port')
- db_name = Js08Settings.get('db_name')
- self._model.connect_to_db(db_host, db_port, db_name)
+ # db_host = Js08Settings.get('db_host')
+ # db_port = Js08Settings.get('db_port')
+ # db_name = Js08Settings.get('db_name')
+ # self._model.connect_to_db(db_host, db_port, db_name)
if getattr(sys, 'frozen', False):
directory = sys._MEIPASS
diff --git a/src/efficiency_chart.py b/src/efficiency_chart.py
index 653c4f7..3b66a9e 100644
--- a/src/efficiency_chart.py
+++ b/src/efficiency_chart.py
@@ -6,7 +6,6 @@
# cotjdals5450@gmail.com (Seong Min Chae)
# 5jx2oh@gmail.com (Jongjin Oh)
-
import os
import cv2
diff --git a/src/model.py b/src/model.py
index 4889d3c..39689d6 100644
--- a/src/model.py
+++ b/src/model.py
@@ -7,7 +7,8 @@
# 5jx2oh@gmail.com (Jongjin Oh)
import os
-from PyQt5.QtCore import QSettings, QStandardPaths
+from PyQt5.QtCore import (QSettings, QStandardPaths, QRect)
+from PyQt5.QtGui import (QImage)
class JS06Settings:
@@ -39,3 +40,18 @@ def get(cls, key):
def restore_defaults(cls):
for key, value in cls.defaults.items():
cls.set(key, value)
+
+
+class JS06SimpleTarget:
+
+ def __init__(self,
+ label: str, wedge: str, azimuth: float,
+ distance: float, roi: QRect, mask: QImage,
+ input_width: int, input_height: int):
+ super().__init__()
+ self.label = label
+ self.wedge = wedge
+ self.azimuth = azimuth
+ self.distance = distance
+ self.roi = roi
+
diff --git a/src/nd01.py b/src/nd01.py
index 695c092..0ca6dc2 100644
--- a/src/nd01.py
+++ b/src/nd01.py
@@ -5,6 +5,7 @@
# Authors:
# cotjdals5450@gmail.com (Seong Min Chae)
# 5jx2oh@gmail.com (Jongjin Oh)
+
import collections
import sys
import os
@@ -167,6 +168,8 @@ class VisibilityView(QChartView):
def __init__(self, parent: QWidget, maxlen: int):
super().__init__(parent)
+ self.setMinimumSize(200, 200)
+ self.setMaximumSize(600, 400)
now = QDateTime.currentSecsSinceEpoch()
zeros = [(t * 1000, -1) for t in range(now - maxlen * 60, now, 60)]
@@ -176,6 +179,9 @@ def __init__(self, parent: QWidget, maxlen: int):
chart = QChart()
chart.legend().setVisible(False)
+ # chart.legend().setColor(QColor(255, 255, 255, 255))
+ # chart.setTitleBrush(QColor(255, 255, 255, 255))
+ # chart.setBackgroundBrush(QColor(0, 0, 0, 255))
self.setChart(chart)
self.series = QLineSeries(name='Prevailing Visibility')
chart.addSeries(self.series)
@@ -183,19 +189,42 @@ def __init__(self, parent: QWidget, maxlen: int):
axis_x = QDateTimeAxis()
axis_x.setFormat('hh:mm')
axis_x.setTitleText('Time')
+ # axis_x.setLabelsColor(QColor(255, 255, 255, 255))
left = QDateTime.fromMSecsSinceEpoch(self.data[0][0])
right = QDateTime.fromMSecsSinceEpoch(self.data[-1][0])
axis_x.setRange(left, right)
chart.setAxisX(axis_x, self.series)
+ print(left, right)
axis_y = QValueAxis()
axis_y.setRange(0, 20)
+ # axis_y.setLabelsColor(QColor(255, 255, 255, 255))
axis_y.setLabelFormat('%d')
axis_y.setTitleText('Distance (km)')
chart.setAxisY(axis_y, self.series)
+ # data_point = [QPointF[t, v] for t, v in self.data]
+ # self.series.append(data_point)
+
+ @pyqtSlot(int, dict)
+ def refresh_stats(self, epoch: int, wedge_vis: dict):
+ wedge_vis_list = list(wedge_vis.values())
+ prev_vis = self.prevailing_visibility(wedge_vis_list)
+ self.data.append((epoch * 1000, prev_vis))
+
+ left = QDateTime.fromMSecsSinceEpoch(self.data[0][0])
+ right = QDateTime.fromMSecsSinceEpoch(self.data[-1][0])
+ self.chart().axisX().setRange(left, right)
+
data_point = [QPointF[t, v] for t, v in self.data]
- self.series.append(data_point)
+ self.series.replace(data_point)
+
+ def prevailing_visibility(self, wedge_vis: list) -> float:
+ if None in wedge_vis:
+ return 0
+ sorted_vis = sorted(wedge_vis, reverse=True)
+ prevailing = sorted_vis[(len(sorted_vis) - 1) // 2]
+ return prevailing
class DiscernmentView(QChartView):
@@ -203,14 +232,15 @@ class DiscernmentView(QChartView):
def __init__(self, parent: QWidget):
super().__init__(parent)
self.setRenderHint(QPainter.Antialiasing)
+ self.setMinimumSize(200, 200)
self.setMaximumSize(600, 400)
chart = QPolarChart(title='Discernment Visibility')
chart.legend().setAlignment(Qt.AlignRight)
chart.legend().setMarkerShape(QLegend.MarkerShapeCircle)
- chart.legend().setColor(QColor(255, 255, 255, 255))
- chart.setTitleBrush(QColor(255, 255, 255, 255))
- chart.setBackgroundBrush(QColor(0, 0, 0, 255))
+ # chart.legend().setColor(QColor(255, 255, 255, 255))
+ # chart.setTitleBrush(QColor(255, 255, 255, 255))
+ # chart.setBackgroundBrush(QColor(0, 0, 0, 255))
self.setChart(chart)
self.positives = QScatterSeries(name='Positive')
@@ -224,7 +254,7 @@ def __init__(self, parent: QWidget):
axis_x = QValueAxis()
axis_x.setTickCount(9)
- axis_x.setLabelsColor(QColor(255, 255, 255, 255))
+ # axis_x.setLabelsColor(QColor(255, 255, 255, 255))
axis_x.setRange(0, 360)
axis_x.setLabelFormat('%d \xc2\xb0')
axis_x.setTitleText('Azimuth (deg)')
@@ -233,9 +263,9 @@ def __init__(self, parent: QWidget):
chart.setAxisX(axis_x, self.negatives)
axis_y = QValueAxis()
- axis_y.setLabelsColor(QColor(255, 255, 255, 255))
+ # axis_y.setLabelsColor(QColor(255, 255, 255, 255))
axis_y.setRange(0, 20)
- axis_y.setLabelFormat('%d')
+ axis_y.setLabelFormat('%d km')
axis_y.setTitleText('Distance (km)')
axis_y.setTitleVisible(False)
chart.setAxisY(axis_y, self.positives)
@@ -279,6 +309,9 @@ def __init__(self, q):
self.km_mile_convert = False
self.date = None
+ # self.front_video_widget.media_player.pause()
+ # self.rear_video_widget.media_player.pause()
+
self.front_video_widget = VideoWidget(self)
self.front_video_widget.on_camera_change("rtsp://admin:sijung5520@192.168.100.101/profile2/media.smp")
@@ -330,6 +363,12 @@ def __init__(self, q):
self.show()
+ def front_camera_pause(self, event):
+ self.front_video_widget.media_player.pause()
+
+ def rear_camera_pause(self, event):
+ self.rear_video_widget.media_player.pause()
+
def alert_test(self):
self.alert.setIcon(QIcon('resources/asset/red.png'))
@@ -455,34 +494,34 @@ def clock(self, data):
current_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(float(data)))
self.date = current_time[2:4] + current_time[5:7]
self.real_time_label.setText(current_time)
- self._plot.update_plot(int(float(data)))
-
- result = 0
- for i in self._plot.plotData['y']:
- result += i
- p_vis_km = f'{format(round(int(result / len(self._plot.plotData["y"])), 2), ",")}'
- p_vis_nm = f'{format(round(int(result / len(self._plot.plotData["y"])) / 1609, 2), ",")}'
-
- if self.km_mile_convert:
- self.c_vis_label.setText(f'{format(round(self._plot.plotData["y"][-1] / 1609, 2), ",")} mile')
- self.p_vis_label.setText(f'{p_vis_nm} mile')
-
- elif self.km_mile_convert is False:
- self.c_vis_label.setText(f'{format(self._plot.plotData["y"][-1], ",")} m')
- self.p_vis_label.setText(f'{p_vis_km} m')
-
- data_time = self._plot.plotData['x']
- if int(float(data)) - 3600 * 3 in self._plot.plotData['x']:
- index = data_time.index(int(float(data)) - 3600 * 3)
- self._plot.plotData['x'].pop(index)
- self._plot.plotData['y'].pop(index)
-
+ # self._plot.update_plot(int(float(data)))
+
+ # result = 0
+ # for i in self._plot.plotData['y']:
+ # result += i
+ # p_vis_km = f'{format(round(int(result / len(self._plot.plotData["y"])), 2), ",")}'
+ # p_vis_nm = f'{format(round(int(result / len(self._plot.plotData["y"])) / 1609, 2), ",")}'
+
+ # if self.km_mile_convert:
+ # self.c_vis_label.setText(f'{format(round(self._plot.plotData["y"][-1] / 1609, 2), ",")} mile')
+ # self.p_vis_label.setText(f'{p_vis_nm} mile')
+ #
+ # elif self.km_mile_convert is False:
+ # self.c_vis_label.setText(f'{format(self._plot.plotData["y"][-1], ",")} m')
+ # self.p_vis_label.setText(f'{p_vis_km} m')
+ #
+ # data_time = self._plot.plotData['x']
+ # if int(float(data)) - 3600 * 3 in self._plot.plotData['x']:
+ # index = data_time.index(int(float(data)) - 3600 * 3)
+ # self._plot.plotData['x'].pop(index)
+ # self._plot.plotData['y'].pop(index)
+ #
self.thumbnail_refresh()
if current_time[-2:] == "00":
self.thumbnail_refresh()
-
- if int(p_vis_km.replace(',', '')) <= JS06Settings.get('visibility_alert_limit'):
- self.alert.setIcon(QIcon('resources/asset/red.png'))
+ #
+ # if int(p_vis_km.replace(',', '')) <= JS06Settings.get('visibility_alert_limit'):
+ # self.alert.setIcon(QIcon('resources/asset/red.png'))
def thumbnail_refresh(self):
one_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 3600))
@@ -556,6 +595,10 @@ def on_camera_change(self, uri: str):
else:
pass
+ def mouseDoubleClickEvent(self, event):
+ self.media_player.pause()
+ print('Mouse double click')
+
class MainCtrl(QObject):
front_camera_changed = pyqtSignal(str)
diff --git a/src/nd01_settings.py b/src/nd01_settings.py
index 5ae3f73..69d19b0 100644
--- a/src/nd01_settings.py
+++ b/src/nd01_settings.py
@@ -6,7 +6,6 @@
# cotjdals5450@gmail.com (Seong Min Chae)
# 5jx2oh@gmail.com (Jongjin Oh)
-
import os
import cv2
diff --git a/src/video_thread_mp.py b/src/video_thread_mp.py
index 1d5a0ef..a7621e4 100644
--- a/src/video_thread_mp.py
+++ b/src/video_thread_mp.py
@@ -1,7 +1,15 @@
-# !/usr/bin/env python3
+#!/usr/bin/env python3
+#
+# Copyright 2021-2022 Sijung Co., Ltd.
+#
+# Authors:
+# cotjdals5450@gmail.com (Seong Min Chae)
+# 5jx2oh@gmail.com (Jongjin Oh)
+
import os
import cv2
import time
+import traceback
import numpy as np
import pandas as pd
import multiprocessing as mp
@@ -15,36 +23,35 @@
def producer(q):
cap = cv2.VideoCapture("rtsp://admin:sijung5520@192.168.100.100/profile2/media.smp")
- while True:
- epoch = time.strftime("%Y%m%d%H%M%S", time.localtime(time.time()))
- date = epoch[2:6]
-
- if epoch[-2:] == "00":
- try:
- image_save_path = JS06Settings.get('image_save_path')
- os.makedirs(f'{image_save_path}/vista/{date}', exist_ok=True)
- os.makedirs(f'{image_save_path}/resize/{date}', exist_ok=True)
-
- _, frame = cap.read()
- if JS06Settings.get('image_size') == 0:
- cv2.imwrite(f'{image_save_path}/vista/{date}/{epoch}.png', frame)
- elif JS06Settings.get('image_size') == 1:
- frame = cv2.resize(frame, (1920, 840), interpolation=cv2.INTER_LINEAR)
- cv2.imwrite(f'{image_save_path}/vista/{date}/{epoch}.png', frame)
- frame = cv2.resize(frame, (315, 131), interpolation=cv2.INTER_NEAREST)
- cv2.imwrite(f'{image_save_path}/resize/{date}/{epoch}.jpg', cv2.resize(frame, (315, 131)))
- cv2.destroyAllWindows()
-
- # left_range, right_range, distance = get_target("PNM_9030V")
- # visibility = minprint(epoch[:-2], left_range, right_range, distance, frame)
- #
- # q.put(visibility)
- time.sleep(1)
- except Exception as e:
- print(e)
- cap.release()
- cap = cv2.VideoCapture("rtsp://admin:sijung5520@192.168.100.100/profile2/media.smp")
- continue
+ if cap.isOpened():
+ while True:
+ epoch = time.strftime("%Y%m%d%H%M%S", time.localtime(time.time()))
+ date = epoch[2:6]
+
+ if epoch[-2:] == "00":
+ try:
+ image_save_path = JS06Settings.get('image_save_path')
+ os.makedirs(f'{image_save_path}/vista/{date}', exist_ok=True)
+ os.makedirs(f'{image_save_path}/resize/{date}', exist_ok=True)
+
+ ret, frame = cap.read()
+ if ret:
+ if JS06Settings.get('image_size') == 0:
+ cv2.imwrite(f'{image_save_path}/vista/{date}/{epoch}.png', frame)
+ elif JS06Settings.get('image_size') == 1:
+ frame = cv2.resize(frame, (1920, 840), interpolation=cv2.INTER_LINEAR)
+ cv2.imwrite(f'{image_save_path}/vista/{date}/{epoch}.png', frame)
+ frame = cv2.resize(frame, (315, 131), interpolation=cv2.INTER_NEAREST)
+ cv2.imwrite(f'{image_save_path}/resize/{date}/{epoch}.jpg', cv2.resize(frame, (315, 131)))
+ time.sleep(1)
+ cap.release()
+
+ except:
+ print(traceback.format_exc())
+ cap.release()
+ cap = cv2.VideoCapture("rtsp://admin:sijung5520@192.168.100.100/profile2/media.smp")
+ # continue
+ cv2.destroyAllWindows()
def minprint(epoch, left_range, right_range, distance, cv_img):
From ace006f9dc04cc404d3894102a90d5e876705be8 Mon Sep 17 00:00:00 2001
From: Oh-JongJin <5jx2oh@gmail.com>
Date: Mon, 14 Feb 2022 14:46:21 +0900
Subject: [PATCH 05/23] Fix a part of save csv file in save_target function
---
src/calculation/curve_save.py | 124 -----------------------------
src/calculation/gary_visibility.py | 106 ------------------------
src/nd01_settings.py | 71 ++++++++---------
3 files changed, 34 insertions(+), 267 deletions(-)
delete mode 100644 src/calculation/curve_save.py
delete mode 100644 src/calculation/gary_visibility.py
diff --git a/src/calculation/curve_save.py b/src/calculation/curve_save.py
deleted file mode 100644
index 62bf6dd..0000000
--- a/src/calculation/curve_save.py
+++ /dev/null
@@ -1,124 +0,0 @@
-import itertools
-import os
-
-import numpy as np
-import pandas as pd
-
-import scipy
-from scipy.optimize import curve_fit
-from PyQt5 import QtWidgets, QtGui, QtCore
-
-curved_flag = True
-# cam_name = cam_name
-hanhwa_dist = []
-hanhwa_x = []
-hanhwa_r = []
-hanhwa_g = []
-hanhwa_b = []
-# epoch = epoch
-# rgbsavedir = os.path.join(f"rgb/{cam_name}")
-# extsavedir = os.path.join(f"extinction/{cam_name}")
-
-def select_max_rgb(r, g, b):
-
- select_color = ""
- c_list = [r, g, b]
-
- c_index = c_list.index(max(c_list))
-
- if c_index == 0:
- select_color = "red"
- elif c_index == 1:
- select_color = "green"
- else :
- select_color = "blue"
- return select_color
-
-def cal_curve(hanhwa: pd.DataFrame):
- # hanhwa = pd.read_csv(f"{rgbsavedir}/{epoch}.csv")
- print(hanhwa)
- hanhwa = hanhwa.sort_values(by=['distance'])
- hanhwa_dist = hanhwa[['distance']].squeeze().to_numpy()
- hanhwa_x = np.linspace(hanhwa_dist[0], hanhwa_dist[-1], 100, endpoint=True)
- hanhwa_x.sort()
- hanhwa_r = hanhwa[['r']].squeeze().to_numpy()
- hanhwa_g = hanhwa[['g']].squeeze().to_numpy()
- hanhwa_b = hanhwa[['b']].squeeze().to_numpy()
-
- r1_init = hanhwa_r[0] * 0.7
- g1_init = hanhwa_g[0] * 0.7
- b1_init = hanhwa_b[0] * 0.7
-
- r2_init = hanhwa_r[-1] * 1.3
- g2_init = hanhwa_g[-1] * 1.3
- b2_init = hanhwa_b[-1] * 1.3
-
- select_color = select_max_rgb(r2_init, g2_init, b2_init)
-
- r_ext_init = [r1_init, r2_init, 1]
- g_ext_init = [g1_init, g2_init, 1]
- b_ext_init = [b1_init, b2_init, 1]
-
- try:
-
- hanhwa_opt_r, hanhwa_cov_r = curve_fit(func, hanhwa_dist, hanhwa_r, p0=r_ext_init, maxfev=5000)
- hanhwa_opt_g, hanhwa_cov_g = curve_fit(func, hanhwa_dist, hanhwa_g, p0=g_ext_init, maxfev=5000)
- hanhwa_opt_b, hanhwa_cov_b = curve_fit(func, hanhwa_dist, hanhwa_b, p0=b_ext_init, maxfev=5000)
-
- except Exception as e:
- print("error msg: ", e)
- return
- list1 = []
- list2 = []
- list3 = []
-
- list1.append(hanhwa_opt_r[0])
- list1.append(hanhwa_opt_g[0])
- list1.append(hanhwa_opt_b[0])
-
- list2.append(hanhwa_opt_r[1])
- list2.append(hanhwa_opt_g[1])
- list2.append(hanhwa_opt_b[1])
-
- list3.append(hanhwa_opt_r[2])
- list3.append(hanhwa_opt_g[2])
- list3.append(hanhwa_opt_b[2])
-
- hanhwa_err_r = np.sqrt(np.diag(hanhwa_cov_r))
- hanhwa_err_g = np.sqrt(np.diag(hanhwa_cov_g))
- hanhwa_err_b = np.sqrt(np.diag(hanhwa_cov_b))
-
- print_result(hanhwa_opt_r, hanhwa_opt_g, hanhwa_opt_b, hanhwa_err_r, hanhwa_err_g, hanhwa_err_b)
-
- print(f"Red channel: {extcoeff_to_vis(hanhwa_opt_r[2], hanhwa_err_r[2], 3)} km")
- print(f"Green channel: {extcoeff_to_vis(hanhwa_opt_g[2], hanhwa_err_g[2], 3)} km")
- print(f"Blue channel: {extcoeff_to_vis(hanhwa_opt_b[2], hanhwa_err_b[2], 3)} km")
-
- return list1, list2, list3, select_color
- # update_extinc_signal.emit(list1, list2, list3, select_color)
-
- try:
- os.mkdir(extsavedir)
- except Exception as e:
- pass
-
-# @staticmethod
-def func(x, c1, c2, a):
- return c2 + (c1 - c2) * np.exp(-a * x)
-
-def print_result(opt_r, opt_g, opt_b, err_r, err_g, err_b):
- print(f"Red channel: (",
- f"C1: {opt_r[0]:.2f} ± {err_r[0]:.2f}, ",
- f"C2: {opt_r[1]:.2f} ± {err_r[1]:.2f}, ",
- f"alpha: {opt_r[2]:.2f} ± {err_r[2]:.2f})")
- print(f"Green channel: (",
- f"C1: {opt_g[0]:.2f} ± {err_g[0]:.2f}, ",
- f"C2: {opt_g[1]:.2f} ± {err_g[1]:.2f}, ",
- f"alpha: {opt_g[2]:.2f} ± {err_g[2]:.2f})")
- print(f"Blue channel: (",
- f"C1: {opt_b[0]:.2f} ± {err_b[0]:.2f}, ",
- f"C2: {opt_b[1]:.2f} ± {err_b[1]:.2f}, ",
- f"alpha: {opt_b[2]:.2f} ± {err_b[2]:.2f})")
-
-def extcoeff_to_vis(optimal, error, coeff=3.291):
- return coeff / (optimal + np.array((1, 0, -1)) * error)
diff --git a/src/calculation/gary_visibility.py b/src/calculation/gary_visibility.py
deleted file mode 100644
index 113eebf..0000000
--- a/src/calculation/gary_visibility.py
+++ /dev/null
@@ -1,106 +0,0 @@
-import itertools
-import os
-
-import numpy as np
-import pandas as pd
-
-import scipy
-from scipy.optimize import curve_fit
-import matplotlib
-import matplotlib.pyplot as plt
-
-
-def minprint(self):
- """지정한 구역들에서 소산계수 산출용 픽셀을 출력하는 함수"""
- result = ()
- cnt = 1
- min_x = []
- min_y = []
-
- for upper_left, lower_right in zip(left_range, right_range):
- result = minrgb(upper_left, lower_right)
- print(f"target{cnt}의 소산계수 검출용 픽셀위치 = ", result)
- min_x.append(result[0])
- min_y.append(result[1])
- cnt += 1
-
- get_rgb(epoch)
-
- curved_thread = CurvedThread(camera_name, epoch)
- curved_thread.update_extinc_signal.connect(extinc_print)
- curved_thread.run()
-
- list_test()
-
- graph_dir = os.path.join(f"extinction/{camera_name}")
-def minrgb(self, upper_left, lower_right):
- """드래그한 영역의 RGB 최솟값을 추출한다"""
-
- up_y = min(upper_left[1], lower_right[1])
- down_y = max(upper_left[1], lower_right[1])
-
- left_x = min(upper_left[0], lower_right[0])
- right_x = max(upper_left[0], lower_right[0])
-
- test = cp_image[up_y:down_y, left_x:right_x, :]
-
- # 드래그한 영역의 RGB 값을 각각 추출한다.
- # r = test[:, :]
- # g = test[:, :, 1]
- # b = test[:, :, 2]
-
- # RGB값을 각 위치별로 모두 더한다.
- # RGB 최댓값이 255로 정해져있어 값을 초과하면 0부터 시작된다. numpy의 clip 함수를 이용해 array의 최댓값을 수정한다.
- # r = np.clip(r, 0, 765)
- # sum_rgb = r + g + b
-
- # RGB 값을 합한 뒤 가장 최솟값의 index를 추출한다.
- t_idx = np.where(test == np.min(test))
-
- show_min_y = t_idx[0][0] + up_y
- show_min_x = t_idx[1][0] + left_x
-
- return (show_min_x, show_min_y)
-
-def get_rgb(self, epoch: str):
- r_list = []
- g_list = []
- b_list = []
-
- for x, y in zip(min_x, min_y):
-
- r_list.append(cp_image[y, x, 0])
- g_list.append(cp_image[y, x, 1])
- b_list.append(cp_image[y, x, 2])
-
- print("red : ", cp_image[y, x, 0])
- print("green : ", cp_image[y, x, 1])
- print("blue: ", cp_image[y, x, 2])
-
-
- save_rgb(r_list, g_list, b_list, epoch)
-
-def save_rgb(self, r_list, g_list, b_list, epoch):
- """Save the rgb information for each target."""
- try:
- save_path = os.path.join(f"rgb/{camera_name}")
- os.mkdir(save_path)
-
- except Exception as e:
- pass
-
- if r_list:
- r_list = list(map(int, r_list))
- g_list = list(map(int, g_list))
- b_list = list(map(int, b_list))
- print(b_list)
-
- col = ["target_name", "r", "g", "b", "distance"]
- result = pd.DataFrame(columns=col)
- result["target_name"] = target_name
- result["r"] = r_list
- result["g"] = g_list
- result["b"] = b_list
- result["distance"] = distance
- print(result.head(10))
- result.to_csv(f"{save_path}/{epoch}.csv", mode="w", index=False)
\ No newline at end of file
diff --git a/src/nd01_settings.py b/src/nd01_settings.py
index 69d19b0..f0828b6 100644
--- a/src/nd01_settings.py
+++ b/src/nd01_settings.py
@@ -49,10 +49,6 @@ def __init__(self, *args, **kwargs):
self.right_range = []
self.distance = []
- col = ['datetime', 'camera_direction',
- 'N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW',
- 'prevailing_visibility']
-
self.isDrawing = False
self.draw_flag = False
self.cam_flag = False
@@ -103,8 +99,6 @@ def __init__(self, *args, **kwargs):
self.image_label.mouseMoveEvent = self.lbl_mouseMoveEvent
self.image_label.mouseReleaseEvent = self.lbl_mouseReleaseEvent
- self.get_target()
-
self.buttonBox.accepted.connect(self.accept_click)
self.buttonBox.rejected.connect(self.reject)
@@ -122,22 +116,23 @@ def btn_off(self, e):
self.flip_button.setIcon(QIcon('resources/asset/flip_off.png'))
def image_load(self):
- # self.image_label.update()
+ self.left_range = None
+ self.right_range = None
if self.cam_flag:
src = "rtsp://admin:sijung5520@192.168.100.100/profile2/media.smp"
self.target_setting_label.setText(' Rear Target Setting')
self.current_camera = 'PNM_9030V'
- self.get_target()
+ self.get_target(self.current_camera)
else:
src = "rtsp://admin:sijung5520@192.168.100.101/profile2/media.smp"
self.target_setting_label.setText(' Front Target Setting')
self.current_camera = 'PNM_9022V'
- self.get_target()
+ self.get_target(self.current_camera)
try:
- print(f'현재 카메라 {self.current_camera}')
+ print(f'Current camera - {self.current_camera.replace("_", " ")}')
os.makedirs(f'{JS06Settings.get("target_csv_path")}/{self.current_camera}', exist_ok=True)
cap = cv2.VideoCapture(src)
@@ -175,15 +170,17 @@ def lbl_paintEvent(self, event):
painter.drawPixmap(QRect(0, 0, self.image_label.width(),
self.image_label.height()), bk_image)
- for corner1, corner2, in zip(self.left_range, self.right_range):
- br = QBrush(QColor(100, 10, 10, 40))
- painter.setBrush(br)
- painter.setPen(QPen(Qt.red, 2, Qt.SolidLine))
- corner1_1 = int(corner1[0] / self.video_width * self.image_label.width())
- corner1_2 = int(corner1[1] / self.video_height * self.image_label.height())
- corner2_1 = int((corner2[0] - corner1[0]) / self.video_width * self.image_label.width())
- corner2_2 = int((corner2[1] - corner1[1]) / self.video_height * self.image_label.height())
- painter.drawRect(QRect(corner1_1, corner1_2, corner2_1, corner2_2))
+ if self.left_range:
+ for corner1, corner2, in zip(self.left_range, self.right_range):
+ br = QBrush(QColor(100, 10, 10, 40))
+ painter.setBrush(br)
+ painter.setPen(QPen(Qt.red, 2, Qt.SolidLine))
+ corner1_1 = int(corner1[0] / self.video_width * self.image_label.width())
+ corner1_2 = int(corner1[1] / self.video_height * self.image_label.height())
+ corner2_1 = int((corner2[0] - corner1[0]) / self.video_width * self.image_label.width())
+ corner2_2 = int((corner2[1] - corner1[1]) / self.video_height * self.image_label.height())
+ self.target = QRect(corner1_1, corner1_2, corner2_1, corner2_2)
+ painter.drawRect(self.target)
if self.isDrawing:
br = QBrush(QColor(100, 10, 10, 40))
@@ -324,30 +321,30 @@ def save_vis(self):
def save_target(self, camera: str):
- print(f'타겟을 저장합니다 - {camera}')
- if self.left_range:
+ file = f'{JS06Settings.get("target_csv_path")}/{camera}/{camera}.csv'
+ if self.left_range and os.path.isfile(file):
col = ['target_name', 'left_range', 'right_range', 'distance']
result = pd.DataFrame(columns=col)
result['target_name'] = self.target_name
result['left_range'] = self.left_range
result['right_range'] = self.right_range
result['distance'] = self.distance
- result.to_csv(f'{JS06Settings.get("target_csv_path")}/{camera}/{camera}.csv',
- mode='w', index=False)
- print(f'{JS06Settings.get("target_csv_path")}/{camera}/{camera}.csv - SAVED!!!!')
-
- def get_target(self):
- print("타겟을 불러옵니다.")
-
- save_path = os.path.join(f'{JS06Settings.get("target_csv_path")}/{self.current_camera}')
- if os.path.isfile(f'{save_path}/{self.current_camera}.csv'):
- target_df = pd.read_csv(f'{save_path}/{self.current_camera}.csv')
- self.target_name = target_df["target_name"].tolist()
- self.left_range = self.str_to_tuple(target_df["left_range"].tolist())
- self.right_range = self.str_to_tuple(target_df["right_range"].tolist())
- self.distance = target_df["distance"].tolist()
- else:
- print('no file...')
+ result.to_csv(file, mode='w', index=False)
+ print(f'[{camera}.csv SAVED]')
+
+ def get_target(self, camera: str):
+
+ save_path = os.path.join(f'{JS06Settings.get("target_csv_path")}/{camera}')
+
+ if os.path.isfile(f'{save_path}/{camera}.csv') is False:
+ makeFile = pd.DataFrame(columns=['target_name', 'left_range', 'right_range', 'distance'])
+ makeFile.to_csv(f'{save_path}/{camera}.csv', mode='w', index=False)
+
+ target_df = pd.read_csv(f'{save_path}/{camera}.csv')
+ self.target_name = target_df["target_name"].tolist()
+ self.left_range = self.str_to_tuple(target_df["left_range"].tolist())
+ self.right_range = self.str_to_tuple(target_df["right_range"].tolist())
+ self.distance = target_df["distance"].tolist()
def accept_click(self):
From 734ef93d0706ca458827033e0242094e0b3cefce Mon Sep 17 00:00:00 2001
From: Oh-JongJin <5jx2oh@gmail.com>
Date: Tue, 15 Feb 2022 09:18:09 +0900
Subject: [PATCH 06/23] Modify target paintEvent when camera direction flip
---
src/nd01_settings.py | 2 +-
src/resources/settings.ui | 3 +++
src/video_thread_mp.py | 2 +-
3 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/src/nd01_settings.py b/src/nd01_settings.py
index f0828b6..1c7d872 100644
--- a/src/nd01_settings.py
+++ b/src/nd01_settings.py
@@ -170,7 +170,7 @@ def lbl_paintEvent(self, event):
painter.drawPixmap(QRect(0, 0, self.image_label.width(),
self.image_label.height()), bk_image)
- if self.left_range:
+ if self.left_range and self.right_range:
for corner1, corner2, in zip(self.left_range, self.right_range):
br = QBrush(QColor(100, 10, 10, 40))
painter.setBrush(br)
diff --git a/src/resources/settings.ui b/src/resources/settings.ui
index c76e08b..99d30c4 100644
--- a/src/resources/settings.ui
+++ b/src/resources/settings.ui
@@ -95,6 +95,9 @@ color: #ffffff;
15
+
+ PointingHandCursor
+
border:0px;
background-color: #1b3146;
diff --git a/src/video_thread_mp.py b/src/video_thread_mp.py
index a7621e4..5f093ea 100644
--- a/src/video_thread_mp.py
+++ b/src/video_thread_mp.py
@@ -100,7 +100,7 @@ def minrgb(upper_left, lower_right, cp_image):
show_min_y = t_idx[0][0] + up_y
show_min_x = t_idx[1][0] + left_x
- return (show_min_x, show_min_y)
+ return show_min_x, show_min_y
def get_rgb(epoch: str, min_x, min_y, cp_image, distance):
From 072d170cc0503b6b7b260746a9eff2589cabfa8c Mon Sep 17 00:00:00 2001
From: Oh-JongJin <5jx2oh@gmail.com>
Date: Tue, 15 Feb 2022 14:19:57 +0900
Subject: [PATCH 07/23] Rearrange image capture path
---
requirements.txt | 3 +-
src/controller.py | 8 +--
src/nd01.py | 68 ++++++++++++++++----------
src/nd01_settings.py | 1 +
src/resources/main_window.ui | 12 ++---
src/resources/settings.ui | 86 ++++++++++++++++++++++++++++-----
src/resources/thumbnail_view.ui | 2 +-
src/video_thread_mp.py | 35 +++++++++++---
8 files changed, 158 insertions(+), 57 deletions(-)
diff --git a/requirements.txt b/requirements.txt
index 50f7a13..762c930 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,4 +4,5 @@ PyQtGraph
pyechart
influxdb
opencv-python
-pandas
\ No newline at end of file
+pandas
+psutil
\ No newline at end of file
diff --git a/src/controller.py b/src/controller.py
index ad15fe4..e9325aa 100644
--- a/src/controller.py
+++ b/src/controller.py
@@ -1,9 +1,9 @@
#!/usr/bin/env python3
#
-# Copyright 2021-2022 Sijung Co., Ltd.
+# Copyright 2020-2021 Sijung Co., Ltd.
#
# Authors:
-# cotjdals5450@gmail.com (Seong Min Chae)
+# ruddyscent@gmail.com (Kyungwon Chun)
# 5jx2oh@gmail.com (Jongjin Oh)
import json
@@ -20,8 +20,8 @@
from PyQt5.QtGui import QImage
from PyQt5.QtMultimedia import QVideoFrame
-from .model import (Js08AttrModel, , Js08IoRunner,
- Js08Settings, Js08SimpleTarget, Js08Wedge)
+from model import (Js08AttrModel, Js08CameraTableModel, Js08IoRunner,
+ Js08Settings, Js08SimpleTarget, Js08Wedge)
class JS06MainCtrl(QObject):
diff --git a/src/nd01.py b/src/nd01.py
index 0ca6dc2..8adb686 100644
--- a/src/nd01.py
+++ b/src/nd01.py
@@ -194,7 +194,6 @@ def __init__(self, parent: QWidget, maxlen: int):
right = QDateTime.fromMSecsSinceEpoch(self.data[-1][0])
axis_x.setRange(left, right)
chart.setAxisX(axis_x, self.series)
- print(left, right)
axis_y = QValueAxis()
axis_y.setRange(0, 20)
@@ -281,17 +280,24 @@ def refresh_stats(self, positives: list, negatives: list):
class ThumbnailView(QMainWindow):
- def __init__(self, image_file_name: str, date: int):
+ def __init__(self, image_file_name: str, year: int, date: int):
super().__init__()
ui_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
- "ui/thumbnail_view.ui")
+ "resources/thumbnail_view.ui")
uic.loadUi(ui_path, self)
+ print(f'{JS06Settings.get("image_save_path")}/{year}/vista/{date}/{image_file_name}.png')
- self.front_image.setPixmap(QPixmap(f'D:/ND-01/vista/{date}/{image_file_name}.png')
- .scaled(self.front_image.width(), self.front_image.height()))
- self.rear_image.setPixmap(QPixmap(f'D:/ND-01/vista/{date}/{image_file_name}.png')
- .scaled(self.rear_image.width(), self.rear_image.height()))
+ self.front_image.setPixmap(
+ QPixmap(
+ f'{JS06Settings.get("image_save_path")}/{year}/vista/{date}/{image_file_name}.png').scaled(
+ self.front_image.width(),
+ self.front_image.height()))
+ self.rear_image.setPixmap(
+ QPixmap(
+ f'{JS06Settings.get("image_save_path")}/{year}/vista/{date}/{image_file_name}.png').scaled(
+ self.rear_image.width(),
+ self.rear_image.height()))
class ND01MainWindow(QMainWindow):
@@ -307,6 +313,7 @@ def __init__(self, q):
self._polar = DiscernmentView(self)
self.view = None
self.km_mile_convert = False
+ self.year = None
self.date = None
# self.front_video_widget.media_player.pause()
@@ -381,7 +388,7 @@ def reset_StyleSheet(self):
self.label_6hour.setStyleSheet('')
def thumbnail_view(self, file_name: str):
- self.view = ThumbnailView(file_name, self.date)
+ self.view = ThumbnailView(file_name, self.year, self.date)
self.view.setGeometry(QRect(self.video_horizontalLayout.geometry().x(),
self.video_horizontalLayout.geometry().y() + 21,
self.video_horizontalLayout.geometry().width(),
@@ -492,8 +499,10 @@ def unit_convert(self, event):
@pyqtSlot(str)
def clock(self, data):
current_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(float(data)))
- self.date = current_time[2:4] + current_time[5:7]
+ self.year = current_time[:4]
+ self.date = current_time[5:7] + current_time[8:10]
self.real_time_label.setText(current_time)
+
# self._plot.update_plot(int(float(data)))
# result = 0
@@ -524,12 +533,19 @@ def clock(self, data):
# self.alert.setIcon(QIcon('resources/asset/red.png'))
def thumbnail_refresh(self):
- one_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 3600))
- two_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 3600 * 2))
- three_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 3600 * 3))
- four_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 3600 * 4))
- five_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 3600 * 5))
- six_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 3600 * 6))
+ # one_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 3600))
+ # two_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 3600 * 2))
+ # three_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 3600 * 3))
+ # four_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 3600 * 4))
+ # five_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 3600 * 5))
+ # six_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 3600 * 6))
+
+ one_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 60))
+ two_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 60 * 2))
+ three_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 60 * 3))
+ four_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 60 * 4))
+ five_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 60 * 5))
+ six_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 60 * 6))
self.label_1hour_time.setText(time.strftime('%H:%M', time.localtime(time.time() - 3600)))
self.label_2hour_time.setText(time.strftime('%H:%M', time.localtime(time.time() - 3600 * 2)))
@@ -539,17 +555,17 @@ def thumbnail_refresh(self):
self.label_6hour_time.setText(time.strftime('%H:%M', time.localtime(time.time() - 3600 * 6)))
self.label_1hour.setPixmap(
- QPixmap(f'{JS06Settings.get("image_save_path")}/resize/{self.date}/{one_hour_ago}.jpg'))
+ QPixmap(f'{JS06Settings.get("image_save_path")}/{self.year}/resize/{self.date}/{one_hour_ago}.jpg'))
self.label_2hour.setPixmap(
- QPixmap(f'{JS06Settings.get("image_save_path")}/resize/{self.date}/{two_hour_ago}.jpg'))
+ QPixmap(f'{JS06Settings.get("image_save_path")}/{self.year}/resize/{self.date}/{two_hour_ago}.jpg'))
self.label_3hour.setPixmap(
- QPixmap(f'{JS06Settings.get("image_save_path")}/resize/{self.date}/{three_hour_ago}.jpg'))
+ QPixmap(f'{JS06Settings.get("image_save_path")}/{self.year}/resize/{self.date}/{three_hour_ago}.jpg'))
self.label_4hour.setPixmap(
- QPixmap(f'{JS06Settings.get("image_save_path")}/resize/{self.date}/{four_hour_ago}.jpg'))
+ QPixmap(f'{JS06Settings.get("image_save_path")}/{self.year}/resize/{self.date}/{four_hour_ago}.jpg'))
self.label_5hour.setPixmap(
- QPixmap(f'{JS06Settings.get("image_save_path")}/resize/{self.date}/{five_hour_ago}.jpg'))
+ QPixmap(f'{JS06Settings.get("image_save_path")}/{self.year}/resize/{self.date}/{five_hour_ago}.jpg'))
self.label_6hour.setPixmap(
- QPixmap(f'{JS06Settings.get("image_save_path")}/resize/{self.date}/{six_hour_ago}.jpg'))
+ QPixmap(f'{JS06Settings.get("image_save_path")}/{self.year}/resize/{self.date}/{six_hour_ago}.jpg'))
def keyPressEvent(self, e):
"""Override function QMainwindow KeyPressEvent that works when key is pressed"""
@@ -621,7 +637,6 @@ def decompose_front_targets(self, _: str):
if __name__ == '__main__':
-
from PyQt5.QtWidgets import QApplication
mp.freeze_support()
@@ -630,14 +645,15 @@ def decompose_front_targets(self, _: str):
_producer = producer
- p = Process(name='clock', target=clock, args=(q, ), daemon=True)
- _p = Process(name="producer", target=_producer, args=(_q, ), daemon=True)
+ p = Process(name='clock', target=clock, args=(q,), daemon=True)
+ _p = Process(name='producer', target=_producer, args=(_q,), daemon=True)
p.start()
_p.start()
- os.makedirs('D:/ND-01/vista', exist_ok=True)
- os.makedirs('D:/ND-01/resize', exist_ok=True)
+ os.makedirs(f'{JS06Settings.get("data_csv_path")}', exist_ok=True)
+ os.makedirs(f'{JS06Settings.get("target_csv_path")}', exist_ok=True)
+ os.makedirs(f'{JS06Settings.get("image_save_path")}', exist_ok=True)
app = QApplication(sys.argv)
window = ND01MainWindow(q)
diff --git a/src/nd01_settings.py b/src/nd01_settings.py
index 1c7d872..7152f45 100644
--- a/src/nd01_settings.py
+++ b/src/nd01_settings.py
@@ -337,6 +337,7 @@ def get_target(self, camera: str):
save_path = os.path.join(f'{JS06Settings.get("target_csv_path")}/{camera}')
if os.path.isfile(f'{save_path}/{camera}.csv') is False:
+ os.makedirs(f'{save_path}', exist_ok=True)
makeFile = pd.DataFrame(columns=['target_name', 'left_range', 'right_range', 'distance'])
makeFile.to_csv(f'{save_path}/{camera}.csv', mode='w', index=False)
diff --git a/src/resources/main_window.ui b/src/resources/main_window.ui
index 71425ec..1fdebc9 100644
--- a/src/resources/main_window.ui
+++ b/src/resources/main_window.ui
@@ -291,7 +291,7 @@ background-color: #1b3146;
- background-color: rgb(255, 255, 255);
+
@@ -347,7 +347,7 @@ color:#ffffff;
- background-color: rgb(255, 255, 255);
+
@@ -403,7 +403,7 @@ color:#ffffff;
- background-color: rgb(255, 255, 255);
+
@@ -459,7 +459,7 @@ color:#ffffff;
- background-color: rgb(255, 255, 255);
+
@@ -515,7 +515,7 @@ color:#ffffff;
- background-color: rgb(255, 255, 255);
+
@@ -571,7 +571,7 @@ color:#ffffff;
- background-color: rgb(255, 255, 255);
+
diff --git a/src/resources/settings.ui b/src/resources/settings.ui
index 99d30c4..e4e2b01 100644
--- a/src/resources/settings.ui
+++ b/src/resources/settings.ui
@@ -663,21 +663,81 @@ color: #ffffff;
-
-
-
-
- Noto Sans
- 10
-
+
+
+ 0
-
- background-color:rgb(27,49,70);
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ Noto Sans
+ 10
+
+
+
+
color: #ffffff;
-
-
- Other QSetting 1
-
-
+
+
+ Auto file delete:
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ background-color: rgb(255, 255, 255);
+
+
+ GB
+
+
+
+
+
+ 20000
+
+
+ 1
+
+
+ 100
+
+
+ 10
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ color: rgb(255, 255, 255);
+
+
+ Activation
+
+
+
+
-
diff --git a/src/resources/thumbnail_view.ui b/src/resources/thumbnail_view.ui
index f8c1849..4bb64e1 100644
--- a/src/resources/thumbnail_view.ui
+++ b/src/resources/thumbnail_view.ui
@@ -315,7 +315,7 @@ color: rgb(255, 255, 255);
-
+
diff --git a/src/video_thread_mp.py b/src/video_thread_mp.py
index 5f093ea..2f666f3 100644
--- a/src/video_thread_mp.py
+++ b/src/video_thread_mp.py
@@ -9,6 +9,10 @@
import os
import cv2
import time
+
+import shutil
+import psutil
+
import traceback
import numpy as np
import pandas as pd
@@ -26,24 +30,30 @@ def producer(q):
if cap.isOpened():
while True:
epoch = time.strftime("%Y%m%d%H%M%S", time.localtime(time.time()))
- date = epoch[2:6]
+ year = epoch[:4]
+ date = epoch[4:8]
if epoch[-2:] == "00":
try:
image_save_path = JS06Settings.get('image_save_path')
- os.makedirs(f'{image_save_path}/vista/{date}', exist_ok=True)
- os.makedirs(f'{image_save_path}/resize/{date}', exist_ok=True)
+ os.makedirs(f'{image_save_path}/{year}', exist_ok=True)
+ os.makedirs(f'{image_save_path}/{year}/vista/{date}', exist_ok=True)
+ os.makedirs(f'{image_save_path}/{year}/resize/{date}', exist_ok=True)
ret, frame = cap.read()
if ret:
if JS06Settings.get('image_size') == 0:
- cv2.imwrite(f'{image_save_path}/vista/{date}/{epoch}.png', frame)
+ cv2.imwrite(f'{image_save_path}/{year}/vista/{date}/{epoch}.png', frame)
elif JS06Settings.get('image_size') == 1:
frame = cv2.resize(frame, (1920, 840), interpolation=cv2.INTER_LINEAR)
- cv2.imwrite(f'{image_save_path}/vista/{date}/{epoch}.png', frame)
+ cv2.imwrite(f'{image_save_path}/{year}/vista/{date}/{epoch}.png', frame)
frame = cv2.resize(frame, (315, 131), interpolation=cv2.INTER_NEAREST)
- cv2.imwrite(f'{image_save_path}/resize/{date}/{epoch}.jpg', cv2.resize(frame, (315, 131)))
+ cv2.imwrite(f'{image_save_path}/{year}/resize/{date}/{epoch}.jpg', cv2.resize(frame, (315, 131)))
time.sleep(1)
+
+ total, used, free = shutil.disk_usage('D:\\')
+ print(byte_transform(free, 'GB'))
+
cap.release()
except:
@@ -54,6 +64,19 @@ def producer(q):
cv2.destroyAllWindows()
+# Auto file delete
+def byte_transform(bytes, to, bsize=1024):
+ """Unit conversion of byte received from shutil
+
+ :return: Capacity of the selected unit (int)
+ """
+ unit = {'KB': 1, 'MB': 2, 'GB': 3, 'TB': 4}
+ r = float(bytes)
+ for i in range(unit[to]):
+ r = r / bsize
+ return int(r)
+
+
def minprint(epoch, left_range, right_range, distance, cv_img):
"""A function that outputs pixels for calculating the dissipation coefficient in the specified areas"""
print("minprint 시작")
From 00f55b51145b07e60ad2ec715cc087da786ade3a Mon Sep 17 00:00:00 2001
From: Oh-JongJin <5jx2oh@gmail.com>
Date: Thu, 17 Feb 2022 16:52:33 +0900
Subject: [PATCH 08/23] Update docstring
---
src/auto_file_delete.py | 98 ++++++++++++++++
src/model.py | 1 +
src/nd01.py | 43 +++----
src/nd01_settings.py | 10 ++
src/resources/file_auto_delete.ui | 188 ++++++++++++++++++++++++++++++
src/resources/settings.ui | 78 ++-----------
src/video_thread_mp.py | 42 ++++---
7 files changed, 344 insertions(+), 116 deletions(-)
create mode 100644 src/auto_file_delete.py
create mode 100644 src/resources/file_auto_delete.ui
diff --git a/src/auto_file_delete.py b/src/auto_file_delete.py
new file mode 100644
index 0000000..e564c4f
--- /dev/null
+++ b/src/auto_file_delete.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+
+import os
+import shutil
+import sys
+
+import psutil
+from model import JS06Settings
+
+
+class AutoFileDelete:
+ """
+ Delete the oldest folder from the path specified by user
+ """
+
+ def __init__(self):
+
+ drive = []
+ # Save all of the user's drives in drive variable.
+ for i in range(len(psutil.disk_partitions())):
+ drive.append(str(psutil.disk_partitions()[i])[18:19])
+
+ # Set the drive as the reference to D
+ self.diskLabel = 'D://'
+ self.total, self.used, self.free = shutil.disk_usage(self.diskLabel)
+
+ self.path = None
+
+ self.need_storage = 100
+ self.main()
+
+ def byte_transform(self, bytes, to, bsize=1024):
+ """
+ Unit conversion of byte received from shutil
+
+ :return: Capacity of the selected unit (int)
+ """
+ unit = {'KB': 1, 'MB': 2, 'GB': 3, 'TB': 4}
+ r = float(bytes)
+ for i in range(unit[to]):
+ r = r / bsize
+ return int(r)
+
+ def delete_oldest_files(self, path, minimum_storage_GB: int):
+ """
+ The main function of this Program
+ Find oldest file and proceed with deletion
+
+ :param path: Path to proceed with a auto-delete
+ :param minimum_storage_GB: Minimum storage space desired by the user
+ :return: None
+ """
+ is_old = {}
+
+ while minimum_storage_GB >= self.byte_transform(self.free, 'GB'):
+
+ for f in os.listdir(path):
+
+ i = os.path.join(path, f)
+ is_old[f'{i}'] = int(os.path.getctime(i))
+
+ value = list(is_old.values())
+ key = {v: k for k, v in is_old.items()}
+ oldest = key.get(min(value))
+
+ box = input(f'Are you sure to delete "{oldest}" folder?')
+ if box == "":
+ print('yes')
+ # Main syntax for deleting folders
+ shutil.rmtree(oldest)
+ else:
+ print('no')
+ sys.exit()
+
+ print('Already you have enough storage.')
+
+ def main(self):
+ """
+ Delete files by comparing 'need_storage' with the current storage space
+
+ :return: None
+ """
+ # If storage space required is more than current storage space
+ if 100 >= self.byte_transform(self.free, 'GB'):
+ # Specify the Vista folder path of the d drive as a path variable
+ self.path = os.path.join(self.diskLabel, 'vista')
+ try:
+ self.delete_oldest_files(self.path, 100)
+ except FileNotFoundError:
+ print(f'[{self.path}] - Not Found Error')
+
+ else:
+ print('Input storage again')
+
+
+if __name__ == "__main__":
+
+ start = AutoFileDelete()
diff --git a/src/model.py b/src/model.py
index 39689d6..d0bef84 100644
--- a/src/model.py
+++ b/src/model.py
@@ -20,6 +20,7 @@ class JS06Settings:
'image_save_path': os.path.join('D:\\JS06', 'image'),
'image_size': 0,
'visibility_alert_limit': 1000,
+ 'afd_activate': False,
'login_id': 'admin',
'login_pw': '1234'
}
diff --git a/src/nd01.py b/src/nd01.py
index 8adb686..89fec87 100644
--- a/src/nd01.py
+++ b/src/nd01.py
@@ -280,22 +280,22 @@ def refresh_stats(self, positives: list, negatives: list):
class ThumbnailView(QMainWindow):
- def __init__(self, image_file_name: str, year: int, date: int):
+ def __init__(self, image_file_name: str, date: int):
super().__init__()
ui_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
"resources/thumbnail_view.ui")
uic.loadUi(ui_path, self)
- print(f'{JS06Settings.get("image_save_path")}/{year}/vista/{date}/{image_file_name}.png')
+ print(f'{JS06Settings.get("image_save_path")}/vista/{date}/{image_file_name}.png')
self.front_image.setPixmap(
QPixmap(
- f'{JS06Settings.get("image_save_path")}/{year}/vista/{date}/{image_file_name}.png').scaled(
+ f'{JS06Settings.get("image_save_path")}/vista/{date}/{image_file_name}.png').scaled(
self.front_image.width(),
self.front_image.height()))
self.rear_image.setPixmap(
QPixmap(
- f'{JS06Settings.get("image_save_path")}/{year}/vista/{date}/{image_file_name}.png').scaled(
+ f'{JS06Settings.get("image_save_path")}/vista/{date}/{image_file_name}.png').scaled(
self.rear_image.width(),
self.rear_image.height()))
@@ -313,12 +313,9 @@ def __init__(self, q):
self._polar = DiscernmentView(self)
self.view = None
self.km_mile_convert = False
- self.year = None
+ # self.year = None
self.date = None
- # self.front_video_widget.media_player.pause()
- # self.rear_video_widget.media_player.pause()
-
self.front_video_widget = VideoWidget(self)
self.front_video_widget.on_camera_change("rtsp://admin:sijung5520@192.168.100.101/profile2/media.smp")
@@ -328,21 +325,7 @@ def __init__(self, q):
self.video_horizontalLayout.addWidget(self.front_video_widget)
self.video_horizontalLayout.addWidget(self.rear_video_widget)
- # self.scene = QGraphicsScene()
- # self.vis_plot.setScene(self.scene)
- # self.plotWidget = self._plot.pw
- # self.plotWidget.resize(600, 400)
- # self.scene.addWidget(self.plotWidget)
-
self.graph_horizontalLayout.addWidget(self._plot)
-
- # self.scene_polar = QGraphicsScene()
- # self.polar_plot.setScene(self.scene_polar)
- # self.polarWidget = self._polar.pw
- # self.polarWidget.resize(600, 400)
- # self.scene_polar.addWidget(self.polarWidget)
-
- # self.discernment_widget = DiscernmentView(self)
self.graph_horizontalLayout.addWidget(self._polar)
self.setting_button.enterEvent = self.btn_on
@@ -388,7 +371,7 @@ def reset_StyleSheet(self):
self.label_6hour.setStyleSheet('')
def thumbnail_view(self, file_name: str):
- self.view = ThumbnailView(file_name, self.year, self.date)
+ self.view = ThumbnailView(file_name, self.date)
self.view.setGeometry(QRect(self.video_horizontalLayout.geometry().x(),
self.video_horizontalLayout.geometry().y() + 21,
self.video_horizontalLayout.geometry().width(),
@@ -499,7 +482,7 @@ def unit_convert(self, event):
@pyqtSlot(str)
def clock(self, data):
current_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(float(data)))
- self.year = current_time[:4]
+ # self.year = current_time[:4]
self.date = current_time[5:7] + current_time[8:10]
self.real_time_label.setText(current_time)
@@ -555,17 +538,17 @@ def thumbnail_refresh(self):
self.label_6hour_time.setText(time.strftime('%H:%M', time.localtime(time.time() - 3600 * 6)))
self.label_1hour.setPixmap(
- QPixmap(f'{JS06Settings.get("image_save_path")}/{self.year}/resize/{self.date}/{one_hour_ago}.jpg'))
+ QPixmap(f'{JS06Settings.get("image_save_path")}/resize/{self.date}/{one_hour_ago}.jpg'))
self.label_2hour.setPixmap(
- QPixmap(f'{JS06Settings.get("image_save_path")}/{self.year}/resize/{self.date}/{two_hour_ago}.jpg'))
+ QPixmap(f'{JS06Settings.get("image_save_path")}/resize/{self.date}/{two_hour_ago}.jpg'))
self.label_3hour.setPixmap(
- QPixmap(f'{JS06Settings.get("image_save_path")}/{self.year}/resize/{self.date}/{three_hour_ago}.jpg'))
+ QPixmap(f'{JS06Settings.get("image_save_path")}/resize/{self.date}/{three_hour_ago}.jpg'))
self.label_4hour.setPixmap(
- QPixmap(f'{JS06Settings.get("image_save_path")}/{self.year}/resize/{self.date}/{four_hour_ago}.jpg'))
+ QPixmap(f'{JS06Settings.get("image_save_path")}/resize/{self.date}/{four_hour_ago}.jpg'))
self.label_5hour.setPixmap(
- QPixmap(f'{JS06Settings.get("image_save_path")}/{self.year}/resize/{self.date}/{five_hour_ago}.jpg'))
+ QPixmap(f'{JS06Settings.get("image_save_path")}/resize/{self.date}/{five_hour_ago}.jpg'))
self.label_6hour.setPixmap(
- QPixmap(f'{JS06Settings.get("image_save_path")}/{self.year}/resize/{self.date}/{six_hour_ago}.jpg'))
+ QPixmap(f'{JS06Settings.get("image_save_path")}/resize/{self.date}/{six_hour_ago}.jpg'))
def keyPressEvent(self, e):
"""Override function QMainwindow KeyPressEvent that works when key is pressed"""
diff --git a/src/nd01_settings.py b/src/nd01_settings.py
index 7152f45..5e2b5d4 100644
--- a/src/nd01_settings.py
+++ b/src/nd01_settings.py
@@ -25,6 +25,7 @@
QChart)
from model import JS06Settings
from efficiency_chart import EfficiencyChart
+from auto_file_delete import AutoFileDelete
class ND01SettingWidget(QDialog):
@@ -99,6 +100,8 @@ def __init__(self, *args, **kwargs):
self.image_label.mouseMoveEvent = self.lbl_mouseMoveEvent
self.image_label.mouseReleaseEvent = self.lbl_mouseReleaseEvent
+ self.afd_checkBox.stateChanged.connect(self.run_afd)
+
self.buttonBox.accepted.connect(self.accept_click)
self.buttonBox.rejected.connect(self.reject)
@@ -359,6 +362,13 @@ def accept_click(self):
self.close()
+ def run_afd(self, state):
+ if state == Qt.Checked:
+ if not JS06Settings.get('afd_activate'):
+ JS06Settings.set('afd_activate', True)
+ else:
+ JS06Settings.set('afd_activate', False)
+
if __name__ == '__main__':
import sys
diff --git a/src/resources/file_auto_delete.ui b/src/resources/file_auto_delete.ui
new file mode 100644
index 0000000..766a89a
--- /dev/null
+++ b/src/resources/file_auto_delete.ui
@@ -0,0 +1,188 @@
+
+
+ Form
+
+
+ Qt::WindowModal
+
+
+
+ 0
+ 0
+ 424
+ 460
+
+
+
+
+ KoPubWorld돋움체 Medium
+ 10
+ 7
+ false
+ false
+
+
+
+ ArrowCursor
+
+
+ AFD
+
+
+
+ resource/afd.pngresource/afd.png
+
+
+ font: 57 10pt "KoPubWorld돋움체 Medium";
+
+
+
-
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Qt::Sunday
+
+
+ true
+
+
+ QCalendarWidget::LongDayNames
+
+
+ QCalendarWidget::NoVerticalHeader
+
+
+ true
+
+
+ true
+
+
+
+ -
+
+
+ border: 1px solid black;
+
+
+
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Required storage space:
+
+
+
+ -
+
+
+ GB
+
+
+
+
+
+ 99999
+
+
+
+
+
+ -
+
+
+
+
+
+ Open Path
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ border-color: rgb(255, 119, 226);
+
+
+ 0
+
+
+ 0
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+
+ KoPubWorld돋움체 Medium
+ 10
+ 7
+ false
+ false
+
+
+
+ PointingHandCursor
+
+
+
+
+
+ C l o s e (Ctrl + W)
+
+
+
+ -
+
+
+
+
+
+ Qt::AlignCenter
+
+
+
+
+
+
+
+
diff --git a/src/resources/settings.ui b/src/resources/settings.ui
index e4e2b01..b4c2fd4 100644
--- a/src/resources/settings.ui
+++ b/src/resources/settings.ui
@@ -670,7 +670,7 @@ color: #ffffff;
-
-
+
0
0
@@ -690,41 +690,10 @@ color: #ffffff;
- -
-
-
-
- 0
- 0
-
-
-
- background-color: rgb(255, 255, 255);
-
-
- GB
-
-
-
-
-
- 20000
-
-
- 1
-
-
- 100
-
-
- 10
-
-
-
-
-
+
0
0
@@ -735,44 +704,19 @@ color: #ffffff;
Activation
+
+ false
+
+
+ false
+
+
+ false
+
- -
-
-
-
- Noto Sans
- 10
-
-
-
- background-color:rgb(27,49,70);
-color: #ffffff;
-
-
- Other QSetting 2
-
-
-
- -
-
-
-
- Noto Sans
- 10
-
-
-
- background-color:rgb(27,49,70);
-color: #ffffff;
-
-
- Other QSetting 3
-
-
-
-
diff --git a/src/video_thread_mp.py b/src/video_thread_mp.py
index 2f666f3..edaa22c 100644
--- a/src/video_thread_mp.py
+++ b/src/video_thread_mp.py
@@ -26,35 +26,39 @@
def producer(q):
- cap = cv2.VideoCapture("rtsp://admin:sijung5520@192.168.100.100/profile2/media.smp")
+ cap = cv2.VideoCapture('rtsp://admin:sijung5520@192.168.100.100/profile2/media.smp')
+
if cap.isOpened():
while True:
- epoch = time.strftime("%Y%m%d%H%M%S", time.localtime(time.time()))
- year = epoch[:4]
- date = epoch[4:8]
+ epoch = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time()))
+ year = epoch[:4] # 2022
+ date = epoch[4:8] # 0215
- if epoch[-2:] == "00":
+ if epoch[-2:] == '00':
try:
image_save_path = JS06Settings.get('image_save_path')
- os.makedirs(f'{image_save_path}/{year}', exist_ok=True)
- os.makedirs(f'{image_save_path}/{year}/vista/{date}', exist_ok=True)
- os.makedirs(f'{image_save_path}/{year}/resize/{date}', exist_ok=True)
+ os.makedirs(f'{image_save_path}/vista/{date}', exist_ok=True)
+ os.makedirs(f'{image_save_path}/resize/{date}', exist_ok=True)
ret, frame = cap.read()
- if ret:
- if JS06Settings.get('image_size') == 0:
- cv2.imwrite(f'{image_save_path}/{year}/vista/{date}/{epoch}.png', frame)
- elif JS06Settings.get('image_size') == 1:
- frame = cv2.resize(frame, (1920, 840), interpolation=cv2.INTER_LINEAR)
- cv2.imwrite(f'{image_save_path}/{year}/vista/{date}/{epoch}.png', frame)
- frame = cv2.resize(frame, (315, 131), interpolation=cv2.INTER_NEAREST)
- cv2.imwrite(f'{image_save_path}/{year}/resize/{date}/{epoch}.jpg', cv2.resize(frame, (315, 131)))
- time.sleep(1)
-
+ if not ret:
+ cap.release()
+ cap = cv2.VideoCapture('rtsp://admin:sijung5520@192.168.100.100/profile2/media.smp')
+ print('Found Error; Rebuilding stream')
+
+ if JS06Settings.get('image_size') == 0:
+ cv2.imwrite(f'{image_save_path}/vista/{date}/{epoch}.png', frame)
+ elif JS06Settings.get('image_size') == 1:
+ frame = cv2.resize(frame, (1920, 840), interpolation=cv2.INTER_LINEAR)
+ cv2.imwrite(f'{image_save_path}/vista/{date}/{epoch}.png', frame)
+ frame = cv2.resize(frame, (315, 131), interpolation=cv2.INTER_NEAREST)
+ cv2.imwrite(f'{image_save_path}/resize/{date}/{epoch}.jpg', cv2.resize(frame, (315, 131)))
+
+ if JS06Settings.get('afd_activate'):
total, used, free = shutil.disk_usage('D:\\')
print(byte_transform(free, 'GB'))
- cap.release()
+ time.sleep(1)
except:
print(traceback.format_exc())
From 51cc145aec6f21f3e79d9b2b5721fba354517e74 Mon Sep 17 00:00:00 2001
From: Oh-JongJin <5jx2oh@gmail.com>
Date: Thu, 17 Feb 2022 17:11:41 +0900
Subject: [PATCH 09/23] Remove unused modules
---
src/auto_file_delete.py | 11 ++++----
src/nd01.py | 2 --
src/nd01_settings.py | 1 -
src/video_thread_mp.py | 61 ++++-------------------------------------
4 files changed, 11 insertions(+), 64 deletions(-)
diff --git a/src/auto_file_delete.py b/src/auto_file_delete.py
index e564c4f..f6aa3d6 100644
--- a/src/auto_file_delete.py
+++ b/src/auto_file_delete.py
@@ -5,7 +5,6 @@
import sys
import psutil
-from model import JS06Settings
class AutoFileDelete:
@@ -13,7 +12,7 @@ class AutoFileDelete:
Delete the oldest folder from the path specified by user
"""
- def __init__(self):
+ def __init__(self, need_storage: int):
drive = []
# Save all of the user's drives in drive variable.
@@ -26,7 +25,7 @@ def __init__(self):
self.path = None
- self.need_storage = 100
+ self.need_storage = need_storage
self.main()
def byte_transform(self, bytes, to, bsize=1024):
@@ -81,11 +80,11 @@ def main(self):
:return: None
"""
# If storage space required is more than current storage space
- if 100 >= self.byte_transform(self.free, 'GB'):
+ if self.need_storage >= self.byte_transform(self.free, 'GB'):
# Specify the Vista folder path of the d drive as a path variable
self.path = os.path.join(self.diskLabel, 'vista')
try:
- self.delete_oldest_files(self.path, 100)
+ self.delete_oldest_files(self.path, self.need_storage)
except FileNotFoundError:
print(f'[{self.path}] - Not Found Error')
@@ -95,4 +94,4 @@ def main(self):
if __name__ == "__main__":
- start = AutoFileDelete()
+ start = AutoFileDelete(100)
diff --git a/src/nd01.py b/src/nd01.py
index 89fec87..8942a36 100644
--- a/src/nd01.py
+++ b/src/nd01.py
@@ -313,7 +313,6 @@ def __init__(self, q):
self._polar = DiscernmentView(self)
self.view = None
self.km_mile_convert = False
- # self.year = None
self.date = None
self.front_video_widget = VideoWidget(self)
@@ -482,7 +481,6 @@ def unit_convert(self, event):
@pyqtSlot(str)
def clock(self, data):
current_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(float(data)))
- # self.year = current_time[:4]
self.date = current_time[5:7] + current_time[8:10]
self.real_time_label.setText(current_time)
diff --git a/src/nd01_settings.py b/src/nd01_settings.py
index 5e2b5d4..81084ff 100644
--- a/src/nd01_settings.py
+++ b/src/nd01_settings.py
@@ -25,7 +25,6 @@
QChart)
from model import JS06Settings
from efficiency_chart import EfficiencyChart
-from auto_file_delete import AutoFileDelete
class ND01SettingWidget(QDialog):
diff --git a/src/video_thread_mp.py b/src/video_thread_mp.py
index edaa22c..328259c 100644
--- a/src/video_thread_mp.py
+++ b/src/video_thread_mp.py
@@ -9,19 +9,13 @@
import os
import cv2
import time
-
-import shutil
-import psutil
-
-import traceback
import numpy as np
import pandas as pd
-import multiprocessing as mp
-from multiprocessing import Process, Queue
-from PyQt5.QtCore import QThread, pyqtSignal, QObject
+
import curve_save
from model import JS06Settings
+from auto_file_delete import AutoFileDelete
def producer(q):
@@ -31,8 +25,7 @@ def producer(q):
if cap.isOpened():
while True:
epoch = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time()))
- year = epoch[:4] # 2022
- date = epoch[4:8] # 0215
+ date = epoch[4:8]
if epoch[-2:] == '00':
try:
@@ -55,32 +48,18 @@ def producer(q):
cv2.imwrite(f'{image_save_path}/resize/{date}/{epoch}.jpg', cv2.resize(frame, (315, 131)))
if JS06Settings.get('afd_activate'):
- total, used, free = shutil.disk_usage('D:\\')
- print(byte_transform(free, 'GB'))
+ AutoFileDelete(100)
time.sleep(1)
- except:
- print(traceback.format_exc())
+ except Exception as e:
+ print(e)
cap.release()
cap = cv2.VideoCapture("rtsp://admin:sijung5520@192.168.100.100/profile2/media.smp")
# continue
cv2.destroyAllWindows()
-# Auto file delete
-def byte_transform(bytes, to, bsize=1024):
- """Unit conversion of byte received from shutil
-
- :return: Capacity of the selected unit (int)
- """
- unit = {'KB': 1, 'MB': 2, 'GB': 3, 'TB': 4}
- r = float(bytes)
- for i in range(unit[to]):
- r = r / bsize
- return int(r)
-
-
def minprint(epoch, left_range, right_range, distance, cv_img):
"""A function that outputs pixels for calculating the dissipation coefficient in the specified areas"""
print("minprint 시작")
@@ -231,31 +210,3 @@ def str_to_tuple(before_list):
tuple_list = [i.split(',') for i in before_list]
tuple_list = [(int(i[0][1:]), int(i[1][:-1])) for i in tuple_list]
return tuple_list
-
-
-class VideoThread(QThread):
- update_pixmap_signal = pyqtSignal(str)
-
- def __init__(self, src: str = "", file_type: str = "None", q: Queue = None):
- super().__init__()
- self._run_flag = False
- self.src = src
- self.file_type = file_type
- self.q = q
-
- def run(self):
- self._run_flag = True
- ## 영상 입력이 카메라일 때
- if self.file_type == "Video":
- print("비디오 쓰레드 시작")
- while self._run_flag:
- if not self.q.empty():
- cv_img = self.q.get()
- self.update_pixmap_signal.emit(cv_img)
- # shut down capture system
-
- def stop(self):
- """Sets run flag to False and waits for thread to finish"""
- self._run_flag = False
- self.quit()
- self.wait()
From 964fc79158212d4217d8bd27602be2ce1d2940a5 Mon Sep 17 00:00:00 2001
From: Oh-JongJin <5jx2oh@gmail.com>
Date: Fri, 18 Feb 2022 13:26:41 +0900
Subject: [PATCH 10/23] Apply file deletion function completely
---
src/auto_file_delete.py | 5 ++++-
src/model.py | 1 +
src/nd01_settings.py | 17 ++++++-----------
src/resources/settings.ui | 33 ++++++++++++++++++++++++++++++++-
src/video_thread_mp.py | 29 +++++++++++++++--------------
5 files changed, 58 insertions(+), 27 deletions(-)
diff --git a/src/auto_file_delete.py b/src/auto_file_delete.py
index f6aa3d6..9fb5d93 100644
--- a/src/auto_file_delete.py
+++ b/src/auto_file_delete.py
@@ -10,6 +10,8 @@
class AutoFileDelete:
"""
Delete the oldest folder from the path specified by user
+
+ :param need_storage: The variable arguments are used based on maximum storage space
"""
def __init__(self, need_storage: int):
@@ -20,7 +22,8 @@ def __init__(self, need_storage: int):
drive.append(str(psutil.disk_partitions()[i])[18:19])
# Set the drive as the reference to D
- self.diskLabel = 'D://'
+ if 'D' in drive:
+ self.diskLabel = 'D://'
self.total, self.used, self.free = shutil.disk_usage(self.diskLabel)
self.path = None
diff --git a/src/model.py b/src/model.py
index d0bef84..f93fe21 100644
--- a/src/model.py
+++ b/src/model.py
@@ -21,6 +21,7 @@ class JS06Settings:
'image_size': 0,
'visibility_alert_limit': 1000,
'afd_activate': False,
+ 'need_storage': 100,
'login_id': 'admin',
'login_pw': '1234'
}
diff --git a/src/nd01_settings.py b/src/nd01_settings.py
index 81084ff..8adf122 100644
--- a/src/nd01_settings.py
+++ b/src/nd01_settings.py
@@ -7,7 +7,7 @@
# 5jx2oh@gmail.com (Jongjin Oh)
import os
-
+import traceback
import cv2
import pandas as pd
from PyQt5 import uic
@@ -92,15 +92,15 @@ def __init__(self, *args, **kwargs):
self.pw_lineEdit.setText(JS06Settings.get('login_pw'))
self.image_size_comboBox.setCurrentIndex(JS06Settings.get('image_size'))
- # self.image_size_comboBox.currentTextChanged.connect(self.image_size_changed)
+
+ self.afd_checkBox.setChecked(JS06Settings.get('afd_activate'))
+ self.afd_spinBox.setValue(JS06Settings.get('need_storage'))
self.image_label.paintEvent = self.lbl_paintEvent
self.image_label.mousePressEvent = self.lbl_mousePressEvent
self.image_label.mouseMoveEvent = self.lbl_mouseMoveEvent
self.image_label.mouseReleaseEvent = self.lbl_mouseReleaseEvent
- self.afd_checkBox.stateChanged.connect(self.run_afd)
-
self.buttonBox.accepted.connect(self.accept_click)
self.buttonBox.rejected.connect(self.reject)
@@ -358,16 +358,11 @@ def accept_click(self):
JS06Settings.set('visibility_alert_limit', self.vis_limit_spinBox.value())
JS06Settings.set('login_id', self.id_lineEdit.text())
JS06Settings.set('login_pw', self.pw_lineEdit.text())
+ JS06Settings.set('afd_activate', self.afd_checkBox.isChecked())
+ JS06Settings.set('need_storage', self.afd_spinBox.value())
self.close()
- def run_afd(self, state):
- if state == Qt.Checked:
- if not JS06Settings.get('afd_activate'):
- JS06Settings.set('afd_activate', True)
- else:
- JS06Settings.set('afd_activate', False)
-
if __name__ == '__main__':
import sys
diff --git a/src/resources/settings.ui b/src/resources/settings.ui
index b4c2fd4..4b1a91c 100644
--- a/src/resources/settings.ui
+++ b/src/resources/settings.ui
@@ -693,7 +693,7 @@ color: #ffffff;
-
-
+
0
0
@@ -715,6 +715,37 @@ color: #ffffff;
+ -
+
+
+
+ 0
+ 0
+
+
+
+ background-color: rgb(255, 255, 255);
+
+
+ GB
+
+
+
+
+
+ 20000
+
+
+ 1
+
+
+ 0
+
+
+ 10
+
+
+
-
diff --git a/src/video_thread_mp.py b/src/video_thread_mp.py
index 328259c..2700ad9 100644
--- a/src/video_thread_mp.py
+++ b/src/video_thread_mp.py
@@ -27,25 +27,26 @@ def producer(q):
epoch = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time()))
date = epoch[4:8]
- if epoch[-2:] == '00':
+ # if epoch[-2:] == '00':
+ if epoch[-1:] == '0':
try:
image_save_path = JS06Settings.get('image_save_path')
os.makedirs(f'{image_save_path}/vista/{date}', exist_ok=True)
os.makedirs(f'{image_save_path}/resize/{date}', exist_ok=True)
- ret, frame = cap.read()
- if not ret:
- cap.release()
- cap = cv2.VideoCapture('rtsp://admin:sijung5520@192.168.100.100/profile2/media.smp')
- print('Found Error; Rebuilding stream')
-
- if JS06Settings.get('image_size') == 0:
- cv2.imwrite(f'{image_save_path}/vista/{date}/{epoch}.png', frame)
- elif JS06Settings.get('image_size') == 1:
- frame = cv2.resize(frame, (1920, 840), interpolation=cv2.INTER_LINEAR)
- cv2.imwrite(f'{image_save_path}/vista/{date}/{epoch}.png', frame)
- frame = cv2.resize(frame, (315, 131), interpolation=cv2.INTER_NEAREST)
- cv2.imwrite(f'{image_save_path}/resize/{date}/{epoch}.jpg', cv2.resize(frame, (315, 131)))
+ # ret, frame = cap.read()
+ # if not ret:
+ # cap.release()
+ # cap = cv2.VideoCapture('rtsp://admin:sijung5520@192.168.100.100/profile2/media.smp')
+ # print('Found Error; Rebuilding stream')
+
+ # if JS06Settings.get('image_size') == 0:
+ # cv2.imwrite(f'{image_save_path}/vista/{date}/{epoch}.png', frame)
+ # elif JS06Settings.get('image_size') == 1:
+ # frame = cv2.resize(frame, (1920, 840), interpolation=cv2.INTER_LINEAR)
+ # cv2.imwrite(f'{image_save_path}/vista/{date}/{epoch}.png', frame)
+ # frame = cv2.resize(frame, (315, 131), interpolation=cv2.INTER_NEAREST)
+ # cv2.imwrite(f'{image_save_path}/resize/{date}/{epoch}.jpg', cv2.resize(frame, (315, 131)))
if JS06Settings.get('afd_activate'):
AutoFileDelete(100)
From 52d79743cdc1a16ce51c0414c0544307acc02fe4 Mon Sep 17 00:00:00 2001
From: Oh-JongJin <5jx2oh@gmail.com>
Date: Fri, 18 Feb 2022 14:38:43 +0900
Subject: [PATCH 11/23] Modify data for save image file name
---
src/video_thread_mp.py | 39 ++++++++++++++++++++-------------------
1 file changed, 20 insertions(+), 19 deletions(-)
diff --git a/src/video_thread_mp.py b/src/video_thread_mp.py
index 2700ad9..738f396 100644
--- a/src/video_thread_mp.py
+++ b/src/video_thread_mp.py
@@ -25,40 +25,41 @@ def producer(q):
if cap.isOpened():
while True:
epoch = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time()))
- date = epoch[4:8]
+ date = epoch[2:6]
- # if epoch[-2:] == '00':
- if epoch[-1:] == '0':
+ if epoch[-2:] == '00':
try:
image_save_path = JS06Settings.get('image_save_path')
os.makedirs(f'{image_save_path}/vista/{date}', exist_ok=True)
os.makedirs(f'{image_save_path}/resize/{date}', exist_ok=True)
- # ret, frame = cap.read()
- # if not ret:
- # cap.release()
- # cap = cv2.VideoCapture('rtsp://admin:sijung5520@192.168.100.100/profile2/media.smp')
- # print('Found Error; Rebuilding stream')
+ ret, frame = cap.read()
+ if not ret:
+ cap.release()
+ cap = cv2.VideoCapture('rtsp://admin:sijung5520@192.168.100.100/profile2/media.smp')
+ print('Found Error; Rebuilding stream')
- # if JS06Settings.get('image_size') == 0:
- # cv2.imwrite(f'{image_save_path}/vista/{date}/{epoch}.png', frame)
- # elif JS06Settings.get('image_size') == 1:
- # frame = cv2.resize(frame, (1920, 840), interpolation=cv2.INTER_LINEAR)
- # cv2.imwrite(f'{image_save_path}/vista/{date}/{epoch}.png', frame)
- # frame = cv2.resize(frame, (315, 131), interpolation=cv2.INTER_NEAREST)
- # cv2.imwrite(f'{image_save_path}/resize/{date}/{epoch}.jpg', cv2.resize(frame, (315, 131)))
+ if JS06Settings.get('image_size') == 0: # Original size
+ cv2.imwrite(f'{image_save_path}/vista/{date}/{epoch}.png', frame)
+ elif JS06Settings.get('image_size') == 1: # FHD size
+ frame = cv2.resize(frame, (1920, 840), interpolation=cv2.INTER_LINEAR)
+ cv2.imwrite(f'{image_save_path}/vista/{date}/{epoch}.png', frame)
+ frame = cv2.resize(frame, (315, 131), interpolation=cv2.INTER_NEAREST) # Thumbnail size
+ cv2.imwrite(f'{image_save_path}/resize/{date}/{epoch}.jpg', cv2.resize(frame, (315, 131)))
if JS06Settings.get('afd_activate'):
- AutoFileDelete(100)
+ AutoFileDelete(JS06Settings.get('need_storage'))
time.sleep(1)
- except Exception as e:
- print(e)
+ except:
+ print(traceback.format_exc())
cap.release()
cap = cv2.VideoCapture("rtsp://admin:sijung5520@192.168.100.100/profile2/media.smp")
- # continue
+
cv2.destroyAllWindows()
+ else:
+ print('cap closed')
def minprint(epoch, left_range, right_range, distance, cv_img):
From 6098491264e5b46c233a695dad5ad988ef973c94 Mon Sep 17 00:00:00 2001
From: Oh-JongJin <5jx2oh@gmail.com>
Date: Wed, 23 Feb 2022 12:50:44 +0900
Subject: [PATCH 12/23] Apply file delete through QCalender widget
---
src/auto_file_delete.py | 132 +++++++++++-------
src/model.py | 2 -
src/nd01_settings.py | 16 ++-
...ile_auto_delete.ui => auto_file_delete.ui} | 98 +------------
src/resources/settings.ui | 94 +++++--------
src/video_thread_mp.py | 6 +-
6 files changed, 129 insertions(+), 219 deletions(-)
rename src/resources/{file_auto_delete.ui => auto_file_delete.ui} (50%)
diff --git a/src/auto_file_delete.py b/src/auto_file_delete.py
index 9fb5d93..a66384e 100644
--- a/src/auto_file_delete.py
+++ b/src/auto_file_delete.py
@@ -1,100 +1,130 @@
#!/usr/bin/env python3
+#
+# Copyright 2021-2022 9th grade 5th class.
+#
+# Authors:
+# 5jx2oh@gmail.com
import os
+import psutil
import shutil
-import sys
-import psutil
+from PyQt5.QtWidgets import (QDialog, QApplication, QMenuBar,
+ QAction, QFileDialog, QMessageBox,
+ qApp)
+from PyQt5.QtCore import QDate
+from PyQt5 import uic
-class AutoFileDelete:
+def byte_transform(bytes, to, bsize=1024):
"""
- Delete the oldest folder from the path specified by user
+ Unit conversion of byte received from shutil
- :param need_storage: The variable arguments are used based on maximum storage space
+ :return: Capacity of the selected unit (int)
"""
+ unit = {'KB': 1, 'MB': 2, 'GB': 3, 'TB': 4}
+ r = float(bytes)
+ for i in range(unit[to]):
+ r = r / bsize
+ return int(r)
+
+
+class FileAutoDelete(QDialog):
+
+ def __init__(self):
+ super().__init__()
+
+ ui_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+ "resources/auto_file_delete.ui")
+ uic.loadUi(ui_path, self)
- def __init__(self, need_storage: int):
+ # self.setFixedSize(self.width(), self.height())
drive = []
# Save all of the user's drives in drive variable.
for i in range(len(psutil.disk_partitions())):
drive.append(str(psutil.disk_partitions()[i])[18:19])
- # Set the drive as the reference to D
- if 'D' in drive:
- self.diskLabel = 'D://'
- self.total, self.used, self.free = shutil.disk_usage(self.diskLabel)
+ self.calendarWidget.activated.connect(self.showDate)
self.path = None
+ self.date = None
- self.need_storage = need_storage
- self.main()
+ self.exit_pushButton.clicked.connect(self.exit_click)
- def byte_transform(self, bytes, to, bsize=1024):
+ def exit_click(self):
+ self.close()
+
+ def showDate(self, date):
+ self.date = date.toString('yyMMdd')
+ self.check_file_date(r'D:\JS06\image\vista') # JS06Setting.get('image_save_path')
+
+ def check_file_date(self, path: str):
+ is_old = []
+
+ for f in os.listdir(path):
+ if int(f) <= int(self.date):
+ is_old.append(int(f))
+
+ if is_old:
+ dlg = QMessageBox.question(self, 'Warning', f'Delete {is_old} folder?',
+ QMessageBox.Yes | QMessageBox.No)
+ if dlg == QMessageBox.Yes:
+ print('DELETE!!')
+ self.delete_select_date(path, is_old)
+ else:
+ QMessageBox.information(self, 'Information', 'There is no data before the selected date.')
+
+ def delete_select_date(self, path: str, folder: list):
"""
- Unit conversion of byte received from shutil
+ Delete the list containing the folder name
- :return: Capacity of the selected unit (int)
+ :param path: Path to proceed with a auto-delete
+ :param folder: Data older than the date selected as the calendarWidget
"""
- unit = {'KB': 1, 'MB': 2, 'GB': 3, 'TB': 4}
- r = float(bytes)
- for i in range(unit[to]):
- r = r / bsize
- return int(r)
- def delete_oldest_files(self, path, minimum_storage_GB: int):
+ for i in range(len(folder)):
+ a = os.path.join(path, str(folder[i]))
+ # shutil.rmtree(a)
+ print(f'{a} delete complete.')
+
+ def delete_oldest_files(self, path: str, minimum_storage_GB=100):
"""
The main function of this Program
Find oldest file and proceed with deletion
:param path: Path to proceed with a auto-delete
:param minimum_storage_GB: Minimum storage space desired by the user
- :return: None
"""
is_old = {}
- while minimum_storage_GB >= self.byte_transform(self.free, 'GB'):
+ if minimum_storage_GB >= byte_transform(self.free, 'GB'):
for f in os.listdir(path):
-
i = os.path.join(path, f)
is_old[f'{i}'] = int(os.path.getctime(i))
value = list(is_old.values())
key = {v: k for k, v in is_old.items()}
- oldest = key.get(min(value))
-
- box = input(f'Are you sure to delete "{oldest}" folder?')
- if box == "":
- print('yes')
- # Main syntax for deleting folders
- shutil.rmtree(oldest)
- else:
- print('no')
- sys.exit()
+ old_folder = key.get(min(value))
+ print(old_folder)
- print('Already you have enough storage.')
-
- def main(self):
- """
- Delete files by comparing 'need_storage' with the current storage space
-
- :return: None
- """
- # If storage space required is more than current storage space
- if self.need_storage >= self.byte_transform(self.free, 'GB'):
- # Specify the Vista folder path of the d drive as a path variable
- self.path = os.path.join(self.diskLabel, 'vista')
try:
- self.delete_oldest_files(self.path, self.need_storage)
- except FileNotFoundError:
- print(f'[{self.path}] - Not Found Error')
+ # shutil.rmtree(old_folder)
+ self.progressBar.setValue(self.progressBar.value() + 1)
+ except IndexError:
+ pass
+
+ self.progressBar.setValue(100)
else:
- print('Input storage again')
+ print('Already you have enough storage.')
if __name__ == "__main__":
+ import sys
- start = AutoFileDelete(100)
+ app = QApplication(sys.argv)
+ window = FileAutoDelete()
+ window.show()
+ sys.exit(app.exec_())
diff --git a/src/model.py b/src/model.py
index f93fe21..39689d6 100644
--- a/src/model.py
+++ b/src/model.py
@@ -20,8 +20,6 @@ class JS06Settings:
'image_save_path': os.path.join('D:\\JS06', 'image'),
'image_size': 0,
'visibility_alert_limit': 1000,
- 'afd_activate': False,
- 'need_storage': 100,
'login_id': 'admin',
'login_pw': '1234'
}
diff --git a/src/nd01_settings.py b/src/nd01_settings.py
index 8adf122..ab13294 100644
--- a/src/nd01_settings.py
+++ b/src/nd01_settings.py
@@ -25,6 +25,7 @@
QChart)
from model import JS06Settings
from efficiency_chart import EfficiencyChart
+from auto_file_delete import FileAutoDelete
class ND01SettingWidget(QDialog):
@@ -35,7 +36,8 @@ def __init__(self, *args, **kwargs):
ui_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
"resources/settings.ui")
uic.loadUi(ui_path, self)
- # self.setWindowFlag(Qt.FramelessWindowHint)
+ self.showFullScreen()
+ self.setWindowFlag(Qt.FramelessWindowHint)
self.begin = QPoint()
self.end = QPoint()
@@ -82,6 +84,7 @@ def __init__(self, *args, **kwargs):
self.data_csv_path_button.clicked.connect(self.data_csv_path)
self.target_csv_path_button.clicked.connect(self.target_csv_path)
self.image_save_path_button.clicked.connect(self.image_save_path)
+ self.afd_button.clicked.connect(self.afd_btn_click)
self.data_csv_path_textBrowser.setPlainText(JS06Settings.get('data_csv_path'))
self.target_csv_path_textBrowser.setPlainText(JS06Settings.get('target_csv_path'))
@@ -93,9 +96,6 @@ def __init__(self, *args, **kwargs):
self.image_size_comboBox.setCurrentIndex(JS06Settings.get('image_size'))
- self.afd_checkBox.setChecked(JS06Settings.get('afd_activate'))
- self.afd_spinBox.setValue(JS06Settings.get('need_storage'))
-
self.image_label.paintEvent = self.lbl_paintEvent
self.image_label.mousePressEvent = self.lbl_mousePressEvent
self.image_label.mouseMoveEvent = self.lbl_mouseMoveEvent
@@ -299,6 +299,12 @@ def image_save_path(self):
else:
pass
+ def afd_btn_click(self):
+ dlg = FileAutoDelete()
+ dlg.show()
+ # dlg.setWindowModality(Qt.ApplicationModal)
+ dlg.exec_()
+
def save_vis(self):
col = ['datetime', 'camera_direction',
@@ -358,8 +364,6 @@ def accept_click(self):
JS06Settings.set('visibility_alert_limit', self.vis_limit_spinBox.value())
JS06Settings.set('login_id', self.id_lineEdit.text())
JS06Settings.set('login_pw', self.pw_lineEdit.text())
- JS06Settings.set('afd_activate', self.afd_checkBox.isChecked())
- JS06Settings.set('need_storage', self.afd_spinBox.value())
self.close()
diff --git a/src/resources/file_auto_delete.ui b/src/resources/auto_file_delete.ui
similarity index 50%
rename from src/resources/file_auto_delete.ui
rename to src/resources/auto_file_delete.ui
index 766a89a..b60829d 100644
--- a/src/resources/file_auto_delete.ui
+++ b/src/resources/auto_file_delete.ui
@@ -9,8 +9,8 @@
0
0
- 424
- 460
+ 482
+ 334
@@ -36,95 +36,14 @@
font: 57 10pt "KoPubWorld돋움체 Medium";
-
-
-
-
-
-
-
-
-
-
-
-
-
+
0
0
-
- Qt::Sunday
-
-
- true
-
-
- QCalendarWidget::LongDayNames
-
-
- QCalendarWidget::NoVerticalHeader
-
-
- true
-
-
- true
-
-
-
- -
-
-
- border: 1px solid black;
-
-
-
-
-
- Qt::AlignCenter
-
-
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
- Required storage space:
-
-
-
- -
-
-
- GB
-
-
-
-
-
- 99999
-
-
-
-
-
- -
-
-
-
-
-
- Open Path
-
-
@@ -169,15 +88,8 @@
C l o s e (Ctrl + W)
-
-
- -
-
-
-
-
-
- Qt::AlignCenter
+
+ Ctrl+W
diff --git a/src/resources/settings.ui b/src/resources/settings.ui
index 4b1a91c..525a776 100644
--- a/src/resources/settings.ui
+++ b/src/resources/settings.ui
@@ -260,7 +260,7 @@ color: #ffffff;
-
-
+
0
0
@@ -272,7 +272,7 @@ color: #ffffff;
-
+ background-color:rgb(27,49,70);
color: #ffffff;
@@ -332,7 +332,7 @@ color: #ffffff;
-
-
+
0
0
@@ -344,7 +344,7 @@ color: #ffffff;
-
+ background-color:rgb(27,49,70);
color: #ffffff;
@@ -404,7 +404,7 @@ color: #ffffff;
-
-
+
0
0
@@ -416,7 +416,7 @@ color: #ffffff;
-
+ background-color:rgb(27,49,70);
color: #ffffff;
@@ -476,7 +476,7 @@ color: #ffffff;
-
-
+
0
0
@@ -488,7 +488,7 @@ color: #ffffff;
-
+ background-color:rgb(27,49,70);
color: #ffffff;
@@ -526,7 +526,7 @@ color: #ffffff;
-
-
+
0
0
@@ -538,7 +538,7 @@ color: #ffffff;
-
+ background-color:rgb(27,49,70);
color: #ffffff;
@@ -584,7 +584,7 @@ color: #ffffff;
-
-
+
0
0
@@ -596,7 +596,7 @@ color: #ffffff;
-
+ background-color:rgb(27,49,70);
color: #ffffff;
@@ -627,7 +627,7 @@ color: #ffffff;
-
-
+
0
0
@@ -639,7 +639,7 @@ color: #ffffff;
-
+ background-color:rgb(27,49,70);
color: #ffffff;
@@ -665,12 +665,12 @@ color: #ffffff;
-
- 0
+ 6
-
-
+
0
0
@@ -682,67 +682,34 @@ color: #ffffff;
-
+ background-color:rgb(27,49,70);
color: #ffffff;
- Auto file delete:
+ File delete:
-
-
+
-
+
0
0
-
- color: rgb(255, 255, 255);
-
-
- Activation
-
-
- false
-
-
- false
-
-
- false
-
-
-
- -
-
-
-
- 0
- 0
-
+
+
+ 16777215
+ 16777215
+
- background-color: rgb(255, 255, 255);
-
-
- GB
-
-
-
-
-
- 20000
-
-
- 1
-
-
- 0
+ background-color:rgb(27,49,70);
+color: #ffffff;
-
- 10
+
+ ↩
@@ -772,6 +739,9 @@ color: #ffffff;
QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::Ok
+
+ false
+
diff --git a/src/video_thread_mp.py b/src/video_thread_mp.py
index 738f396..f023d3c 100644
--- a/src/video_thread_mp.py
+++ b/src/video_thread_mp.py
@@ -15,7 +15,6 @@
import curve_save
from model import JS06Settings
-from auto_file_delete import AutoFileDelete
def producer(q):
@@ -25,7 +24,7 @@ def producer(q):
if cap.isOpened():
while True:
epoch = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time()))
- date = epoch[2:6]
+ date = epoch[2:8]
if epoch[-2:] == '00':
try:
@@ -47,9 +46,6 @@ def producer(q):
frame = cv2.resize(frame, (315, 131), interpolation=cv2.INTER_NEAREST) # Thumbnail size
cv2.imwrite(f'{image_save_path}/resize/{date}/{epoch}.jpg', cv2.resize(frame, (315, 131)))
- if JS06Settings.get('afd_activate'):
- AutoFileDelete(JS06Settings.get('need_storage'))
-
time.sleep(1)
except:
From 9351225adb33b53c7d7d8963cc9978389ac2298d Mon Sep 17 00:00:00 2001
From: Oh-JongJin <5jx2oh@gmail.com>
Date: Wed, 23 Feb 2022 13:56:39 +0900
Subject: [PATCH 13/23] Simplified file delete UI
---
src/auto_file_delete.py | 42 ++---------
src/resources/auto_file_delete.ui | 118 +++++++++++++++---------------
src/resources/settings.ui | 8 +-
3 files changed, 70 insertions(+), 98 deletions(-)
diff --git a/src/auto_file_delete.py b/src/auto_file_delete.py
index a66384e..fd6e7e1 100644
--- a/src/auto_file_delete.py
+++ b/src/auto_file_delete.py
@@ -15,6 +15,8 @@
from PyQt5.QtCore import QDate
from PyQt5 import uic
+from model import JS06Settings
+
def byte_transform(bytes, to, bsize=1024):
"""
@@ -49,6 +51,7 @@ def __init__(self):
self.path = None
self.date = None
+ self.date_convert = None
self.exit_pushButton.clicked.connect(self.exit_click)
@@ -57,7 +60,9 @@ def exit_click(self):
def showDate(self, date):
self.date = date.toString('yyMMdd')
- self.check_file_date(r'D:\JS06\image\vista') # JS06Setting.get('image_save_path')
+ self.date_convert = date.toString('yyyy/MM/dd')
+ self.check_file_date(os.path.join(JS06Settings.get('image_save_path'),
+ 'vista'))
def check_file_date(self, path: str):
is_old = []
@@ -67,10 +72,9 @@ def check_file_date(self, path: str):
is_old.append(int(f))
if is_old:
- dlg = QMessageBox.question(self, 'Warning', f'Delete {is_old} folder?',
+ dlg = QMessageBox.question(self, 'Warning', f'Delete folder before {self.date_convert} ?',
QMessageBox.Yes | QMessageBox.No)
if dlg == QMessageBox.Yes:
- print('DELETE!!')
self.delete_select_date(path, is_old)
else:
QMessageBox.information(self, 'Information', 'There is no data before the selected date.')
@@ -88,38 +92,6 @@ def delete_select_date(self, path: str, folder: list):
# shutil.rmtree(a)
print(f'{a} delete complete.')
- def delete_oldest_files(self, path: str, minimum_storage_GB=100):
- """
- The main function of this Program
- Find oldest file and proceed with deletion
-
- :param path: Path to proceed with a auto-delete
- :param minimum_storage_GB: Minimum storage space desired by the user
- """
- is_old = {}
-
- if minimum_storage_GB >= byte_transform(self.free, 'GB'):
-
- for f in os.listdir(path):
- i = os.path.join(path, f)
- is_old[f'{i}'] = int(os.path.getctime(i))
-
- value = list(is_old.values())
- key = {v: k for k, v in is_old.items()}
- old_folder = key.get(min(value))
- print(old_folder)
-
- try:
- # shutil.rmtree(old_folder)
- self.progressBar.setValue(self.progressBar.value() + 1)
- except IndexError:
- pass
-
- self.progressBar.setValue(100)
-
- else:
- print('Already you have enough storage.')
-
if __name__ == "__main__":
import sys
diff --git a/src/resources/auto_file_delete.ui b/src/resources/auto_file_delete.ui
index b60829d..0f8386f 100644
--- a/src/resources/auto_file_delete.ui
+++ b/src/resources/auto_file_delete.ui
@@ -9,15 +9,15 @@
0
0
- 482
- 334
+ 298
+ 282
KoPubWorld돋움체 Medium
10
- 7
+ 50
false
false
@@ -33,65 +33,65 @@
resource/afd.pngresource/afd.png
- font: 57 10pt "KoPubWorld돋움체 Medium";
+
-
-
-
-
- 0
- 0
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- border-color: rgb(255, 119, 226);
-
-
- 0
-
-
- 0
-
-
- Qt::AlignCenter
-
-
-
- -
-
-
-
- KoPubWorld돋움체 Medium
- 10
- 7
- false
- false
-
-
-
- PointingHandCursor
-
-
-
-
-
- C l o s e (Ctrl + W)
-
-
- Ctrl+W
-
-
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ Noto Sans
+
+
+
+ background-color: rgb(255, 255, 255);
+color: rgb(0, 0, 0);
+
+
+
+
+
+ Qt::Sunday
+
+
+ true
+
+
+
+ -
+
+
+
+ Noto Sans
+ 10
+ 50
+ false
+ false
+
+
+
+ PointingHandCursor
+
+
+
+
+
+ C l o s e (Ctrl + W)
+
+
+ Ctrl+W
+
+
+
+
diff --git a/src/resources/settings.ui b/src/resources/settings.ui
index 525a776..0eee323 100644
--- a/src/resources/settings.ui
+++ b/src/resources/settings.ui
@@ -471,7 +471,7 @@ color: #ffffff;
-
- 0
+ 6
-
@@ -521,7 +521,7 @@ color: #ffffff;
-
- 0
+ 6
-
@@ -579,7 +579,7 @@ color: #ffffff;
-
- 0
+ 6
-
@@ -622,7 +622,7 @@ color: #ffffff;
-
- 0
+ 6
-
From 0c6ec665a337427870ceb226011b5e7929a4fc44 Mon Sep 17 00:00:00 2001
From: Oh-JongJin <5jx2oh@gmail.com>
Date: Wed, 23 Feb 2022 15:00:32 +0900
Subject: [PATCH 14/23] Delete useless code
---
src/nd01_settings.py | 1 -
src/resources/auto_file_delete.ui | 21 ++++++++++++++++-----
2 files changed, 16 insertions(+), 6 deletions(-)
diff --git a/src/nd01_settings.py b/src/nd01_settings.py
index ab13294..19ca738 100644
--- a/src/nd01_settings.py
+++ b/src/nd01_settings.py
@@ -302,7 +302,6 @@ def image_save_path(self):
def afd_btn_click(self):
dlg = FileAutoDelete()
dlg.show()
- # dlg.setWindowModality(Qt.ApplicationModal)
dlg.exec_()
def save_vis(self):
diff --git a/src/resources/auto_file_delete.ui b/src/resources/auto_file_delete.ui
index 0f8386f..268f40c 100644
--- a/src/resources/auto_file_delete.ui
+++ b/src/resources/auto_file_delete.ui
@@ -26,11 +26,10 @@
ArrowCursor
- AFD
+ File delete
-
-
- resource/afd.pngresource/afd.png
+
+ This is this.
@@ -51,6 +50,9 @@
Noto Sans
+
+ Select a date and erase the data before that date
+
background-color: rgb(255, 255, 255);
color: rgb(0, 0, 0);
@@ -68,6 +70,12 @@ color: rgb(0, 0, 0);
-
+
+
+ 0
+ 0
+
+
Noto Sans
@@ -80,11 +88,14 @@ color: rgb(0, 0, 0);
PointingHandCursor
+
+ Exit this window
+
- C l o s e (Ctrl + W)
+ Close
Ctrl+W
From 407931bc9610573633f376d140618f0dd576c9a0 Mon Sep 17 00:00:00 2001
From: Oh-JongJin <5jx2oh@gmail.com>
Date: Mon, 28 Feb 2022 14:36:09 +0900
Subject: [PATCH 15/23] Add login view
---
src/auto_file_delete.py | 7 +-
src/controller.py | 91 +++++++---------------
src/login_view.py | 58 ++++++++++++++
src/model.py | 2 +-
src/nd01.py | 89 +++++++++++++++------
src/nd01_settings.py | 30 ++-----
src/resources/asset/f_logo.png | Bin 0 -> 3218 bytes
src/resources/login_window.ui | 138 +++++++++++++++++++++++++++++++++
src/video_thread_mp.py | 36 +++++++++
9 files changed, 339 insertions(+), 112 deletions(-)
create mode 100644 src/login_view.py
create mode 100644 src/resources/asset/f_logo.png
create mode 100644 src/resources/login_window.ui
diff --git a/src/auto_file_delete.py b/src/auto_file_delete.py
index fd6e7e1..3621f80 100644
--- a/src/auto_file_delete.py
+++ b/src/auto_file_delete.py
@@ -9,10 +9,7 @@
import psutil
import shutil
-from PyQt5.QtWidgets import (QDialog, QApplication, QMenuBar,
- QAction, QFileDialog, QMessageBox,
- qApp)
-from PyQt5.QtCore import QDate
+from PyQt5.QtWidgets import (QDialog, QApplication, QMessageBox)
from PyQt5 import uic
from model import JS06Settings
@@ -89,7 +86,7 @@ def delete_select_date(self, path: str, folder: list):
for i in range(len(folder)):
a = os.path.join(path, str(folder[i]))
- # shutil.rmtree(a)
+ shutil.rmtree(a)
print(f'{a} delete complete.')
diff --git a/src/controller.py b/src/controller.py
index e9325aa..8ab9bdf 100644
--- a/src/controller.py
+++ b/src/controller.py
@@ -13,35 +13,27 @@
import cv2
import numpy as np
-# import onnxruntime as ort
from PyQt5.QtCore import (QDateTime, QDir, QObject, QRect, QThread,
QThreadPool, QTime, QTimer, pyqtSignal, pyqtSlot)
from PyQt5.QtGui import QImage
-from PyQt5.QtMultimedia import QVideoFrame
-from model import (Js08AttrModel, Js08CameraTableModel, Js08IoRunner,
- Js08Settings, Js08SimpleTarget, Js08Wedge)
-
-class JS06MainCtrl(QObject):
+class JS08MainCtrl(QObject):
abnormal_shutdown = pyqtSignal()
- front_camera_changed = pyqtSignal(str) # uri
- rear_camera_changed = pyqtSignal(str) # uri
+ front_camera_changed = pyqtSignal(str) # uri
+ rear_camera_changed = pyqtSignal(str) # uri
front_target_decomposed = pyqtSignal()
rear_target_decomposed = pyqtSignal()
- target_assorted = pyqtSignal(list, list) # positives, negatives
- wedge_vis_ready = pyqtSignal(int, dict) # epoch, wedge visibility
+ target_assorted = pyqtSignal(list, list) # positives, negatives
+ wedge_vis_ready = pyqtSignal(int, dict) # epoch, wedge visibility
- # def __init__(self, model: Js08AttrModel):
def __init__(self):
super().__init__()
self.writer_pool = QThreadPool.globalInstance()
self.writer_pool.setMaxThreadCount(1)
- self._model = model
-
self.num_working_cam = 0
self.front_simple_targets = []
@@ -50,34 +42,11 @@ def __init__(self):
self.front_target_prepared = False
self.rear_target_prepared = False
- self.init_db()
-
self.observation_timer = QTimer(self)
- # self.front_camera_changed.connect(self.decompose_front_targets)
- # self.rear_camera_changed.connect(self.decompose_rear_targets)
self.worker_running = False
self.start_observation_timer()
- def init_db(self):
- # db_host = Js08Settings.get('db_host')
- # db_port = Js08Settings.get('db_port')
- # db_name = Js08Settings.get('db_name')
- # self._model.connect_to_db(db_host, db_port, db_name)
-
- if getattr(sys, 'frozen', False):
- directory = sys._MEIPASS
- else:
- directory = os.path.dirname(__file__)
- attr_path = os.path.join(directory, 'resources', 'attr.json')
- with open(attr_path, 'r') as f:
- attr_json = json.load(f)
- camera_path = os.path.join(directory, 'resources', 'camera.json')
- with open(camera_path, 'r') as f:
- camera_json = json.load(f)
-
- self._model.setup_db(attr_json, camera_json)
-
@pyqtSlot(str)
def decompose_front_targets(self, _: str) -> None:
"""Make list of SimpleTarget by decoposing compound targets.
@@ -85,7 +54,6 @@ def decompose_front_targets(self, _: str) -> None:
Parameters:
"""
self.front_target_prepared = False
- # self.decompose_targets('front')
self.front_target_prepared = True
@pyqtSlot(str)
@@ -112,9 +80,9 @@ def decompose_targets(self, direction: str) -> None:
elif direction == 'rear':
targets = attr['rear_camera']['targets']
id = str(attr['rear_camera']['camera_id'])
-
- base_path = Js08Settings.get('image_base_path')
-
+
+ base_path = Js08Settings.get('image_base_path')
+
# Prepare model.
# TODO(Kyungwon): Put the model file into Qt Resource Collection.
if getattr(sys, 'frozen', False):
@@ -127,7 +95,7 @@ def decompose_targets(self, direction: str) -> None:
input_shape = sess.get_inputs()[0].shape
input_height = input_shape[1]
input_width = input_shape[2]
-
+
for tg in targets:
wedge = tg['wedge']
azimuth = tg['azimuth']
@@ -172,7 +140,7 @@ def read_mask(self, path: str) -> np.ndarray:
def start_observation_timer(self) -> None:
print('DEBUG(start_observation_timer):', QTime.currentTime().toString())
- self.observation_timer.setInterval(1000) # every one second
+ self.observation_timer.setInterval(1000) # every one second
self.observation_timer.timeout.connect(self.start_worker)
self.observation_timer.start()
@@ -193,11 +161,11 @@ def start_worker(self) -> None:
rear_uri = self.get_rear_camera_uri()
self.worker = Js08InferenceWorker(
self.epoch,
- front_uri,
- rear_uri,
- self.front_simple_targets,
+ front_uri,
+ rear_uri,
+ self.front_simple_targets,
self.rear_simple_targets
- )
+ )
self.worker_thread = QThread()
self.worker.moveToThread(self.worker_thread)
self.worker_thread.started.connect(self.worker.run)
@@ -216,7 +184,7 @@ def postproduction(self):
epoch: seconds since epoch
"""
epoch = self.epoch
-
+
pos, neg = self.assort_discernment()
self.target_assorted.emit(pos, neg)
wedge_vis = self.wedge_visibility()
@@ -286,7 +254,7 @@ def save_image(self, dir: str, filename: str, image: QImage) -> None:
path = QDir.cleanPath(os.path.join(dir, filename))
runner = Js08IoRunner(path, image)
self.writer_pool.start(runner)
-
+
def grab_image(self, direction: str) -> QImage:
"""
Parameters:
@@ -320,7 +288,7 @@ def get_target(self, direction: str) -> list:
def get_camera_table_model(self) -> dict:
cameras = self.get_cameras()
- table_model = Js08CameraTableModel(cameras)
+ table_model = Js08CameraTableModel(cameras)
return table_model
def check_exit_status(self) -> bool:
@@ -335,7 +303,7 @@ def update_cameras(self, cameras: list, update_target: bool = False) -> None:
for cam_id in cam_id_in_db:
if cam_id not in cam_id_in_arg:
self._model.delete_camera(cam_id)
-
+
# if `cameras` does not have 'targets' field, add an empty list for it.
for cam in cameras:
if 'targets' not in cam:
@@ -349,7 +317,7 @@ def update_cameras(self, cameras: list, update_target: bool = False) -> None:
if c_arg['_id'] == c_db['_id']:
c_arg['targets'] = c_db['targets']
continue
-
+
# if '_id' is empty, delete the field
for cam in cameras:
if not cam['_id']:
@@ -369,7 +337,7 @@ def get_attr(self) -> dict:
# if self._attr.count_documents({}):
# attr_doc = list(self._attr.find().sort("_id", -1).limit(1))[0]
return attr_doc
-
+
def insert_attr(self, model: dict) -> None:
self._model.insert_attr(model)
@@ -379,7 +347,7 @@ def restore_defaults(self) -> None:
@pyqtSlot(bool)
def set_normal_shutdown(self) -> None:
- Js08Settings.set('normal_shutdown', True)
+ Js08Settings.set('normal_shutdown', True)
def get_cameras(self) -> list:
return self._model.read_cameras()
@@ -387,14 +355,15 @@ def get_cameras(self) -> list:
class Js08InferenceWorker(QObject):
finished = pyqtSignal()
-
- def __init__(self, epoch: int, front_uri: str, rear_uri: str, front_decomposed_targets: list, rear_decomposed_targets: list) -> None:
+
+ def __init__(self, epoch: int, front_uri: str, rear_uri: str, front_decomposed_targets: list,
+ rear_decomposed_targets: list) -> None:
"""
Parameters:
ctrl:
"""
super().__init__()
-
+
# TODO(Kyungwon): Put the model file into Qt Resource Collection.
if getattr(sys, 'frozen', False):
directory = sys._MEIPASS
@@ -470,7 +439,7 @@ def run(self):
self.save_image(dir, filename, front_image)
filename = f'vista-rear-{now.toString("yyyy-MM-dd-hh-mm")}.png'
self.save_image(dir, filename, rear_image)
-
+
# Discriminate the targets of front camera
self.classify_batch(self.front_targets, front_image)
@@ -479,7 +448,7 @@ def run(self):
self.finished.emit()
- def classify_batch(self, targets: List[Js08SimpleTarget], vista: QImage):
+ def classify_batch(self, targets: list, vista: QImage):
"""Discriminate image batch
Parameters:
@@ -498,9 +467,9 @@ def classify_batch(self, targets: List[Js08SimpleTarget], vista: QImage):
for i, target in enumerate(targets):
if i % self.batch_size == 0:
data = np.zeros(
- (self.batch_size, self.input_height, self.input_width, 3),
+ (self.batch_size, self.input_height, self.input_width, 3),
dtype=np.float32
- )
+ )
roi_image = target.clip_roi(vista)
arr = target.img_to_arr(roi_image, self.input_width, self.input_height)
@@ -522,4 +491,4 @@ def classify_batch(self, targets: List[Js08SimpleTarget], vista: QImage):
if save_target_clip:
postfix = 'pos' if target.discernment else 'neg'
filename = f'{target.label}_{postfix}.png'
- self.save_image(dir, filename, masked_img_list[i])
\ No newline at end of file
+ self.save_image(dir, filename, masked_img_list[i])
diff --git a/src/login_view.py b/src/login_view.py
new file mode 100644
index 0000000..a357ec4
--- /dev/null
+++ b/src/login_view.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3
+#
+# Copyright 2021-2022 Sijung Co., Ltd.
+#
+# Authors:
+# cotjdals5450@gmail.com (Seong Min Chae)
+# 5jx2oh@gmail.com (Jongjin Oh)
+
+import os
+import sys
+import time
+
+from PyQt5.QtWidgets import QDialog
+from PyQt5.QtCore import Qt
+from PyQt5 import uic
+
+from model import JS06Settings
+
+
+class LoginWindow(QDialog):
+
+ def __init__(self):
+ super().__init__()
+
+ ui_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+ 'resources/login_window.ui')
+ uic.loadUi(ui_path, self)
+ self.setWindowFlag(Qt.WindowCloseButtonHint, False)
+ self.setWindowFlag(Qt.WindowContextHelpButtonHint, False)
+ self.show()
+
+ self.login_button.clicked.connect(self.login_click)
+
+ self.id = JS06Settings.get('login_id')
+ self.pw = JS06Settings.get('login_pw')
+
+ self.flag = 0
+
+ def login_click(self):
+ if self.id_lineEdit.text() == self.id and self.pw_lineEdit.text() == self.pw:
+ self.close()
+ else:
+ self.alert_label.setText('ID or P/W is not correct')
+ self.flag = self.flag + 1
+ if self.flag >= 5:
+ self.alert_label.setText('P/W = 1 + 2 + 3 + 4')
+
+ def keyPressEvent(self, event):
+ if event.key() == Qt.Key_Escape:
+ pass
+
+
+if __name__ == '__main__':
+ from PyQt5.QtWidgets import QApplication
+
+ app = QApplication(sys.argv)
+ window = LoginWindow()
+ sys.exit(app.exec_())
diff --git a/src/model.py b/src/model.py
index 39689d6..12c0b17 100644
--- a/src/model.py
+++ b/src/model.py
@@ -7,7 +7,7 @@
# 5jx2oh@gmail.com (Jongjin Oh)
import os
-from PyQt5.QtCore import (QSettings, QStandardPaths, QRect)
+from PyQt5.QtCore import (QSettings, QRect)
from PyQt5.QtGui import (QImage)
diff --git a/src/nd01.py b/src/nd01.py
index 8942a36..5cce687 100644
--- a/src/nd01.py
+++ b/src/nd01.py
@@ -29,10 +29,12 @@
QChart, QDateTimeAxis)
from PyQt5 import uic
-from video_thread_mp import producer
+from login_view import LoginWindow
+from video_thread_mp import video_read, video_write, producer
from nd01_settings import ND01SettingWidget
from model import JS06Settings
from save_db import main
+from controller import JS08MainCtrl
def clock(queue):
@@ -205,10 +207,11 @@ def __init__(self, parent: QWidget, maxlen: int):
# data_point = [QPointF[t, v] for t, v in self.data]
# self.series.append(data_point)
- @pyqtSlot(int, dict)
- def refresh_stats(self, epoch: int, wedge_vis: dict):
- wedge_vis_list = list(wedge_vis.values())
- prev_vis = self.prevailing_visibility(wedge_vis_list)
+ def refresh_stats(self):
+ # wedge_vis_list = list(wedge_vis.values())
+ # wedge_vis_list = [10, 10, 11, 12, 13, 14, 15]
+ prev_vis = self.prevailing_visibility()
+ epoch = QDateTime.currentSecsSinceEpoch()
self.data.append((epoch * 1000, prev_vis))
left = QDateTime.fromMSecsSinceEpoch(self.data[0][0])
@@ -218,9 +221,9 @@ def refresh_stats(self, epoch: int, wedge_vis: dict):
data_point = [QPointF[t, v] for t, v in self.data]
self.series.replace(data_point)
- def prevailing_visibility(self, wedge_vis: list) -> float:
- if None in wedge_vis:
- return 0
+ def prevailing_visibility(self) -> float:
+ wedge_vis = [10, 10, 11, 12, 13, 14, 15]
+
sorted_vis = sorted(wedge_vis, reverse=True)
prevailing = sorted_vis[(len(sorted_vis) - 1) // 2]
return prevailing
@@ -270,8 +273,32 @@ def __init__(self, parent: QWidget):
chart.setAxisY(axis_y, self.positives)
chart.setAxisY(axis_y, self.negatives)
- @pyqtSlot(list, list)
- def refresh_stats(self, positives: list, negatives: list):
+ def refresh_stats(self):
+ # negatives = [(0, 1.5), (0, 2), (0, 2.5), (0, 3), (0, 0.15), (0, 0.35), (0, 0.55), (0, 1), (0, 1.5), (0, 2),
+ # (0, 2.5), (0, 3), (0, 0.15), (0, 0.35), (0, 0.55), (0, 1), (45, 1.5), (45, 2), (45, 2.5), (45, 3),
+ # (45, 0.15), (45, 0.35), (45, 0.55), (45, 1), (45, 1.5), (45, 2), (45, 2.5), (45, 3), (45, 0.15),
+ # (45, 0.35), (45, 0.55), (45, 1), (90, 1.5), (90, 2), (90, 2.5), (90, 3), (90, 0.15), (90, 0.35),
+ # (90, 0.55), (90, 1), (90, 1.5), (90, 2), (90, 2.5), (90, 3), (90, 0.15), (90, 0.35), (90, 0.55),
+ # (90, 1), (135, 1.5), (135, 2), (135, 2.5), (135, 3), (135, 0.15), (135, 0.35), (135, 0.55),
+ # (135, 1), (135, 1.5), (135, 2), (135, 2.5), (135, 3), (135, 0.15), (135, 0.35), (135, 0.55),
+ # (135, 1), (180, 1.5), (180, 2), (180, 2.5), (180, 3), (180, 0.15), (180, 0.35), (180, 0.55),
+ # (180, 1), (180, 1.5), (180, 2), (180, 2.5), (180, 3), (180, 0.15), (180, 0.35), (180, 0.55),
+ # (180, 1), (225, 1.5), (225, 2), (225, 2.5), (225, 3), (225, 0.15), (225, 0.35), (225, 0.55),
+ # (225, 1), (225, 1.5), (225, 2), (225, 2.5), (225, 3), (225, 0.1), (225, 0.3), (225, 0.5),
+ # (225, 1), (270, 1.5), (270, 2), (270, 2.5), (270, 3), (270, 0.15), (270, 0.35), (270, 0.55),
+ # (270, 1), (270, 1.5), (270, 2), (270, 2.5), (270, 3), (270, 0.15), (270, 0.35), (270, 0.55),
+ # (270, 1), (315, 1.5), (315, 2), (315, 2.5), (315, 3), (315, 0.15), (315, 0.35), (315, 0.55),
+ # (315, 1), (315, 1.5), (315, 2), (315, 2.5), (315, 3), (315, 0.15), (315, 0.35), (315, 0.55),
+ # (315, 1)]
+ positives = []
+ negatives = [(0, 4), (0, 9), (0, 14),
+ (45, 5), (45, 10), (45, 15),
+ (90, 5), (90, 10), (90, 15),
+ (135, 5), (135, 10), (135, 15),
+ (180, 5), (180, 10), (180, 15),
+ (225, 5), (225, 10), (225, 15),
+ (270, 5), (270, 10), (270, 15),
+ (315, 5), (315, 10), (315, 15)]
pos_point = [QPointF(a, d) for a, d in positives]
self.positives.replace(pos_point)
neg_point = [QPointF(a, d) for a, d in negatives]
@@ -305,12 +332,24 @@ class ND01MainWindow(QMainWindow):
def __init__(self, q):
super().__init__()
+ login_window = LoginWindow()
+ login_window.exec_()
+
ui_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
"resources/main_window.ui")
uic.loadUi(ui_path, self)
self.showFullScreen()
+
+ self._ctrl = JS08MainCtrl
+
self._plot = VisibilityView(self, 1440)
+ # self._ctrl.wedge_vis_ready.connect(self._plot.refresh_stats)
+ # self._plot.refresh_stats()
+
self._polar = DiscernmentView(self)
+ # self._ctrl.target_assorted.connect(self._polar.refresh_stats)
+ self._polar.refresh_stats()
+
self.view = None
self.km_mile_convert = False
self.date = None
@@ -492,13 +531,16 @@ def clock(self, data):
# p_vis_km = f'{format(round(int(result / len(self._plot.plotData["y"])), 2), ",")}'
# p_vis_nm = f'{format(round(int(result / len(self._plot.plotData["y"])) / 1609, 2), ",")}'
- # if self.km_mile_convert:
- # self.c_vis_label.setText(f'{format(round(self._plot.plotData["y"][-1] / 1609, 2), ",")} mile')
- # self.p_vis_label.setText(f'{p_vis_nm} mile')
- #
- # elif self.km_mile_convert is False:
- # self.c_vis_label.setText(f'{format(self._plot.plotData["y"][-1], ",")} m')
- # self.p_vis_label.setText(f'{p_vis_km} m')
+ vis = np.random.randint(10000, 15000)
+ p_vis = np.random.randint(10000, 15000)
+
+ if self.km_mile_convert:
+ self.c_vis_label.setText(f'{format(round(vis / 1609, 2), ",")} mile')
+ self.p_vis_label.setText(f'{format(round(p_vis / 1609, 2), ",")} mile')
+
+ elif self.km_mile_convert is False:
+ self.c_vis_label.setText(f'{format(vis, ",")} m')
+ self.p_vis_label.setText(f'{format(p_vis, ",")} m')
#
# data_time = self._plot.plotData['x']
# if int(float(data)) - 3600 * 3 in self._plot.plotData['x']:
@@ -623,14 +665,22 @@ def decompose_front_targets(self, _: str):
mp.freeze_support()
q = Queue()
_q = Queue()
+ # _qr = Queue()
+ # _qw = Queue()
_producer = producer
+ # _read = video_read
+ # _write = video_write
p = Process(name='clock', target=clock, args=(q,), daemon=True)
_p = Process(name='producer', target=_producer, args=(_q,), daemon=True)
+ # _r = Process(name='read', target=_read, args=(_qr,), daemon=True)
+ # _w = Process(name='write', target=_write, args=(_qw,), daemon=True)
p.start()
_p.start()
+ # _r.start()
+ # _w.start()
os.makedirs(f'{JS06Settings.get("data_csv_path")}', exist_ok=True)
os.makedirs(f'{JS06Settings.get("target_csv_path")}', exist_ok=True)
@@ -639,8 +689,3 @@ def decompose_front_targets(self, _: str):
app = QApplication(sys.argv)
window = ND01MainWindow(q)
sys.exit(app.exec_())
-
- # MainWindow = QMainWindow()
- # ui = ND01MainWindow()
- # ui.show()
- # sys.exit(app.exec_())
diff --git a/src/nd01_settings.py b/src/nd01_settings.py
index 19ca738..13d8021 100644
--- a/src/nd01_settings.py
+++ b/src/nd01_settings.py
@@ -11,18 +11,13 @@
import cv2
import pandas as pd
from PyQt5 import uic
-from PyQt5.QtCore import (QPoint, QRect, Qt,
- QPointF)
+from PyQt5.QtCore import (QPoint, QRect, Qt)
from PyQt5.QtGui import (QPixmap, QPainter, QBrush,
QColor, QPen, QImage,
QIcon)
-from PyQt5.QtWidgets import (QApplication, QLabel, QInputDialog,
- QDialog, QAbstractItemView, QVBoxLayout,
- QGridLayout, QPushButton, QMessageBox,
- QFileDialog)
-from PyQt5.QtChart import (QChartView, QLegend, QLineSeries,
- QPolarChart, QScatterSeries, QValueAxis,
- QChart)
+from PyQt5.QtWidgets import (QApplication, QInputDialog, QDialog,
+ QMessageBox, QFileDialog)
+
from model import JS06Settings
from efficiency_chart import EfficiencyChart
from auto_file_delete import FileAutoDelete
@@ -30,9 +25,9 @@
class ND01SettingWidget(QDialog):
- def __init__(self, *args, **kwargs):
+ def __init__(self):
- super().__init__(*args, **kwargs)
+ super().__init__()
ui_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
"resources/settings.ui")
uic.loadUi(ui_path, self)
@@ -44,7 +39,6 @@ def __init__(self, *args, **kwargs):
self.upper_left = ()
self.lower_right = ()
- # self.min_xy = ()
self.target_name = []
self.left_range = []
@@ -65,15 +59,6 @@ def __init__(self, *args, **kwargs):
self.image_load()
- # 그림 그리는 삐뮤디 생성
- # self.blank_lbl = QLabel(self.image_label)
- # # self.blank_lbl.setGeometry(0, 0, 1200, 500)
- # self.blank_lbl.paintEvent = self.lbl_paintEvent
- #
- # self.blank_lbl.mousePressEvent = self.lbl_mousePressEvent
- # self.blank_lbl.mouseMoveEvent = self.lbl_mouseMoveEvent
- # self.blank_lbl.mouseReleaseEvent = self.lbl_mouseReleaseEvent
-
self.efficiencyChart = EfficiencyChart(self)
self.value_verticalLayout.addWidget(self.efficiencyChart)
@@ -181,8 +166,7 @@ def lbl_paintEvent(self, event):
corner1_2 = int(corner1[1] / self.video_height * self.image_label.height())
corner2_1 = int((corner2[0] - corner1[0]) / self.video_width * self.image_label.width())
corner2_2 = int((corner2[1] - corner1[1]) / self.video_height * self.image_label.height())
- self.target = QRect(corner1_1, corner1_2, corner2_1, corner2_2)
- painter.drawRect(self.target)
+ painter.drawRect(QRect(corner1_1, corner1_2, corner2_1, corner2_2))
if self.isDrawing:
br = QBrush(QColor(100, 10, 10, 40))
diff --git a/src/resources/asset/f_logo.png b/src/resources/asset/f_logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..369a90e98c123a9567da7b29cfa1b250e244e8a6
GIT binary patch
literal 3218
zcmbVPc|25m8y~wOWovOQV+K(%GiJeLESVYWkjWNNW1L|yX3PvTv{2dz$=G}!z0nKjZ5(2?qkxpJPI2CSqCuv0kA!T5(tn1N;M{;j3@r^C0gMiL0m2i3xn}^JR=^`h{+CwAM+ff&OH`
z0~{)w&f?OUVc<1Jia#@gi-U-i{uKg)MI!x89LD*PC{fB_d1Ia`*3!)(k~zur6%6=ujx3cqTQ10fcew@i>U+
zi%|eQ0E;IOP;iuu3DE{kAebT$rbrar3~q)own3qYL=^rf$3J0h5J)841c@-gk$bF7@ksn0A`*kbKz?8Y{-2z|L}FlT#qmGI@^ed6fos#hOJB73yLo^xQOB@F
zt)VB(OaXyp8}0GdZv0o12ckmV2wN{LRaG+#(!FKA3LZQ=tABdh+@vi6O%c
z`g=C1xG31;Wj8?+6yswhXFX61@q`s^S98!K54@DwQ&;T+>`(nu(;t@}3^sE6ho=Nn
zT?0oC&I6IOH*Gmf&zQ8@nVgb{vy!e^bMG$N%{MTzVIyuvJ5%P5_kfr(G3)ay4o?@v
zoFdtiyyd}N7Y;~GtB9e*SnvLz)EutoiSz3O`?81y68+bodClTkXFkG59||FdpE&w;
z8BT_2#SNOf#46WRSOmKG{u-;Wuxt4Z4L)7C*i$6-zVzvXWjlsKPj7zJh_0>5bH9!4
zA$r5@rCrVNM2}D*zup#2x>cdQ>EMY(iLE!}bO(d1`V8z(b!fFH3w>UuW=`C^a9gTw
zqgb(Q=PFd^Wp43f4i!?vRD$ho5E>D3_L09Hw(|~Obm<`(f7ak>Z_`#KSqq+#zoK-G
z7X4}5Q%5Ohc6AFlkutB6;4i1iIyUt>!_&9NCdfx|sd2)(22~-mW9ep)Ro2CTLm^24
zigLOt5t19J*2S`OZAZIihJ^@4zoRnuFQsq4GM1hmWB0IsR1Z=HS>`Pq8c-es_CyHVb$9l+*6f
zrw)j1_^)l^$y=(%+MGS_-c6N8r50b=8O1Yg_0cUl<2>_b2KO*zP#V`X-mxH`mHD=<
zGs0Hk*qEy(4zt~tcr6ztCGcw*WIVumCyMIbaXHr8*70QW=KXK4l
zCuZn??CAWuJf`GDvr>4M(lo8lGb^8+@klHS_Ym9CuliU@ELS!7xZ*t%`GY1+0#`{a^R{{>=!Kr+8*a#?QI0&=ezu6e6K
zWx{oV577~R>QU{}eEG%agrsw$-<|XeQk9DJ6KTZy98K0f_(1ZEmL{OpXYg6IOd-Kz
zRW&{p6ze(ku}7;b@1hi`Mi3=x$`9=-Nl_*kpIhju4JKrlFZ`M-f2l9$P+^JU
z>fwg6(D+Ne)~EP+$#PB(PgN{w+sn|u6cjFxHs}d3`k!CSMZ&Ir2xfx1gX`2+6nqYq
zUtU4$HDojnx>Uliqz^@r@ph)KN=9~{j>>e-mx859Xjmo!3
z@jqnN^Zn4B3ozZm-EF&RaWc0OZ~D^Zu=b{07gKKo&HRGCJ}>i|=8X`n|t)%JgqicYTyMMplC9L<@m+}ko!QQ9xd8qOFx
zbto_!Q|%=X1Olr
z`oqb_Y{e_ij?-ydu7oa@vfH{K*%EKssWXq{@R=@{>@d`P)3}zlK?TX;QPaK!dycG+
z<<`P{^Woz5W7XCQB!6#H6>Kr~kEd~yROdpvMaBC4RyV=#9VU-aWx3wWv{y`OvDM&W;_R1BafS&YXqYPG0T1sxE!(uHl(2r4h#9bduw{jN}7mVbUn+I4Fl
zZE--8H+4KtpcWh)JSW(G#G+{|Qm!d52y|^UWR?cLvTcK37G;^6UY)`-b!T*c9@-;E+QN*!yPSxA0UU|K{IdOlJiaS%#29N-
z2?}*zb_$a8AkrXJ*T`}d!u7)QQ%)`
zZEI10Caq}B4<||sX#z;)cuYxnXO}}2Oy`O3jal11rQWi4cFsh0bN)y
+
+ Dialog
+
+
+
+ 0
+ 0
+ 290
+ 300
+
+
+
+ Login
+
+
+ background-color:rgb(22,32,42);
+
+
+
-
+
+
+
+ Noto Sans
+
+
+
+ background-color:rgb(27,49,70);
+
+
+
-
+
+
+
+ Noto Sans
+
+
+
+ background-color: #ffffff;
+
+
+ ID
+
+
+
+ -
+
+
+
+
+
+
+
+
+ asset/f_logo.png
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ true
+
+
+
+ 0
+ 0
+
+
+
+
+ Noto Sans
+ 10
+
+
+
+ color:#ff0000;
+
+
+ QFrame::Plain
+
+
+
+
+
+ Qt::AutoText
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+
+ Noto Sans
+
+
+
+ background-color: #ffffff;
+
+
+ QLineEdit::PasswordEchoOnEdit
+
+
+ Password
+
+
+
+
+
+
+ -
+
+
+
+ Noto Sans
+ 75
+ true
+
+
+
+ background-color:rgb(27,49,70);color:#ffffff;
+
+
+ Login
+
+
+
+
+
+
+
+
diff --git a/src/video_thread_mp.py b/src/video_thread_mp.py
index f023d3c..329a273 100644
--- a/src/video_thread_mp.py
+++ b/src/video_thread_mp.py
@@ -7,6 +7,7 @@
# 5jx2oh@gmail.com (Jongjin Oh)
import os
+
import cv2
import time
import numpy as np
@@ -47,6 +48,8 @@ def producer(q):
cv2.imwrite(f'{image_save_path}/resize/{date}/{epoch}.jpg', cv2.resize(frame, (315, 131)))
time.sleep(1)
+ cap.release()
+ cap = cv2.VideoCapture("rtsp://admin:sijung5520@192.168.100.100/profile2/media.smp")
except:
print(traceback.format_exc())
@@ -58,6 +61,39 @@ def producer(q):
print('cap closed')
+def video_read():
+
+ cap = cv2.VideoCapture('rtsp://admin:sijung5520@192.168.100.100/profile2/media.smp')
+
+ if cap.isOpened():
+ ret, frame = cap.read()
+ q.put(frame)
+ while ret:
+ print('ret')
+ ret, frame = cap.read()
+ q.put(frame)
+
+
+def video_write():
+ while True:
+ epoch = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time()))
+ date = epoch[2:8]
+
+ if epoch[-1:] == '0' and not q.empty():
+ print(epoch)
+ frame = q.get()
+
+ if JS06Settings.get('image_size') == 0: # Original size
+ cv2.imwrite(f'{image_save_path}/vista/{date}/{epoch}.png', frame)
+ elif JS06Settings.get('image_size') == 1: # FHD size
+ frame = cv2.resize(frame, (1920, 840), interpolation=cv2.INTER_LINEAR)
+ cv2.imwrite(f'{image_save_path}/vista/{date}/{epoch}.png', frame)
+ frame = cv2.resize(frame, (315, 131), interpolation=cv2.INTER_NEAREST) # Thumbnail size
+ cv2.imwrite(f'{image_save_path}/resize/{date}/{epoch}.jpg', cv2.resize(frame, (315, 131)))
+
+ time.sleep(1)
+
+
def minprint(epoch, left_range, right_range, distance, cv_img):
"""A function that outputs pixels for calculating the dissipation coefficient in the specified areas"""
print("minprint 시작")
From 0450857bde6f2924680a6390ae541b99b838c87b Mon Sep 17 00:00:00 2001
From: Oh-JongJin <5jx2oh@gmail.com>
Date: Wed, 2 Mar 2022 10:46:51 +0900
Subject: [PATCH 16/23] Simplified codes
---
src/login_view.py | 1 +
src/video_thread_mp.py | 34 ----------------------------------
2 files changed, 1 insertion(+), 34 deletions(-)
diff --git a/src/login_view.py b/src/login_view.py
index a357ec4..12ab98b 100644
--- a/src/login_view.py
+++ b/src/login_view.py
@@ -30,6 +30,7 @@ def __init__(self):
self.show()
self.login_button.clicked.connect(self.login_click)
+ self.login_button.setShortcut('Return')
self.id = JS06Settings.get('login_id')
self.pw = JS06Settings.get('login_pw')
diff --git a/src/video_thread_mp.py b/src/video_thread_mp.py
index 329a273..5174153 100644
--- a/src/video_thread_mp.py
+++ b/src/video_thread_mp.py
@@ -13,7 +13,6 @@
import numpy as np
import pandas as pd
-
import curve_save
from model import JS06Settings
@@ -61,39 +60,6 @@ def producer(q):
print('cap closed')
-def video_read():
-
- cap = cv2.VideoCapture('rtsp://admin:sijung5520@192.168.100.100/profile2/media.smp')
-
- if cap.isOpened():
- ret, frame = cap.read()
- q.put(frame)
- while ret:
- print('ret')
- ret, frame = cap.read()
- q.put(frame)
-
-
-def video_write():
- while True:
- epoch = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time()))
- date = epoch[2:8]
-
- if epoch[-1:] == '0' and not q.empty():
- print(epoch)
- frame = q.get()
-
- if JS06Settings.get('image_size') == 0: # Original size
- cv2.imwrite(f'{image_save_path}/vista/{date}/{epoch}.png', frame)
- elif JS06Settings.get('image_size') == 1: # FHD size
- frame = cv2.resize(frame, (1920, 840), interpolation=cv2.INTER_LINEAR)
- cv2.imwrite(f'{image_save_path}/vista/{date}/{epoch}.png', frame)
- frame = cv2.resize(frame, (315, 131), interpolation=cv2.INTER_NEAREST) # Thumbnail size
- cv2.imwrite(f'{image_save_path}/resize/{date}/{epoch}.jpg', cv2.resize(frame, (315, 131)))
-
- time.sleep(1)
-
-
def minprint(epoch, left_range, right_range, distance, cv_img):
"""A function that outputs pixels for calculating the dissipation coefficient in the specified areas"""
print("minprint 시작")
From 6e16e4c05f700d2be62b5dd262093e87a8e329e3 Mon Sep 17 00:00:00 2001
From: Oh-JongJin <5jx2oh@gmail.com>
Date: Fri, 4 Mar 2022 15:10:45 +0900
Subject: [PATCH 17/23] Add Target table, Value chart in setting window
---
src/{curve_save.py => cal_ext_coef.py} | 58 ++---
src/efficiency_chart.py | 65 ------
src/nd01.py | 99 +--------
src/nd01_settings.py | 204 +++++++++++++++++-
.../{settings.ui => setting_window.ui} | 107 ++++++++-
src/target_info.py | 151 +++++++++++++
src/video_thread_mp.py | 163 +-------------
7 files changed, 488 insertions(+), 359 deletions(-)
rename src/{curve_save.py => cal_ext_coef.py} (73%)
delete mode 100644 src/efficiency_chart.py
rename src/resources/{settings.ui => setting_window.ui} (88%)
create mode 100644 src/target_info.py
diff --git a/src/curve_save.py b/src/cal_ext_coef.py
similarity index 73%
rename from src/curve_save.py
rename to src/cal_ext_coef.py
index 62bf6dd..006a942 100644
--- a/src/curve_save.py
+++ b/src/cal_ext_coef.py
@@ -6,22 +6,16 @@
import scipy
from scipy.optimize import curve_fit
-from PyQt5 import QtWidgets, QtGui, QtCore
curved_flag = True
-# cam_name = cam_name
hanhwa_dist = []
hanhwa_x = []
hanhwa_r = []
hanhwa_g = []
hanhwa_b = []
-# epoch = epoch
-# rgbsavedir = os.path.join(f"rgb/{cam_name}")
-# extsavedir = os.path.join(f"extinction/{cam_name}")
-def select_max_rgb(r, g, b):
- select_color = ""
+def select_max_rgb(r, g, b):
c_list = [r, g, b]
c_index = c_list.index(max(c_list))
@@ -30,10 +24,12 @@ def select_max_rgb(r, g, b):
select_color = "red"
elif c_index == 1:
select_color = "green"
- else :
+ else:
select_color = "blue"
+
return select_color
+
def cal_curve(hanhwa: pd.DataFrame):
# hanhwa = pd.read_csv(f"{rgbsavedir}/{epoch}.csv")
print(hanhwa)
@@ -45,6 +41,10 @@ def cal_curve(hanhwa: pd.DataFrame):
hanhwa_g = hanhwa[['g']].squeeze().to_numpy()
hanhwa_b = hanhwa[['b']].squeeze().to_numpy()
+ print("오리지날 green", hanhwa_g)
+
+ print("소산계수 산출용 green 리스트 : ", hanhwa_g)
+
r1_init = hanhwa_r[0] * 0.7
g1_init = hanhwa_g[0] * 0.7
b1_init = hanhwa_b[0] * 0.7
@@ -52,9 +52,9 @@ def cal_curve(hanhwa: pd.DataFrame):
r2_init = hanhwa_r[-1] * 1.3
g2_init = hanhwa_g[-1] * 1.3
b2_init = hanhwa_b[-1] * 1.3
-
+
select_color = select_max_rgb(r2_init, g2_init, b2_init)
-
+
r_ext_init = [r1_init, r2_init, 1]
g_ext_init = [g1_init, g2_init, 1]
b_ext_init = [b1_init, b2_init, 1]
@@ -68,6 +68,7 @@ def cal_curve(hanhwa: pd.DataFrame):
except Exception as e:
print("error msg: ", e)
return
+
list1 = []
list2 = []
list3 = []
@@ -94,31 +95,30 @@ def cal_curve(hanhwa: pd.DataFrame):
print(f"Green channel: {extcoeff_to_vis(hanhwa_opt_g[2], hanhwa_err_g[2], 3)} km")
print(f"Blue channel: {extcoeff_to_vis(hanhwa_opt_b[2], hanhwa_err_b[2], 3)} km")
+ os.makedirs(extsavedir, exist_ok=True)
+
return list1, list2, list3, select_color
- # update_extinc_signal.emit(list1, list2, list3, select_color)
- try:
- os.mkdir(extsavedir)
- except Exception as e:
- pass
# @staticmethod
def func(x, c1, c2, a):
return c2 + (c1 - c2) * np.exp(-a * x)
+
def print_result(opt_r, opt_g, opt_b, err_r, err_g, err_b):
- print(f"Red channel: (",
- f"C1: {opt_r[0]:.2f} ± {err_r[0]:.2f}, ",
- f"C2: {opt_r[1]:.2f} ± {err_r[1]:.2f}, ",
- f"alpha: {opt_r[2]:.2f} ± {err_r[2]:.2f})")
- print(f"Green channel: (",
- f"C1: {opt_g[0]:.2f} ± {err_g[0]:.2f}, ",
- f"C2: {opt_g[1]:.2f} ± {err_g[1]:.2f}, ",
- f"alpha: {opt_g[2]:.2f} ± {err_g[2]:.2f})")
- print(f"Blue channel: (",
- f"C1: {opt_b[0]:.2f} ± {err_b[0]:.2f}, ",
- f"C2: {opt_b[1]:.2f} ± {err_b[1]:.2f}, ",
- f"alpha: {opt_b[2]:.2f} ± {err_b[2]:.2f})")
-
-def extcoeff_to_vis(optimal, error, coeff=3.291):
+ print(f'Red channel: (',
+ f'C1: {opt_r[0]:.2f} ± {err_r[0]:.2f}, ',
+ f'C2: {opt_r[1]:.2f} ± {err_r[1]:.2f}, ',
+ f'alpha: {opt_r[2]:.2f} ± {err_r[2]:.2f})')
+ print(f'Green channel: (',
+ f'C1: {opt_g[0]:.2f} ± {err_g[0]:.2f}, ',
+ f'C2: {opt_g[1]:.2f} ± {err_g[1]:.2f}, ',
+ f'alpha: {opt_g[2]:.2f} ± {err_g[2]:.2f})')
+ print(f'Blue channel: (',
+ f'C1: {opt_b[0]:.2f} ± {err_b[0]:.2f}, ',
+ f'C2: {opt_b[1]:.2f} ± {err_b[1]:.2f}, ',
+ f'alpha: {opt_b[2]:.2f} ± {err_b[2]:.2f})')
+
+
+def extcoeff_to_vis(optimal, error, coeff=3.912):
return coeff / (optimal + np.array((1, 0, -1)) * error)
diff --git a/src/efficiency_chart.py b/src/efficiency_chart.py
deleted file mode 100644
index 3b66a9e..0000000
--- a/src/efficiency_chart.py
+++ /dev/null
@@ -1,65 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright 2021-2022 Sijung Co., Ltd.
-#
-# Authors:
-# cotjdals5450@gmail.com (Seong Min Chae)
-# 5jx2oh@gmail.com (Jongjin Oh)
-
-import os
-
-import cv2
-import pandas as pd
-from PyQt5 import uic
-from PyQt5.QtCore import (QPoint, QRect, Qt,
- QPointF)
-from PyQt5.QtGui import (QPixmap, QPainter, QBrush,
- QColor, QPen, QImage,
- QIcon)
-from PyQt5.QtWidgets import (QApplication, QLabel, QInputDialog,
- QDialog, QAbstractItemView, QVBoxLayout,
- QGridLayout, QPushButton, QMessageBox,
- QFileDialog, QWidget)
-from PyQt5.QtChart import (QChartView, QLegend, QLineSeries,
- QPolarChart, QScatterSeries, QValueAxis,
- QChart)
-from model import JS06Settings
-
-
-class EfficiencyChart(QChartView):
-
- def __init__(self, parent: QWidget):
- super().__init__(parent)
- self.setMaximumSize(600, 400)
-
- series = QLineSeries()
-
- load = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 30]
- eff = [0, 0.5726, 0.7321, 0.7978, 0.8398, 0.8616, 0.8798,
- 0.8903, 0.9002, 0.9062, 0.9127, 0.9267, 0.9379, 0.9430, 0.9448]
-
- for i, e in zip(load, eff):
- series.append(QPointF(float(i), float(e) * 100))
-
- chart = QChart()
- chart.addSeries(series)
- chart.setTitle('Efficiency vs. Load')
- chart.setAnimationOptions(QChart.SeriesAnimations)
-
- axisX = QValueAxis()
- axisX.setRange(0, 30)
- axisX.setLabelFormat("%.1f")
- axisX.setTickCount(7)
-
- axisY = QValueAxis()
- axisY.setRange(0, 100)
- axisY.setLabelFormat("%d")
- axisY.setMinorTickCount(5)
-
- chart.addAxis(axisX, Qt.AlignBottom)
- chart.addAxis(axisY, Qt.AlignLeft)
-
- series.attachAxis(axisX)
- series.attachAxis(axisY)
-
- chart.legend().setVisible(False)
\ No newline at end of file
diff --git a/src/nd01.py b/src/nd01.py
index 5cce687..e7ba219 100644
--- a/src/nd01.py
+++ b/src/nd01.py
@@ -19,8 +19,8 @@
from PyQt5.QtGui import (QPixmap, QIcon, QPainter,
QColor)
-from PyQt5.QtWidgets import (QMainWindow, QWidget, QGraphicsScene,
- QFrame, QVBoxLayout, QStyle)
+from PyQt5.QtWidgets import (QMainWindow, QWidget, QFrame,
+ QVBoxLayout)
from PyQt5.QtCore import (Qt, pyqtSlot, pyqtSignal,
QRect, QTimer, QObject,
QThread, QPointF, QDateTime)
@@ -30,10 +30,9 @@
from PyQt5 import uic
from login_view import LoginWindow
-from video_thread_mp import video_read, video_write, producer
+from video_thread_mp import producer
from nd01_settings import ND01SettingWidget
from model import JS06Settings
-from save_db import main
from controller import JS08MainCtrl
@@ -86,86 +85,6 @@ def tickStrings(self, values, scale, spacing):
return [time.strftime("%H:%M:%S", time.localtime(local_time)) for local_time in values]
-# class PlotWidget(QWidget):
-#
-# def __init__(self, parent=None):
-# QWidget.__init__(self, parent)
-#
-# self.pw = pg.PlotWidget(
-# labels={'left': 'Visibility (km)'},
-# axisItems={'bottom': TimeAxisItem(orientation='bottom')}
-# )
-# # self.setMaximumSize(600, 400)
-#
-# self.pw.showGrid(x=True, y=True)
-# self.pdi = self.pw.plot(pen='w') # PlotDataItem obj 반환.
-#
-# self.p1 = self.pw.plotItem
-#
-# self.p2 = pg.ViewBox()
-# self.p1.showAxis('right')
-# self.p1.scene().addItem(self.p2)
-# self.p1.getAxis('right').linkToView(self.p2)
-# self.p2.setXLink(self.p1)
-# self.p1.getAxis('right').setLabel('Axis 2', color='#ffff00')
-#
-# self.p2.setGeometry(self.p1.vb.sceneBoundingRect())
-# # self.p2.addItem(pg.PlotCurveItem([10, 20, 40, 80, 400, 2000], pen='y'))
-# # self.pdi.sigClicked.connect(self.onclick)
-#
-# self.plotData = {'x': [], 'y': []}
-# self.pre_plotData = {'x': [], 'y': []}
-#
-# def update_plot(self, new_time_data: int):
-# self.plotData['x'].append(new_time_data)
-# self.plotData['y'].append(np.random.randint(10000, 15000))
-#
-# # 항상 x축 시간을 설정한 범위 ( -3 시간 전 ~ 10 분 후 )만 보여줌.
-# # self.pw.setXRange(new_time_data - 3600 * 3, new_time_data + 600, padding=0)
-# # self.pw.setYRange(-1, 21, padding=0)
-#
-# data = []
-# for i in self.plotData['y']:
-# i = i / 1000
-# data.append(i)
-# self.pdi.setData(self.plotData['x'], data)
-#
-# # self.pdi.setData([1643873584, 1643873585, 1643873586, 1643873587, 1643873588],
-# # [12, 11, 10, 14, 13])
-#
-# def onclick(self, plot, points):
-# for point in points:
-# print(point.pos())
-
-
-# class PolarWidget(QWidget):
-#
-# def __init__(self, parent=None):
-# QWidget.__init__(self, parent)
-#
-# self.pw = pg.plot(
-# labels={'left': 'Visibility (km)'},
-# axisItems={'bottom': TimeAxisItem(orientation='bottom')}
-# )
-#
-# self.pw.showGrid(x=True, y=True)
-# self.pdi = self.pw.plot(pen='w')
-#
-# self.pw.addLine(x=0, pen=0.2)
-# self.pw.addLine(y=0, pen=0.2)
-# for r in range(2, 20, 2):
-# circle = pg.QtGui.QGraphicsEllipseItem(-r, -r, r * 2, r * 2)
-# circle.setPen(pg.mkPen(0.2))
-# self.pw.addItem(circle)
-#
-# theta = np.linspace(0, 2 * np.pi, 8)
-# radius = np.random.normal(loc=10, size=8)
-#
-# x = radius * np.cos(theta)
-# y = radius * np.sin(theta)
-# self.pw.plot(x, y)
-
-
class VisibilityView(QChartView):
def __init__(self, parent: QWidget, maxlen: int):
@@ -535,8 +454,8 @@ def clock(self, data):
p_vis = np.random.randint(10000, 15000)
if self.km_mile_convert:
- self.c_vis_label.setText(f'{format(round(vis / 1609, 2), ",")} mile')
- self.p_vis_label.setText(f'{format(round(p_vis / 1609, 2), ",")} mile')
+ self.c_vis_label.setText(f'{format(round(vis / 1609, 2), ",")} NM')
+ self.p_vis_label.setText(f'{format(round(p_vis / 1609, 2), ",")} NM')
elif self.km_mile_convert is False:
self.c_vis_label.setText(f'{format(vis, ",")} m')
@@ -665,22 +584,14 @@ def decompose_front_targets(self, _: str):
mp.freeze_support()
q = Queue()
_q = Queue()
- # _qr = Queue()
- # _qw = Queue()
_producer = producer
- # _read = video_read
- # _write = video_write
p = Process(name='clock', target=clock, args=(q,), daemon=True)
_p = Process(name='producer', target=_producer, args=(_q,), daemon=True)
- # _r = Process(name='read', target=_read, args=(_qr,), daemon=True)
- # _w = Process(name='write', target=_write, args=(_qw,), daemon=True)
p.start()
_p.start()
- # _r.start()
- # _w.start()
os.makedirs(f'{JS06Settings.get("data_csv_path")}', exist_ok=True)
os.makedirs(f'{JS06Settings.get("target_csv_path")}', exist_ok=True)
diff --git a/src/nd01_settings.py b/src/nd01_settings.py
index 13d8021..bbb19b1 100644
--- a/src/nd01_settings.py
+++ b/src/nd01_settings.py
@@ -9,17 +9,24 @@
import os
import traceback
import cv2
+import numpy as np
import pandas as pd
+from scipy.optimize import curve_fit
+
from PyQt5 import uic
-from PyQt5.QtCore import (QPoint, QRect, Qt)
+from PyQt5.QtCore import (QPoint, QRect, Qt,
+ QPointF)
from PyQt5.QtGui import (QPixmap, QPainter, QBrush,
QColor, QPen, QImage,
- QIcon)
+ QIcon, QFont, QPalette,
+ QLinearGradient, qRgb)
from PyQt5.QtWidgets import (QApplication, QInputDialog, QDialog,
- QMessageBox, QFileDialog)
+ QMessageBox, QFileDialog, QHeaderView,
+ QTableWidget, QTableWidgetItem, QLabel)
+from PyQt5.QtChart import QChart, QChartView, QLineSeries, QValueAxis
from model import JS06Settings
-from efficiency_chart import EfficiencyChart
+import target_info
from auto_file_delete import FileAutoDelete
@@ -29,7 +36,7 @@ def __init__(self):
super().__init__()
ui_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
- "resources/settings.ui")
+ "resources/setting_window.ui")
uic.loadUi(ui_path, self)
self.showFullScreen()
self.setWindowFlag(Qt.FramelessWindowHint)
@@ -55,12 +62,22 @@ def __init__(self):
self.cp_image = None
self.end_drawing = None
- self.current_camera = ""
+ self.current_camera = "PNM_9022V"
self.image_load()
+ self.get_target(self.current_camera)
+
+ # Add QChart Widget in value_verticalLayout
+ if len(self.distance) > 4:
+ self.chart_view = self.chart_draw()
+ self.value_verticalLayout.addWidget(self.chart_view)
- self.efficiencyChart = EfficiencyChart(self)
- self.value_verticalLayout.addWidget(self.efficiencyChart)
+ if len(self.left_range) > 0:
+ self.show_target_table()
+
+ self.red_checkBox.clicked.connect(self.chart_update)
+ self.green_checkBox.clicked.connect(self.chart_update)
+ self.blue_checkBox.clicked.connect(self.chart_update)
self.flip_button.clicked.connect(self.camera_flip)
self.flip_button.enterEvent = self.btn_on
@@ -89,6 +106,171 @@ def __init__(self):
self.buttonBox.accepted.connect(self.accept_click)
self.buttonBox.rejected.connect(self.reject)
+ def show_target_table(self):
+ min_x = []
+ min_y = []
+
+ copy_image = self.cp_image.copy()
+ row_count = len(self.distance)
+ self.tableWidget.setRowCount(row_count)
+ self.tableWidget.setColumnCount(3)
+ self.tableWidget.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
+
+ for upper_left, lower_right in zip(self.left_range, self.right_range):
+ result = target_info.minrgb(upper_left, lower_right, copy_image)
+ min_x.append(result[0])
+ min_y.append(result[1])
+
+ self.r_list.append(copy_image[result[1], result[0], 0])
+ self.g_list.append(copy_image[result[1], result[0], 1])
+ self.b_list.append(copy_image[result[1], result[0], 2])
+
+ for i in range(0, row_count):
+ crop_image = copy_image[min_y[i] - 50: min_y[i] + 50, min_x[i] - 50: min_x[i] + 50, :].copy()
+ item = self.getImageLabel(crop_image)
+ self.tableWidget.setCellWidget(i, 0, item)
+
+ item2 = QTableWidgetItem(f'target {i + 1}')
+ item2.setTextAlignment(Qt.AlignVCenter | Qt.AlignHCenter)
+ item2.setForeground(QBrush(QColor(255, 255, 255)))
+ self.tableWidget.setItem(i, 1, item2)
+
+ item3 = QTableWidgetItem(f'{self.distance[i]} km')
+ item3.setTextAlignment(Qt.AlignVCenter | Qt.AlignHCenter)
+ item3.setForeground(QBrush(QColor(255, 255, 255)))
+ self.tableWidget.setItem(i, 2, item3)
+
+ def getImageLabel(self, image):
+ imageLabel = QLabel()
+ imageLabel.setScaledContents(True)
+ height, width, channel = image.shape
+ bytesPerLine = channel * width
+
+ qImage = QImage(image.data.tobytes(), 100, 100, bytesPerLine, QImage.Format_RGB888)
+ imageLabel.setPixmap(QPixmap.fromImage(qImage))
+
+ return imageLabel
+
+ def func(self, x, c1, c2, a):
+ return c2 + (c1 - c2) * np.exp(-a * x)
+
+ def chart_update(self):
+ if self.value_verticalLayout.count() == 0:
+ self.chart_view = self.chart_draw()
+ self.value_verticalLayout.addWidget(self.chart_view)
+ else:
+ new_chart_view = self.chart_draw()
+ self.value_verticalLayout.removeWidget(self.chart_view)
+ self.value_verticalLayout.addWidget(new_chart_view, 0)
+ self.value_verticalLayout.update()
+ self.chart_view = new_chart_view
+
+ def chart_draw(self):
+ """세팅창 그래프 칸에 소산계수 차트를 그리는 함수"""
+
+ # self.distance = [0.22, 1.6, 2.5, 6.0, 20.0]
+ # self.distance = [0.22, 1.6, 2.5, 6.0, 20.0]
+ self.x = np.linspace(self.distance[0], self.distance[-1], 100, endpoint=True)
+ self.x.sort()
+
+ self.r_list = [13, 43, 71, 67, 82]
+ self.g_list = [9, 27, 76, 71, 114]
+ self.b_list = [9, 23, 87, 82, 149]
+
+ hanhwa_opt_r, hanhwa_cov_r = curve_fit(self.func, self.distance, self.r_list, maxfev=5000)
+ hanhwa_opt_g, hanhwa_cov_g = curve_fit(self.func, self.distance, self.g_list, maxfev=5000)
+ hanhwa_opt_b, hanhwa_cov_b = curve_fit(self.func, self.distance, self.b_list, maxfev=5000)
+
+ # chart object
+ chart = QChart()
+ font = QFont()
+ font.setPixelSize(20)
+ font.setBold(3)
+ # palette = QPalette()
+ # palette.setColor(QPalette.Window, QColor('red'))
+ chart.setTitleFont(font)
+ # chart.setPalette(palette)
+ chart.setTitle('Extinction coefficient Graph')
+ chart.setAnimationOptions(QChart.SeriesAnimations)
+ chart.setBackgroundBrush(QBrush(QColor(255, 255, 255)))
+
+ # backgroundGradient = QLinearGradient()
+ # backgroundGradient.setStart(QPointF(0, 0))
+ # backgroundGradient.setFinalStop(QPointF(0, 1))
+ # backgroundGradient.setColorAt(0.0, qRgb(255, 0, 0))
+ # backgroundGradient.setColorAt(1.0, qRgb(0, 255, 0))
+ # chart.setBackgroundBrush(backgroundGradient)
+
+ # chart.createDefaultAxes()
+ axis_x = QValueAxis()
+ axis_x.setTickCount(7)
+ axis_x.setLabelFormat('%i')
+ axis_x.setTitleText('Distance(km)')
+ axis_x.setRange(0, 20)
+ chart.addAxis(axis_x, Qt.AlignBottom)
+
+ axis_y = QValueAxis()
+ axis_y.setTickCount(7)
+ axis_y.setLabelFormat('%i')
+ axis_y.setTitleText('Intensity')
+ axis_y.setRange(0, 255)
+ chart.addAxis(axis_y, Qt.AlignLeft)
+
+ # Red Graph
+ if self.red_checkBox.isChecked():
+ series1 = QLineSeries()
+ series1.setName('Red')
+ pen = QPen()
+ pen.setWidth(2)
+ series1.setPen(pen)
+ series1.setColor(QColor('Red'))
+
+ for dis in self.x:
+ series1.append(*(dis, self.func(dis, *hanhwa_opt_r)))
+ chart.addSeries(series1) # data feeding
+ series1.attachAxis(axis_x)
+ series1.attachAxis(axis_y)
+
+ # Green Graph
+ if self.green_checkBox.isChecked():
+ series2 = QLineSeries()
+ series2.setName('Green')
+ pen = QPen()
+ pen.setWidth(2)
+ series2.setPen(pen)
+ series2.setColor(QColor('Green'))
+ for dis in self.x:
+ series2.append(*(dis, self.func(dis, *hanhwa_opt_g)))
+ chart.addSeries(series2) # data feeding
+
+ series2.attachAxis(axis_x)
+ series2.attachAxis(axis_y)
+
+ # Blue Graph
+ if self.blue_checkBox.isChecked():
+ series3 = QLineSeries()
+ series3.setName('Blue')
+ pen = QPen()
+ pen.setWidth(2)
+ series3.setPen(pen)
+ series3.setColor(QColor('Blue'))
+ for dis in self.x:
+ series3.append(*(dis, self.func(dis, *hanhwa_opt_b)))
+ chart.addSeries(series3) # data feeding
+
+ series3.attachAxis(axis_x)
+ series3.attachAxis(axis_y)
+
+ chart.legend().setAlignment(Qt.AlignRight)
+
+ # displaying chart
+ chart.setBackgroundBrush(QBrush(QColor(22, 32, 42)))
+ chart_view = QChartView(chart)
+ chart_view.setRenderHint(QPainter.Antialiasing)
+ chart_view.setMaximumSize(800, 500)
+
+ return chart_view
+
def camera_flip(self):
if self.cam_flag:
self.cam_flag = False
@@ -250,6 +432,12 @@ def lbl_mouseReleaseEvent(self, event):
self.distance.append(text)
# self.min_xy = self.minrgb(self.upper_left, self.lower_right)
self.target_name.append("target_" + str(len(self.left_range)))
+
+ print(self.left_range)
+ print(self.right_range)
+ print(self.distance)
+ print(self.target_name)
+
self.save_target(self.current_camera)
self.isDrawing = False
self.end_drawing = True
diff --git a/src/resources/settings.ui b/src/resources/setting_window.ui
similarity index 88%
rename from src/resources/settings.ui
rename to src/resources/setting_window.ui
index 0eee323..4518e69 100644
--- a/src/resources/settings.ui
+++ b/src/resources/setting_window.ui
@@ -180,19 +180,13 @@ color: #ffffff;
-
-
-
+
+
- 0
- 250
+ 700
+ 500
-
- background-color:rgb(25,39,52);
-
-
-
-
@@ -468,6 +462,99 @@ color: #ffffff;
+ -
+
+
+ 0
+
+
+ QLayout::SetMaximumSize
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ Noto Sans
+ 10
+
+
+
+ background-color:rgb(27,49,70);
+color: #ffffff;
+
+
+ Graph setting:
+
+
+
+ -
+
+
+
+ Noto Sans
+ 50
+ false
+
+
+
+ color: rgb(255, 255, 255);
+
+
+ Red
+
+
+ true
+
+
+
+ -
+
+
+
+ Noto Sans
+ 50
+ false
+
+
+
+ color: rgb(255, 255, 255);
+
+
+ Green
+
+
+ true
+
+
+
+ -
+
+
+
+ Noto Sans
+ 50
+ false
+
+
+
+ color: rgb(255, 255, 255);
+
+
+ Blue
+
+
+ true
+
+
+
+
+
-
diff --git a/src/target_info.py b/src/target_info.py
new file mode 100644
index 0000000..6ce96ea
--- /dev/null
+++ b/src/target_info.py
@@ -0,0 +1,151 @@
+#!/usr/bin/env python3
+import os
+import pandas as pd
+import numpy as np
+
+import cv2
+import datetime
+import time
+import cal_ext_coef
+
+
+def minprint(epoch, left_range, right_range, distance, cv_img):
+ """A function that outputs pixels for calculating the dissipation coefficient in the specified areas"""
+ cp_image = cv_img.copy()
+ cnt = 1
+ min_x = []
+ min_y = []
+
+ for upper_left, lower_right in zip(left_range, right_range):
+ result = minrgb(upper_left, lower_right, cp_image)
+ min_x.append(result[0])
+ min_y.append(result[1])
+ cnt += 1
+
+ visibility = get_rgb(epoch, min_x, min_y, cp_image, distance)
+ return visibility
+
+
+def minrgb(upper_left, lower_right, cp_image):
+ """Extracts the minimum RGB value of the dragged area"""
+
+ up_y = min(upper_left[1], lower_right[1])
+ down_y = max(upper_left[1], lower_right[1])
+
+ left_x = min(upper_left[0], lower_right[0])
+ right_x = max(upper_left[0], lower_right[0])
+
+ test = cp_image[up_y:down_y, left_x:right_x, :]
+
+ r = test[:, :, 0]
+ g = test[:, :, 1]
+ b = test[:, :, 2]
+
+ r = np.clip(r, 0, 765)
+ sum_rgb = r + g + b
+
+ t_idx = np.where(sum_rgb == np.min(sum_rgb))
+
+ show_min_y = t_idx[0][0] + up_y
+ show_min_x = t_idx[1][0] + left_x
+
+ return show_min_x, show_min_y
+
+
+def get_rgb(epoch: str, min_x, min_y, cp_image, distance):
+ """Gets the RGB values of the coordinates."""
+ r_list = []
+ g_list = []
+ b_list = []
+
+ for x, y in zip(min_x, min_y):
+ r_list.append(cp_image[y, x, 0])
+ g_list.append(cp_image[y, x, 1])
+ b_list.append(cp_image[y, x, 2])
+
+ visibility = save_rgb(r_list, g_list, b_list, epoch, distance)
+ return visibility
+
+
+def save_rgb(r_list, g_list, b_list, epoch, distance):
+ """Save the rgb information for each target."""
+ try:
+ save_path = os.path.join(f"rgb/PNM_9030V")
+ os.mkdir(save_path)
+
+ except:
+ pass
+
+ if r_list:
+ r_list = list(map(int, r_list))
+ g_list = list(map(int, g_list))
+ b_list = list(map(int, b_list))
+
+ col = ["target_name", "r", "g", "b", "distance"]
+ result = pd.DataFrame(columns=col)
+ result["target_name"] = [f"target_{num}" for num in range(1, len(r_list) + 1)]
+ result["r"] = r_list
+ result["g"] = g_list
+ result["b"] = b_list
+ result["distance"] = distance
+ result.to_csv(f"{save_path}/{epoch}.csv", mode="w", index=False)
+ list1, list2, list3, select_color = cal_ext_coef.cal_curve(result)
+ visibility = extinc_print(list1, list2, list3, select_color)
+ print(result)
+ print("Save rgb")
+
+ return visibility
+
+
+def extinc_print(c1_list: list = [0, 0, 0], c2_list: list = [0, 0, 0], alp_list: list = [0, 0, 0],
+ select_color: str = ""):
+ """Select an appropriate value among visibility by wavelength."""
+
+ if select_color == "red":
+ visibility = visibility_print(alp_list[0])
+ elif select_color == "green":
+ visibility = visibility_print(alp_list[1])
+ else:
+ visibility = visibility_print(alp_list[2])
+
+ return visibility
+
+
+def visibility_print(ext_g: float = 0.0):
+ """Print the visibility"""
+
+ vis_value = (3.912 / ext_g)
+ if vis_value > 20:
+ vis_value = 20
+ elif vis_value < 0.01:
+ vis_value = 0.01
+
+ vis_value_str = f"{vis_value:.3f}"
+ return vis_value_str
+
+
+def get_target(camera_name: str):
+ """Retrieves target information of a specific camera."""
+
+ save_path = os.path.join(f"target/{camera_name}")
+ # print("Get target information")
+ if os.path.isfile(f"{save_path}/{camera_name}.csv"):
+ target_df = pd.read_csv(f"{save_path}/{camera_name}.csv")
+ target_name = target_df["target_name"].tolist()
+ left_range = target_df["left_range"].tolist()
+ left_range = str_to_tuple(left_range)
+ right_range = target_df["right_range"].tolist()
+ right_range = str_to_tuple(right_range)
+ distance = target_df["distance"].tolist()
+ return target_name, left_range, right_range, distance
+ else:
+ # print("Target Information Not Found")
+ return [], [], [], []
+
+
+def str_to_tuple(before_list):
+ """A function that converts the tuple list, which is the location information of the stored targets,
+ into a string and converts it back into a tuple form."""
+ tuple_list = [i.split(',') for i in before_list]
+ tuple_list = [(int(i[0][1:]), int(i[1][:-1])) for i in tuple_list]
+ return tuple_list
diff --git a/src/video_thread_mp.py b/src/video_thread_mp.py
index 5174153..2d3c954 100644
--- a/src/video_thread_mp.py
+++ b/src/video_thread_mp.py
@@ -13,8 +13,8 @@
import numpy as np
import pandas as pd
-import curve_save
from model import JS06Settings
+import target_info
def producer(q):
@@ -28,6 +28,12 @@ def producer(q):
if epoch[-2:] == '00':
try:
+ target_name, left_range, right_range, distance = target_info.get_target("PNM_9030V")
+ if len(left_range) < 4:
+ continue
+ else:
+ pass
+
image_save_path = JS06Settings.get('image_save_path')
os.makedirs(f'{image_save_path}/vista/{date}', exist_ok=True)
os.makedirs(f'{image_save_path}/resize/{date}', exist_ok=True)
@@ -46,6 +52,9 @@ def producer(q):
frame = cv2.resize(frame, (315, 131), interpolation=cv2.INTER_NEAREST) # Thumbnail size
cv2.imwrite(f'{image_save_path}/resize/{date}/{epoch}.jpg', cv2.resize(frame, (315, 131)))
+ visibility = target_info.minprint(epoch[:-2], left_range, right_range, distance, frame)
+ q.put(visibility)
+
time.sleep(1)
cap.release()
cap = cv2.VideoCapture("rtsp://admin:sijung5520@192.168.100.100/profile2/media.smp")
@@ -58,155 +67,3 @@ def producer(q):
cv2.destroyAllWindows()
else:
print('cap closed')
-
-
-def minprint(epoch, left_range, right_range, distance, cv_img):
- """A function that outputs pixels for calculating the dissipation coefficient in the specified areas"""
- print("minprint 시작")
- # epoch = time.strftime("%Y%m%d%H%M", time.localtime(time.time()))
- cp_image = cv_img.copy()
- result = ()
- cnt = 1
- min_x = []
- min_y = []
-
- for upper_left, lower_right in zip(left_range, right_range):
- result = minrgb(upper_left, lower_right, cp_image)
- min_x.append(result[0])
- min_y.append(result[1])
- cnt += 1
-
- visibility = get_rgb(epoch, min_x, min_y, cp_image, distance)
- return visibility
-
-
-def minrgb(upper_left, lower_right, cp_image):
- """Extracts the minimum RGB value of the dragged area"""
-
- up_y = min(upper_left[1], lower_right[1])
- down_y = max(upper_left[1], lower_right[1])
-
- left_x = min(upper_left[0], lower_right[0])
- right_x = max(upper_left[0], lower_right[0])
-
- test = cp_image[up_y:down_y, left_x:right_x, :]
-
- r = test[:, :, 0]
- g = test[:, :, 1]
- b = test[:, :, 2]
-
- r = np.clip(r, 0, 765)
- sum_rgb = r + g + b
-
- t_idx = np.where(sum_rgb == np.min(sum_rgb))
-
- print("red : ", cp_image[t_idx[0][0] + up_y, t_idx[1][0] + left_x, 0])
- print("green : ", cp_image[t_idx[0][0] + up_y, t_idx[1][0] + left_x, 1])
- print("blue : ", cp_image[t_idx[0][0] + up_y, t_idx[1][0] + left_x, 2])
- show_min_y = t_idx[0][0] + up_y
- show_min_x = t_idx[1][0] + left_x
-
- return show_min_x, show_min_y
-
-
-def get_rgb(epoch: str, min_x, min_y, cp_image, distance):
- """Gets the RGB values of the coordinates."""
- r_list = []
- g_list = []
- b_list = []
-
- for x, y in zip(min_x, min_y):
- r_list.append(cp_image[y, x, 0])
- g_list.append(cp_image[y, x, 1])
- b_list.append(cp_image[y, x, 2])
-
- print("red list : ", r_list)
- print("green list : ", g_list)
- print("blue list : ", b_list)
-
- visibility = save_rgb(r_list, g_list, b_list, epoch, distance)
- return visibility
-
-
-def save_rgb(r_list, g_list, b_list, epoch, distance):
- """Save the rgb information for each target."""
- try:
- save_path = os.path.join(f"rgb/PNM_9030V")
- os.mkdir(save_path)
-
- except Exception as e:
- pass
-
- if r_list:
- r_list = list(map(int, r_list))
- g_list = list(map(int, g_list))
- b_list = list(map(int, b_list))
-
- col = ["target_name", "r", "g", "b", "distance"]
- result = pd.DataFrame(columns=col)
- result["target_name"] = [f"target_{num}" for num in range(1, len(r_list) + 1)]
- result["r"] = r_list
- result["g"] = g_list
- result["b"] = b_list
- result["distance"] = distance
- result.to_csv(f"{save_path}/{epoch}.csv", mode="w", index=False)
- list1, list2, list3, select_color = curve_save.cal_curve(result)
- visibility = extinc_print(list1, list2, list3, select_color)
- print(result)
- print("Save rgb")
-
- return visibility
-
-
-def extinc_print(c1_list: list = [0, 0, 0], c2_list: list = [0, 0, 0], alp_list: list = [0, 0, 0],
- select_color: str = ""):
- """Select an appropriate value among visibility by wavelength."""
- g_ext = round(alp_list[1], 1)
-
- if select_color == "red":
- visibility = visibility_print(alp_list[0])
- elif select_color == "green":
- visibility = visibility_print(alp_list[1])
- else:
- visibility = visibility_print(alp_list[2])
-
- return visibility
-
-
-def visibility_print(ext_g: float = 0.0):
- """Print the visibility"""
- vis_value = 0
-
- vis_value = (3.912 / ext_g)
- if vis_value > 20:
- vis_value = 20
- elif vis_value < 0.01:
- vis_value = 0.01
-
- # self.data_storage(vis_value)
- vis_value_str = f"{vis_value:.2f}" + " km"
- return vis_value_str
-
-
-def get_target(camera_name: str):
- """Retrieves target information of a specific camera."""
-
- save_path = os.path.join(f"target/{camera_name}")
- print("Get target information")
- if os.path.isfile(f"{save_path}/{camera_name}.csv"):
- target_df = pd.read_csv(f"{save_path}/{camera_name}.csv")
- target_name = target_df["target_name"].tolist()
- left_range = target_df["left_range"].tolist()
- left_range = str_to_tuple(left_range)
- right_range = target_df["right_range"].tolist()
- right_range = str_to_tuple(right_range)
- distance = target_df["distance"].tolist()
- return left_range, right_range, distance
-
-
-def str_to_tuple(before_list):
- """A function that converts the tuple list, which is the location information of the stored targets,
- into a string and converts it back into a tuple form."""
- tuple_list = [i.split(',') for i in before_list]
- tuple_list = [(int(i[0][1:]), int(i[1][:-1])) for i in tuple_list]
- return tuple_list
From 0827135d05c03566cecc3634b6aac7879e447eed Mon Sep 17 00:00:00 2001
From: Oh-JongJin <5jx2oh@gmail.com>
Date: Tue, 8 Mar 2022 10:30:11 +0900
Subject: [PATCH 18/23] Version 0.1
---
src/nd01.py | 2 +-
src/nd01_settings.py | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/nd01.py b/src/nd01.py
index e7ba219..9258e74 100644
--- a/src/nd01.py
+++ b/src/nd01.py
@@ -599,4 +599,4 @@ def decompose_front_targets(self, _: str):
app = QApplication(sys.argv)
window = ND01MainWindow(q)
- sys.exit(app.exec_())
+ sys.exit(app.exec())
diff --git a/src/nd01_settings.py b/src/nd01_settings.py
index bbb19b1..6dd23fd 100644
--- a/src/nd01_settings.py
+++ b/src/nd01_settings.py
@@ -73,6 +73,7 @@ def __init__(self):
self.value_verticalLayout.addWidget(self.chart_view)
if len(self.left_range) > 0:
+ self.tableWidget.setEditTriggers(QTableWidget.NoEditTriggers)
self.show_target_table()
self.red_checkBox.clicked.connect(self.chart_update)
From 62e1edd0c398a3458e78f8c394766db788aa50a1 Mon Sep 17 00:00:00 2001
From: Oh-JongJin <5jx2oh@gmail.com>
Date: Tue, 15 Mar 2022 17:13:12 +0900
Subject: [PATCH 19/23] Add azimuth angle image line in setting view
---
src/cal_ext_coef.py | 34 +++---
src/controller.py | 6 +-
src/curve_thread.py | 35 ++++++
src/nd01.py | 221 ++++++++++++++++++++++-------------
src/nd01_settings.py | 70 ++++++++---
src/resources/main_window.ui | 4 +-
src/target_info.py | 62 +++++-----
src/test.png | Bin 0 -> 10899673 bytes
src/video_thread_mp.py | 108 ++++++++++-------
9 files changed, 344 insertions(+), 196 deletions(-)
create mode 100644 src/curve_thread.py
create mode 100644 src/test.png
diff --git a/src/cal_ext_coef.py b/src/cal_ext_coef.py
index 006a942..4caccf5 100644
--- a/src/cal_ext_coef.py
+++ b/src/cal_ext_coef.py
@@ -3,6 +3,7 @@
import numpy as np
import pandas as pd
+import traceback
import scipy
from scipy.optimize import curve_fit
@@ -21,18 +22,19 @@ def select_max_rgb(r, g, b):
c_index = c_list.index(max(c_list))
if c_index == 0:
- select_color = "red"
+ select_color = 'red'
elif c_index == 1:
- select_color = "green"
+ select_color = 'green'
else:
- select_color = "blue"
+ select_color = 'blue'
+ select_color = 'green'
return select_color
def cal_curve(hanhwa: pd.DataFrame):
# hanhwa = pd.read_csv(f"{rgbsavedir}/{epoch}.csv")
- print(hanhwa)
+ # print(hanhwa)
hanhwa = hanhwa.sort_values(by=['distance'])
hanhwa_dist = hanhwa[['distance']].squeeze().to_numpy()
hanhwa_x = np.linspace(hanhwa_dist[0], hanhwa_dist[-1], 100, endpoint=True)
@@ -41,10 +43,6 @@ def cal_curve(hanhwa: pd.DataFrame):
hanhwa_g = hanhwa[['g']].squeeze().to_numpy()
hanhwa_b = hanhwa[['b']].squeeze().to_numpy()
- print("오리지날 green", hanhwa_g)
-
- print("소산계수 산출용 green 리스트 : ", hanhwa_g)
-
r1_init = hanhwa_r[0] * 0.7
g1_init = hanhwa_g[0] * 0.7
b1_init = hanhwa_b[0] * 0.7
@@ -65,8 +63,8 @@ def cal_curve(hanhwa: pd.DataFrame):
hanhwa_opt_g, hanhwa_cov_g = curve_fit(func, hanhwa_dist, hanhwa_g, p0=g_ext_init, maxfev=5000)
hanhwa_opt_b, hanhwa_cov_b = curve_fit(func, hanhwa_dist, hanhwa_b, p0=b_ext_init, maxfev=5000)
- except Exception as e:
- print("error msg: ", e)
+ except Exception:
+ print(traceback.format_exc())
return
list1 = []
@@ -85,17 +83,17 @@ def cal_curve(hanhwa: pd.DataFrame):
list3.append(hanhwa_opt_g[2])
list3.append(hanhwa_opt_b[2])
- hanhwa_err_r = np.sqrt(np.diag(hanhwa_cov_r))
- hanhwa_err_g = np.sqrt(np.diag(hanhwa_cov_g))
- hanhwa_err_b = np.sqrt(np.diag(hanhwa_cov_b))
+ # hanhwa_err_r = np.sqrt(np.diag(hanhwa_cov_r))
+ # hanhwa_err_g = np.sqrt(np.diag(hanhwa_cov_g))
+ # hanhwa_err_b = np.sqrt(np.diag(hanhwa_cov_b))
- print_result(hanhwa_opt_r, hanhwa_opt_g, hanhwa_opt_b, hanhwa_err_r, hanhwa_err_g, hanhwa_err_b)
+ # print_result(hanhwa_opt_r, hanhwa_opt_g, hanhwa_opt_b, hanhwa_err_r, hanhwa_err_g, hanhwa_err_b)
- print(f"Red channel: {extcoeff_to_vis(hanhwa_opt_r[2], hanhwa_err_r[2], 3)} km")
- print(f"Green channel: {extcoeff_to_vis(hanhwa_opt_g[2], hanhwa_err_g[2], 3)} km")
- print(f"Blue channel: {extcoeff_to_vis(hanhwa_opt_b[2], hanhwa_err_b[2], 3)} km")
+ # print(f"Red channel: {extcoeff_to_vis(hanhwa_opt_r[2], hanhwa_err_r[2], 3)} km")
+ # print(f"Green channel: {extcoeff_to_vis(hanhwa_opt_g[2], hanhwa_err_g[2], 3)} km")
+ # print(f"Blue channel: {extcoeff_to_vis(hanhwa_opt_b[2], hanhwa_err_b[2], 3)} km")
- os.makedirs(extsavedir, exist_ok=True)
+ # os.makedirs(extsavedir, exist_ok=True)
return list1, list2, list3, select_color
diff --git a/src/controller.py b/src/controller.py
index 8ab9bdf..4268be7 100644
--- a/src/controller.py
+++ b/src/controller.py
@@ -140,7 +140,7 @@ def read_mask(self, path: str) -> np.ndarray:
def start_observation_timer(self) -> None:
print('DEBUG(start_observation_timer):', QTime.currentTime().toString())
- self.observation_timer.setInterval(1000) # every one second
+ self.observation_timer.setInterval(10000) # every 10 seconds
self.observation_timer.timeout.connect(self.start_worker)
self.observation_timer.start()
@@ -230,13 +230,13 @@ def wedge_visibility(self) -> dict:
wedge_vis = {w: None for w in Js08Wedge}
for t in self.front_simple_targets:
if t.discernment:
- if wedge_vis[t.wedge] == None:
+ if wedge_vis[t.wedge] is None:
wedge_vis[t.wedge] = t.distance
elif wedge_vis[t.wedge] < t.distance:
wedge_vis[t.wedge] = t.distance
for t in self.rear_simple_targets:
if t.discernment:
- if wedge_vis[t.wedge] == None:
+ if wedge_vis[t.wedge] is None:
wedge_vis[t.wedge] = t.distance
elif wedge_vis[t.wedge] < t.distance:
wedge_vis[t.wedge] = t.distance
diff --git a/src/curve_thread.py b/src/curve_thread.py
new file mode 100644
index 0000000..ed108fc
--- /dev/null
+++ b/src/curve_thread.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+#
+# Copyright 2021-2022 Sijung Co., Ltd.
+#
+# Authors:
+# cotjdals5450@gmail.com (Seong Min Chae)
+# 5jx2oh@gmail.com (Jongjin Oh)
+
+from multiprocessing import Queue
+
+from PyQt5.QtCore import QThread, pyqtSignal
+
+
+class CurveThread(QThread):
+ poped = pyqtSignal(str)
+
+ def __init__(self, _q: Queue = None):
+ super().__init__()
+ self._run_flag = False
+ self.q = _q
+
+ def run(self):
+
+ self._run_flag = True
+ while self._run_flag:
+ if not self.q.empty():
+ visibility = self.q.get()
+ self.poped.emit(visibility)
+ # shut down capture system
+
+ def stop(self):
+ """Sets run flag to False and waits for thread to finish"""
+ self._run_flag = False
+ self.quit()
+ self.wait()
diff --git a/src/nd01.py b/src/nd01.py
index 9258e74..3b84a1f 100644
--- a/src/nd01.py
+++ b/src/nd01.py
@@ -18,9 +18,9 @@
from multiprocessing import Process, Queue
from PyQt5.QtGui import (QPixmap, QIcon, QPainter,
- QColor)
+ QColor, QPaintEvent, QPen)
from PyQt5.QtWidgets import (QMainWindow, QWidget, QFrame,
- QVBoxLayout)
+ QVBoxLayout, QLabel)
from PyQt5.QtCore import (Qt, pyqtSlot, pyqtSignal,
QRect, QTimer, QObject,
QThread, QPointF, QDateTime)
@@ -34,6 +34,7 @@
from nd01_settings import ND01SettingWidget
from model import JS06Settings
from controller import JS08MainCtrl
+from curve_thread import CurveThread
def clock(queue):
@@ -69,20 +70,20 @@ def resume(self):
self.running = True
-class TimeAxisItem(pg.AxisItem):
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.setLabel(text='Time (sec)', units=None)
- self.enableAutoSIPrefix(False)
-
- def tickStrings(self, values, scale, spacing):
- """
- override 하여, tick 옆에 써지는 문자를 원하는대로 수정함.
- values --> x축 값들
- 숫자로 이루어진 Iterable data(하나씩 차례로 반환 가능한 object -> ex) List[int]) list, str, tuple 등등
- """
- return [time.strftime("%H:%M:%S", time.localtime(local_time)) for local_time in values]
+# class TimeAxisItem(pg.AxisItem):
+#
+# def __init__(self, *args, **kwargs):
+# super().__init__(*args, **kwargs)
+# self.setLabel(text='Time (sec)', units=None)
+# self.enableAutoSIPrefix(False)
+#
+# def tickStrings(self, values, scale, spacing):
+# """
+# override 하여, tick 옆에 써지는 문자를 원하는대로 수정함.
+# values --> x축 값들
+# 숫자로 이루어진 Iterable data(하나씩 차례로 반환 가능한 object -> ex) List[int]) list, str, tuple 등등
+# """
+# return [time.strftime("%H:%M:%S", time.localtime(local_time)) for local_time in values]
class VisibilityView(QChartView):
@@ -123,28 +124,33 @@ def __init__(self, parent: QWidget, maxlen: int):
axis_y.setTitleText('Distance (km)')
chart.setAxisY(axis_y, self.series)
- # data_point = [QPointF[t, v] for t, v in self.data]
- # self.series.append(data_point)
+ data_point = [QPointF(t, v) for t, v in self.data]
+ self.series.append(data_point)
- def refresh_stats(self):
- # wedge_vis_list = list(wedge_vis.values())
- # wedge_vis_list = [10, 10, 11, 12, 13, 14, 15]
- prev_vis = self.prevailing_visibility()
- epoch = QDateTime.currentSecsSinceEpoch()
+ # @pyqtSlot(int, dict)
+ def refresh_stats(self, epoch: int, vis_list: list):
+ # vis_list = list(vis.values())
+
+ if len(vis_list) == 0:
+ vis_list = [0]
+ prev_vis = self.prevailing_visibility(vis_list)
+ # epoch = QDateTime.currentSecsSinceEpoch()
self.data.append((epoch * 1000, prev_vis))
left = QDateTime.fromMSecsSinceEpoch(self.data[0][0])
right = QDateTime.fromMSecsSinceEpoch(self.data[-1][0])
self.chart().axisX().setRange(left, right)
- data_point = [QPointF[t, v] for t, v in self.data]
+ data_point = [QPointF(t, v) for t, v in self.data]
self.series.replace(data_point)
- def prevailing_visibility(self) -> float:
- wedge_vis = [10, 10, 11, 12, 13, 14, 15]
+ def prevailing_visibility(self, vis: list) -> float:
+ if None in vis:
+ return 0
- sorted_vis = sorted(wedge_vis, reverse=True)
+ sorted_vis = sorted(vis, reverse=True)
prevailing = sorted_vis[(len(sorted_vis) - 1) // 2]
+
return prevailing
@@ -193,22 +199,7 @@ def __init__(self, parent: QWidget):
chart.setAxisY(axis_y, self.negatives)
def refresh_stats(self):
- # negatives = [(0, 1.5), (0, 2), (0, 2.5), (0, 3), (0, 0.15), (0, 0.35), (0, 0.55), (0, 1), (0, 1.5), (0, 2),
- # (0, 2.5), (0, 3), (0, 0.15), (0, 0.35), (0, 0.55), (0, 1), (45, 1.5), (45, 2), (45, 2.5), (45, 3),
- # (45, 0.15), (45, 0.35), (45, 0.55), (45, 1), (45, 1.5), (45, 2), (45, 2.5), (45, 3), (45, 0.15),
- # (45, 0.35), (45, 0.55), (45, 1), (90, 1.5), (90, 2), (90, 2.5), (90, 3), (90, 0.15), (90, 0.35),
- # (90, 0.55), (90, 1), (90, 1.5), (90, 2), (90, 2.5), (90, 3), (90, 0.15), (90, 0.35), (90, 0.55),
- # (90, 1), (135, 1.5), (135, 2), (135, 2.5), (135, 3), (135, 0.15), (135, 0.35), (135, 0.55),
- # (135, 1), (135, 1.5), (135, 2), (135, 2.5), (135, 3), (135, 0.15), (135, 0.35), (135, 0.55),
- # (135, 1), (180, 1.5), (180, 2), (180, 2.5), (180, 3), (180, 0.15), (180, 0.35), (180, 0.55),
- # (180, 1), (180, 1.5), (180, 2), (180, 2.5), (180, 3), (180, 0.15), (180, 0.35), (180, 0.55),
- # (180, 1), (225, 1.5), (225, 2), (225, 2.5), (225, 3), (225, 0.15), (225, 0.35), (225, 0.55),
- # (225, 1), (225, 1.5), (225, 2), (225, 2.5), (225, 3), (225, 0.1), (225, 0.3), (225, 0.5),
- # (225, 1), (270, 1.5), (270, 2), (270, 2.5), (270, 3), (270, 0.15), (270, 0.35), (270, 0.55),
- # (270, 1), (270, 1.5), (270, 2), (270, 2.5), (270, 3), (270, 0.15), (270, 0.35), (270, 0.55),
- # (270, 1), (315, 1.5), (315, 2), (315, 2.5), (315, 3), (315, 0.15), (315, 0.35), (315, 0.55),
- # (315, 1), (315, 1.5), (315, 2), (315, 2.5), (315, 3), (315, 0.15), (315, 0.35), (315, 0.55),
- # (315, 1)]
+
positives = []
negatives = [(0, 4), (0, 9), (0, 14),
(45, 5), (45, 10), (45, 15),
@@ -232,7 +223,6 @@ def __init__(self, image_file_name: str, date: int):
ui_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
"resources/thumbnail_view.ui")
uic.loadUi(ui_path, self)
- print(f'{JS06Settings.get("image_save_path")}/vista/{date}/{image_file_name}.png')
self.front_image.setPixmap(
QPixmap(
@@ -248,18 +238,18 @@ def __init__(self, image_file_name: str, date: int):
class ND01MainWindow(QMainWindow):
- def __init__(self, q):
+ def __init__(self, q, _q):
super().__init__()
- login_window = LoginWindow()
- login_window.exec_()
+ # login_window = LoginWindow()
+ # login_window.exec_()
ui_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
"resources/main_window.ui")
uic.loadUi(ui_path, self)
self.showFullScreen()
- self._ctrl = JS08MainCtrl
+ # self._ctrl = JS08MainCtrl
self._plot = VisibilityView(self, 1440)
# self._ctrl.wedge_vis_ready.connect(self._plot.refresh_stats)
@@ -267,14 +257,18 @@ def __init__(self, q):
self._polar = DiscernmentView(self)
# self._ctrl.target_assorted.connect(self._polar.refresh_stats)
- self._polar.refresh_stats()
+ # self._polar.refresh_stats()
self.view = None
self.km_mile_convert = False
+ self.visibility = 0
+ self.pm_text = 0
self.date = None
+ self.q_list = []
+ self.q_list_scale = 300
self.front_video_widget = VideoWidget(self)
- self.front_video_widget.on_camera_change("rtsp://admin:sijung5520@192.168.100.101/profile2/media.smp")
+ self.front_video_widget.on_camera_change("rtsp://admin:sijung5520@192.168.100.100/profile2/media.smp")
self.rear_video_widget = VideoWidget(self)
self.rear_video_widget.on_camera_change("rtsp://admin:sijung5520@192.168.100.100/profile2/media.smp")
@@ -292,6 +286,10 @@ def __init__(self, q):
self.consumer.poped.connect(self.clock)
self.consumer.start()
+ self.video_thread = CurveThread(_q)
+ self.video_thread.poped.connect(self.print_data)
+ self.video_thread.start()
+
self.click_style = 'border: 1px solid red;'
self.alert.clicked.connect(self.alert_test)
@@ -335,9 +333,10 @@ def thumbnail_view(self, file_name: str):
self.video_horizontalLayout.geometry().height()))
self.view.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint)
self.view.setWindowModality(Qt.ApplicationModal)
- self.view.show()
+ self.view.raise_()
def thumbnail_click1(self, e):
+
name = self.label_1hour_time.text()[:2] + self.label_1hour_time.text()[3:]
epoch = time.strftime("%Y%m%d", time.localtime(time.time()))
self.thumbnail_view(epoch + name + "00")
@@ -436,12 +435,32 @@ def unit_convert(self, event):
elif self.km_mile_convert is False:
self.km_mile_convert = True
+ @pyqtSlot(str)
+ def print_data(self, visibility):
+ visibility_float = round(float(visibility), 3)
+
+ if len(self.q_list) == 0 or self.q_list_scale != len(self.q_list):
+ self.q_list = []
+ for i in range(self.q_list_scale):
+ self.q_list.append(visibility_float)
+ result_vis = np.mean(self.q_list)
+ else:
+ self.q_list.pop(0)
+ self.q_list.append(visibility_float)
+ result_vis = np.mean(self.q_list)
+
+ self.visibility = round(float(result_vis), 3)
+ # print(f'Visibility: {self.visibility} km = {int(self.visibility * 1000)} m')
+
@pyqtSlot(str)
def clock(self, data):
current_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(float(data)))
self.date = current_time[5:7] + current_time[8:10]
+ self.year_date = current_time[2:4] + current_time[5:7] + current_time[8:10]
self.real_time_label.setText(current_time)
+ self._plot.refresh_stats(QDateTime.currentSecsSinceEpoch(), self.q_list)
+
# self._plot.update_plot(int(float(data)))
# result = 0
@@ -450,27 +469,28 @@ def clock(self, data):
# p_vis_km = f'{format(round(int(result / len(self._plot.plotData["y"])), 2), ",")}'
# p_vis_nm = f'{format(round(int(result / len(self._plot.plotData["y"])) / 1609, 2), ",")}'
- vis = np.random.randint(10000, 15000)
- p_vis = np.random.randint(10000, 15000)
+ if self.visibility != 0:
+ ext = 3.912 / self.visibility
+ hd = 89
+ self.pm_text = round((ext * 1000 / 4 / 2.5) / (1 + 5.67 * ((hd / 100) ** 5.8)), 2)
if self.km_mile_convert:
- self.c_vis_label.setText(f'{format(round(vis / 1609, 2), ",")} NM')
- self.p_vis_label.setText(f'{format(round(p_vis / 1609, 2), ",")} NM')
+ self.c_vis_label.setText(f'{format(round(self.visibility / 1609, 2), ",")} mile')
elif self.km_mile_convert is False:
- self.c_vis_label.setText(f'{format(vis, ",")} m')
- self.p_vis_label.setText(f'{format(p_vis, ",")} m')
- #
+ self.c_vis_label.setText(f'{format(int(self.visibility * 1000), ",")} m')
+ self.p_vis_label.setText(f'{self.pm_text} ㎍/㎥')
+
# data_time = self._plot.plotData['x']
# if int(float(data)) - 3600 * 3 in self._plot.plotData['x']:
# index = data_time.index(int(float(data)) - 3600 * 3)
# self._plot.plotData['x'].pop(index)
# self._plot.plotData['y'].pop(index)
- #
+
self.thumbnail_refresh()
if current_time[-2:] == "00":
self.thumbnail_refresh()
- #
+
# if int(p_vis_km.replace(',', '')) <= JS06Settings.get('visibility_alert_limit'):
# self.alert.setIcon(QIcon('resources/asset/red.png'))
@@ -482,32 +502,39 @@ def thumbnail_refresh(self):
# five_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 3600 * 5))
# six_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 3600 * 6))
- one_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 60))
- two_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 60 * 2))
- three_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 60 * 3))
- four_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 60 * 4))
- five_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 60 * 5))
- six_hour_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 60 * 6))
-
- self.label_1hour_time.setText(time.strftime('%H:%M', time.localtime(time.time() - 3600)))
- self.label_2hour_time.setText(time.strftime('%H:%M', time.localtime(time.time() - 3600 * 2)))
- self.label_3hour_time.setText(time.strftime('%H:%M', time.localtime(time.time() - 3600 * 3)))
- self.label_4hour_time.setText(time.strftime('%H:%M', time.localtime(time.time() - 3600 * 4)))
- self.label_5hour_time.setText(time.strftime('%H:%M', time.localtime(time.time() - 3600 * 5)))
- self.label_6hour_time.setText(time.strftime('%H:%M', time.localtime(time.time() - 3600 * 6)))
+ one_min_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 60))
+ two_min_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 60 * 2))
+ three_min_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 60 * 3))
+ four_min_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 60 * 4))
+ five_min_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 60 * 5))
+ six_min_ago = time.strftime('%Y%m%d%H%M00', time.localtime(time.time() - 60 * 6))
+
+ # self.label_1hour_time.setText(time.strftime('%H:%M', time.localtime(time.time() - 3600)))
+ # self.label_2hour_time.setText(time.strftime('%H:%M', time.localtime(time.time() - 3600 * 2)))
+ # self.label_3hour_time.setText(time.strftime('%H:%M', time.localtime(time.time() - 3600 * 3)))
+ # self.label_4hour_time.setText(time.strftime('%H:%M', time.localtime(time.time() - 3600 * 4)))
+ # self.label_5hour_time.setText(time.strftime('%H:%M', time.localtime(time.time() - 3600 * 5)))
+ # self.label_6hour_time.setText(time.strftime('%H:%M', time.localtime(time.time() - 3600 * 6)))
+
+ self.label_1hour_time.setText(time.strftime('%H:%M', time.localtime(time.time() - 60)))
+ self.label_2hour_time.setText(time.strftime('%H:%M', time.localtime(time.time() - 60 * 2)))
+ self.label_3hour_time.setText(time.strftime('%H:%M', time.localtime(time.time() - 60 * 3)))
+ self.label_4hour_time.setText(time.strftime('%H:%M', time.localtime(time.time() - 60 * 4)))
+ self.label_5hour_time.setText(time.strftime('%H:%M', time.localtime(time.time() - 60 * 5)))
+ self.label_6hour_time.setText(time.strftime('%H:%M', time.localtime(time.time() - 60 * 6)))
self.label_1hour.setPixmap(
- QPixmap(f'{JS06Settings.get("image_save_path")}/resize/{self.date}/{one_hour_ago}.jpg'))
+ QPixmap(f'{JS06Settings.get("image_save_path")}/resize/PNM_9030V/{self.year_date}/{one_min_ago}.jpg'))
self.label_2hour.setPixmap(
- QPixmap(f'{JS06Settings.get("image_save_path")}/resize/{self.date}/{two_hour_ago}.jpg'))
+ QPixmap(f'{JS06Settings.get("image_save_path")}/resize/PNM_9030V/{self.year_date}/{two_min_ago}.jpg'))
self.label_3hour.setPixmap(
- QPixmap(f'{JS06Settings.get("image_save_path")}/resize/{self.date}/{three_hour_ago}.jpg'))
+ QPixmap(f'{JS06Settings.get("image_save_path")}/resize/PNM_9030V/{self.year_date}/{three_min_ago}.jpg'))
self.label_4hour.setPixmap(
- QPixmap(f'{JS06Settings.get("image_save_path")}/resize/{self.date}/{four_hour_ago}.jpg'))
+ QPixmap(f'{JS06Settings.get("image_save_path")}/resize/PNM_9030V/{self.year_date}/{four_min_ago}.jpg'))
self.label_5hour.setPixmap(
- QPixmap(f'{JS06Settings.get("image_save_path")}/resize/{self.date}/{five_hour_ago}.jpg'))
+ QPixmap(f'{JS06Settings.get("image_save_path")}/resize/PNM_9030V/{self.year_date}/{five_min_ago}.jpg'))
self.label_6hour.setPixmap(
- QPixmap(f'{JS06Settings.get("image_save_path")}/resize/{self.date}/{six_hour_ago}.jpg'))
+ QPixmap(f'{JS06Settings.get("image_save_path")}/resize/PNM_9030V/{self.year_date}/{six_min_ago}.jpg'))
def keyPressEvent(self, e):
"""Override function QMainwindow KeyPressEvent that works when key is pressed"""
@@ -516,6 +543,21 @@ def keyPressEvent(self, e):
if e.key() == Qt.Key_D:
self.showNormal()
+ # def paintEvent(self, event: QPaintEvent):
+ # qp = QPainter()
+ # qp.begin(self)
+ # self.drawLines(qp)
+ # qp.end()
+ #
+ # def drawLines(self, qp):
+ # pen = QPen(Qt.black, 20, Qt.SolidLine)
+ #
+ # qp.setPen(pen)
+ # qp.drawLine(240, 0, 240, 1070)
+ # qp.drawLine(480, 0, 480, 411)
+ # qp.drawLine(720, 0, 720, 411)
+ # print('paint')
+
class VideoWidget(QWidget):
"""Video stream player using QVideoWidget"""
@@ -537,6 +579,7 @@ def __init__(self, parent: QObject = None):
self.image_media = self.instance.media_list_new('')
self.video_frame = QFrame()
+ self.blank_label = QLabel()
if sys.platform == 'win32':
self.media_player.set_hwnd(self.video_frame.winId())
@@ -544,6 +587,20 @@ def __init__(self, parent: QObject = None):
layout = QVBoxLayout(self)
layout.addWidget(self.video_frame)
+ # def paintEvent(self, event: QPaintEvent):
+ # qp = QPainter()
+ # qp.begin(self)
+ # self.drawLines(qp)
+ # qp.end()
+ #
+ # def drawLines(self, qp):
+ # pen = QPen(Qt.white, 1, Qt.DotLine)
+ #
+ # qp.setPen(pen)
+ # qp.drawLine(240, 0, 240, 411)
+ # qp.drawLine(480, 0, 480, 411)
+ # qp.drawLine(720, 0, 720, 411)
+
@pyqtSlot(str)
def on_camera_change(self, uri: str):
# uri = "rtsp://admin:sijung5520@192.168.100.100/profile2/media.smp"
@@ -553,10 +610,6 @@ def on_camera_change(self, uri: str):
else:
pass
- def mouseDoubleClickEvent(self, event):
- self.media_player.pause()
- print('Mouse double click')
-
class MainCtrl(QObject):
front_camera_changed = pyqtSignal(str)
@@ -598,5 +651,5 @@ def decompose_front_targets(self, _: str):
os.makedirs(f'{JS06Settings.get("image_save_path")}', exist_ok=True)
app = QApplication(sys.argv)
- window = ND01MainWindow(q)
+ window = ND01MainWindow(q, _q)
sys.exit(app.exec())
diff --git a/src/nd01_settings.py b/src/nd01_settings.py
index 6dd23fd..7ecb405 100644
--- a/src/nd01_settings.py
+++ b/src/nd01_settings.py
@@ -54,7 +54,7 @@ def __init__(self):
self.isDrawing = False
self.draw_flag = False
- self.cam_flag = False
+ self.cam_flag = True
self.video_width = 0
self.video_height = 0
@@ -62,20 +62,27 @@ def __init__(self):
self.cp_image = None
self.end_drawing = None
- self.current_camera = "PNM_9022V"
+ self.chart_view = None
+
+ self.r_list = []
+ self.g_list = []
+ self.b_list = []
+
+ self.current_camera = "PNM_9030V"
self.image_load()
- self.get_target(self.current_camera)
+ # self.get_target(self.current_camera)
# Add QChart Widget in value_verticalLayout
- if len(self.distance) > 4:
- self.chart_view = self.chart_draw()
- self.value_verticalLayout.addWidget(self.chart_view)
-
if len(self.left_range) > 0:
self.tableWidget.setEditTriggers(QTableWidget.NoEditTriggers)
self.show_target_table()
+ if len(self.distance) > 4:
+ # self.chart_view = self.chart_draw()
+ # self.value_verticalLayout.addWidget(self.chart_view)
+ self.chart_update()
+
self.red_checkBox.clicked.connect(self.chart_update)
self.green_checkBox.clicked.connect(self.chart_update)
self.blue_checkBox.clicked.connect(self.chart_update)
@@ -110,6 +117,9 @@ def __init__(self):
def show_target_table(self):
min_x = []
min_y = []
+ self.r_list = []
+ self.g_list = []
+ self.b_list = []
copy_image = self.cp_image.copy()
row_count = len(self.distance)
@@ -156,6 +166,9 @@ def func(self, x, c1, c2, a):
return c2 + (c1 - c2) * np.exp(-a * x)
def chart_update(self):
+ if self.chart_view is None:
+ self.chart_view = self.chart_draw()
+
if self.value_verticalLayout.count() == 0:
self.chart_view = self.chart_draw()
self.value_verticalLayout.addWidget(self.chart_view)
@@ -174,9 +187,9 @@ def chart_draw(self):
self.x = np.linspace(self.distance[0], self.distance[-1], 100, endpoint=True)
self.x.sort()
- self.r_list = [13, 43, 71, 67, 82]
- self.g_list = [9, 27, 76, 71, 114]
- self.b_list = [9, 23, 87, 82, 149]
+ # self.r_list = [13, 43, 71, 67, 82]
+ # self.g_list = [9, 27, 76, 71, 114]
+ # self.b_list = [9, 23, 87, 82, 149]
hanhwa_opt_r, hanhwa_cov_r = curve_fit(self.func, self.distance, self.r_list, maxfev=5000)
hanhwa_opt_g, hanhwa_cov_g = curve_fit(self.func, self.distance, self.g_list, maxfev=5000)
@@ -289,13 +302,13 @@ def image_load(self):
self.left_range = None
self.right_range = None
- if self.cam_flag:
+ if self.cam_flag: # cam_flag is True = PNM_9030V = Rear
src = "rtsp://admin:sijung5520@192.168.100.100/profile2/media.smp"
self.target_setting_label.setText(' Rear Target Setting')
self.current_camera = 'PNM_9030V'
self.get_target(self.current_camera)
- else:
+ else: # cam_flag is False = PNM_9022V = Front
src = "rtsp://admin:sijung5520@192.168.100.101/profile2/media.smp"
self.target_setting_label.setText(' Front Target Setting')
self.current_camera = 'PNM_9022V'
@@ -305,6 +318,7 @@ def image_load(self):
print(f'Current camera - {self.current_camera.replace("_", " ")}')
os.makedirs(f'{JS06Settings.get("target_csv_path")}/{self.current_camera}', exist_ok=True)
+ # os.makedirs(f'{JS06Settings.get("target_csv_path")}/rgb/{self.current_camera}', exist_ok=True)
cap = cv2.VideoCapture(src)
ret, cv_img = cap.read()
cp_image = cv_img.copy()
@@ -340,6 +354,25 @@ def lbl_paintEvent(self, event):
painter.drawPixmap(QRect(0, 0, self.image_label.width(),
self.image_label.height()), bk_image)
+ painter.setPen(QPen(Qt.white, 1, Qt.DotLine))
+ painter.drawLine(self.image_label.width() / 4, 0,
+ self.image_label.width() / 4, self.image_label.height())
+ painter.drawLine(self.image_label.width() / 2, 0,
+ self.image_label.width() / 2, self.image_label.height())
+ painter.drawLine(self.image_label.width() * 3 / 4, 0,
+ self.image_label.width() * 3 / 4, self.image_label.height())
+
+ if self.cam_flag:
+ painter.drawText(0 + 10, 20, '0 °')
+ painter.drawText(self.image_label.width() / 4 + 10, 20, '45 °')
+ painter.drawText(self.image_label.width() / 2 + 10, 20, '90 °')
+ painter.drawText(self.image_label.width() * 3 / 4 + 10, 20, '135 °')
+ elif self.cam_flag is False:
+ painter.drawText(0 + 10, 20, '180 °')
+ painter.drawText(self.image_label.width() / 4 + 10, 20, '225 °')
+ painter.drawText(self.image_label.width() / 2 + 10, 20, '270 °')
+ painter.drawText(self.image_label.width() * 3 / 4 + 10, 20, '315 °')
+
if self.left_range and self.right_range:
for corner1, corner2, in zip(self.left_range, self.right_range):
br = QBrush(QColor(100, 10, 10, 40))
@@ -368,6 +401,13 @@ def lbl_paintEvent(self, event):
self.isDrawing = False
painter.end()
+ def drawLines(self, qp):
+ pen = QPen(Qt.white, 1, Qt.DotLine)
+ qp.setPen(qp)
+ # qp.drawLines()
+ # print(self.geometry())
+ # print(self.size())
+
def str_to_tuple(self, before_list):
"""저장된 타겟들의 위치정보인 튜플 리스트가 문자열로 바뀌어 다시 튜플형태로 변환하는 함수"""
tuple_list = [i.split(',') for i in before_list]
@@ -434,16 +474,10 @@ def lbl_mouseReleaseEvent(self, event):
# self.min_xy = self.minrgb(self.upper_left, self.lower_right)
self.target_name.append("target_" + str(len(self.left_range)))
- print(self.left_range)
- print(self.right_range)
- print(self.distance)
- print(self.target_name)
-
self.save_target(self.current_camera)
self.isDrawing = False
self.end_drawing = True
- print(f'{text} km')
else:
self.isDrawing = False
self.image_label.update()
diff --git a/src/resources/main_window.ui b/src/resources/main_window.ui
index 1fdebc9..1dbf5cf 100644
--- a/src/resources/main_window.ui
+++ b/src/resources/main_window.ui
@@ -917,7 +917,7 @@ background-color: #1b3146;
color: #ffffff;
- Prediction Visibility
+ PM 2.5
@@ -974,7 +974,7 @@ background-color: #1b3146;
color:#ffffff
- - km
+ - ㎍/㎥
Qt::AlignCenter
diff --git a/src/target_info.py b/src/target_info.py
index 6ce96ea..513bb01 100644
--- a/src/target_info.py
+++ b/src/target_info.py
@@ -3,13 +3,11 @@
import pandas as pd
import numpy as np
-import cv2
-import datetime
-import time
import cal_ext_coef
+from model import JS06Settings
-def minprint(epoch, left_range, right_range, distance, cv_img):
+def minprint(epoch, left_range, right_range, distance, cv_img, camera):
"""A function that outputs pixels for calculating the dissipation coefficient in the specified areas"""
cp_image = cv_img.copy()
cnt = 1
@@ -22,7 +20,7 @@ def minprint(epoch, left_range, right_range, distance, cv_img):
min_y.append(result[1])
cnt += 1
- visibility = get_rgb(epoch, min_x, min_y, cp_image, distance)
+ visibility = get_rgb(epoch, min_x, min_y, cp_image, distance, camera)
return visibility
@@ -49,10 +47,10 @@ def minrgb(upper_left, lower_right, cp_image):
show_min_y = t_idx[0][0] + up_y
show_min_x = t_idx[1][0] + left_x
- return show_min_x, show_min_y
+ return (show_min_x, show_min_y)
-def get_rgb(epoch: str, min_x, min_y, cp_image, distance):
+def get_rgb(epoch: str, min_x, min_y, cp_image, distance, camera):
"""Gets the RGB values of the coordinates."""
r_list = []
g_list = []
@@ -63,36 +61,33 @@ def get_rgb(epoch: str, min_x, min_y, cp_image, distance):
g_list.append(cp_image[y, x, 1])
b_list.append(cp_image[y, x, 2])
- visibility = save_rgb(r_list, g_list, b_list, epoch, distance)
+ visibility = save_rgb(r_list, g_list, b_list, epoch, distance, camera)
+
return visibility
-def save_rgb(r_list, g_list, b_list, epoch, distance):
+def save_rgb(r_list, g_list, b_list, epoch, distance, camera):
"""Save the rgb information for each target."""
- try:
- save_path = os.path.join(f"rgb/PNM_9030V")
- os.mkdir(save_path)
- except:
- pass
+ save_path = os.path.join(f'rgb/{camera}')
+ os.makedirs(save_path, exist_ok=True)
if r_list:
r_list = list(map(int, r_list))
g_list = list(map(int, g_list))
b_list = list(map(int, b_list))
- col = ["target_name", "r", "g", "b", "distance"]
+ col = ['target_name', 'r', 'g', 'b', 'distance']
result = pd.DataFrame(columns=col)
- result["target_name"] = [f"target_{num}" for num in range(1, len(r_list) + 1)]
- result["r"] = r_list
- result["g"] = g_list
- result["b"] = b_list
- result["distance"] = distance
- result.to_csv(f"{save_path}/{epoch}.csv", mode="w", index=False)
+ result['target_name'] = [f'target_{num}' for num in range(1, len(r_list) + 1)]
+ result['r'] = r_list
+ result['g'] = g_list
+ result['b'] = b_list
+ result['distance'] = distance
+ result.to_csv(f'{save_path}/{epoch}.csv', mode="w", index=False)
list1, list2, list3, select_color = cal_ext_coef.cal_curve(result)
+
visibility = extinc_print(list1, list2, list3, select_color)
- print(result)
- print("Save rgb")
return visibility
@@ -100,12 +95,14 @@ def save_rgb(r_list, g_list, b_list, epoch, distance):
def extinc_print(c1_list: list = [0, 0, 0], c2_list: list = [0, 0, 0], alp_list: list = [0, 0, 0],
select_color: str = ""):
"""Select an appropriate value among visibility by wavelength."""
+ g_ext = round(alp_list[1], 1)
+ visibility = None
- if select_color == "red":
+ if select_color == 'red':
visibility = visibility_print(alp_list[0])
- elif select_color == "green":
+ elif select_color == 'green':
visibility = visibility_print(alp_list[1])
- else:
+ elif select_color == 'blue':
visibility = visibility_print(alp_list[2])
return visibility
@@ -113,6 +110,7 @@ def extinc_print(c1_list: list = [0, 0, 0], c2_list: list = [0, 0, 0], alp_list:
def visibility_print(ext_g: float = 0.0):
"""Print the visibility"""
+ vis_value = 0
vis_value = (3.912 / ext_g)
if vis_value > 20:
@@ -120,17 +118,18 @@ def visibility_print(ext_g: float = 0.0):
elif vis_value < 0.01:
vis_value = 0.01
- vis_value_str = f"{vis_value:.3f}"
+ vis_value_str = f'{vis_value:.3f}'
+
return vis_value_str
def get_target(camera_name: str):
"""Retrieves target information of a specific camera."""
- save_path = os.path.join(f"target/{camera_name}")
- # print("Get target information")
- if os.path.isfile(f"{save_path}/{camera_name}.csv"):
- target_df = pd.read_csv(f"{save_path}/{camera_name}.csv")
+ save_path = JS06Settings.get('target_csv_path')
+ # save_path = os.path.join(f"target/{camera_name}")
+ if os.path.isfile(f'{save_path}/{camera_name}/{camera_name}.csv'):
+ target_df = pd.read_csv(f'{save_path}/{camera_name}/{camera_name}.csv')
target_name = target_df["target_name"].tolist()
left_range = target_df["left_range"].tolist()
left_range = str_to_tuple(left_range)
@@ -139,7 +138,6 @@ def get_target(camera_name: str):
distance = target_df["distance"].tolist()
return target_name, left_range, right_range, distance
else:
- # print("Target Information Not Found")
return [], [], [], []
diff --git a/src/test.png b/src/test.png
new file mode 100644
index 0000000000000000000000000000000000000000..883cdae538699cad876f2dd0b24c75e46a773974
GIT binary patch
literal 10899673
zcmdSBd03P8*6z8Z*k>`a(D}en;kxKM*e#?r5@KADBGXDV4NW}-v8Chd%q=}T3&j~oO{%j
zj1NfFiWy6=H;++%nbaj)b9Lcq)vM>@8S^jBs|Ao-=D$|*36#YLII`PlCS`|*9Q|_C
zG{|vdDm7`FCW^Y0I(Ylmp8KSn(k!Q6qp`^!HABjjay_M_etLG?z(fS0#MnNfl50a0
zgezs4c+ZlA>$Erq+209%p2*RW#`nuF9PH0F8Ai=)Qx0v*WcNOiDX8jMdU4km5fT&71Z~?M-Q=}}#U}JvDi{J9&km#|H-dx4t8KbvbPY$K(41~=F
zKJW19oI`Iq9<$>gOiAuqePgqFTrX42p&)Y|{f-PpPCo0F%ruVnm~6kysw{Zc>hg#G
z^6&o3(;16l`!n8tCo~)-(p^fGQbZS|CBmA%bDKYQIVYrcTgDCqe19W6aDj|&3Y^Rl
zs8yw1qy{Va$cc)D4Sn>!wfPL{gH3A?V@4SoiiXB><1WQritS$;bB8n>?aAdCIhAb1
zAG_9($S-g-+N(9a`};wXclPr}N@XHt3SGE`ru(6x9`S*ro2yE7rJw0N5{;H_o&BD)
zL()hMUgY|YQrtqtUOcwwlR~6U4sm{R+CK4Pr-VWO9g^lcZp>%Lyi)OqDFQ#)8}A!?;4DW3ewto6HS|ja))`d#Uhe3Dc|*%hBLjE#z?!Zx64aucjeI
z1#c<{;!#yXkzwE^>qp+pM}*OA4>Oa_CZ3kR#c;TFHLSg<~U@lD^zHe#2%{#56|{UfuG0
zgYgED6+ht6rN-*!RDXC&ikIYqbhN}imax0VH`PM4gU6PG
ztN6`{OV?81UX@|ch0d{F5<~p%nWxOOof3vdl{kn~Pw_g3M!E5*!Xp8rYEJ0m+og*7
zGiYy0oivCSOY38cO1Hq#SUe9qXofi9xNeeDY$jT_*11qv9W1#FzuRIvVmZ>lb&Jh|
zqXD}WBR%+{cs6>y6Pn~xV=zpM%l?jfT_Zb~mkh_Y`wUgrQ_*QlD3YElY!n5N+2QPw
zz}am$)0N#LeM98IRUgEQD<`ls#TDeZ*&sL^P6e>jjcaFUajQ{+W;e=-xfIF?B0pP&
zy7CIB3<0STG=x20aM&PV_D08D*&B|n$9mrWQFZLZ}xLlK-+x(P>!mkgvrkCzK@vUEkptugLFuEPSqQ4jGcu@)C^xny}qLj
zg(owS^0phIpxIj!YKeZ)w9OksBA-R)D51Sa;ROVbN{4MK30khJe_wz6<1%WY^Kp|b
zN47(LmJ;1tK%JY&LEF$snjzhr$F98b?4TUHE~CVo$LnbAKzpMpQ>&iIJZ1Khjz5q=
zqMoU_^4t|rzeiVP3szCP)*n>rmpzCyYfDSW&Gu#j>rUA#9l=>{sH@}xW!6695AEoo
z)oAa8&7{@pa)L>gxR0!}KW&y*yC|qldwDPH`Qsn{Rn@Opp4sMySWFQLVulu;T+4Bc
zi46Qb2#zU9%5a;cQs|8)-umZg
zjUShDKk;u`tW)*23a@HYNgv!i>_I3h^dOW=eI!BIWli$sN;+$MU0#dAnuQ&7f0eof
z*R^JLoz|{K@TzykqbH7=bPm#Yr0G@l{$@A+!>L!lJx_o2aD3^Hi_5?6|5EX>t*pO_
zEPlRz^$G3d0rkm*wa1cuPe$90XQV(8yE2}Zs53Buqs^D@af1WZazYMoeyEKVxi-~K
z^h%>mS<>ie41yj2_rY0D>8)JF@y97ma{PS?v=j?T!tL>(Ddqq<4Zrba!S&LfM!CeH
zs(eeW$N7C44G&qFhj-adtt!x;xF)vAWcIp7+sJguHYAVTbP3Y7p9t>b>zz+jVhy
z_F~WLXQs}@@f^X%JqiSJP^wq#aP;+$b2oa&x<4)l;oN%w?l1?Wh&6jneAf4B7~Hu=
za5+kDO$efM_Fkww=2=@j>ofcAs@RM`s<8#yHTnXQ0-o!VeB}IjI
zj;f0|fiB!9y2rZ54nvU{038kfo;Ff1O{|xmvq>6>c8@hQc8b5`Hu;>6J-bAaf4Q$g
zuve3yIp{68EGP__JtvAD$k%p4l6?L^K^?B3oFKTpr#=Hw+QUUT#B2tYLAnmARHM0W
ziy2;>!4TAtym{sooD1BbcrQENUai5sdte7O25SU0eFfs@nhS5pM-uYs2!j*U5GnH5)fgG!fHgt=oDSkXKe-%=rS+)(S`*P(TPJh$0
zJh1%AWie;@#ls$gbJ~1>Vo`>|C&{**+T+J7`q^r53m*YXQPwmT;z}s_j_M8fuT_j8
zn9JDyAf6Y*^l}H9W;5Tg4Ziop3!pUd4DCx>R@FG*$?e|i(ScKa=uv(GQN@%Zc$bE*wx%|@_yUS4C){!e7PG*9S0Sx*V0+t!^9L-n%mi&s#1o-FlGmyJyDl
zGmpAfgnJXt)GE~rv*8xHZM1om=Ot;}0h)bp{OuoCmVfJcdu3hEo2L*eu2?>0+Bvd8
zwNoCfa@P3iC@YP8%BuOxYlrpxozvbdITYNQa6+|^W@?^as14YT9D?@jDdo^J(%Vcm
z7F;Xgft~&OkPR-IBtvGX7Vh1lzRE$Z}(r!Bqh?w
zgZr9Q@4YwR{>l?)GC!g%7$&d2`|1o&C9=!FFnj~y83%k*X&tRp6^lsGI17D
z+*7!{XIvM<5mPC@RM&0PD#l$h5SXOxdc#r4?qPvPto6>c2`
zlXv%6p3x~B{ISQhg&p_{xUb{6`lCU!b~LBB>GBv*=mJun&-XU-QAo3+ak3x7A7!G>
zRhShP=59rtNOZ279@!IQqs`(NTV?E
z2@A5OE2cH4cQ4fx@8_QBHe~4PfA53CDoMHkzC14FHjvz7kG@D13l4JOVqJLKM93R%
zl%_TjI(XGWF9{S2EKF3bX!Px(D;K!+Tr5Z&35d|1{;4BTnkwcOyxRyBh#ZcD5ldno
z$8bRpUuJn`Pxe;bZXAQLvLR4+t@;bZS+nsk4swl0iHj+4*_EMK3TP7)RB>b0pAtW>
zr(&*ly^A?iCL*_iI6#tWA)&aH6Pife64HHv60{I6sIt;C7;YkH%r;?`J6Ko(*H6*d
zO~-|=g{GEvf<~gdq+|rJBrtG$BxwTie;1xpzzw?bXSOv@yeqmBOmSJog$}3gyML
z1|iQ=DmL;CJ_iYPa8KF^2>n~jYPjmKWE!ajkZCt8j|4++2F(_D^V~WH~~)f_4(J2j%B>D&zQ5HtAfpb
zsBz`t(e#vcELS&Ig~zt7>u@)o=K|b2m2C0u};W
zFuxsV>b9^;-S6*KIxcudBq()aW(F@Z>p0j`3Fx(V>_KM*V@TT4$Fv)T=<(C3u2zXfR?QYB-
zA-kn3Aub9GUAI(kVJnc3QL@|P65+!V-Qe%X<`rEaFX1nq9|_{P#pZzF{u~5|wIY)p
zn~&9r?iPVYvKE=FZo_n)k{Nj;wD1UV)d1q^^=
z;PYXZ{&T18oTJ3D8M)mD8t=7Y_NhX1w-1cyRLNHUMh{6n1zaZcf27`kV2ZmIBzWeaOY&P&gM
z;**=@`0|W0#4JYH58q6J`Y&16h*d7YJGuzS{RO#`FH+GNCQ|DQ-hka#(5DCVXfJc+
zKdQ+RZ(OFB8=a4KKMXMQb^(}5M4bOJ!>g7TqkRF<;thTxI-?%!Dn)75VxTFy4J$yLWy
z0b?axs}8c)OcOJqck2u~@$vH&R#LnCYsP9?hqiTZN7X8y8Xr-RG|0H_^}S!fV#8Yn
z4qyUE>-)u_V9yVa5`dd%$Dmgpesc$~L)WCkQ??pVp)3;&fEqIxma(w~neRk=b8-%l
z;H@)Fa5}qI@ihn*n2wkIh3Y>@SM^_8qnjN!ySru#(9-SPMw26gJ!v))#dj{-{TL?W
zj+2I9YO95@!XTBX%C!^8K(2Y}`)TIOe}f2+{%T;q>@((_4!Zi?bzjUC-nJ
z^6$H%)}va_austFKZtnvZEaJ<*@^NoEF!^PF6<&yP|66_An$qzpekxG2%y5j-dy7!
zBKbTD_%R?*aFNd}DdmE5JL(%h!75)t5!JZ+~%l
zd+%bpq)Xe}5KJ*fAOmInnP^lHkPWL50mlawV}O)l<)+Zcl#$T9CEpHa%>#>LVmFoJ7G137MB
z?%IH-QiUrxLR
z-^?!&?%^1Ye&SLoJTxTmlGqeoDUF8=2zrL$g<3;kn0e{l@50SIK|j$lZ^n>7`m#
zwt>``42u#0k#ySt3$F=TsZ{}^=Q%O`=gv&S18z+J1`Gw;%@Yv+CMtdSZuG!=u3Q8-
z8EWeXL$1!~!a1D*8UoxNaiST*UrE0wdP;j!>{4t1+*>{_eUDBoN9ge-J$XpFlAx+(
zL~XATv=m6w(8^j7T`D6B`WfgVQ)4d#mt(?|B0`lky1(jn-`YuE69^z+&;Rt`)2*;w
z0BroH15&+JQV;c4D`~jg2NM6F>7P_vO5$Eq#fd?$C8N*BHYx4nY7IYG0!_n^7rjW<1cJdWU9
zClX8yaKeF(|F4oQ5x2{sGhn?B*Umq(A76y{TD$ai`E|zIspZAmik_K*aoO3eA8+jV
zKu2cZ-0Q39yG>(=6NK6Sx%cisBhLWXxUn``cv*E=d`l$@F!;2#ubxWXsxMbNg}B9cof*EF>haEft22TX63B=)K#a~NoqEeAf!I-fCk`~@(E;netDvv6+a~hF#OV7##?i+%rQ8P
zWO}B6?iINwswLAg?q%y5F!coaS8qXJ`98bAa4{GTs9?doUfkXMY%ugRl4f^*=eVq`P
ziKC_5b~2mHO9pTETJ-d{q)9s&m3ddtepF|!T@3)$KkM{Qlh@6A7H{?}ge^ZEU#ud}
zC3WdukYYFlASE<^{xc#{Z6tdcT2Y2S%#v7XcK=5m$*q27(3TKGH|u}Xkrcmzc9#DK
zdVQbx4cu-2DGSks0H1@Af`4p8s-R4*70s$uV_dnwxrPG_>^S-bbn<%k1IOB!c4oP7
zyTdeNdXMt2e|^Os*zEZIli7j6q>nXsXNs%x7nf^Q`<3K&$;o-2-SpY_j@K$?wmF5A
z{q^2+{4x9SFM726_I_^K88fClm2PD!q?%m==>nV9Y=VCmdayQm*#ESmG_Jo_NXv0i$+)Tz0H?NCqI6?ZVk4RP~+1T*{g;XG*s(RB51;*$!YNpku!QUI;
zSh6KB;tQRi)HO&U-n7Jte{Bp4MBt^nv@p+m%x7DGZ>%Eqp
zc82PYz0(omX`#NYxKULdiB4wYPm7#48$p=pJimA2r+}4qhY3z)#QnZ0+
ze<#Wn>iZUi=y0Hoghm~ugvbi=$zRMKzSn7*enGUmy3-dY_N~@CMNLo8@1Z7rT01xAwfA={c_2BQIL#+C4>F
zta)T$cXT8KF%PlGqCU{lsU#b8!mI@`Ojf`BfR5K%-v}#F0MA1KwL_d3`*`idWTOJC
zhZ&EQ>s0XCMcIArMElZxyo4lN(-^Iz_>}P~Bq@&g=pf~ojNwn-y%i%NX)kK_3GPi~
z3+|D^DE+Ai+~HN?bSi5`L*fxF0IDq5dDITC3}!fq55JTvsoEx==m)(!l{!&ljVzLc
z)Nv2_O!&A4M{fv_CF5!a^Ew<%Jb&+>*X5*5Ig@|}V
zW|FwxqO-EUUEHWtUIqC3HOyzc1=#Z28DE}CxlO=A!>1R6P
zq-3(Kss#*{}-
zJRw>0Su%{gp{Rad`91*t72O6bo4cyXLp6@|56NBlIad
zDk;Tq1?rnMMbbKo)}cw49nk5g%&ED~Apd?}|7m`hf1;m#eELfpGc7r-7d1}{=Hlf`
z)onVoG9&XB@aoka<;%3Z*;5n0w9Xw%pR3AP?pZo@lHka9m{J%PN1yhEQVQhL4$;OR
zzvcl{#Wvf}ZIg4EP&L&emKSjDhC{tH6`MP07BGCB*>XS2sAWZTV*@Jp0#a@B_3_7c
ztYyW6jd!f4^cx+)t+OSa<{ac6xb$fRk&YLXPzNl`m>gn(paf@HVrUg{JNhiW46#El
zHc6XzNd|Kb5mQnzcjpXm`@({VBW^%kB^C2r-w4XifGZN(nQ_`yHXni+adaza7KxF7SpUn
zlfYDie7qefnt`!t{H2|fldvVrV*EQn5(nbKXh|kZHa~JYr|;*TxFB
zj0m3HfatMByLlj0iSW3bAiVGwo^y~Gf|-wMR4nCQj*
z3;_RuA%RD11NX;p5W-ZD2xsD^BXG1#OEO6Rl_CeG>UM7146RDv?sGyDfElH=LKx}0Z
zLfdxFfTo?In^%G6=_3OqUbHWF_tIW;)_b>J3(2A^a_3$U
z`Jcu9-e8&ax9HQe0m97;eSw>19w`j2-7=7NeY^{{0r&fr7@mg*z4gt|-uXy35L8=u
z4<`m*KH|L83mHDxb{w9ekK{mQXbX%u&+QkeGOpBt(jI$}MUGn!d*ahxqCI)w#T}$3
zFx3N2_Fh&vdFF-^NT-W?Y#0ZmSBD)QIBSP94>`>9_-2GnxGfInspiGO#dFUn3|};+
zp}yf_Xmyv!6Dhb`v1ZaJi`v>Z#c>ECUm)JWfhon~J&ox4_#7I%WRN3!gcYgLwe9jD
zdD8e-uO4N*?pdB&LE1eTZ%;-4v0WW^*U60kytA^`z-(G^8Uf@}?#er&5@lt!av3Dy
z?XBT*XxgGlW)U0Y5N#+j?L^p62e4@Y9Cj1DRTp&~u8WHO+}=q(IKg9VP*L9uCxlC~;-{{p!>ydeOA5Zc5D5LkFOt2ljRnRWZox4G$?&A$P*qmKs3@>*OP4t_f
zO*5fdY*bFdJ5APu_6vNgfpA|EwEu(t*2z`M#jSdriu0ROCNQ>jXbq}vmlg2D*~!iC
zHP~7shOpU*QJwL})bfVoIGSd3Bn$_J(MK5FsBGfgy|DdnUq9$sf;l<=X1cIl?k*?z
zp$uq`N}XKuoz({uooicRd<1F@h6OT(9+CixfxP%||7mh@q)%tL!r(gCD~qOHjqI|g
zxz$2H(6g2i!M)r(Bm3Y4QB7pE_@~m^%`+8gk&(Wgnq+HG;?0^mRp3NiBwOrUIdKxVG_$-=<+Au>ov}l;pES!i;Y|Erk8^`%?N8unYlhYSrbwg4
ztzw+nCK?*G_OvNrO%GA5MMX6Z6eByxDU8N4Z~
z=N;-3K@rt1mNe(gqh+LdV;M
zpPwb;wHX;wC4uhbf#b)dpJlgZb(fC$IEV4{c@>4hqiDPz6K?(zVUx(y=m_$7Cr=-f
z1IzG4Gk9Dq4&bS3>%blD$}>v+c(PtZN9_)4yT%63dOD|v0*TL2@q0C;IN`mp6z~vQxz1oi;Ys%
z80F|8_KG@tfLSi4>LV8$D7GPB{svb^Sh`ETfL94fZ35MP7zpOG479EDcuRLwHb~!2
zNfF)C>YTwk+oCIVQlm3%7U@2c{Qn0+-LLY~X`P^j2<(tV?T8KL?2Pqo3rM|irrSoe
zQ-L;YD+aNJUE~HwzzuK!U
zAi}b9PXeu~&;MWpH~7DnY)_=um$&iyie3*}ceZ4T`5l*1zIzr4Qxka4XE#oh>8`wc
z6LaA@Y=uOfY;*Q@^_pTvL$hfkg8JDjVGL$k|$s&EqziZ{H(>zRN3>qSSED6QF;sIOmxQFhtB-4~nrxpRm9`owF7
z66$Iz`Lu>i>Bog-KVsg|^)X}i!X9Dm<`-QMk50YnT3+0b*t^M2)(;^r8r(cU~KlKNWSxL
zXY;P&?gDKpv;&M{)aF*^Z;%%Kj{ace2k^>WG0`LlzvX
z|KfVIpkIrD=N{N(TH=Nr+Yx(HOn=IBRcIgvgRFTYVQ!LpHhO}28$j^}41LE4>y!&w
zZM!%4Mh$dmsDQQti!GN1V9;Q9jC32HSDuXzK4)X(tpy0cvK;&g#9sr3XNfIhO7wLL
zrOkUu`*IC~^^p5M5%tn-P`x0Wk>0>rmVr6Hg%UM(K^`k4FRe8utAbsFlK7l
zcg$yFdc0z^-QlG~-c3?NCpAyx@fmQ%=K<9??{tJTpoF$nv}v|EY%V-62Y1(eJ$psf
z7juQ4ViIqnzf$lkix9;t+wu;F+-G$UO{}DN&~>nXAICwbirsCa)z&(~P8%cnVmCd$
z=W8lHI|i&;7{0doPbi|}rFVpH%LSo?Yl%^mw`8|1AMRgd
zqj4ChxzvZp_vL|g)iU*fI`-GDpLrEdq7V$KRH=T#YnKt#gpL9~kZ#9E)NhPtnhZU{+Z
z3guZkc^K@G4CSuDTgC!C5D?yD69Tk<$KjgC}J0=J`fBO|Dg5E;2(Gxa^U871h@1dw8mo
z^5d6}K(WV4Mj0rMx-^@*5AgR;I0t*uql1@Pfd}1nY*s>p(*tC&QMAn)eG(41YBTZq
z&rWQ+>G+VQhE%=!hz3)|+mNZw%KDnhXA_4vY%P(?V(EVS%ZN+#GWR35>)Y{K>%BSBu^Ks<&@#7h!yT%3#xK2jT6Jt`dUy#I
z?$j>5vBWhcDdqNg+okB@%bBT7x&iR__!pXh-$1}4e(yKmA=wRry7>r>`zdH8L}VZ5
zdhqt9S>O`iJN6WV$vZI9Pe%*Hq?;4P@XQFCRNYCUO}J$&Ov4d{ZXw;LsrvbXUp3TY
z)prV3*H$IMQe-TExnZAFR=9QSS>dn1j0UK-!7~`=Be#hF
zVU}}AUw=O9c|q!dc{tUG2CfnIoq;zNH=~G4o-js0J=3Pa12F8J1mv(AM(H>^VZ}$<
z*Oc9`j09d7!U%b%9(%K?N7^lT>zLCj>ANKQ6fW^N@g@Lb{g5=ld&HYB{8hP~<|Mc&
zP7KirOkG2M+6WogyP@x|@NK~NKJZh>D&D|X0QHb4A+xD}=PJcw&VlX%C@jDv-m!;r
zD1y9)>=AaG_8?W9h-L~sKd|jWTADyhx*IX|*ixa4G2Mmr@C71`De>M!u5hZj6%vf(
zB;4Ld*g6%7HGakuY0ia~^A4Rssk%`9?GgYS>D&sp6fw|f;d;hEK$dFGU8MliB&;6J
zu_7HpqAvD9l{ils6vVJ1Z}4Hf$_~aeRRwM;Vvu|=Mf>Wd|Dx~i3M~LS>AniAZaBan
zK(|+0F&_C9x$1-WK0^q454(w03(C-h{ts22zE}=8XiLky149v#RpAA=LaH&&Jr)>I
z3@m010GAc$!Z&5dqYy$28LjM4=&rPl&{V3Z`T7M|kpk@P1y4QpBM=yUq|m0*-u0V~
za+qYJ`bq=nt>DZx)2KfKU||s&;OLbd|KC6uSJ*!kvILK`!?vSkzVkhT!AkegK3V)Q
zs#E3a^NxSl^l_lD|AYgt?aKQV)v53)Ky|F(e{#?)1~jUuH%U=x%Uw$|&mpwW7iPS~
zCe(V~&giQ1?}>j*j`Xdw%-R;_%4;4~2$)r}0;FR(Ss?3}$_56)=S@5oXN!j_!k^Im
z0lnbV1z_XJ2ADI)N(;zYFsC);Q2&8Tt(hkFkG4c7OSDf9Grka*qEZ#29f*cdtXggQZ5j~X8R@@nVGjvFV=}F9OF*%Ar!X{O?yE3
zwZ_*CIfm3#O0$(%u%Ku4LNvu%{^yCdgQ&m1KZyVF3v%~HWgWU?T7H3Px&!KN6K6B~
z?zyf_C5Ex-ruf}JaI=(B`WV8Yj5kxu*mBKgm?Eb>Iz-4Ma+THRGFyHX7OjX)B>EhaIkY&
zzmbUvW<&yWHW*E^>a_1Tc-%Jga-@O!{^iTCg7M{p8vbda|H7l^S2LjAV3RXYwyU86
zVH_Vsv!i`n__pVV91ZztP!l;q*mzvA7K
zxeH6jp6-9!n(=ZfV`*wxYCq1RW#ZIL>{WVkrWC1mz(47j1uwH<{{>=7OdurJ56hjD^t%nFDvxks6;Gdop03Zi2sdVQa#LWA_
zOGIJGEuh#rEnvkxwdfBl&GVkI1g~CW$amNhRX(qeIt~cfsi0#mE|pzpmx&LKc-9s)TCPCaB39>#%aDX7GFE0Vq%Z;Bg0
z3VfD!@ZwkwB|0q*#BAt>l^F7?#Rr7Caosz4SR~46nGYuF1UVX+mblL*K`Y5Z!tjU$
z1TA$0^P!L99>BT_3!WZVVfm&3){OMQVR#~KHGILaH|Oy2U6K*;N%6D(v;S)4U1C22
zlg}njHUX4Ug_dTNmqHUDd1buDd8ubJi2JWtH{)WT(xe{+qRb2plEp6Al(3*qP{yas
zcx)R=+&M&xlr@Ma?qZ`H2qlBIhG9|B!#C7;xL?+%Q0BcX91UUWY{wZs9Tg4IcCgD&nHyF8<@H^Qd>$W
z|tw%&{@?L@&ES8Nw^Se4bogMjdxMrhn$CxG>DyPSyA
zP#2u1>hsk$j@U|eL%tA9+(GQ{Kb^h|i^W2^dM@{az_B8kB(sDr)Bt#b#i*&Qffw7#zs)r3-F(x`Y;PX5e=?l+$0D*bkB1G
zM;FwWA7tR5^q82zxaTX^={+^AnU2PF>DWrp@g7W+tL%@Vc1Oo(kA6Av<=oSo8uAXc
z!|T?qO9Urw%DAb~u&s9Zx3S~4ch9J2!93h1R+>RU^&}DaPkzU&E5WKjIks8)*(e+(
z60-3_I`ECk;j-^i)xiTzL6B&Gvau^B`P{Gu*-`^gKIB4zBd@`va7a3+xxb!E7ku+R
z865s-@%i$T;m6&n^u<60R+_&8b9hb!#bSHy@Vzb-3;uD%m0D3IxVKt%Qgkh69n59E
z!KUy3`0XJmwS3q3+nMBzurloFtNpdZQ|B|uf$X$6Zt$q@Ff^#2P2X^5PmVF^&y{QF
zrjD}u3C!*CWbet5=w_)QAfZ*?egUmJ55nQWM137e=`?QGX>;#V+HIxQsu-GG2yZ
zHF$XL)nUJAts*H)7FI+0B^`w;NdZMBZ~+4vSc6E3wYcd^QF9%|0GxY~t^A5@gH_~4
zwE?n6GuMB%u)9$X8D?izQOj<17*EbYCAqv34x(>jJcUZSC61-#-HL|pXM+I7FG6Ou
zkOCVc^O#7QBgns34EI9cbq)1wff8fmj@Yo-?Z_*GBWP%UC3ySUofKQ5ptK8u0St27
z$X(NUG2B|h&-J937KFf(9(-_AyB7I8B?GzW3sa3c#kR;15quAfAn$bc#@d}A9%E%~
z2M4KvuxNe}DK
zB$SUWL>i+Dp%Gz}jrsHRl^Oy=>hEhv?7JFbZY>GyE)^Do@sgdIi;#Gq0Xuy8&(J{L
z8EPY*+M(dVA~Z`Qd96W!fAk?6Y=;wFcI48J9mSEX-coJ-wa~s1P{op#EQav9K^~D8
z{Xz>ZP`>IK+aNb3SE8;pXkGzDu~Szpt%4w9k*`x@dW@h<7II`_Beu-Vl}
z6?8mcW)YfpP1vc;JB48)K8A@B-eDr#pP1-0U?TG-PGD+#P$yO?-Xo$1by8#n5mEf<
zd;gAzV0GpmJb6I+0G^O=zj!-NXLGv#m32lmN4YYUBZkj4M)_X9(zm66}3
z2K1)l(20IhGi8@w{9Wp!f7JYkJxgmavY}Z89Smk8F<2U@@U1MQgjRsln6Lir;T5FH
zE`fh0Ux`2!a`!M73{A?;YVGzjKV$s>yQmks3|}3m0bXw2`QBXZ
zb*lzL0Gr&(T_ca`8NLe(`oSQVV6$5Ut+~%x@Cgp>9)wDYsy~0?R)lj*|7iiV0#Ybc
z&tSc3$*o*ZyeF0xY~Q5?=;P+3>cuQR%XkCf;DhJy~ANSo-5;m3_vmKyV(
zLbOjNJ*~e>P8bp-<2=Rv#SV$PCPyZqSK0`rTo9sNC(_6C-JrLFyfcYnE4o#hm+~nq
z?SiNiQe0|iCbYN_e84D+i3Jnkfh5~xhSG=((vEXV9d-xqZ$EYSZu0&g7Ft`!ubz5k
zwp90YRgfc1XdTgdN++f}b?#}Jvpl}G
zPI?1o40)?No3Z+09mdhd=Jr@)bR4=eMRRokr_E3wggL*nwsbu53MKo)V#iPedzxqV
zUb-z}Zrx!8@{N7iiz?&6$8n)F+$)=iU=RwhoZpDNSAs(gdw$11@FCJ!vOd_cGuF=E
zz)R>+)`>Ab_E!{R7FX*w9!t^7ibRVW4&9gwC9@KF^4OM~u
zW>q&Wvf2Sghhm!u)Uu$3Rd^P9byop9EfgdF7$X@l!Vb?Fu)BfqG)F8n7(-}Sji&uY
z2#$L2ES{e)zE!Gi3^I9sXp1J7)D)VwOQQBJFsMLaI1;oeb@OC9I^z{lGG?KtlHhw^
zc>_8XB>P>EDHKoc&e;4m2NP2xjz=g!rH(>sEnOT37*3~hWhSRS$h4+)zD79j%<$j6
zL_(sfW<8PQqOlOS8tvJ^j^eS3Hj8ttwzK)dH3rGCTWQNHjz&eRX4
zXjAbpDXA0K*(~@5nk>9{nk*V$(p4F0SP17c1~^s%g8hEM5&lvOb2JJ?}Scriu&sdOe
z&G}KY7R;8iSqY5ih@cP&Kn0+8%keV9>;z`}lLQS^WBSQI4Zrq~^?*m0;%j}gJOfH`rW9%h5(1M>2L6O|)%iKLuxseFlmg
zz@ZqR{D@s#Co8jL3Pwu*S@C0N)SJvZH%D0au3fvfkk+1QiF*P3-2T{O0D$W@I%Ac<
z{3dOy#~2KRUOA7?16DkxeB2t$iTEmeiE6);Vr965e#;;_ud%H}(VZiASo
zzwCL7K_Opav(!=UsEj?ZLxR2J)2lvm9%>FMcVYV$u}|%(ef!2`c?H4z0Za6udywc1
zNJ~#oZ)j>Rw^fd&tp%i{T4;8_Fl=&6VO&$ESj|&(Lp47pdkS^-h2Fi|&oW102qe-s
z)n>It0<)<4{lG4LTEYmf-^o8sJ&fVLR87tt<^)0@60WN^hNYgKL>MHFur9(2
zHeJrMpYf7hC9QOJ0r;22L$f~-@kH(e5pqY&A+9rq
zgVsW~%yoOU-_%^iijEqKYp^0S9QkgDy@BGd6|kcB^LpBu!t3w*y-JKzEgi-TJdAs5
z^BelU5oj^WG$S?bOFXd;6v!1G$+b51$Uars2BUn?>T3?*5RbQs2F0Pgzf$$F=xhgU
z*C2v8hMBG>G?>v5177f>xxu`mJNL7hL&7!r9U8#g+CLB>dyjO176t0cpFIFWY-R`6
z81=XGsWM{dWzYpM21azF)N?insjZV^2+Ia78N)3j@21=(BP8DHh+Eh=C0x7&`1m?>
zNsO*_gr@9Hzc!1Nc;NtZ1xpi2ztcM6i!IQLuajB{_Gx;bxYRnois8%MC9#Ekg$*@9e&B6qr+v=jt603V@9Ax@
zg)o>NFv`|}>$1%vYIke{M3I3e;dRIVsg!T@hUDS@WUK%*)J%wKJT9&5T1
zUE$tDizufuG6ZJtj6Gv#BNbRW6_MY`f+HC)Qs=h|+*kZN=r|xz@TcEC@%~5f1Ne~y
zhPjDo-MOt6YpXe@SuZU;UtW5?^gLr8Zo9IJ8OvL{<~QUj*3I_bYs<7tJHt72t!!{Y
z4L*5Fe|;DoyO+D*>Y|JJ81^6jJN9S6^!goOT}?#$)#de4)i@mvOum;9T2ibcAcbUR
z=?G+fI-d{{<7EZ8
zW5B8)75{186@vzBX%;#hPWO`O*{~VfTLr{B;*F=624b9_n%6aky%TYF=fy;1%qex
z$xn0noW4Y4Q*GTeqIVK&(0eF|4htK+GRc!)XA(5YF+zRd3u(}bksUIduh|3XIl)22
z9$O1zaU|jAOYHa;;(mAeqU6XQWT$^nkGYz$ykx%o%w=Eqn`hssPoDpg?I$0kXDg%K
zO;ZxD`dJpxO#GOP^MFTevmD}*TQhIM(m4yWGbGGX=3ooHD`6}QE~Hg^>R@vFiy=q5
zEw}#-lgVS#!>v=nmH*ka-G|W>Xxf6duQY8ZN$$$dH#;oL!9a#_*nEw{;3`l3Ij57d
zxl@Zn67J(wns}p82l`Ksg|$^sV*2$v@dH=@pqiM6f#!j;p5KBUfpfy`?0@y{+u8pF
zW)>Q#h4A5fL@yXz!Q-76=7FH*P(yz?5tK!(#wKJqP=u&z6o0ID9QTg4Z~{AxRahhW
zF#Mf146KQXMG|Xhefr~H!RIFs-Tw`+Nh=hZp8fZV9u5;B|AC^X`>jx$e_}Hjod{vK
zX>dqZ;nHK8EWoNMQyhFaBZYLyNTEdCYG#k5rgY<>
z=u%!f|y
z8tCL+^_1lDvGGgrIKiBig-c%W31%1Hb#PzVblU#e!KH-Jhc)3Ni~wpW{eR58d03No
z+64?KSP?{uQ8sZwp*$j6WQnr5fwDsaNQ+9tW)T&F3KS~Pw2JIo1SBAeLISt|ZH0;o
zHbD{vYSj^@C^RH8wiHcl6{^B_9&9_)dE1%yyT132@499NMnK7v=Q+P~pZnbB9_?+s
zh(7zOf3fJ?x5gjP%glLd!4#Lf(s~t>ruDSevX+2>z;tn;DDm$X;a-on$k|csQXlqo
zMB{Mgp&5G)=FQNl|8;IDUI2VIhPTmPImoEcd&+R2sF$VIeXPuIv~TM97TO9x5GpmP!H`Ar
z6x`y~upN+^he7xZjGs~=;xNU8S)NHK+oZ4Q7mV)XCwpn%@?(FRy|(?WyB?}{PmM9c
z7P%u|5*EF9lQ2P4T*SIID_5ES8;{P_QLi_oe5qMgckx8iuL6982DBHdTWA?Xb#^{4
zRw6Pp@=jiOiNKv>TKpb68?!EDUuEC<)}D;k_1$%a-q)iZ7aV`4U*OKDDtxfk`CLFl
zlkyn*mxc=uuYL6Ld8+gDh|AQ-(yLQntgH0-{_#)vlatVLj?Re&F;vMx;Xt=o3kiBX
z$F#-9g1h5sWv4bRer2!WZ3nv~A62PN$qrRUIwA&8iDtB(Q0#s?Y|vr(GGDjBGR0OI
zsgmLA#>7>363=m%g==uUD?$?Ja6%b4(&BF`NtXogS4z3i^;O!W<+N;i!gu;R`)iY$
zkAz9QxcWQkCGP0aTGpu0GTz*&_OVDy8$JSyuPWp40w+1$kM=ec@a_}vQo;tO#<9g%
zMn#tQ+>-Y6bzQs}Vh-W@4?@gDk$N0N*J5yQH_~L8W3n1qbHY75hi}SAf#wnPcUzwz
z!SJ0N15#>-&kyrIb^oum7@;oPTNurU5Y+-+|KB+J(m-emwhKLBS!qOto_dV6H4^mrUb;!KG$)xf2ZcB%(sod
zF<)wPc7T|x0?sku`A10a75J!<^Re=9O^nH%LF?HE=a^6Ar}B+x5j=s-vWc96@+GGm
z81rqn#|MCPG{oV+%M?3#@6+^KZ4ni&QOztyf^C8tLZSbIsQzl<8^a~6n)T`1#(d^m
zJZeNY3n!bR(hjyq%tM9Wm*x|?uN>G*Ps6N+@mknv0)MySr25*v{ZCqcq8QeV)3PaVhQPrRgD;*Omu;EF|w)yi!_gglQ;}rY#S@f26Odle9znNMg_k0h?&G
zO)eUAAf1;XGtSG9Z=`Pyfr_krFO^zeszA>@_F_Z-s(hl#>u(Tc!j`$So(`7G8!b0p
zMK;=4x817#N>jJw_j~ucR%mHL{_ZaRrNaUeeZdokF=@DLY3@vTq&xEM(ftE5>2;A%
zWA~>0gf7L)^>W8tUOG43`f}NUc%Nma)hd4m>cMHRgOOJNm(nZ(ZLC$7QN5H?px4e=
zy}u8>M)pq2snyNvD}BZxt-v^b>)!0Y-vV&&MauGK-}DNzm{3&tJ~F&qU$qL4^NcWv
zk<}_iF7;d%I{h3M8|TxCLN~ZvwAfj>(0W5x_K6bfH#pjBXEvDGKlDM_i%;_1c&@81
zNsfi)`qhOBIEL$Q*0?!FhvrNef38AUyj}1f_`wcn7M1EI=)_-63TV2c()vXM
z)0K~&{jh8L7c>GIs5_3W>r#S_Q|rj;x(AByKiL}=9DRr&B_W7G44@^BCVGVyh<)nR
zQ;ZR$!q5Xz%FL<@o|(b8DvUB6PU5+RPE5DVnRY7TX1`zAmz+{sTKe(I^&cBApDSJM
zIcYgLHMDZWu;A^5n~r=Z$fB%X)OP2|wu5W-9Nya#mo@fq$MqMA6&aSI)j6O75WXkL-$Bl`k8{y4b^P(-G-AX>iDRxOQpNPfV
zxpQ8yn(LK5X9hOar>id@ssh}R1!`~ZvuVpfNrF#|MXx!r$fxSDQ}$J5>5|2>9Qvl8
zK+L&0^m-Dh3%fpg`D0>7iN{D)@s6r~`=k)^=B;I(Rk66PX~4-<=QS2>V(i;C5=dN2
z7vJBY>VftbeUe24QSup$gk6lNi?USDPj0uV&6-onZ+DeN5`OWwhRZ}(MMj*HXnL;x
zoMP3*OZSj1rq0(N{G0SSasHI#nZDP)55y}byM>072JJovNegc~zuilAoKaPDUvQ3R
zSjYOrQ0GSXLBRz!J~CeEGeW;Au@8U`QA2io#X~pMtQ7nn%E|i#t83*0{M3!AZic5H
zxdtuy-3xF_`j%6?C$X#8>?pTj=yH|W(VXoJ$AVJt5#6>`GSk3tjUeOm=p~w6b{h+5
zJ8O2?r`x*NxVKx|c)owz*$sv^(#(uhZ^rz1)@F_Q)}0y=e&Nu{Rj(8ou7o89QM}
zov^7bcB<*r9+e!2Lejb_+nC5J8*6Ail2sbm?%lj!tSvg9W!dQv@%FBb!m13z!dLZr
zyO8@%q?p7Tz@1nk)~2Ks?dV!A3U=GKP4=8ob%I4Pez;ZEDjKe59L8%!_*t{3441xp
zIMX3UF&9k(Ya)fQ&ke|WtxTK^+L)XnHsUxL2Nmz3Cl8eDAlx-fE(9b;EK6ZL7YrXa
zjQ`}6;^(IVBTik;JX3dG=q91I$O31M5hpjEf@IJ5&GVnHyllQNrhAR1bg$aWoGmO0
zaRKGfJ?UwQ_QAL%dN(xk9L#QXTQ^0smJm0(=@d))e6zy=!cm)k`^keSCQRHJW83EQ
zlRP4=M>3^ldiM*=i7FdhL^Y0+=*f;kvG%OV6X=^8o@R$jG?8`dvPH(vx>S@azRB3B
zgT`rdRw;kXe?ZqIUu-OL;vmqmrrDo%-DH1_q69B4SB$`cJ`tV3BaLjD-B0fh+ZNBk
zyPwgy!RMiyY@Yw>Ai}RHVDg)s6p=T7FhFR_7^@iL_uGd13ahe=P2%@bwA9?s82UHW
zN%pG2o?@LGgS;hC0zL0y(iuE9b-kEuF7Cwm>rLFa2pk83H@@4XhiR!uipzcJ2BC=w
zyqH_s9I`%a)xSm1oiaC%DVIBS=Ecmd!#Gc?yEzFuxc&q
zu&OH32~$o)-_zvl1xcmm+(Cn;=s0G+*iF@KQzWNZw^^Pz{e2=D4~uX-rWe6c-*GGJ
zp6sm~W
z7_mPI=q!JBwz8MM?-O#8IT2BmUApu|A|@I%uBu)kjE>BEQf&@LEDQ_mjHpp`KX?Em
z5cA-=%B!!Rf1A~tz4Y)z-IHt6hkHXhm87%(v2CwxtbO7;G!TrM3bM=0xg90ZQbAXI
z6l5vtu-Cl9Yx~LRBCh)GF@uhf@k(a=isx|Js|vQP-f0_IXfD~P7I$K>rIT*V2WB7JpO&M6hm#qU(Gdbd^fjn=vfuZyTxqHo)$w~sKDMt=GZ
zHv7IQ*^{YPzEe_Pk1%lz9t;cLF@yGcm3R4L@!Io()5UbqEe||?t8$$PJY5Ij9Jbme
z+sPc?}&T?wlcRbdU*V+$?@@VWb1G
z%E|wT+EJKkc@gEvF|Tbeq#N-kZc7WZg88w;xOXyPs%28$+|*gCQnZD{7)RZ`=~?$O
zgD_+I<;X2zgnvh-uw;kvp}?_YRA1Vhh8U~uI2qcu8kNTqL7b-D2kXwIhs1t=iw*50
zP}yd{0S|uHb(I`&>db%QHlr-v3%G4n{bfbVmle^UFMnzg;?QQ#gg}V{Q$6R1n25BF
z7v?@78{tU3C(%B$drNiXN9^GGa2*ez)pj9Qlr7Z*Q$bv4tr3%erLc7@&hzMYK0
zoJ`&Km78yR1Bm-flmWB3egdLHn|5}FD>lH0s|uoX=FlU_2o8R%v&N#0k>+mfEsByx
z4PCjvFlNPq#Q@st+uKhLRy1d3ABlAvEM6MeD9H%T@v8$6KChJRG=8p|ADCL=Xv?%N
z$yFD)&njzn5MKFL?Cx3k^G{Rcq}Jk1OP&n1IY8IqE4oXzsOFd~h(=YZLS(D?HN`d8
zGwi_A$H(XUHQagp6Mg_d{?WwLNmwDDzJ4(joZw^W)A4l4V$#pqF?aFkQs==34`{O@
zRGL^S^r}6)RB5GpYnd`7E}Fz}u8GUGD!-5q0OAjZA6^zoHjmMC8nW)0ljQ<6v!`Us
zTY^tDhS#=5v+LZjAVHWMXK8A6R#J#VD6m&4d1K_k6|Sm-Io7p2d5r}(At0d_K<;Gv
z;?e%?KN-0C#!h|f@_K~yYEJi@e3ihtqf`_f(R@?qrJ4)c&KxHTCQeEiA31@nOfe$K
zNg{QDtzH5rcCA8&rvkBg@A75tjf@~`+Qxk(KTKhe0KJ-1hiw9RC(TkpYsv8V=p!hPgWbeUG#C@j~wuL`rT84`aa<_L|$9oAcY!KRd
z(IN!y#D1%i_p*`}6P{F%%nTz@8T#+`I>{wgrXztUIEr+rya&@feV23>FEsDehj+1Ho%=5)Vz_
z)W1<{SU6Tc@pjH86ZtB}m|nNG=)wtkpg#$`Z`)Tl+W>V)1hg0_kghhqbcU6JLV(g>
z7HloaW#m<8oJXOe8=onzGP}%p&I`Rk5^oaw)VmMz0QC)w!^*()Q2fkl9DSgzRHp5u
zDr5c$5cSN?hS{WRt%6`wVGz@!!*ufuC(CT
z*tSC<0Q${%tF^lfG~voL{ZlX(+j?DHNHfWM=izBdi|$y(t91-R|6*g}`O5omSU2);
zgmlrBtX%x=h>52c4P9Qrd2>Zq&^FJxL7u+C2FA`Hxej?caqbIjd7%~Ju8Cv}t&Q_T
z*Fa9a(JDsQGH{q$Z>9O3^ixN;Mf*4L?VPh$~>((^`r;W)1lUToqMi
z?Vvu#4eGFBQjgdf*z}E#yH_5+pDwp!j}k^+RVM({iop_N^XH7z(;v@8p&4b9bVd@b
zXGn!_gM54%@ldPj@RBehYm@BPbPVuKE%Fv#PeQuaoLR7W%(Tq#^uB&QH2r$$)z4`N
z1$%jUT9IF)I@IFudRBqXZecwfGG^g-vVy$_0-K@*=b@RiMcUEQ<4i>q$4{O~9q&mZ
zLcEfV>u`S5h>R&0(3@@~3>?3?&Tl?}bx3aJu^0L25&p{bMHDT!{EK?G)p?AdL~dJ|
zxw>vsS@Ryjd1}**-&i-jsXTP$i&`CB|E7AS8kPF>FW)MI{j^hwsH$%TOXR?WtQ7u!
z69A^b++?xn3Uy67M(cTO+{njz+S+(Z;isD8REUCNrw>89qn25@R%*INTFFcfQ}+ZnOIz$tL`<4
zcKK9TdSg__?u%Qiia5%~Mhzw^@z!9oIl`BsD`tz;z1r+0RgS#$f1~A$j_tUn&K_0z
zO}Z#0BsI!jAzaDM$^jiM!tO(fSsSl2D(k}!r!n7fb-nLh`4p(PVr}~6%j<*Ar<*Me
zteGn)hlvabU*XV#RIYn4%HZtY4q6P1pL+fBndB>UPgC*o8k8tUei{Esv${@|boC6i
zr?_;%`Sio|)OSahI{3PpELd%7C2Kn(c7NCu;Ld0qkL}D?tGFXI70u%0LSh-z9KTXw
z?jt}ixPE(dh!Hs09^f*$Xn~>2-X->QnR0^$XF@|`=!l)h)21?0Kf;Ie9?^;9F#=}oOtTnWe=qMP%DE%AVdwHP(ZBr@C803>;
z!9m6F!zYGRnk2{28pi~<>qWAdZ`#v61CSN0zZ6)KFa|N$aiBSRt%D>Kb-gy_Qbhb_
zp?2{{l>2mfbJocs6f5EMysd`=1Q59rz>u25JN
zQ5wHh!6|$%pzVsa8cy?)23`3&&aN<W#l~z0=8BVk#XA;VDoOQ_dF1%=Q<-t^k9nZB352w9FJfrhK+PNa
zo>{C|^&jDW!ANvN|H}Np#vGJS^yCWZH3C8xDOaH`xZP<@zHcZuf&=6os+RQR;#Qoy
zzm^gH#L2DB0i-rV
z3XEjCzv~bfbZ0J#&rdb&fXRq9I4$Y?hqm4S6ju(KkkV4~uS^}*2ok>^H@XcrY`?WV
zF!8yzRW=S6lB&mBXE+42u#HbQyK~DZ&bH<|4DfNz0tLK~e^3xM82K4phr)Lb}
zq5UhYuRf6w(z*c2>!;zKhl=9}+dW
z&rTQT;YZW2UqE;PAnsNr_ay=Nc}N8o{sHC{TUS9@jz-X0K0{-G;~4I`$-=|{0*G7W
z&+q$*KK0Mtu31%tC1aj2Qrbt#e-&Pk^XOZ(yK`2XE{S!Yj;+6WWy4tE9Lj=**|m<4
z>XNPcf$Ux+I8r65?7E1vvAwe$9$rbCekkAf-Q_#m`5&!3pA=OU;*eY~(b>Vxh&lhE
zbbP$`9u6d3b|BP~cOTE4=)qv~BguQiGX)uzn@;#n4Y(`~fA#b9RNAYbcTL-lUcU4A
zfb+?LThrOKd{!ZEtpHXD~2ROG{RHNK&tXZXhs@Z1IXy+BkH>A`D
zx?m-#s%{R*&{5upCaG1VHpBIxv;F9qbZj*U?LOUkdg$UmmMjjJk2}o_HI=6Q?2`KO
z$4etCzh1Dls&GbsF+A0LV>{3BbWkIVXdJ`SEKmKDer<1aEInj}MI>v4`eH0rJ((%e
zX61-2Ya!*@lT|UJeRtfgh#_nz2>73jJu5IYPzZ_OXCA3HEZQa%)3)=CtKtRsL6~QR
zatbj)oeR)1R|;U$FL_
zLCPX300T+~adq}c!M4=7loU1AW<^6(Y9J{1Sh*mO@5uajwfs2axmR13_67Q+stJ3K
zU)z!2)N&mnnCh@1?>1Pr`(YE3tLKw7jT-S15dNHp^R@7{)Q+*!?v4J^z%a6gaH9~c
zp-J8vIY^u=Hr*NJI10LMx%@Y>e%@17xWTZHO-e5n`Yh~Dt?2x;X?2@A$G*Yjr4KY}
zW0T@=HFs`_<@<`WsF^m)7g0_M!-k~yPf0RQKzKH7%hPjTc~tk>W+5@jh{O%@3AzJm
zfsCsrvnJ~`pnaSDO43v3gPhwRgR7j3bH&sndK;0@XHXYxmFeRJI3ikf+2KWR@cUHr
zO`!I1hS8oGReUMvgR0sb8l;-&Gm1Uo|5IZ*|Nh%BzweI|NXxREDEp~N-j#2}aH<{r
zPlzt)VEPJ8(ZdwhGdZT!l2Sdx1dHmK*3ui$uELMGdqG}hk}|V#hP9fXvUM^^&)MET
z!?AJs@@2@T-|e67OM5W`+j5t|&&!^KE
zcIVvYrdKA5Y5f{*5Z!ItwvKy8+e&r{nO*f?w&W{C#}Y;w)g&4zyUz>ix}r>>R-63`
zJ#SRW>xVupT;{5Z%rdUIT7%kZ&b9IwPZd9QJ0Z!jc
z(^(K6|KW=sFl?Wp_BjY8XoxtaxaHUq*ZkPbh~GP^mCzfMfN8Ce%HVKY47wnCH7NF{
zwOtd>?T3132xxu;xXzzOl-c5$ZFqfCPxJ3i|ClzpYx?JFQ}=5i+8FBS%5Oy|5%Zn^
z!ib>6FUTh62a00pMwwaeN3&WD22<=qxS3ATDn!sdb31yKo*+uRVs?&jw~}J(Z#v7<
zu-RPntD$2B;RrTB)8Ovy1Jo|R5buy%D)q@5wlwX28m1)N!rlT7zTVO&JjZ1!I_>F<
za~=jP(-^Pn>jg!dao8GC
zbvx)FK_Mp%ko3%`(lw+G*e7Gp79Q6?q4BRh&X$(Wz1~-ul;FU4K^V)4s6ckc7Vkxr
zVZ(IwoTM&oD68)UBzvIkbw+_PMQ`ak$vSD7gInIQaN@8gasGYra%1O!WHu?61DVY5
zIh2P4oa4v;PTy?Te!
zlH=xiJO^g;1qs%91j*?|;CQkBsmtj^`0D*T{l=;)Qak?v4NlRpKI8oR|IWPu`GqI9
zffV1bQM8bk{VEm(IYC2Dj#%5K$tm?7ILi`*;j+8u>>w^X#(U;DTu-VLk4le~>h;lZ
zIsqlokvs%F_Bk&$=Lh`U0(WVcZ}Uw_?UBK6Tpvb}wEOUKoWM}X+WHd$o`VRNqXQy2
zlREcb74vDAl(_x~_YZadHpzX#Ox))2nfY0+M{McoqIX_;))!4rZz>Ykb;o-wI=9&(
zWRY-H?NIUh+#fp!%d3?2TN4*Ac;@)hWY^Iw;~264k*VNV9Ds!c+ZNnNaMF8T&j@eE36XcYfD)v=P(bU)_ZIkNTu2k#T;*3|V*AkQAf7
zAys$I4xb>xzDBW`?a99Hbr@5qGZIZC;bOtLkSkx-X2yB19v$hAumbCH)EK|@$Tm`{
z>C`R`hu)sb?c5Xd-8a&msw;iHLjsAV%%(b~?-~;PZS`V=qk4ud3;j?O2jWju>N}y+
zll{~ufqw-5{VJ3vns)b^%|Sr-4_$rjoAzv)WNbgf!*`POh^)Q#UF<94uOBZ(C6KQ~
z@zt#eYL5}0&vJqc9!or|Fl<_OX<>1G&|FG`#}c`ZFvn9gP?9e#@ua*k>0`iCP-tg|
zr!3ls6KD)|hC2{OR~Ch+deIt^_W$&j0kGUI(R{Fi3nCYb6TlZ=NMC&*_~LI~fQb!T
zl-YbmX2M0i9Ed?NEX!o8%6plKM77OB{`lCE4!w#jAc8VGnvZ$j)yFy(zK8mh-mlkbn8qE%5bHp+0GDO$w1C*VV(Q+$SxT^Z-fJ_f8
zC}TZ3IsJWK2EKE&aeGK8mWnA6LFXJigU+SzdL@|w$pgPUUpsmLZf+88BSRog@Pw$QyigmC$
zk+CefPV(coG*chDdt5{~1UcG_h919^;8RuJfPLDd2R6l<9r@+i$=99mNV;52e)i+F
z?XNorEDu|czH*s*G5sTw&3?8#EYF79#v#14xThrX?(GN&iVdIkZkH+Lv}iBgCM)VP
z((72Eu2VVpcV%S?LezY%fZgwR;lbec1W^3@>E*mtE5@uKjZF!qwN~
zvjww}f~I3LAUoz{*IzsqFhDIpMlS_Th6d>Z&U@b=;?IL|IDp76A*C0^9sb02cIg|1
zriJfVqtHfB3>K~;B|Kf-ug!{Vr?Rk?h!o~aiLe}mlZf+Yg!qqtr}eQ2r&x&7BV?A}
zK!VNHm5_p1+h)#&WTXPHmc%b%c^Mkt5Li=YI;mjwiAZEL(i(!n`^fFCxV;dj;H~UK!
z>@TTXf3?4y{5ZaQKMs6sZm=kbTH-J|=eeL%$Oo9|uK#j>)A{ie@PG&b^t`a(ss|?+v3L;cU}=lP*59%8sPL(ufmvNies-jk9}7L_`N}6esPRS|3@+&kOrf|b&>J8wa~nx1
z{+L=RI;yW+@v5yHxX=<;rLp4mNQF?T3$1UpPp%D^d}}Y{{~3Cl3st5VAjtgd5kdh(eU{u%~{V*VEZXE-91HQ$cy-^AbUJ-$k-wnbkKb92}FdzDva
zIYiRq`I2&VlLmFuxU74#bH+s>HSbEzlbJJU?wb24nxqab{P`F^tz6T7U#UwMXn#PZ
z_|w3%Dr}wTO|G(UN~oAR(&5nBuZO7q>bdx=l7HNcSjK8?FuQy
zr|s_6O4g!cj+n&sfQB&FtWUDP}&@MHZ)g8Ym$2v73z_;!W
zI;$A+DQKsTw!f@PJCPuXxPx1g)?AhGmU?AT>
zq^y13t+a?n&%m#aL!I6mrFt>q!JC<(e#G~%JcZ-W|j1y
z&~?j~k&NH!o_!wGt}<6=!7X=()tukqybP}a71Ygd;BI}=Wtld4)8%DL+ROb@J=faa
zyE=!df^6ftr0#BHFt)9V9>Nc`dFZbUuOxY7!zBZ{ScVE33M?Fc`988O?EIQPU^_{)
zCKqt>YhZxUCpkYxWyzG-vst;YGOXTN%ks2qJtvy!fMHD4x(spy*prN}X1QK(*V~}`
zZ7VMv`@wJ0kyy>i4~qBv6Ih>1W0GQfjyPL70`hxbs14%|d?L#UbV$F}xd0v>cvf+n
z?9pbDZboqNjj}yT%S1Yz(k+bMrNn>B4>w)?Gb59!@7w#yx&~72I({Lo`|bA
zv>phK#~#)Cr1BGvLGs=ba5W+1e_W~0xHO0xa)E!Cfb*xp^tr6{Ab9u1@-Vh`U{O+1zqZ1NM
z&)8sST^M~gqEv{t7}A;EAhl{M8j|{kHHzEyqN|-woM?vg-%0BXw0C;htF>w@!8g)O
zNiA|qena;Sl)g93)&(PkNxaCx0C&SF)+*5x3TJ7hcwUa>%>vIZgjmBjgF@>)6x;8|
zYa%ZU5@~q?ItM+j&J`y@v&F0MZ=sc-Qek)s-$TVqrEL_7*6_z-;y!@#u0>t}DDN|U
z|FrQO-{Yj5NFh}l1-8;sd~fyB<26O5WRujx*RW|JzEUUoX4v-gxDQwtREp|q4d4Hj
z#h(=o7iQfwr`uYCp=i`Y(jEVKtclOhso&R{QVD_VL);qqBhNL9&PcRu@o02WI=If5v>5&$Ugmn!RYzGSgBu(`7Vz
z+gh0yVu~Qys@8HRN2kXvp_to_#tH`@z{j8itX?Mf`5EjNQ%Ju{IXBbfN6G((p{CUq
zWZ!iHgSv;Gx15BJ`qJmEDwtGFNlbhJ+esg-fi^l*6rJ0?=B-v0#zX%Z2G822R-wJL
z1{iV7TK0`0zmW;*-pZ`8U}x#YUC+Y2grZl;%~2l3`l5T_!7z660y2}Vv@@50hJ+TS
z;1nFo`lz|D^E_>G8Aq$!*p6~xYnUJ=?r&jKH&@kLDvK1c?cd!>X;09<*$neelwja-
zZhHHDKzlr&?7G8c;>SOCH>2Z0=yv|AuvV2vKjm-+*J~b+wDA0@)-x*lDG^HYs=+(xghY{jQ1QTAEC~tKr`v&%Ke6pMXPG>~$)s3@
zhHImH5}wq->=eSPeNIB-c0Oi$0}m!9LRt5xty_OkB{t4C?2zQ4(xV6V`+Kjx96&qZ
z(vX~-bDnh6&BNyM=GDm?RW&$ENbQw$%F+3aNbo?6H-GSJ24v1!$y?;pm#wz+
zwBl?%T35|w(Bj?Cdw~-l<)vn-L*K#D=g=EcGL;?AT~O!@nqtc^{bJ`quYG)p2XXiS
z`~j4ur*QzAJ+Vz15Rvd&J440c!6SCP-FD6)>K*r6lDD9;^CxD!=pbr2{kEhX#!W`$
zwR`QXQPj|rJAc(@n)fTgRN2VL^CK-FWOp<^V4>yUCu
z_r+fCyfVedokr2IzM#$PEZtxA47YFlG
z^Oj#aqgXzaHvRhXfnV@!D2cWpw(phfF=u2*%lDodgR_
z8!iDV6y`1{?e$5UBYlNyR^7POT9g&7cEjr-eUiahjeR7|9K?%Q99Z-(Tpnp3~7e=sD^~B?O5An#XN^LBaW|aQjZkYbH7Oj
zdkOi2eU)+%IUm87e1YY()yx`}-AI72UedDaWTWKo$EBd)j8Ogd+C|UJg;hoPC5bBj
z_RmRIIrfD>$3$neUWEv=8X#SZs+6SxGnN-pW^tnUdHzhg%3mMF%=q+6m#H7qCbXu%
zdpVSP|B7mvh3m+gf@`V-%pn6(vk`dDk~&LvP*U8`3I&wYnCl?dLHO$>=b6rWM1H;i
zsWBXPZgbrt=xcoR7t0Y~1Frz=NRZN7vR*{l&f}$j+Uv%JiO72m_FNgnf3YA1W}JS9
zITbLo*m)|8bUp_vW~H!673cD!>rr})S7Az&xDz_MgV~>YuO1moG$j_dtzFWQslNSq
zWR`w$E_#P{8W+2CZyk?(Ka$ZtVjs&=xic3}hJMF3WloQciIvPN@@$?-wTkAz#6KZe
zRW}-^g;^iY64hHt2*bj$)TQe(J=r%cz4mg1&MR>e7eYxNtOzaYj1t`7!P&v|YFq10G?YQZ2LTLtqG4696XY1sRG6q{
zB{^na+_I&2TrHYDBbl&|xsLIYHp`007ZDjeR{YWYLp>sN%DWAl_(@w-jDy%MY8BQ@
zofox(&jjVoA^*U+5g{lycD2AI#z||5Tm@{&dVI`V({7V;IaKowR4-bDMVr2NP>e9j
zLuhQ=D}A^ky0;&DG%)UCr03dh`eH2(=H6CpCSe`g`23c3X;(?EMwlQ)(=MBfDxkO4
zv3sr;?Fa2Dy?B!sDwL-h?WGuIVpK~DX``gYr&w+HE!_kqyH-=H_3`_a)(quY*N%)?
zPCuCbsV?o8x@!|(oy50wE~zlPTx81V@EgEV;TgF@%YR_y(ruAvS)1s1wkxuX8HW!L
zminpKi&-z>civ=zMH;o)K#N^^8YTG;Whh`(zi&40czghqA{}+*fc2zV|L{7KON{tc
z6qhg2jn5jXZNEPpX$pb)Lb-TVvUliFF-qQT(k~=srwpyLjiG+mO7{50YtrYFUb_3F
zx}irhKbz!Sl8~!n9ETQ#fB9;sG0}kE=?w|SQVm{ePpzROr%oDqCn-co|BV9o)w04t
zpIPq}8`rjnUA&=Z0Pn;39+OhW&f|tQ1;&i4@A$wa5kDUvpKCntL5^?eG2zi$G~YTM
zP=@e0!SD}XEW|4SzJ!Es6H?!}P3o~mleM|IK~fiHXmjl3n}MFtEy7h${CaeEIdc}T
zw2ur$j2JLLbH-gb&v+O04ffMGtIIV2^z%^B!#uMATK0e!?TmC2!!ACj3;0>Mnw;Ya
zq#`n7H7ClG@MPm1c?PN5$7N#X}xi~0C-WWWL
zK)5%P-O5wV(JNj`;7II?xaclB?gInib4};t;`rh^wI_PMnlz?On-v
zWKXmEoR^Yp@NnX!hinsJgjIEs;i=BBHDLjg9%ZW$FSUCat^k!Ng?5uw{DVIbbVjdM
z0jTzaMibM6dMC|k=xh`F_B5j`wIiab7J==Jl1yoFVCc6+kgcn-AadWmUUjygjt;;s
zM2t5C;Tzm>LXf@^Qm0ncz@$!aV0*P>6JcjP03o9v`iDx4GDdS|wCGoL*QVa=5q)`W
zPsnlFlk~?bYagmiz6brI=PcETY6OH^?De~IFa@^}8-rcthTr92^d+>O5j0P}m28xn
zrNq1syC($Ysy{-h_nM9|BS@M55lX2oq?1e|$Vr7Uc4A&dq@Nr%l8z62!s09!*-@gY
z!+#E?y45y_El&4u%EB5Q>Fin^nE%$M7bC7J5(bQNvBwf`S5^IHx#iUB3-xdGXT4~w
zPgWt89TnrKsTQ#j!ml%dC3d94LiYH;2{Qj5#1zfRAYdw1M5%xUk^w9H#0D6yo9k=Y
zaE-Uu{H9f)DOuLs!0anyeUNvz)xPpqC|w=4*|sDw8iTpUPjZtaJ<{01mdl+3*m&M6
zEtCelsy-2*d>*o`^dT_tGV|nE!o@v4&Rb=h9%Xt9SHG>>q|g2NNRO3~|Ez<0Tp^tT
zJtH&5N;%pQZPw&7;H|0LEYDn>3f)*W6}bFkpqiG2LklPD?#yjnCtUx%Ib>q4q?1I`
zI}UP;6pefXSz+vUKcjKo$it0KF4C5#Jj(P8SMR=Zd-#}I#g?jqI{3ePF{m17k?T;r
zl$UOB3l8JsT-$ehXfPWRl(^YBv~;>@uYO07>wMR%=X{2$rbufF+1VU*dG(bG_sW&a
zm+1puFw>k;IU=UkbXJ)~i*ztphl0=ZFL-KXL#zEn!N2m<&>ny$8eAD)n$N>whr8|7
z!q~Va#c16WWi5RWhW^oLxrUu+*2q`{szzJ-8%+T&F3V%DbS2@Mvdj>}<^L3lVPDq1
ze1zqxjf`?NPEU!cTRZa|mld4gTzIX)3gCsvhpFU$!A{v?Y_`*9?AwEm;$n^G4&kJf
z@cDqWNPM$slNvr>EQF7%TJf=T!RTu@&Klf5g@e#uWStUxrI}`h~iIL~owl=9RX$399W-^wr1iCtGjSQ539zs%d
zKG4`R!WU*~L+gI~Q%v{NBd`_!SAcaEBo+J@fGrXe$?h^^lB|XX0B}jNs>bP~{*$cE
zBZANVJ6Tm>1aai@cjGrJM8x^M+c1dZD?E#S;QM_6V`enw#S5T_fveHyttlHsp2u(Q
zYVkBkR69KBKoY3Z-UGfUDwGW6~KF4`6HwF!qJXMGfMx
zRl2%^w^9G~T9xA}MKQ}Qa$z@o=EW$Bb>r$qo2qKq9*1kLUQ+o`i73!118_E4A4yP}
zrA*n{tFKemc%<>JSu~jA48TGzfHEpvPbKp|rLo+kGnB7UHDILX^-Y(l8f&+o;oHO1
zBeJrCIvzY&Qq6~+N~`pV$-hM_4KxVb(19l>j~t6kDfYDK&q;!Im*o^LK1&N2d3XTH
zRj~G6B*QS>8H$Hqm^et@FVNX<5}nO`pa~kN8*h@ovQ#qXUdbGc*k$|+OXa}QK3PD6
z@}4!@tx+xd%KOwD+{PJSYK)tq86{?G-wRXeMWu9Bu~k)vH^I2mX`)t<(GjNDy5&Ei
zDzg1{hw8_huye&JxDV>7yhZCq3U!*J^IF%|rVkouR_(qRBAtsNsEZ34V$OHzlHcs&
zryLxfQwHu5xT@y#?U<0jt5lUP!w$Yqv}V}0Wk)@Rcxj3YQ_`8)#-r16a)rmhx9|}m
zN#N7|;=cyg8S2DGtv(&DwKLKD+qc;s`PPX>n>e|JXZJ03asBL*n<@{d4-Q`6$lI~z
z^IZ8kpUT06pYjgIM0Q1PiMW+>F3GSVspD|d@KEhal!9rRW+u0|7DB2URWbm(R8t=P4(
z{cnCjx0s(c9+Q`_0hT+rV^s;p4kQdeAhT49Dw%#BzeL9=VEW7Z&n
zPBLs6OiPGdd+l?<#khMA*&{j^my~h|Bd;S9HD~{oD5dvt;`iMXm
zxd!o4>D)oOGSRp7xWk;Gg{JOmHza~JA*mi`o3W2Bb*L98+(-_pSMW`45a-WH^e89K
zB9eqB*<*FwOM>|n8$>l?gt|!H6EImKt<7mbqRJTK{Bl}^`L?Up3MYC9HZWzLI;6#vtV>$i%SEV^un{+s#yplCXvX`g|j`tLu(7PYhWYD+`lLDGnql|%-_GcBA$HXtZ
zWb_=ZB4p$rh%?7{kRIhitzrdMKOqPa=__P-N$-RsZWjm#mEw;!!P)O~E{VC|v6&30nxNG9Q8Jko)EAR|YQ@nEM}PTgoLyr~>}$Ys
zXM_b;zKHqxj<3sWwCdo0LIBl;sT&XyFt7gP@DD;m7nRAXY`L$imveU;f1+IO2M6^r
zGh}s>3qczuuxV=@61^hha*PODt3Ja1Ei#TC(8So@`o#r~)~nG1EAoD!
zw8}kJ<)B(GJ3DJV`+>ua-rg}{IMg+u^4L)~ws%GH^6;E>E!Z|6%3Wx6*P)NL+K%G#
z!;)&G8$F&)bm)gW%&}VFmmxiEogs?qcB{m$5^^!9%d|db-0uxOZ9zcA*PF*b_-cy^
z<+AmWU-pB<9iF_o3%V;0X-8o%zS45ZvHr}u3l6xu-y_@Ur#rq#L2Un_>TwvkcUWy-
zKYy^?aV7gfraQLyI@r`Yu62(YEcZDC<35u1i0Yt@w=8i802AGu_2rrJ&g3}le36rA
zQ@s1R+SF>5-hNTIJO)$M=~4}~9r{w&Dsg6vn4>4=AF}S(TWlI0tjsda^ENg3d7%qEI$rA4Vdm^`^HXZxTAx_P
z`0b?K7)v}vEcHG_sj#SCcSrRFx4<|-unB~>@;0`pU2bt)kuRIwD~KX{d^dC*vF3K1
zhZR6a7PDUxx;wZZQ0>zVxE*D)91coOCw3%XoyFq0VE5Ft6w~JSrzh`2JxKfIE3Gia)6iT%zRxrb9=6PX+)t`p?nsCQ@7>F)8UC4;W0_3Z2af0>Qx5_Gl+&I&z`G&2{ij__{ePZ*KeAb3384E4opw#1ScTxFJ@lho-){@c1u
znhk`NoSHe*Z!@0uMq1!Lm=g;bm<}+@RC0o)>&qK
zx`uj9C`#3uR)hc?381Q8^wOFd7=cnCPSzbS1aL@LxQX4qDu!X
z@))7-3A2es93*T7rfPN-ZA-;GHyP^(PnOM9Z{kA1V2Ruf`OxX_BDvtdf-J6Vt+_tZ
zso!pZX@4<49a2glA6HfrS5o1~iRQI$jkK4P6>rkq=^j}rRxiibsa6TI_
zpdn4*ry4M-C3Nx{B?C8*i#k{XxWGZg4lhRJk|yGq3Xd6P8?#5f^1RgCNrKznBdw9n
z8X?ym3#Cn+?p?;Bq8xjA@)6-)-XLsZn{dV@G@X~&5kVSN#_psVs&9f=E_9}VLdT12
zsX>@M%qHyEwr2PJ+LWOmR6swNTID}k#Yd>S^b^C|olTfgxzA5h#S3#K;~9}Udx%Wi
z?82CNq4--K&XS=5GLkyeI|xgO@_2(ac(=wA6a_Q;rSDrMZNn|~FzjgxVPE|RWHqL&
zCvBTuWWE`$QSZw0hym#p*)kk2Fzu7JRnk}%c_ocl#hHZRE|`h&yOth==NX?PZjT&P
zc}5u3<6%WA8}`YPn;MdKzMhTkh?qP~`vqHWpAAh(E~LTe#!dS$G5%4VMOXQ}>0|ST
zW>GG;@msBoAjvvf%lw3e+DDO}y3{+!YYyVx5ub+t=O_0&3kU9Mw^_WT*dV-0BzZQi
zPk40dw%)XwRoc7z=~niG5Tn8y
zCedNyK5`w74<5vg;}c)&ezoDO%HFU8@lm~VVf(Nne8O%ibJxY+UO((Oq)fZJbSiz<
z>+gf7znlI=YnlxAw)SJwMs?W_${mUvPeW1Asp%{NuY
zO{E~Y@5#4q=x|ZU$v@9gP@2yJ+j&fXEROLL-VZY`7XV`-uqUm-Z&E*Je}VW1sIV9n
zv0Zr-;d}OO;N@5lp(m%p$gJ_uOQuKWVZYfi++DKjL*|yo6S3qu1jhloIqQi1)ru9I
zQNH6Q5YEfmH@Jr6_B3Q>cT2YrFT5lNL0auWHJ4#>^4n?^%1H$B$vmJ@7$Kk&k#%~8
z6^4GvUE9txSKN?v57UZ`lkwpT#>1#LG&N}r=b)LErn8OFw&PwV`)d_BoqIWpDKB`~
zDxE@HT{X(wA>7Mo)OByp2x|5A3aIN7$?iWTO@PAGq5|XoT`<3MBH^m>_k0W+co>|UX#G^&c^>+%=A-E=(RX9SdNrY-Nld#
zK0mQ|w%0#4+C5RyOP}gKuQJxFHKK(q3CywQ&Rx0KtbiA*4>Fqede@X;8G`W!k7Vm<
z1-goyc64tk&dz$EyXo~;2w6jZ`I2xRQs-iBVHq$YW05D?N4rP5$KX)A$4$Vq)A4em
z94$lr*7xgJ(TW0%L&iqsDBZft6ki+iaJ7Q{W4;5ZXY>Xi*+Tu
zL7l`;#3oh}TY|zvE-k|-Uh#EA+;_8?7paKfPa)cLZq(}dWzb)FRSFBK%5@XN=8Joi
zHdfB(n65R_k6+HN6uawWuBRlrCHCGcPx{9vhPd-x$8*&3*Y;P+wA*gXy~Pafp<%DA
z)?8wEU#!)ky4KH$-3Y}a_
z=3eBE4puUAt%Hqh1^n>cuGq8z{}CSGsI^so$^OD0cpyNfR!N287u6qk)(Ma!te)%z
zD=)r^Wq*2&Rz}{N%Bs#
zpwdX*xv?rSs@V+{Wz|fWCwP;7xKe`8@PT!K3BA^8Gd(N{y~^6b277q_#&5ic5K*$q
zZ$BZ<6_)3UH&J|7dUDzOj~VA_pa2V2$ZCz^Bmtd3ncCFw?-cQB-6-JyjX)cm9yS5ve+e48S-@JYFtTzVW&FYc^B
z4d1*E5(9;-*rCZGAXNmR}j
z!aV|Xmp8ExA#OdqM&0foNJt6>oj=ZSE+xs_%zx`N^1SDR;i<5bs0UZLt2ITjB5WLN
z9IBzqUa(6Rg<;dDD4dOTDn+aGfSMZF?-BSNQvAHf8I29CU(8hW+B1U=q6~HTUh~kk
zY8E7AXD!zJo7edo)+|W;b`Uli{U(R58gY@
zNP-#5Sk`s{lRnm&Gs}v}`)D2)r^SYISWx+)+6_jSVlJ^iE?}QqWT7}K>q5_jEAZJg
zgmi0NvZWzsd63by1$%uPw>~^A`LG@{01s;=2h|eE*iqwANhT;L7QN%GN!2c1Pv)+J
zoEfZ7@D)D8!hj2Y@xDR}6LL$8JLCx$<-Ly#)23gfO$Sepq`j8ynvzYwtho02aotH&
zt*$=HNWPg6-At~CxrNmN_{*xwNOZtrM)&V;Wjzis2&q0xZEX@r+KTMC$%SM=tcjZ7}sz>U|
zM7L0eAxlw^phU64ip=p)SA`$~G45eXPP0o4n?#j!+wGn8DHV6lFsX$*RPUC1+g4(`
z{Mvr-=I^}{dJ~_Z;T}y;F*I=s!)59d501qM9xYim>+7Whf!`}}ATfL`Ql3Fjhm0fX
z>VhS2Fmz@=*8|sT%?kB*+Un-D?Z#4e`VHM{6GBU}HB%TANzNzlS$Ao<
zyoy7kr;iLldLgG`Sim;!D<4^}bh+M`+y6t|yMQ%$rd^{P1Qa=lG!O*wfF>FcK?I40
zgCgL8GXbHZVgOM@A)!_%s8BpW2q1(ghsZ$%f>0HlRu~xtB|uVvwrYh+6+@z7c0Oz)f3h=JopQz!Z9@~%Q
zzpRR(Ee;Q?T+z{gzI;KOC&yRW9QtMhu_G)~PqNzi1-wq-Nod@j2aDKEXle7V@qKF=
zH-)cm+$*vHRDc6FIFpYA$#VtCGK>>ucr<%J>3Boi51Dusl7ZW^+QOs5bSSwW&FPp|
zzfRn9Kh%tF7|+%3<~-$5@4xU%+AvsQV7v;h#;emB1KH8cgqvF3ZEjTg>5)xpoaohw
z7h8v|ayy;i{4c^91`71tew7Fvxc_5T`H5%v&A9GaU1Dma^HO9Uh6kHJMASJschR
z`q&q@XW(E!TeBorlv<%|+Z!J#b%)|SFAbe$KGL&KO1t(lDwOBB5B(9dx^nmH#e!Gs
zHPU@E;n7P{7D@$paFyD$xK$y$8TqjkaAQ2K{cnIXI0!8;w>8pTY--=@og|OM16W$a
zSh5m=*d$XyTyDyIcI-0CJ@p&DZ!6}$B7BDM_(ZlguAotKO$zEWZWoIflMJt`AOy_!<0mA7*P32B2Ia*am{s+!<`&wg9+QD*}
zJIQhjk`QP&tp@G>;7o%=XzB
z-w@~(ki}6u&?!S(Wku5uLY@A-jeioDl&7sD>$;z5X3~bV2FXC@Kq}G4{~%wQjwlC8
z?gs=K*<2*GvpQHKpM^VW-xP=z{)c~T;k9TP1pNqRbnf5^+RoJ>c1<~dFrxsz$KHKL
zkky0Ew4KrM)v`^~J$xY)0)!6AUu*EB9I#$S!cG~JjSnW*;?N%P
zCs)t2#Cz2Ooyl-ofloVwSrgmc#{E`>W?n+_1%nN&)GZ8hAlb~LnVQl@?~WDCGu&&Z
zNC%w&QG5AkaV?G=?pO%=w>>;E5^Yk=)wbmH%DZPgHeHa-?b;VmUj&eejF@7da@Lx8
zu^90mssduXqfZTQ>uJu|^50g|`jP}S7vW>o=}z9Kd{Ma?yJPC-T~p0J_NzRY_2_C%
zp=$a%(v1YwR~;(tI-=%`tYhy{APDgp;l@EuKUdsn67i>}(N#B-*FYdZEDKlPLs_+H
zAHyL;q?|m0M@7eiW3Wag`JFSENo)Dh&lM>m>}aW$h4bkSpH4RtY|=6fmERQT%X07-
zn68OtUC1etw%fVXJ~muOcaY_%-jjHyXUQuJRyy+MEdXiyZGore$nh=pfqeorS9z^q
zR5hv?dtgE*aU$th&r2;;nV)(Cr0urUwJbw0J<2p~FIi4%f(rXtx@M2xU|n!?_p)Ix
z=XoukaOB64UdFMa9*t
zAvRvrYV90gDIB6%uf4Ya&MVy<;UEw^M-W@*n77A)ltV9bn4^vL7`;G7AEXk3mu9Wqj#>6Bd9Z$HO{1Ee|3W1tBVM;Kp
zb5N4FWPG7tlP6ktJ@3#wkd(o2jcr=8-G>OEK|^u9+BBn8p{OYYK!D%mD8
zeuoTc#Jn+|n$&WRo%F!+9{$R|9Zz8sMM{1gkF|_BsS(&`vj1cqG4ePs>nw1yICx3c
z#k`5h)1`KDi}0SVUk&4;az!!mRr7%GY%{j6MQq=dN+pTjbh65djmRkc-^JGJ6|4)&
znDj@N8Y^E!x(5d9P%XOygVFKUVXT%g9dP((?n4uX!}zkKi}ECrzRA}7ZL4k~7*euk
zt0IYJYH29NHKv)K=dpHsp*o6>;s&zL*C
zQM2>pr$Ga|)AZZ8tp|W3Lv53S#eor5S+Db3YYFHIsscfuPy@{8Q;92sm9_SIgNHVu
zb{n+ZHujLPDGewE>iNfb6KHs{Pa$R6$
zf;BueGq?mx+aJ|1EJ6_<43;#4zyBN9CfDz|%7Sm-unByeJ(=Hof!cy<_h51gd=;$?-+LT!c;HV|k9$I;m|eJXMM)VJc>E(2SXo*1#`wI;e7p
zH=6`)YBHiPCb9xXnz`qsV#XUkWRdOPG+f(wDc^!0@#(W9QXB;Ri6S-10^MP9F0(mY
zNQ|A-Xr&@Yn^3#y5mYI+>o_s4n&&mmr{b7naWI(TzoQ-9V~itjRWDYHt3b6Tyxs
z)pgdym8tQ;sUy!PSutsUy5Gb_ZDifB7noA^x3L|2=9a`rvk@uid;mv6SNM+p
z8kKPXD}?d!^l1y591VRxfjQ`>t9r9F^>yI@y4f1@ZVRgH8{Q7FHP-!O0qEzuYVbsb
zKjvjEH@>FQWG6eXR>ka2eNiXG1S}6!D{Sby6ZfKO4d1o9F{;Vy)oE!YPwcdBvdioM
zB0rby;99cTbrv{45B@rg&-dc>9F0@?0F@7ERx!SNHLKaBT2I}FpSAJGG3~L6&~y8E
z{%CX^NV>y11IJUnOa0glwGhEd3~v-i$-Z;Zw8r+|jZ0!`1gTn?|S{gst4fy=U*Mrpg$JkMgXx
z>-@C#IroyY_jj60epOrlnT?gnV%Id^lkcY%Y;fwluYTZThsgQg4PWNj$kwSedu(
zTEd|yW)MH(Bft4X`Od0Hm7dVX=5Vsbu;fd1B~!zzCh=J+!(OpdSi`^;f$1-sV(;4q
z_9e=%z083hvqiT~p^v$aPkuG@M$*lU)B8ulrGoX03jtxGumadp=Cua0tJDH7oA|Mp
zLetf{S^b-IZRcRfQGX6XwTb3h$IfI`i8RNMbdT-QiOj`W)Pu)3{du)v0
zr>2H)O+}x&^K_HYd!bgjGQ5HMR-)(2dU6`usIjk!{hibB4Wkrbr)MGmkssZ(KcBsn
zj!YK{CbBzMMY$`zMa`#>vT{Zzaylo^U|UuH*2}$e6M?ZjT*6Qu(0S{9w1*bFnqnzT
zCw2EKuw{QhR2K+K`fafCxDGqM{mjP#sV+czaNE30(%KNiWL8<{3irBYXF7|yPW8T*
z#p(FF6Mt0jmHaDzOHDaMTZsyuiVFUcIaw2s)D~g~Adb-r307zG*ny;7UJ6tHCR>t7
z1<|f3b>`kT;b>#>Tm4>UNr|ge>e7?+y+dV@C(J@*h+TZ;CiYN(Y6xO)y(i%UAo~QH
zRpVT+4GA1E&k=JWTi;bK3rgS5*OCbG&jvA~tTsRH9MeX)?Gq}rt-`xwrK0}t!o8&!
zOld`I#{N6==(hZlRYsCh`Qh~_w3y84b*}K%Bx@TMis@F2nifQ+nubMzu0q
zBlg|rziR;qGNPh^+81R-$W-0yG2xn~vA@4qM{eHpnu6u676kb=#Kmu=J!!`sivyVx
zo?9Q+mRxA%m(&^#9QY*i$v|>IJxlo$?QuDzxL~T6T59g6jBFCWs|lgR1+RA%KSDCi
z7?)-Q6V0YCK;azR90fOu5QMbf6iR33yW{K_EpLcZn*ZFA{7r~mh^^ufcA687u`B&O
zq5jo`W8lLYIAN4-j)enIEYq5X1+t9#Ht72p7nFNwaLRXwCt#{{wr2WSZ$J1lvxK6S
zfgd8f)}vKAd|vrEx1^?*hX^x#Ezc5rMDTj1R>~eSX=Pm&AV8SSr{r72@0R!3^+kI^
z+=bv4k}rx+>%d*GC_U|ISQm!Yz*qbw0@8@(1pMJ~ZWeS0L@Wqa3Ypd{{E%p3GqC7f
zynlUOE4;M`;B2gS`95+*whSzoOq_Uh>;0pt*9#`$e*5E-_dnnJ>&EVOU)PDdeeyg9
z;p!XVcA4Jx3J*e32LFCs2;*qd!ZPphw$0)bY^=8NEk?YkFy-H%iFD9Fbrkb_zIGu;
z;W|_dWkDXc|IkAf>VW}e-tCr0N+njV5aD9IL26=iPGDCGmpH{vF-nRZk{06H`WPH`
zbW28;3Xk8q@{$A$H+-h8WB;W3gBM=!CWd{n(7RmLDryvsiLw6xWvnb#%~Mu$bWyxe
zsH3jO=ST8J_T3#E85_L1_B-+gbl|bHc|zTRRMdBWHm6e6j1iy5Sv6zzz_OQljjx|f
zC~kd`>d-N0Ao3s_tM8_^rfU}p=a-KOG*<$tEy2s=Ooq
z==E+Mn{gEVEG!q)F43vqXvDWcp4Lp(7KMpih2Bl8WL)9=)77-_En+(@dCl@snU`WY
z;Uwhw0c2a01s!fX(JXZ=VUUGOeBr@fz)xKGP}W*tnV#rT+344_tZz-9MS8Vtz1wA^
z%@Y+`wN1-at7K##;!yLA+Gj!mdsQJ=L*v{?#>4{#g03|9DEF(pEO3a;i9XkpTnlDwW>LdPL2xa0=jH
zwluhfb6$<@O);k3iji{VC4S?p+B2mDzj{UoDWfLrDCuA1T-?T_^?GOST0Cionb&9?
z%RQpuHXX^DJA9zqOqIrg>kGWhYox|iHc)rkDWs}N+c17^s0}gLza;pt;of*5i?4rF
ze?DaKe%jkM=O!clsxXt0b(LzLOS_g`uWa_Z`IWlz0j%1j`RZt#YXirtw~L_z)EebR7#x#dYdsbe`N&N|=YIghA6kLTD}l=xua2l;wVr5wv>fS7Am
z-TWLVdz3KVq1xS~PNQsCCckP8K?|=ccp`ut%pERdJBXTz6kc`CR(>n7`!!;XnlurN
zi+!}KU?oP9f+z5$Kz8m4{NsUd_mc;sIv9AtGZ*Y>rJ8piQJ?XsB4)&36t&C&PLxBO
zhFaMCz|Dr-Q_?Q5;uM!;jdu3h{cTug~Qd1uy2I9Vg>;}|K)c?2TSrCFNIJN)8eC!(4v%~?H0-tljxU3BToLD9U
z*|jEcd@uUjL%^=pcVksoI31DpN>4bRFk7+8l7nz)O|9U_XDaeFk*H*ahi{BM`(
zbv21qvKs9|KP)SROk-?z1;>DI%w*h?Bf@LwcosKX#~L*qGKa?a_)2HOK*eS1K0>WJ
zV7NKefg#=
z4|Q5~WFwdk49vxQUl&w>m6l(`{E0s2#-1>o3McXA5UO6KE=n~ZU9+PXaaLtzxe1xE
zKg2scdd(DPG;jOp&%vyt$J)FCtcy}NU~1=;4b
zLHV7hgOV~Bx6n%3y2uw;_hw!D^Nn{&-8I2w&PUXok4`wY$KP>3WSr1FcEZ?ARWO@`
zZHH|hVR_g7HGYP^;Sc=0%_N$ymCl#fz*e0zS6}`yqmUCdr$_V=;Lg%_l;p_QOa`)h
z3sp(m@;zx}*A$gcu9`%k`mTRJHq!Aj5u3FP^V|fVJ=pTb0Bw=4x9p;7K^PPJip0%b
z6LweE*deV|ZdFHO>1g$W`+El89c%y-+&8UZaR;voKmQ!uhOiKK1)rBKG
zYI9^m+|_E$WT0qVYRBc)KfKOrwK$lnUK61%ip`n9j3w-guKlA(W8w0<
zyu+1B1$l;k%6om$TgKH-67V#pbLq_rhslBV8J9gwsqM}i8!|1}L?
zoXhh|!m;8Jb(X(F7dxb!hTCIasP0b;K{qAmV?b(qMij=xEFp^omKjUH*cPVf(~dqk
zO;S(l8}No>`2rVLfK~z
zq;E+(xD_Xt7r^QS1w=^~L6n#hM2SC$lKtOpf#3Dz{H27~K6b<-HaE15acCUg)iBGqWg
zw9BHRV$F?Iuom7ICdx9Onwk2HkF3HOZxJ%!3
zmU2m?SBsK1i;XP-sn@_x^K05sK9~^fH6__N+MR(P9oEJ?0d*%>O7yTbi3pvA)1xX8
zveb*K7}h2aBIPc{ug32BBwE8CpWsnsw_63jf+x}FKcakIc5L#=)Y~Z-YT@5!T^_%`
zgO#Ko*_>5JWo^E$^OHcu6z{lN)K*o@vB0P2t@m3du0HA!l@y=RH3P^vj0{I0I(_b(
zX_w~NCSg2HUlSS*o1t%q}ER`=sVB)ltK;k*~4aM;LVtJ?=k(C
zn3_Ru$>6)C1bcTQdyC`G-mSh@oyqqJX97HbD~~1Yhr_^azW}Iyuvd)~ZRj}H_sgPL
zcO?>=Uu27x${CUiwb9^ws5b3<~+Xcs^A^-9yKz-h$_P>`GM~J;wP}Gv=wL
zeYRvFeS`qkQGs3FB?c&SB-6p28eVGnGBR97sIh?6=6a_VU?wK;mhIuG{z;Yf>8DT)
zcY(WBfpc;f{gKLp9e3%2#yfR1nAXgN-9)W$@76=q8R+UFbtO(Ac9pUKDvkRVHL>kp
zQFc|h0S2SoZRCGdu#F#(FQ{P+#9d;Zy+aZR3M5nNq)xlMBp2^_tfeT`tew2z*x1v-
zj@%>Um(_hjoFF&WO-|ITHOp;rZRuKqgMS7|r(4rF1{_Pd+RKivUJ>aVi-hj7qrbjL
zWCX`Xs&TY<1km>7h#lz*7e2#p_)Yy4I4{&d|D0}D#qE(Y{-Yno3w(z?CDtAmX
z$gPQuENj2nS~{<}{I;ZT2F-_?hw`tQ|zO#ulFb92i{^y%iXUraFsInpB%
zej?vd!>}bF_xXm*{dYn>a3U!tgBzp%&TBWWQtaf2Dfs$@T3esx#YJ3=Qzi%3pSKNt
zYE~EDZcRn?6G)By{(%*_Qp8cDHd_iz{rZCHGVJGCesYxpjGecg8{%)3XHNo25|*U}
zcs_P6Ky|%hPr;*rx1HD7ME4D~lolkx+p3kh1yXIm?N^%tI&OIMGsP#gnGrll@CM@@
znH_`mvMAb<4C&vRp4MwM2UmJI8BaTs6*$0V@F)3;@@Sp{qhJOm`+q3aJcn=%=01uc6X?O;qs&
z9DS&**KM(Pci2mR$o5l(+{w#G)EsVueSx@!xYrG6^$yms#F5@&Q*aK!%>Far_>0;~
zuzwOlv5bL4V!C`glN`S%M|4XLEZIu9krHW$`H`6;3g*t!R>gHD&02yZ{6mE8iHKnv
zQZY?=|MYvyQCBnsC0|1ayg!kS0COjtP3L~+MD}B3nvy3WT9DFot(^`{XTofm4&bGB
z`Hys-vPB%_j+$mefiW{>HqiE8FqfDs2eu~IC{
z!*AUxZ02*Lk@Jx{3pFkEH^zfzY&&jG8l4MfBn5*i0W9&s9+YrrYxVTQH04?1nrFf(
zi0KEXA$Hl+i&wzhE|2F^se@Geqna-}El7Uf4t?IW#Nk1KDY3sC*H8_r+g<2QuSs=_
zg&ymo>~`ZeRf!}owK3SUG=CapkAzSG!vimyW$ds6jgg;BEbWG`g(}$Jb{(I;oO-R%
z5kyLrZuB)>-=Lu)#D3wE@N!e^B?GEMQ=^?ZJO(6-oNFo3{6+_&caOTKSqOw(95B+k
zmjib|PntE2+=64EBxZHmT8?Z+PTOBJzlr9>`1m*;7#fk^njFi!7CJw-
zgHF9gKIcU@rI~i=miWl;BIfiP6+N>?K+!Vi8Dy%K{Pxs~C-2=WyWx}*Z5jx`ek@1?Vq08J%qlkV$san0$T8CFR$jaTOvs-I#Ok@Dw~X-|9OW52wb9Cw*|J~*i5
z+dj0>!4NSPVV#kP79>`-fY99(QM6pxYTcZrAR2FK07i>JbLF&L$Q?gr80aD!Pun*Kw!XkZ%j?;`M?F=T9fnD^JDOV1x@S
zF=GJJoB7bIiR1deMt3L6A2}|(esP+V_&}m~ZizZkzo&EbBC7{J$Op0aPV&PkPj>qP
zW{_*nD}x%Jo|i2by!sZ0zyV!o*T`)v<`*Eu!etfbK*xb}>T$hkb
zcY#$Kx14k#gKK20TE&Mwo2OigjbL7p!;-OW5<9|}^zv}rf
zO8I^S|M8@=Df{&RjFbJw9YA4>=e$UBYQK#4FHsX1>uBPP@_wuPm@u{3O0uNv2fvLC
z#q+YSBE{idOCEy^6R!&O$Bz03YWsd>Fp#5P5*5lCsJRiiQokgWm4ckBC0UgHny=PJ
zJ&_}0aPQR(5nTw1e^|pcyVx>v*=GgrQpw5tx@7f+^b
zSjN70_^omL4{!4jy&(Y5{p7N}YPUa_T(%c0fqaF3XhLVUq3*F8k=%osK(C%g1My_?9_OH!UcEcg4at*3X0^vsa!}0}C-H
zn&4FzM?-42Q;Y^i@y}yNnrU?D)y?Hlv`@{!l_%b-1`0L|t^Qrf9``~fbF^kdWT$q7B*QXrcD^I~uKj^>Wm0E`f$uE2J68O0d5&dti^#hY6|4jX$I&;COzQny`F__}9ide19iZ
zw%n`)h5Uh&fDU)9;MhgN+Xu6>XgFA|2~rjOpka)r4FEP$?@
z5-&XXqP(uK8TDK)cUm2l$tQ8bGxZ+^QepK|h$^YgCK(j8m=wBBvPb>_l_R^_u-
zv`kH18B^NNNfKJB(pPmU8_BjYDuWD?ar?2cT8sP+i)EoIl9uky`x`6;k2dKM3KfE-KZR(MGGB#;
zWIa6T9ulL>&5)bKAIFb_36qZ^&0rf$`QSW)q!i{`GI$u!<&p&4P_t;W4-EM@oyed%
zBQc=*HpeO{@DV~f8j3tN$XAMBx##dZNEdM3o)zr}NQZGnKDQ^hAzFEV;5Nk%d0@Ww
znV3`Z+pS(A1bNmWy_~h{6TklRCo2dUTWXy1m3)?m{$kqKC*753oBg1?4
zHazz-L-Y)is%Zls)z0FD5bi_5SsB$Y??%AgLDQvLt-Gz%5X3q9hX7T9Ru#i06#)@4
z)~AcImS=^=%FnYNaI^Z5B8O+-FI*?;3`|@I-;LOYCOd}1%Ti_b?^lWbb2u}hfvm|(a6SL{oZ91_S?
zehleLEPFDkw&+zbHu%8j{<^ESZtgbhKf~tuM*TO>)LV#nL4>c?K#y|ZmwU0@CdUT$
zEQ8J3xNF*SUo1R8NgW2;O9Nl8CSzk-F6@uDIPs;@qS?W-m@QVrq>{vTm7&)%ywi3D`bI;c;
z=bOYj*6!I2vn@;gk=koZO7uiNxMp(9>(3!Rflt#%CBYM|D*Yvfw+_$1U@O
zXDJ404#YzZTTDFBOE7XW7ul70!@zj^DrdjYrK2(n(rL*egyTWf(PMX7AT3#Xirk??
zzwH>=W{CRk`$2*-FtY^iD}=q;O_&%H&we&s16vI&-qMq*&Z{z0h@l*ef+~r-@cWz-
z8qPlEL3nJM8Fp+kt4Ozqfh;BynI#6|MI0|Ns=o1vZ<+Q?++sL!%~X6<&>=4-%+CU8
zo~x&$NT3wFHHYxKhx0sX*PVhL-Hnw5%Bl++(WLFllo}ISJoCVDBX9idFOZpt%!?r8
z6S;~FsEQ1NMR)hPe7-dTE}K}+^vOAG=-F1}Hg6(d_XB}$QA{X7Le6>A(H;*l6j+3*
zVf8dQ5~I%*q0{ohJt`zP;i7cKMW&=vMph7TbE(}2OC-4dn(;CGd>C#PP>&~d7vY7G
z9_fqOYl;KR+&G=HnqaOA2@hep3_81ynDO-y28@`6rp$#jg;9kiTv4n?bDlO4gsVl<
zXX3Zwk%6718FxV~@bM-MkCbwyD+0DvafN^5Vhq|7h<8t|+-TTI+;n8c^B~|h
zs{j$;D>ayxs|(thNbTJjZ=K_4(m^QPe=1~LP~u&D3xSFy^`Q;jRe?$4A+NUup!?If
zgs&TJ4b~;+bJ{l3W&QBxTT?r>nCAM$*d>~?n|OWt5t_~VFG7$`!0G%=qT;xO-Zqv3
zHeIjSjF284;SYw5goxE?#yAr?bl_OJR-okp*E@5h|Du>*1RLX{Scuv_ZK!<5Rgc{e
zfXkyvE87THqA;%