Last active
October 25, 2023 14:56
-
-
Save ImN1/149231ac57d637cfc6c67b6d9d89565f to your computer and use it in GitHub Desktop.
image_viewer.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# -*-coding:utf-8 -*- | |
''' | |
A picture slideshow viewer widget | |
''' | |
import os | |
import natsort | |
from PyQt5 import QtWidgets, QtCore, QtGui | |
# from imnGuis.mQt5Property import Property, PropertyMeta | |
from imnGuis.mQt5State import StateMachine | |
# from imnPaths import mPath | |
from imnPaths.mTree import treeFiles | |
# selfdir = os.path.dirname(__file__) | |
regx = r'[^\.]*\.(bmp|gif|jpe?g|pbm|pgm|png|ppm|svg)$' | |
from functools import partial | |
# from PySide6 import QtCore, QtGui, QtWidgets, QtStateMachine | |
# from imnGuis.mQt6State import StateMachine | |
from imnGuis.mQt5Img import cv2Qim | |
import pandas as pd | |
import numpy as np | |
import pyvips | |
from imnPaths.mTree import treeFilePaths | |
from imnArrays.mDict import Namespace | |
from imnSyntaxs.mVar import empty | |
from imnEnvs.mCatalog import within | |
class FloatingButtonWidget(QtWidgets.QPushButton): | |
'''https://www.deskriders.dev/posts/007-pyqt5-overlay-button-widget/''' | |
def __init__(self, x:int=0, y:int=0, parent=None): | |
super().__init__(parent) | |
self.orginalX = x | |
self.orginalY = y | |
self.padding = None | |
def resizeEvent(self, event): | |
super().resizeEvent(event) | |
self.update_position() | |
def mousePressEvent(self, event): | |
self.parent().floatingButtonClicked.emit(self) | |
def set_padding(self, x, y): | |
self.padding = (x, y) | |
def update_position(self): | |
parent_rect = self.parent().rect() | |
if hasattr(self.parent(), 'viewport'): | |
parent_rect = self.parent().viewport().rect() | |
w, h = self.width(), self.height() | |
if not parent_rect: | |
self.setGeometry(0, 0, w, h) | |
return | |
if self.padding is None: | |
v = max(parent_rect.width()//50, parent_rect.height()//50) | |
self.padding = (v, v) | |
x = self.padding[0] + self.orginalX | |
y = self.padding[1] + self.orginalY | |
if hasattr(self.parent(), 'buttonsPosition'): | |
po = self.parent().buttonsPosition | |
if po&1: | |
x = parent_rect.width() - w - x | |
if (po>>1)&1: | |
y = parent_rect.height() - h - y | |
self.setGeometry(x, y, w, h) | |
class ImageLabel(QtWidgets.QLabel): | |
# floatingButtonClicked = QtCore.Signal(object) | |
# dropFilesFinished = QtCore.Signal(object) | |
floatingButtonClicked = QtCore.pyqtSignal(object) | |
dropFilesFinished = QtCore.pyqtSignal(object) | |
regx = r'[^\.]+\.(bmp|gif|jpe?g|pbm|pgm|png|ppm|svg)$' | |
def __init__(self, parent=None): | |
super(ImageLabel, self).__init__(parent) | |
self.setProperty('cssClass', 'imageLabel') | |
self.images = [] | |
self.current = -1 | |
self.setup_layout() | |
def setup_layout(self): | |
'''如果浮动按钮靠右,或靠底,x,y 默认为 0(不含 padding),距离边缘较远的,需要设定x,y''' | |
self.setMargin(0) | |
self.setContentsMargins(0, 0, 0, 0) | |
self.setAcceptDrops(True) | |
self.setStyleSheet('QLabel{border: 1px solid black;}') | |
# self.buttonsPosition = QtCore.Qt.Corner.TopRightCorner | |
self.buttonsPosition = 1 # 0~3 top-left, top-right, bottom-left, bottom-right | |
self.btn_next = FloatingButtonWidget(parent=self) # parent 是必须的,用于计算位置座标 | |
self.btn_next.setFixedSize(30, 25) | |
self.btn_next.setContentsMargins(0,0,0,0) | |
self.btn_next.setStyleSheet('QPushButton {background-color: transparent;border:0}') | |
self.btn_back = FloatingButtonWidget(parent=self, x=self.btn_next.width()) | |
self.btn_back.setFixedSize(30, 25) | |
self.btn_back.setContentsMargins(0,0,0,0) | |
self.btn_back.setStyleSheet('QPushButton {background-color: transparent;border:0}') | |
self.floatingButtonClicked.connect(self.buttonClick) | |
self.setText(' Drop Images/Folder Here ') | |
self.btn_back.setIcon(self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_ArrowLeft)) | |
self.btn_next.setIcon(self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_ArrowRight)) | |
def resizeEvent(self, event): | |
self.updateGeometry() | |
self.btn_next.update_position() | |
self.btn_back.update_position() | |
def dragEnterEvent(self, event): | |
if event.mimeData().hasUrls(): | |
event.accept() | |
else: | |
event.ignore() | |
def dropEvent(self, event): | |
files = tuple(u.toLocalFile() for u in event.mimeData().urls()) | |
ims = [] | |
for x in files: | |
if os.path.isfile(x) and mPath.regCheckPath(x, self.regx): | |
ims.append(x) | |
continue | |
if os.path.isdir(x): | |
[ims.append(y) for y in treeFilePaths(x, regx=self.regx)] | |
self.images = natsort.natsorted(ims, alg=natsort.ns.PATH) | |
self.dropFilesFinished.emit(self.images) | |
self.nextim() | |
return | |
def buttonClick(self, sender): | |
if sender==self.btn_back: | |
self.nextim(reverse=True) | |
return | |
if sender==self.btn_next: | |
self.nextim() | |
return | |
def set_current(self, value:int=-1): | |
self.current = value | |
def nextim(self, imlist=None, reverse:bool=False): | |
if imlist is None: | |
if empty(self.images): | |
return | |
imlist = self.images | |
if reverse: | |
self.current -= 1 | |
if self.current<0: | |
self.current = len(imlist) - 1 | |
else: | |
self.current += 1 | |
if self.current>=len(imlist): | |
self.current = 0 | |
self.showImage(imlist[self.current]) | |
def showImage(self, im:QtGui.QImage): | |
if empty(im): | |
self.clear() | |
return | |
w, h = self.width()-2, self.height()-2 | |
if isinstance(im, str): | |
if not os.path.exists(im): | |
return | |
vim = pyvips.Image.thumbnail(im, min(w, h), size='down', crop='all') | |
im = cv2Qim(vim.numpy()) | |
if im.width()>w or im.height()>h: | |
im = im.scaled(w, h, QtCore.Qt.KeepAspectRatio, QtCore.Qt.FastTransformation) | |
self.setPixmap(QtGui.QPixmap.fromImage(im)) | |
class ViewPad(QtWidgets.QWidget): | |
# floatingButtonClicked = QtCore.Signal(object) | |
floatingButtonClicked = QtCore.pyqtSignal(object) | |
def __init__(self, showmode:int=1, parent=None): | |
super(ViewPad, self).__init__(parent) | |
# self.parent = parent | |
self.showmode = showmode | |
self.setContentsMargins(0,0,0,0) | |
self.setup_layout() | |
# self.initSignal() | |
# self.initStateMachine() | |
# self.initThread() | |
def setup_layout(self): | |
self.setContentsMargins(0,0,0,0) | |
self.spt = QtWidgets.QSplitter(QtCore.Qt.Orientation.Horizontal) | |
self.imgLabels = Namespace() | |
for p in ('left', 'middle', 'right'): | |
self.imgLabels[p] = ImageLabel(self) | |
self.imgLabels[p].setContentsMargins(0,0,0,0) | |
self.spt.addWidget(self.imgLabels[p]) | |
if p=='middle': | |
self.imgLabels[p].btn_back.hide() | |
self.imgLabels[p].btn_next.hide() | |
# # self.spt.setHandleWidth(0) # 控制缝隙距离 | |
hlyt = QtWidgets.QHBoxLayout() | |
hlyt.setSpacing(0) | |
hlyt.setContentsMargins(0,0,0,0) | |
hlyt.addWidget(self.spt) | |
self.setLayout(hlyt) | |
self.btn = FloatingButtonWidget(parent=self) | |
self.btn.setFixedSize(100, 30) | |
self.btn.set_padding(0, 0) | |
self.btn.setText('Match') | |
self.btn.hide() | |
# self.btn.setStyleSheet("""QPushButton{color: rgba(33,33,33, 0.1);background: rgba(255,255,255, 0.1);opacity: 0.1;} | |
# QPushButton:hover{color: rgba(0,0,0, 1.0);background: rgba(255,255,255, 1);opacity: 1.0;}""") | |
self.floatingButtonClicked.connect(partial(print, 'OK')) | |
def resizeEvent(self, event): | |
self.updateGeometry() | |
self.btn.orginalX = (self.width() - self.btn.width()) // 2 | |
self.btn.orginalY = self.height() - (self.height() // 50 + self.btn.height()) | |
self.btn.update_position() | |
def setup_mode(self, showmode:int): | |
self._showmode = showmode | |
class ViewControls(QtWidgets.QWidget): | |
lang = { | |
'SlideShowViewer.nextToolTip': ' Next ', | |
'SlideShowViewer.backToolTip': ' Back ', | |
'SlideShowViewer.fastToolTip': ' Wait 1s less ', | |
'SlideShowViewer.slowToolTip': ' Wait 1s more ', | |
'SlideShowViewer.skipToolTip': ' Skip All & Clear ', | |
'SlideShowViewer.startToolTip': ' Start slide show ', | |
'SlideShowViewer.pauseToolTip': ' Pause ', | |
'SlideShowViewer.resumeToolTip': ' Resume ', | |
'SlideShowViewer.closeToolTip': ' Close ', | |
'viewerStartMsg': 'Drag Pictures or Picture Folder Here', | |
} | |
def __init__(self, showmode:int=1, parent=None): | |
super(ViewControls, self).__init__(parent) | |
self.started = False | |
self.setFixedWidth(40) | |
self.setup_layout() | |
self.initTranslate() | |
def setup_layout(self): | |
self.lbl_num = QtWidgets.QLabel() | |
self.lbl_num.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) | |
self.btn_single = QtWidgets.QPushButton() | |
self.btn_dual = QtWidgets.QPushButton() | |
self.btn_mirror = QtWidgets.QPushButton() | |
self.btn_triple = QtWidgets.QPushButton() | |
self.btn_pause = QtWidgets.QPushButton() | |
self.btn_next = QtWidgets.QPushButton() | |
self.btn_back = QtWidgets.QPushButton() | |
self.btn_fast = QtWidgets.QPushButton() | |
self.btn_slow = QtWidgets.QPushButton() | |
self.btn_skip = QtWidgets.QPushButton() | |
self.btn_close = QtWidgets.QPushButton() | |
vlyt = QtWidgets.QVBoxLayout() | |
vlyt.setContentsMargins(5, 5, 5, 5) | |
vlyt.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) | |
vlyt.addWidget(self.btn_single) | |
vlyt.addWidget(self.btn_dual) | |
vlyt.addWidget(self.btn_triple) | |
vlyt.addWidget(self.btn_mirror) | |
vlyt.addStretch(1) | |
vlyt.addWidget(self.lbl_num) | |
vlyt.addWidget(self.btn_pause) | |
vlyt.addWidget(self.btn_fast) | |
vlyt.addWidget(self.btn_slow) | |
vlyt.addWidget(self.btn_next) | |
vlyt.addWidget(self.btn_back) | |
vlyt.addWidget(self.btn_skip) | |
vlyt.addWidget(self.btn_close) | |
self.setLayout(vlyt) | |
selfdir = os.path.dirname(__file__) | |
self.btn_single.setIcon(QtGui.QIcon(os.path.join(selfdir, 'image.svg'))) | |
self.btn_dual.setIcon(QtGui.QIcon(os.path.join(selfdir, 'dual_black.svg'))) | |
self.btn_triple.setIcon(QtGui.QIcon(os.path.join(selfdir, 'tri_black.svg'))) | |
self.btn_mirror.setIcon(QtGui.QIcon(os.path.join(selfdir, 'dual_white.svg'))) | |
self.btn_pause.setIcon(self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MediaPlay)) | |
self.btn_next.setIcon(self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MediaSkipForward)) | |
self.btn_back.setIcon(self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MediaSkipBackward)) | |
self.btn_fast.setIcon(self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MediaSeekForward)) | |
self.btn_slow.setIcon(self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MediaSeekBackward)) | |
self.btn_skip.setIcon(self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MediaStop)) | |
self.btn_close.setIcon(self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_LineEditClearButton)) | |
def initTranslate(self): | |
self.btn_pause.setToolTip(self.lang['SlideShowViewer.pauseToolTip']) | |
self.btn_next.setToolTip(self.lang['SlideShowViewer.nextToolTip']) | |
self.btn_back.setToolTip(self.lang['SlideShowViewer.backToolTip']) | |
self.btn_fast.setToolTip(self.lang['SlideShowViewer.fastToolTip']) | |
self.btn_slow.setToolTip(self.lang['SlideShowViewer.slowToolTip']) | |
self.btn_skip.setToolTip(self.lang['SlideShowViewer.skipToolTip']) | |
class Viewer(QtWidgets.QWidget, QtCore.QObject): | |
# imagesChangeSignal = QtCore.Signal() | |
# pauseSignal = QtCore.Signal() | |
imagesChangeSignal = QtCore.pyqtSignal() | |
pauseSignal = QtCore.pyqtSignal() | |
def __init__(self, showmode:int=1, parent=None): | |
super(Viewer, self).__init__(parent) | |
# self._showmode = showmode # useless | |
self.setProperty('showmode', showmode) | |
# self._pause = True # useless | |
# self.setProperty('pause', True) # useless | |
self.setContentsMargins(0,0,0,0) | |
self.wait = 1500 | |
self.waitMin = 1000 | |
self.waitMax = 10000 | |
self.timer = QtCore.QTimer() | |
self.timer.setInterval(self.wait) | |
self.current = 0 | |
self.total = 0 | |
self.images = None | |
self.setup_layout() | |
self.initSignal() | |
self.initStateMachine() | |
# self.initThread() | |
def setup_layout(self): | |
self.setContentsMargins(0,0,0,0) | |
self.pad = ViewPad(self) | |
self.controls = ViewControls() | |
self.slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal) | |
self.slider.setProperty('cssClass', 'circleQSlider') | |
# self.slider.setStyleSheet( | |
# # ''' | |
# # QSlider::groove:horizontal { | |
# # height:10px; | |
# # border:1px solid black; | |
# # border-radius: 5px; | |
# # border-style:inset; | |
# # } | |
# # QSlider::handle:horizontal { | |
# # width:10px; | |
# # height: 15px; | |
# # color:#ccc; | |
# # border:1px solid black; | |
# # border-radius:5px; | |
# # border-style:outset; | |
# # background:qradialgradient(cx:0.3,cy:-0.4,fx:0.3,fy:-0.4,radius:1.35,stop:0 #fff,stop:1 #ccc); | |
# # } | |
# # QSlider::handle:horizontal:hover { | |
# # border-radius: 5px; | |
# # } | |
# # ''' | |
# ''' | |
# QSlider::groove:horizontal { | |
# border-radius: 2px; | |
# height: 3px; | |
# margin: 9px; | |
# background-color: #ccc; | |
# } | |
# QSlider::groove:horizontal:hover { | |
# background-color: #666; | |
# } | |
# QSlider::handle:horizontal { | |
# background-color: #fff; | |
# border: 2px solid #000; | |
# height: 14px; | |
# width: 12px; | |
# margin: -6px 0; | |
# border-radius: 7px; | |
# padding: -6px 0px; | |
# } | |
# ''' | |
# ) | |
self.lbl = QtWidgets.QLabel() | |
self.lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) | |
self.lbl.adjustSize() | |
self.lbl.setText('0 / 0') | |
self.slider.setValue(0) # 数值为下标,显示时 +1 | |
self.slider.setPageStep(1) | |
hlyt = QtWidgets.QHBoxLayout() | |
hlyt.addWidget(self.lbl) | |
hlyt.addWidget(self.slider) | |
vlyt = QtWidgets.QVBoxLayout() | |
vlyt.setContentsMargins(0,0,0,0) | |
vlyt.addWidget(self.pad) | |
vlyt.addLayout(hlyt) | |
hlyt_viewer = QtWidgets.QHBoxLayout() | |
hlyt_viewer.addLayout(vlyt) | |
hlyt_viewer.addWidget(self.controls) | |
self.setLayout(hlyt_viewer) | |
def initTranslate(self): | |
pass | |
def initSignal(self): | |
self.controls.btn_next.clicked.connect(self.nextim) | |
self.controls.btn_back.clicked.connect(self.nextim) | |
self.controls.btn_fast.clicked.connect(self.timewait_change) | |
self.controls.btn_slow.clicked.connect(self.timewait_change) | |
self.controls.btn_skip.clicked.connect(self.end_show) | |
# self.btn_close.clicked.connect(self.close) | |
self.slider.sliderReleased.connect(self.slider_change) | |
self.slider.valueChanged.connect(self.slider_change) | |
self.timer.timeout.connect(self.nextim) | |
# self.slider.sliderPressed.connect(self.timer.stop) | |
[w.dropFilesFinished.connect(self.add_images) for w in self.pad.imgLabels.values()] | |
self.imagesChangeSignal.connect(self.images_changed) | |
def initStateMachine(self): | |
param = {'states': 2, 'init': 0,} # pause: 0 False, 1 True | |
param['property'] = ( | |
# (self, b'paused', (False, True)), | |
(self.controls.btn_pause, 'icon', ( | |
self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MediaPlay), | |
self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MediaPause), | |
)), | |
(self.controls.btn_pause, 'toolTip', ( | |
self.controls.lang['SlideShowViewer.pauseToolTip'], | |
self.controls.lang['SlideShowViewer.resumeToolTip'] | |
)), | |
) | |
param['transition'] = ( | |
{self.controls.btn_pause.clicked: 1,}, # self.paused False -> True | |
{ | |
self.controls.btn_pause.clicked: 0, | |
self.pauseSignal: 0, | |
self.slider.sliderPressed: 0, | |
}, # self.paused True -> False | |
) | |
param['connect'] = ( | |
((self.timer.stop,), None), # self.paused True -> False | |
((self.timer.start,), None), # self.paused False -> True | |
) | |
self.pauseState = StateMachine(param=param) | |
param = {'states': 4, 'init': self.property('showmode'),} # pause: 0 single, 1 dual, 2 triple, 3 mirror | |
param['property'] = ( | |
(self, 'showmode', (0, 1, 2, 3)), | |
(self.pad.spt, 'handleWidth', (0, 5, 5, 0)), | |
(self.controls.btn_single, 'enabled', (False, True, True, True)), | |
(self.controls.btn_dual, 'enabled', (True, False, True, True)), | |
(self.controls.btn_triple, 'enabled', (True, True, False, True)), | |
(self.controls.btn_mirror, 'enabled', (True, True, True, False)), | |
(self.pad.imgLabels['left'], 'alignment', ( | |
QtCore.Qt.AlignmentFlag.AlignCenter|QtCore.Qt.AlignmentFlag.AlignVCenter, | |
QtCore.Qt.AlignmentFlag.AlignCenter|QtCore.Qt.AlignmentFlag.AlignVCenter, | |
QtCore.Qt.AlignmentFlag.AlignCenter|QtCore.Qt.AlignmentFlag.AlignVCenter, | |
QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignVCenter,)), | |
(self.pad.imgLabels['right'], 'alignment', ( | |
0, | |
QtCore.Qt.AlignmentFlag.AlignCenter|QtCore.Qt.AlignmentFlag.AlignVCenter, | |
QtCore.Qt.AlignmentFlag.AlignCenter|QtCore.Qt.AlignmentFlag.AlignVCenter, | |
QtCore.Qt.AlignmentFlag.AlignLeft|QtCore.Qt.AlignmentFlag.AlignVCenter,)), | |
(self.pad.imgLabels['middle'], 'alignment', ( | |
0, | |
0, | |
QtCore.Qt.AlignmentFlag.AlignCenter|QtCore.Qt.AlignmentFlag.AlignVCenter, | |
0,)), | |
(self.pad.imgLabels['right'], 'visible', (False, True, True, True)), | |
(self.pad.imgLabels['middle'], 'visible', (False, False, True, False)), | |
) | |
param['transition'] = ( # key=signal, value=go to state num | |
{ | |
self.controls.btn_dual.clicked: 1, | |
self.controls.btn_triple.clicked: 2, | |
self.controls.btn_mirror.clicked: 3,}, | |
{ | |
self.controls.btn_single.clicked: 0, | |
self.controls.btn_triple.clicked: 2, | |
self.controls.btn_mirror.clicked: 3,}, | |
{ | |
self.controls.btn_single.clicked: 0, | |
self.controls.btn_dual.clicked: 1, | |
self.controls.btn_mirror.clicked: 3,}, | |
{ | |
self.controls.btn_single.clicked: 0, | |
self.controls.btn_dual.clicked: 1, | |
self.controls.btn_triple.clicked: 2,}, | |
) | |
fun_single = partial(self.pad.spt.setSizes, [1, 0, 0]) | |
fun_dual = partial(self.pad.spt.setSizes, [5, 0, 5]) | |
fun_triple = partial(self.pad.spt.setSizes, [5, 5, 5]) | |
fun_mirror = partial(self.pad.spt.setSizes, [5, 0, 5]) | |
param['connect'] = ( | |
((fun_single,), None), | |
((fun_dual,), None), | |
((fun_triple,), None), | |
((fun_mirror,), None), | |
) | |
self.secondPane = StateMachine(param=param) | |
def keyPressEvent(self, keyevent): | |
""" Capture key to exit, next image, previous image, | |
on Escape , Key Right and key left respectively. | |
""" | |
key = keyevent.key() | |
if key == QtCore.Qt.Key_Escape: | |
self.end_show() | |
if key == QtCore.Qt.Key_Left: | |
self.pauseSignal.emit() | |
self.nextim(True) | |
if key == QtCore.Qt.Key_Right: | |
self.pauseSignal.emit() | |
self.nextim() | |
if key == 32: | |
self.controls.btn_pause.click() | |
def images_changed(self): | |
if empty(self.images): | |
self.slider.setRange(0, 100) | |
self.slider.setValue(0) | |
self.lbl.setText('0 / 0') | |
for w in self.pad.imgLabels.values(): | |
w.images = [] | |
w.current = -1 | |
return | |
def _set_child_images(p): | |
w = self.pad.imgLabels[p] | |
if p in self.images.columns: | |
w.images = self.images[p] | |
elif (p:=f'paths_{p}') in self.images.columns: | |
w.images = self.images[p] | |
self.total = len(self.images) | |
self.slider.setRange(0, self.total-1) | |
self.lbl.setText(f'{self.current}/{self.total}') | |
[_set_child_images(p) for p in self.pad.imgLabels.names()] | |
def add_images(self, ims): | |
sender = self.sender() | |
p = self.pad.imgLabels(sender) | |
name = f'paths_{p}' | |
if isinstance(self.images, pd.DataFrame): | |
if self.images.shape[0]>=len(ims): | |
self.images[name] = pd.Series(ims) | |
else: | |
self.images = self.images.join(pd.Series(ims, name=name), how='outer').reset_index(drop=True) | |
else: | |
self.images = pd.DataFrame({name:ims}) | |
self.imagesChangeSignal.emit() | |
def set_images(self, ims:pd.DataFrame): | |
self.images = ims | |
self.imagesChangeSignal.emit() | |
def slider_change(self): | |
self.current = self.slider.value() | |
value = self.current + 1 | |
[w.set_current(self.current) for w in self.pad.imgLabels.values()] | |
total = self.total | |
self.lbl.setText(f'{value}/{total}') | |
QtWidgets.QToolTip.showText(QtGui.QCursor.pos(), f'{value} / {total}', self.slider) | |
self.showImage() | |
def set_current(self, value): | |
self.current = self.slider.value() | |
def end_show(self): | |
self.pauseSignal.emit() | |
[w.clear() for w in self.pad.imgLabels.values()] | |
self.images = None | |
self.imagesChangeSignal.emit() | |
def nextim(self, reverse:bool=False): | |
if self.images is None: | |
return | |
sender = self.sender() | |
if sender in {self.controls.btn_back, self.controls.btn_next}: | |
self.pauseSignal.emit() | |
# self.timer.stop() | |
if sender==self.controls.btn_back: | |
reverse = True | |
if reverse: | |
self.current -= 1 | |
if self.current<0: | |
self.current = len(self.images) - 1 | |
else: | |
self.current += 1 | |
if self.current>=len(self.images): | |
self.current = 0 | |
self.slider.setValue(self.current) | |
# self.showImage() | |
def timewait_change(self): | |
sender = self.sender() | |
if sender==self.controls.btn_slow: | |
self.wait += 1000 | |
if sender==self.controls.btn_fast: | |
self.wait -= 1000 | |
i = within(self.wait, (self.waitMin, self.waitMax)) | |
self.controls.btn_fast.setEnabled(i>=1) | |
self.controls.btn_slow.setEnabled(i<=1) | |
self.wait = (self.waitMin, self.wait, self.waitMax)[i] | |
self.timer.setInterval(self.wait) | |
def showImage(self): | |
if empty(self.images): | |
return | |
[w.showImage(w.images[self.current]) for w in self.pad.imgLabels.values() if not empty(w.images)] | |
# 上面一些不能导入的函数、类,因为多项目通用,所以分在其他文件 | |
# 现抽出来,贴在下面,其他 import 应该没什么用,是以前留下的废代码 | |
# ===================================== | |
# 这个 StateMachine 当初随手写的,没写好,pyqt5 勉强能用,也就懒得改了 | |
# pyqt6不能用,pyqt6 需要多套了一层 QState,而且 namespace 变了 | |
class StateMachine(QtCore.QStateMachine, QtCore.QObject): | |
def __init__(self, parent=None, param=None): | |
''' | |
调用方式例子:\n | |
param = {'states': 2, 'init': 0,} # states 是状态总数,下标从0开始,init 为初始状态,下标值\n | |
param['property'] = ( # 属性值 -> 每组各个状态的值,对应下标\n | |
(self.tray, 'icon', (self._icon(Datas.env_icons['white']), self._icon(Datas.env_icons['green']))),\n | |
(self.tray.tray_menu_show, 'visible', (False, True)),\n | |
(self.tray.tray_menu_hide, 'visible', (True, False)),\n | |
(self, 'visible', (True, False)),\n | |
)\n | |
param['transition'] = ( # 每个状态下如何激活其他状态,key激活条件,value是目标状态序号,字典个数不能少于状态数,顺序对应下标\n | |
{\n | |
self.tray.dblclick:1, \n | |
self.tray.tray_menu_hide.triggered:1, \n | |
self.mainwindowShowHideSignal:1\n | |
}, # mainWindows show -> hide\n | |
{\n | |
self.tray.dblclick:0, \n | |
self.tray.tray_menu_show.triggered:0, \n | |
self.mainwindowShowHideSignal:0\n | |
}, # mainWindows hide -> show\n | |
)\n | |
param['connect'] = (\n | |
状态进出时发出信号,每组对应状态下标,参考上述 if 'connect'...部分\n | |
结构:每行对应一个状态,二元二维 tuple -> (进入状态要执行的多个方法 [tuple],离开状态要执行的多个方法 [tuple])\n | |
进入或离开没有任何追加方法则为 None\n | |
((self.timer.start,), None), # self.paused False -> True\n | |
((self.timer.stop,), None), # self.paused True -> False\n | |
)\n | |
mainShowHideState = StateMachine(self, param) # 定义状态(启用) | |
''' | |
# super().__init__(parent=parent) | |
super(StateMachine, self).__init__(parent=parent) | |
self.states = [QtCore.QState(self) for _ in range(param['states'])] | |
# self.states = [QtCore.QState(self) for _ in range(param['states'])] | |
for i in range(param['states']): | |
for x in param['property']: | |
self.states[i].assignProperty(x[0], x[1], x[2][i]) | |
if 'transition' in param and param['transition'][i]: | |
t = param['transition'][i] | |
for x in t: | |
self.states[i].addTransition(x, self.states[t[x]]) | |
if 'connect' in param and param['connect'][i]: | |
c = param['connect'][i] | |
if c[0]: | |
for x in c[0]: | |
self.states[i].entered.connect(x) | |
if c[1]: | |
for x in c[1]: | |
self.states[i].exited.connect(x) | |
self.setInitialState(self.states[param['init']]) | |
self.start() | |
def current(self): | |
return next((i for i,x in enumerate(self.states) if x.active()), -1) | |
#==================================== | |
def cv2Qim(cvim): # 输入格式为 opencv(numpy.ndarray) | |
if len(cvim.shape)==2: | |
h, w = cvim.shape | |
bytesPerLine = w | |
imformat = QtGui.QImage.Format_Grayscale8 # maybe Format_Indexed8 ? | |
else: | |
h, w, channels = cvim.shape | |
bytesPerLine = channels * w | |
imformat = QtGui.QImage.Format_RGB888 | |
return QtGui.QImage(cvim.data, w, h, bytesPerLine, imformat) | |
#======================================= | |
# empty 用于不确定类型的无效值判断,随手写,逻辑不严谨,出错时再改 | |
def empty(s, none2empty:bool=True, false2empty:bool=False, zero2empty:bool=False, white2empty:bool=False): | |
''' | |
判断 s 是否为空值、白值、None值、以及嵌套的空数组\n | |
仅支持较为常用的变量类型,str|bytes|int|float|Nonetype|pandas|numpy.array|一维dict|tuple|set|list等\n | |
嵌套的字典视为 NOT empty,即二维为空时返回 False\n | |
''' | |
if hasattr(s, 'empty'): | |
return s.empty | |
if isinstance(s, list): | |
return not bool(s) | |
if none2empty and s in {np.nan, None}: | |
return True | |
if isinstance(s, (str, bytes)): | |
if len(s)==0: | |
return True | |
if white2empty and s.isspace(): | |
return True | |
return False | |
if hasattr(s, 'shape'): | |
if s.shape[0]==0: | |
return True | |
if len(s.shape)>1 and s.shape[1]==0: | |
return True | |
return False | |
if s: | |
return False | |
if not false2empty and s is False: return False | |
if not zero2empty and s==0: return False | |
if not none2empty and s is None: return False | |
if isinstance(s, PureIterable) and list(flatten(s)): return False | |
return True | |
#========================================== | |
def within(score, range, included:bool=True): | |
'''设定最大最小值范围,及对照类别 0,1,2;1 表示在range范围内,included表示范围是否也包含边界值''' | |
mi, ma = min(range), max(range) | |
breakpoints = (mi, ma) | |
left = bisect.bisect_left(breakpoints, score) | |
right = bisect.bisect(breakpoints, score) | |
if included: | |
return left or right | |
return (left, right)[bool(left and right)] | |
#======================================== | |
# Namespace 不贴了,很长很乱,实际上就是个加强的 dict,继承自 types.SimpleNamespace | |
# names() 获取 list(dict.keys()), 而__call__() 可以根据 dict 某个 value 获取首个对应的 key [str]格式,如果 key/value 一一对应可定位 key | |
# | |
# treeFilePaths 也不贴了,套了闭包,乱,反正返回就是递归获取所有文件的全路径(不含文件夹,regex 过滤),list[str] 格式 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment