3 Commits

Author SHA1 Message Date
cf913d296e impr: minor improvement 2023-10-14 00:55:12 +08:00
1060590e03 wip: show song jacket in ChartDelegate 2023-10-14 00:55:01 +08:00
858abe3415 refactor: TabOcr_B30 2023-10-13 20:15:16 +08:00
14 changed files with 516 additions and 286 deletions

View File

@ -13,7 +13,7 @@
<property name="windowTitle">
<string notr="true">TabOcr_B30</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
@ -27,61 +27,88 @@
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QGroupBox" name="groupBox_3">
<widget class="QGroupBox" name="groupBox_6">
<property name="title">
<string>knnModelSelector.title</string>
<string>dependencies.title</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="FileSelector" name="knnModelSelector" native="true"/>
<layout class="QGridLayout" name="gridLayout" columnstretch="0,0,0,0,1">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>dependencies.knnModel</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="dependencies_knnModelStatusLabel">
<property name="text">
<string notr="true">...</string>
</property>
</widget>
</item>
<item row="0" column="3" rowspan="3">
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>dependencies.b30KnnModel</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QLabel" name="dependencies_phashDatabaseStatusLabel">
<property name="text">
<string notr="true">...</string>
</property>
</widget>
</item>
<item row="0" column="1" rowspan="3">
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>dependencies.phashDatabase</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLabel" name="dependencies_b30KnnModelStatusLabel">
<property name="text">
<string notr="true">...</string>
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="FileSelector" name="dependencies_knnModelSelector" native="true"/>
</item>
<item row="1" column="4">
<widget class="FileSelector" name="dependencies_b30KnnModelSelector" native="true"/>
</item>
<item row="2" column="4">
<widget class="FileSelector" name="dependencies_phashDatabaseSelector" native="true"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_5">
<property name="title">
<string>b30KnnModelSelector.title</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="FileSelector" name="b30KnnModelSelector" native="true"/>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>phashDatabaseSelector.title</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="FileSelector" name="phashDatabaseSelector" native="true"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>imageSelector.title</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="FileSelector" name="imageSelector" native="true"/>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<widget class="OcrQueue" name="ocrQueue" native="true">
<property name="sizePolicy">

View File

@ -15,8 +15,9 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QComboBox, QGroupBox, QHBoxLayout,
QSizePolicy, QVBoxLayout, QWidget)
from PySide6.QtWidgets import (QApplication, QComboBox, QFrame, QGridLayout,
QGroupBox, QLabel, QSizePolicy, QVBoxLayout,
QWidget)
from ui.implements.components.fileSelector import FileSelector
from ui.implements.components.ocrQueue import OcrQueue
@ -27,8 +28,8 @@ class Ui_TabOcr_B30(object):
TabOcr_B30.setObjectName(u"TabOcr_B30")
TabOcr_B30.resize(555, 461)
TabOcr_B30.setWindowTitle(u"TabOcr_B30")
self.verticalLayout_3 = QVBoxLayout(TabOcr_B30)
self.verticalLayout_3.setObjectName(u"verticalLayout_3")
self.verticalLayout_2 = QVBoxLayout(TabOcr_B30)
self.verticalLayout_2.setObjectName(u"verticalLayout_2")
self.groupBox = QGroupBox(TabOcr_B30)
self.groupBox.setObjectName(u"groupBox")
self.verticalLayout = QVBoxLayout(self.groupBox)
@ -39,65 +40,80 @@ class Ui_TabOcr_B30(object):
self.verticalLayout.addWidget(self.b30TypeComboBox)
self.verticalLayout_3.addWidget(self.groupBox)
self.verticalLayout_2.addWidget(self.groupBox)
self.horizontalLayout = QHBoxLayout()
self.horizontalLayout.setObjectName(u"horizontalLayout")
self.groupBox_3 = QGroupBox(TabOcr_B30)
self.groupBox_3.setObjectName(u"groupBox_3")
self.verticalLayout_4 = QVBoxLayout(self.groupBox_3)
self.verticalLayout_4.setObjectName(u"verticalLayout_4")
self.knnModelSelector = FileSelector(self.groupBox_3)
self.knnModelSelector.setObjectName(u"knnModelSelector")
self.groupBox_6 = QGroupBox(TabOcr_B30)
self.groupBox_6.setObjectName(u"groupBox_6")
self.gridLayout = QGridLayout(self.groupBox_6)
self.gridLayout.setObjectName(u"gridLayout")
self.label = QLabel(self.groupBox_6)
self.label.setObjectName(u"label")
self.label.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter)
self.verticalLayout_4.addWidget(self.knnModelSelector)
self.gridLayout.addWidget(self.label, 0, 0, 1, 1)
self.dependencies_knnModelStatusLabel = QLabel(self.groupBox_6)
self.dependencies_knnModelStatusLabel.setObjectName(u"dependencies_knnModelStatusLabel")
self.dependencies_knnModelStatusLabel.setText(u"...")
self.horizontalLayout.addWidget(self.groupBox_3)
self.gridLayout.addWidget(self.dependencies_knnModelStatusLabel, 0, 2, 1, 1)
self.groupBox_5 = QGroupBox(TabOcr_B30)
self.groupBox_5.setObjectName(u"groupBox_5")
self.verticalLayout_6 = QVBoxLayout(self.groupBox_5)
self.verticalLayout_6.setObjectName(u"verticalLayout_6")
self.b30KnnModelSelector = FileSelector(self.groupBox_5)
self.b30KnnModelSelector.setObjectName(u"b30KnnModelSelector")
self.line_2 = QFrame(self.groupBox_6)
self.line_2.setObjectName(u"line_2")
self.line_2.setFrameShape(QFrame.VLine)
self.line_2.setFrameShadow(QFrame.Sunken)
self.verticalLayout_6.addWidget(self.b30KnnModelSelector)
self.gridLayout.addWidget(self.line_2, 0, 3, 3, 1)
self.label_2 = QLabel(self.groupBox_6)
self.label_2.setObjectName(u"label_2")
self.label_2.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter)
self.horizontalLayout.addWidget(self.groupBox_5)
self.gridLayout.addWidget(self.label_2, 1, 0, 1, 1)
self.dependencies_phashDatabaseStatusLabel = QLabel(self.groupBox_6)
self.dependencies_phashDatabaseStatusLabel.setObjectName(u"dependencies_phashDatabaseStatusLabel")
self.dependencies_phashDatabaseStatusLabel.setText(u"...")
self.verticalLayout_3.addLayout(self.horizontalLayout)
self.gridLayout.addWidget(self.dependencies_phashDatabaseStatusLabel, 2, 2, 1, 1)
self.horizontalLayout_3 = QHBoxLayout()
self.horizontalLayout_3.setObjectName(u"horizontalLayout_3")
self.groupBox_4 = QGroupBox(TabOcr_B30)
self.groupBox_4.setObjectName(u"groupBox_4")
self.verticalLayout_5 = QVBoxLayout(self.groupBox_4)
self.verticalLayout_5.setObjectName(u"verticalLayout_5")
self.phashDatabaseSelector = FileSelector(self.groupBox_4)
self.phashDatabaseSelector.setObjectName(u"phashDatabaseSelector")
self.line = QFrame(self.groupBox_6)
self.line.setObjectName(u"line")
self.line.setFrameShape(QFrame.VLine)
self.line.setFrameShadow(QFrame.Sunken)
self.verticalLayout_5.addWidget(self.phashDatabaseSelector)
self.gridLayout.addWidget(self.line, 0, 1, 3, 1)
self.label_3 = QLabel(self.groupBox_6)
self.label_3.setObjectName(u"label_3")
self.label_3.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter)
self.horizontalLayout_3.addWidget(self.groupBox_4)
self.gridLayout.addWidget(self.label_3, 2, 0, 1, 1)
self.groupBox_2 = QGroupBox(TabOcr_B30)
self.groupBox_2.setObjectName(u"groupBox_2")
self.verticalLayout_2 = QVBoxLayout(self.groupBox_2)
self.verticalLayout_2.setObjectName(u"verticalLayout_2")
self.imageSelector = FileSelector(self.groupBox_2)
self.imageSelector.setObjectName(u"imageSelector")
self.dependencies_b30KnnModelStatusLabel = QLabel(self.groupBox_6)
self.dependencies_b30KnnModelStatusLabel.setObjectName(u"dependencies_b30KnnModelStatusLabel")
self.dependencies_b30KnnModelStatusLabel.setText(u"...")
self.verticalLayout_2.addWidget(self.imageSelector)
self.gridLayout.addWidget(self.dependencies_b30KnnModelStatusLabel, 1, 2, 1, 1)
self.dependencies_knnModelSelector = FileSelector(self.groupBox_6)
self.dependencies_knnModelSelector.setObjectName(u"dependencies_knnModelSelector")
self.horizontalLayout_3.addWidget(self.groupBox_2)
self.gridLayout.addWidget(self.dependencies_knnModelSelector, 0, 4, 1, 1)
self.dependencies_b30KnnModelSelector = FileSelector(self.groupBox_6)
self.dependencies_b30KnnModelSelector.setObjectName(u"dependencies_b30KnnModelSelector")
self.verticalLayout_3.addLayout(self.horizontalLayout_3)
self.gridLayout.addWidget(self.dependencies_b30KnnModelSelector, 1, 4, 1, 1)
self.dependencies_phashDatabaseSelector = FileSelector(self.groupBox_6)
self.dependencies_phashDatabaseSelector.setObjectName(u"dependencies_phashDatabaseSelector")
self.gridLayout.addWidget(self.dependencies_phashDatabaseSelector, 2, 4, 1, 1)
self.gridLayout.setColumnStretch(4, 1)
self.verticalLayout_2.addWidget(self.groupBox_6)
self.ocrQueue = OcrQueue(TabOcr_B30)
self.ocrQueue.setObjectName(u"ocrQueue")
@ -107,7 +123,7 @@ class Ui_TabOcr_B30(object):
sizePolicy.setHeightForWidth(self.ocrQueue.sizePolicy().hasHeightForWidth())
self.ocrQueue.setSizePolicy(sizePolicy)
self.verticalLayout_3.addWidget(self.ocrQueue)
self.verticalLayout_2.addWidget(self.ocrQueue)
self.retranslateUi(TabOcr_B30)
@ -117,10 +133,10 @@ class Ui_TabOcr_B30(object):
def retranslateUi(self, TabOcr_B30):
self.groupBox.setTitle(QCoreApplication.translate("TabOcr_B30", u"b30type", None))
self.groupBox_3.setTitle(QCoreApplication.translate("TabOcr_B30", u"knnModelSelector.title", None))
self.groupBox_5.setTitle(QCoreApplication.translate("TabOcr_B30", u"b30KnnModelSelector.title", None))
self.groupBox_4.setTitle(QCoreApplication.translate("TabOcr_B30", u"phashDatabaseSelector.title", None))
self.groupBox_2.setTitle(QCoreApplication.translate("TabOcr_B30", u"imageSelector.title", None))
self.groupBox_6.setTitle(QCoreApplication.translate("TabOcr_B30", u"dependencies.title", None))
self.label.setText(QCoreApplication.translate("TabOcr_B30", u"dependencies.knnModel", None))
self.label_2.setText(QCoreApplication.translate("TabOcr_B30", u"dependencies.b30KnnModel", None))
self.label_3.setText(QCoreApplication.translate("TabOcr_B30", u"dependencies.phashDatabase", None))
pass
# retranslateUi

View File

@ -79,6 +79,12 @@
</item>
<item row="0" column="1">
<widget class="QStackedWidget" name="options_roisStackedWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="currentIndex">
<number>0</number>
</property>
@ -148,6 +154,12 @@
</item>
<item row="1" column="1">
<widget class="QStackedWidget" name="options_maskerStackedWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="currentIndex">
<number>0</number>
</property>

View File

@ -76,6 +76,11 @@ class Ui_TabOcr_Device(object):
self.options_roisStackedWidget = QStackedWidget(self.options_preciseControlWidget)
self.options_roisStackedWidget.setObjectName(u"options_roisStackedWidget")
sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.options_roisStackedWidget.sizePolicy().hasHeightForWidth())
self.options_roisStackedWidget.setSizePolicy(sizePolicy)
self.page = QWidget()
self.page.setObjectName(u"page")
self.verticalLayout_2 = QVBoxLayout(self.page)
@ -119,6 +124,8 @@ class Ui_TabOcr_Device(object):
self.options_maskerStackedWidget = QStackedWidget(self.options_preciseControlWidget)
self.options_maskerStackedWidget.setObjectName(u"options_maskerStackedWidget")
sizePolicy.setHeightForWidth(self.options_maskerStackedWidget.sizePolicy().hasHeightForWidth())
self.options_maskerStackedWidget.setSizePolicy(sizePolicy)
self.page_3 = QWidget()
self.page_3.setObjectName(u"page_3")
self.verticalLayout_5 = QVBoxLayout(self.page_3)
@ -209,11 +216,11 @@ class Ui_TabOcr_Device(object):
self.ocrQueue = OcrQueue(TabOcr_Device)
self.ocrQueue.setObjectName(u"ocrQueue")
sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.ocrQueue.sizePolicy().hasHeightForWidth())
self.ocrQueue.setSizePolicy(sizePolicy)
sizePolicy1 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
sizePolicy1.setHorizontalStretch(0)
sizePolicy1.setVerticalStretch(0)
sizePolicy1.setHeightForWidth(self.ocrQueue.sizePolicy().hasHeightForWidth())
self.ocrQueue.setSizePolicy(sizePolicy1)
self.verticalLayout_3.addWidget(self.ocrQueue)

View File

@ -0,0 +1,28 @@
import cv2
from arcaea_offline_ocr.phash_db import ImagePhashDatabase
def getCv2StatModelStatusText(model: cv2.ml.StatModel):
if not isinstance(model, cv2.ml.StatModel):
return '<font color="red">ERROR</font>'
varCount = model.getVarCount()
if varCount != 81:
return f'<font color="darkorange">WARN</font>, varCount {varCount}'
else:
return f'<font color="green">OK</font>, varCount {varCount}'
def getPhashDatabaseStatusText(db: ImagePhashDatabase):
if not isinstance(db, ImagePhashDatabase):
return '<font color="red">ERROR</font>'
jacketCount = len(db.jacket_hashes)
partnerIconCount = len(db.partner_icon_hashes)
statusText = f"J{jacketCount} PI{partnerIconCount}"
if partnerIconCount <= 0:
return f'<font color="darkorange">WARN</font>, {statusText}'
else:
return f'<font color="green">OK</font>, {statusText}'

View File

@ -1,28 +0,0 @@
import cv2
import numpy as np
from PySide6.QtGui import QImage
def cv2BgrMatToQImage(mat) -> QImage:
arr = np.ascontiguousarray(mat)
return QImage(
arr.data,
arr.shape[1],
arr.shape[0],
arr.strides[0],
QImage.Format.Format_RGB888,
).rgbSwapped()
def qImageToCvMatBgr(qImg: QImage):
# from Bing AI, references
# 1: https://stackoverflow.com/q/384759/16484891 | CC BY-SA 4.0
# 2: https://stackoverflow.com/q/37552924/16484891 | CC BY-SA 3.0
qImg = qImg.convertToFormat(QImage.Format.Format_RGB888)
qImg = qImg.copy().rgbSwapped()
return np.ndarray(
(qImg.height(), qImg.width(), 3),
buffer=qImg.constBits(),
strides=[qImg.bytesPerLine(), 3, 1],
dtype=np.uint8,
)

View File

@ -2,8 +2,9 @@ import json
import sys
from functools import cached_property
from pathlib import Path
from typing import Literal
from typing import Literal, Optional, overload
from arcaea_offline.models import Chart, Difficulty, Song
from PySide6.QtCore import QFile
from .singleton import Singleton
@ -45,3 +46,41 @@ class Data(metaclass=Singleton):
@property
def arcaeaPath(self):
return self.dataPath / "Arcaea"
@overload
def getJacketPath(self, chart: Chart, /) -> Path | None:
...
@overload
def getJacketPath(
self, song: Song, difficulty: Optional[Difficulty] = None, /
) -> Path | None:
...
def getJacketPath(self, *args) -> Path | None:
if isinstance(args[0], Chart):
chart = args[0]
ratingSpecified = f"{chart.song_id}_{chart.rating_class}"
base = chart.song_id
elif isinstance(args[0], Song):
song = args[0]
difficulty = args[1]
ratingSpecified = (
f"{song.id}_{difficulty.rating_class}"
if isinstance(difficulty, Difficulty)
else song.id
)
base = song.id
else:
raise ValueError()
ratingSpecified += ".jpg"
base += ".jpg"
jacketsPath = self.arcaeaPath / "Song"
if (jacketsPath / ratingSpecified).exists():
return jacketsPath / ratingSpecified
elif (jacketsPath / base).exists():
return jacketsPath / base
else:
return None

View File

@ -1,8 +1,15 @@
from typing import Callable
from enum import IntEnum
from typing import Callable, Literal
from PySide6.QtCore import QEvent, QModelIndex, QObject, QPoint, QSize, Qt
from PySide6.QtCore import QModelIndex, QPoint, QSize, Qt
from PySide6.QtGui import QBrush, QColor, QFont, QFontMetrics, QLinearGradient, QPainter
from PySide6.QtWidgets import QApplication, QStyledItemDelegate, QStyleOptionViewItem
from PySide6.QtWidgets import QStyledItemDelegate, QStyleOptionViewItem
class TextSegmentDelegateVerticalAlign(IntEnum):
Top = 0
Middle = 1
Bottom = 2
class TextSegmentDelegate(QStyledItemDelegate):
@ -15,6 +22,28 @@ class TextSegmentDelegate(QStyledItemDelegate):
GradientWrapperRole = TextRole + 3
FontRole = TextRole + 20
def __init__(self, parent=None):
super().__init__(parent)
# TODO: make this differ by index
self.baseXOffset = 0
self.baseYOffset = 0
self.verticalAlign = TextSegmentDelegateVerticalAlign.Middle
def setVerticalAlign(self, align: Literal["top", "middle", "bottom"]):
if not isinstance(align, str) and align not in ["top", "middle", "bottom"]:
raise ValueError(
"TextSegment only supports top/middle/bottom vertical aligning."
)
if align == "top":
self.verticalAlign = TextSegmentDelegateVerticalAlign.Top
elif align == "middle":
self.verticalAlign = TextSegmentDelegateVerticalAlign.Middle
elif align == "bottom":
self.verticalAlign = TextSegmentDelegateVerticalAlign.Bottom
def getTextSegments(
self, index: QModelIndex, option
) -> list[
@ -50,14 +79,32 @@ class TextSegmentDelegate(QStyledItemDelegate):
height += lineHeight + self.VerticalPadding
return QSize(width, height)
def baseX(self, option: QStyleOptionViewItem, index: QModelIndex):
return option.rect.x() + self.HorizontalPadding + self.baseXOffset
def baseY(self, option: QStyleOptionViewItem, index: QModelIndex):
baseY = option.rect.y() + self.VerticalPadding + self.baseYOffset
if self.verticalAlign != TextSegmentDelegateVerticalAlign.Top:
paintAreaSize: QSize = option.rect.size()
delegateSize = self.sizeHint(option, index)
if self.verticalAlign == TextSegmentDelegateVerticalAlign.Middle:
baseY += round((paintAreaSize.height() - delegateSize.height()) / 2)
elif self.verticalAlign == TextSegmentDelegateVerticalAlign.Bottom:
baseY += paintAreaSize.height() - delegateSize.height()
return baseY
def textMaxWidth(self, option: QStyleOptionViewItem, index: QModelIndex):
return option.rect.width() - (2 * self.HorizontalPadding) - self.baseXOffset
def paint(
self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex
):
self.initStyleOption(option, index)
# draw text only
baseX = option.rect.x() + self.HorizontalPadding
baseY = option.rect.y() + self.VerticalPadding
maxWidth = option.rect.width() - (2 * self.HorizontalPadding)
baseX = self.baseX(option, index)
baseY = self.baseY(option, index)
maxWidth = self.textMaxWidth(option, index)
fm: QFontMetrics = option.fontMetrics
painter.save()
for line in self.getTextSegments(index, option):
@ -69,8 +116,7 @@ class TextSegmentDelegate(QStyledItemDelegate):
# elide text, get font values
text = textFrag[self.TextRole]
fragMaxWidth = maxWidth - (lineBaseX - baseX)
font = textFrag.get(self.FontRole)
if font:
if font := textFrag.get(self.FontRole):
painter.setFont(font)
_fm = QFontMetrics(font)
else:
@ -116,37 +162,3 @@ class TextSegmentDelegate(QStyledItemDelegate):
def super_styledItemDelegate_paint(self, painter, option, index):
return super().paint(painter, option, index)
class NoCommitWhenFocusOutEventFilter(QObject):
"""
--DEPRECATED--
The default QAbstractItemDelegate implementation has a private function
`editorEventFilter()`, when editor sends focusOut/hide event, it emits the
`commitData(editor)` signal. We don't want this since we need to validate
the input, so we filter the event out and handle it by ourselves.
Reimplement `checkIsEditor(self, val) -> bool` to ensure this filter is
working. The default implementation always return `False`.
"""
def checkIsEditor(self, val) -> bool:
return False
def eventFilter(self, object: QObject, event: QEvent) -> bool:
if self.checkIsEditor(object) and event.type() in [
QEvent.Type.FocusOut,
QEvent.Type.Hide,
]:
widget = QApplication.focusWidget()
while widget:
# check if focus changed into editor's child
if self.checkIsEditor(widget):
return False
widget = widget.parentWidget()
object.hide()
object.deleteLater()
return True
return False

View File

@ -1,7 +1,8 @@
from arcaea_offline.models import Chart
from arcaea_offline.models import Chart, Difficulty, Song
from arcaea_offline.utils.rating import rating_class_to_short_text, rating_class_to_text
from PIL import Image
from PySide6.QtCore import QModelIndex, Qt, Signal
from PySide6.QtGui import QColor
from PySide6.QtGui import QColor, QPainter, QPixmap
from PySide6.QtWidgets import (
QFrame,
QHBoxLayout,
@ -13,6 +14,7 @@ from PySide6.QtWidgets import (
QWidget,
)
from ui.extends.shared.data import Data
from ui.implements.components.chartSelector import ChartSelector
from ..utils import keepWidgetInScreen
@ -86,51 +88,139 @@ class ChartDelegate(TextSegmentDelegate):
def getChart(self, index: QModelIndex) -> Chart | None:
return None
def getSong(self, index: QModelIndex) -> Song | None:
return None
def getDifficulty(self, index: QModelIndex) -> Difficulty | None:
return None
def getTextSegments(self, index: QModelIndex, option):
chart = self.getChart(index)
if not isinstance(chart, Chart):
return [
[{self.TextRole: "Chart Invalid", self.ColorRole: QColor("#ff0000")}]
]
song = self.getSong(index)
difficulty = self.getDifficulty(index)
chartConstantString = (
f"{chart.constant / 10:.1f}"
if chart.constant is not None and chart.constant > 0
else "?"
)
chartValid = isinstance(chart, Chart)
songValid = isinstance(song, Song)
difficultyValid = isinstance(difficulty, Difficulty)
if not chartValid and not songValid:
return [
[
{self.TextRole: f"{chart.title}"},
{
self.TextRole: "Chart/Song not set",
self.ColorRole: QColor("#ff0000"),
}
]
]
# get texts
if chartValid:
title = chart.title
else:
title = (
difficulty.title if difficultyValid and difficulty.title else song.title
)
if chartValid and chart.constant is not None:
chartConstantString = f"{chart.constant / 10:.1f}"
elif difficultyValid:
chartConstantString = str(difficulty.rating)
if difficulty.rating_plus:
chartConstantString += "+"
else:
chartConstantString = "?"
if chartValid:
ratingClass = chart.rating_class
elif difficultyValid:
ratingClass = difficulty.rating_class
else:
ratingClass = None
ratingText = (
f"{rating_class_to_text(chart.rating_class)} {chartConstantString}"
if ratingClass is not None
else "Unknown ?"
)
if chartValid:
descText = f"({chart.song_id}, {chart.set})"
else:
descText = f"({song.id}, {song.set})"
# get attributes
ratingClassColor = (
self.RatingClassColors[ratingClass] if ratingClass is not None else None
)
return [
[
{self.TextRole: str(title)},
],
[
{
self.TextRole: f"{rating_class_to_text(chart.rating_class)} {chartConstantString}",
self.ColorRole: self.RatingClassColors[chart.rating_class],
self.TextRole: ratingText,
self.ColorRole: ratingClassColor,
},
],
[
{
self.TextRole: f"({chart.song_id}, {chart.set})",
self.TextRole: descText,
self.ColorRole: option.widget.palette().placeholderText().color(),
},
],
]
def paintWarningBackground(self, index: QModelIndex) -> bool:
return True
def sizeHint(self, option, index):
size = super().sizeHint(option, index)
size.setWidth(size.width() + self.HorizontalPadding + size.height())
return size
def paint(self, painter, option, index):
# draw chartInvalid warning background
chart = self.getChart(index)
if not isinstance(chart, Chart) and self.paintWarningBackground(index):
painter.save()
painter.setPen(Qt.PenStyle.NoPen)
bgColor = QColor(self.ChartInvalidBackgroundColor)
bgColor.setAlpha(50)
painter.setBrush(bgColor)
painter.drawRect(option.rect)
painter.restore()
option.text = ""
data = Data()
chart = self.getChart(index)
song = self.getSong(index)
difficulty = self.getDifficulty(index)
if isinstance(chart, Chart):
jacketPath = data.getJacketPath(chart)
elif isinstance(song, Song):
jacketPath = data.getJacketPath(song, difficulty)
else:
jacketPath = "__TEXT_ONLY__"
if jacketPath == "__TEXT_ONLY__":
super().paint(painter, option, index)
return
textSizeHint = super().sizeHint(option, index)
jacketSize = textSizeHint.height()
self.baseXOffset = self.HorizontalPadding + jacketSize
jacketSizeTuple = (jacketSize, jacketSize)
if jacketPath:
pixmap = (
Image.open(str(jacketPath.resolve()))
.resize(jacketSizeTuple, Image.BICUBIC)
.toqpixmap()
)
else:
pixmap = (
Image.fromqpixmap(QPixmap(":/images/jacket-placeholder.png"))
.resize(jacketSizeTuple, Image.BICUBIC)
.toqpixmap()
)
painter.save()
painter.setRenderHint(QPainter.RenderHint.LosslessImageRendering, True)
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
painter.drawPixmap(
option.rect.x() + self.HorizontalPadding, self.baseY(option, index), pixmap
)
painter.restore()
super().paint(painter, option, index)
def checkIsEditor(self, val):

View File

@ -1,16 +1,20 @@
import logging
import cv2
import numpy as np
from arcaea_offline_ocr.b30.chieri.v4.ocr import ChieriBotV4Ocr
from arcaea_offline_ocr.phash_db import ImagePHashDatabase
from arcaea_offline_ocr.sift_db import SIFTDatabase
from arcaea_offline_ocr.phash_db import ImagePhashDatabase
from arcaea_offline_ocr.utils import imread_unicode
from PIL import Image
from PySide6.QtCore import Signal, Slot
from PySide6.QtWidgets import QWidget
from PySide6.QtWidgets import QFileDialog, QMessageBox, QWidget
from ui.designer.tabs.tabOcr.tabOcr_B30_ui import Ui_TabOcr_B30
from ui.extends.components.ocrQueue import OcrQueueModel
from ui.extends.shared.cv2_utils import cv2BgrMatToQImage, qImageToCvMatBgr
from ui.extends.ocr.dependencies import (
getCv2StatModelStatusText,
getPhashDatabaseStatusText,
)
from ui.extends.shared.language import LanguageChangeEventFilter
from ui.extends.shared.settings import (
B30_KNN_MODEL_FILE,
@ -36,97 +40,119 @@ class TabOcr_B30(Ui_TabOcr_B30, QWidget):
self.b30TypeComboBox.setCurrentIndex(0)
self.b30TypeComboBox.setEnabled(False)
self.imageSelector.filesSelected.connect(self.imageSelected)
self.knnModelSelector.filesSelected.connect(self.knnModelSelected)
self.b30KnnModelSelector.filesSelected.connect(self.b30KnnModelSelected)
self.phashDatabaseSelector.filesSelected.connect(self.phashDatabaseSelected)
self.dependencies_knnModelSelector.filesSelected.connect(self.knnModelSelected)
self.dependencies_b30KnnModelSelector.filesSelected.connect(
self.b30KnnModelSelected
)
self.dependencies_phashDatabaseSelector.filesSelected.connect(
self.phashDatabaseSelected
)
self.imagePath = None # for checking only
self.img = None
self.paddleFolder = None
self.paddle = None
self.knnModel = None
self.b30KnnModel = None
# self.siftDatabase = None
self.phashDatabase = None
self.ocr = None
self.tryPrepareOcr.connect(self.prepareOcr)
logger.info("Applying settings...")
self.knnModelSelector.connectSettings(KNN_MODEL_FILE)
self.b30KnnModelSelector.connectSettings(B30_KNN_MODEL_FILE)
self.phashDatabaseSelector.connectSettings(PHASH_DATABASE_FILE)
self.dependencies_knnModelSelector.connectSettings(KNN_MODEL_FILE)
self.dependencies_b30KnnModelSelector.connectSettings(B30_KNN_MODEL_FILE)
self.dependencies_phashDatabaseSelector.connectSettings(PHASH_DATABASE_FILE)
self.ocrQueueModel = OcrQueueModel(self)
self.ocrQueue.setModel(self.ocrQueueModel)
def imageSelected(self):
if selectedFiles := self.imageSelector.selectedFiles():
imagePath = selectedFiles[0]
self.imagePath = imagePath
self.img = imread_unicode(imagePath)
self.tryPrepareOcr.emit()
# def imageSelected(self):
# if selectedFiles := self.imageSelector.selectedFiles():
# imagePath = selectedFiles[0]
# self.imagePath = imagePath
# self.img = imread_unicode(imagePath)
# self.tryPrepareOcr.emit()
def knnModelSelected(self):
if selectedFiles := self.knnModelSelector.selectedFiles():
knnModelPath = selectedFiles[0]
self.knnModel = cv2.ml.KNearest.load(knnModelPath)
self.tryPrepareOcr.emit()
try:
filePath = self.dependencies_knnModelSelector.selectedFiles()[0]
self.knnModel = cv2.ml.KNearest.load(filePath)
except Exception:
self.knnModel = None
logger.exception("Error loading knn model:")
finally:
self.dependencies_knnModelStatusLabel.setText(
getCv2StatModelStatusText(self.knnModel)
)
def b30KnnModelSelected(self):
if selectedFiles := self.b30KnnModelSelector.selectedFiles():
b30KnnModelPath = selectedFiles[0]
self.b30KnnModel = cv2.ml.KNearest.load(b30KnnModelPath)
self.tryPrepareOcr.emit()
try:
filePath = self.dependencies_b30KnnModelSelector.selectedFiles()[0]
self.b30KnnModel = cv2.ml.KNearest.load(filePath)
except Exception:
self.b30KnnModel = None
logger.exception("Error loading b30 knn model:")
finally:
self.dependencies_b30KnnModelStatusLabel.setText(
getCv2StatModelStatusText(self.b30KnnModel)
)
def phashDatabaseSelected(self):
if selectedFiles := self.phashDatabaseSelector.selectedFiles():
phashDatabasePath = selectedFiles[0]
self.phashDatabase = ImagePHashDatabase(phashDatabasePath)
self.tryPrepareOcr.emit()
try:
filePath = self.dependencies_phashDatabaseSelector.selectedFiles()[0]
self.phashDatabase = ImagePhashDatabase(filePath)
except Exception:
self.phashDatabase = None
logger.exception("Error loading phash database:")
finally:
self.dependencies_phashDatabaseStatusLabel.setText(
getPhashDatabaseStatusText(self.phashDatabase)
)
def prepareOcr(self):
def checkDependencies(self):
b30Type = self.b30TypeComboBox.currentData()
if not b30Type:
return False
elif b30Type == "chieri_v4":
return (
self.knnModel is not None
and self.b30KnnModel is not None
and self.phashDatabase is not None
)
else:
return False
@Slot()
def on_ocr_addImageButton_clicked(self):
if not self.checkDependencies():
QMessageBox.critical(self, None, "Dependencies not configured.")
return
if b30Type == "chieri_v4":
if (
not self.imagePath
or not self.knnModel
or not self.b30KnnModel
or not self.phashDatabase
):
imagePath, _ = QFileDialog.getOpenFileName(
self, None, "", "Image Files (*.png *.jpg *.jpeg *.bmp *.webp);;*"
)
if not imagePath:
return
self.ocrQueueModel.clear()
img = imread_unicode(imagePath, cv2.IMREAD_COLOR)
ocr = ChieriBotV4Ocr(self.knnModel, self.b30KnnModel, self.phashDatabase)
ocr.set_factor(self.img)
ocr.set_factor(img)
self.ocr = ocr
roi = ocr.rois
for component in roi.components(self.img):
qImage = cv2BgrMatToQImage(component.copy())
for component in roi.components(img):
qImage = Image.fromarray(component.copy()).toqimage()
self.ocrQueueModel.addItem(qImage)
self.ocrQueue.resizeTableView()
@Slot()
def on_ocr_startButton_clicked(self):
if (
not self.imagePath
or not self.knnModel
or not self.b30KnnModel
or not self.phashDatabase
):
if not self.ocr:
return
for row in range(self.ocrQueueModel.rowCount()):
index = self.ocrQueueModel.index(row, 0)
qImage = index.data(OcrQueueModel.ImageQImageRole)
cv2Mat = qImageToCvMatBgr(qImage)
cv2Mat = np.array(Image.fromqimage(qImage))
runnable = ChieriV4OcrRunnable(self.ocr, cv2Mat)
self.ocrQueueModel.setData(index, runnable, OcrQueueModel.OcrRunnableRole)
self.ocrQueueModel.setData(

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

View File

@ -692,24 +692,24 @@ validation</translation>
<translation>B30 Image Type</translation>
</message>
<message>
<location filename="../../designer/tabs/tabOcr/tabOcr_B30.ui" line="34"/>
<source>knnModelSelector.title</source>
<translation>Select KNearest Model</translation>
<location filename="../../designer/tabs/tabOcr/tabOcr_B30.ui" line="32"/>
<source>dependencies.title</source>
<translation>OCR Dependencies</translation>
</message>
<message>
<location filename="../../designer/tabs/tabOcr/tabOcr_B30.ui" line="46"/>
<source>b30KnnModelSelector.title</source>
<translation>Select B30 Specialized KNearest Model</translation>
<location filename="../../designer/tabs/tabOcr/tabOcr_B30.ui" line="38"/>
<source>dependencies.knnModel</source>
<translation>KNearest model</translation>
</message>
<message>
<location filename="../../designer/tabs/tabOcr/tabOcr_B30.ui" line="62"/>
<source>phashDatabaseSelector.title</source>
<translation>Select Image PHash Database</translation>
<source>dependencies.b30KnnModel</source>
<translation>B30 KNearest model</translation>
</message>
<message>
<location filename="../../designer/tabs/tabOcr/tabOcr_B30.ui" line="74"/>
<source>imageSelector.title</source>
<translation>Select Image</translation>
<location filename="../../designer/tabs/tabOcr/tabOcr_B30.ui" line="86"/>
<source>dependencies.phashDatabase</source>
<translation>Image pHash database</translation>
</message>
</context>
<context>

View File

@ -691,24 +691,24 @@
<translation>B30 </translation>
</message>
<message>
<location filename="../../designer/tabs/tabOcr/tabOcr_B30.ui" line="34"/>
<source>knnModelSelector.title</source>
<translation> KNearest </translation>
<location filename="../../designer/tabs/tabOcr/tabOcr_B30.ui" line="32"/>
<source>dependencies.title</source>
<translation>OCR </translation>
</message>
<message>
<location filename="../../designer/tabs/tabOcr/tabOcr_B30.ui" line="46"/>
<source>b30KnnModelSelector.title</source>
<translation> B30 KNearest </translation>
<location filename="../../designer/tabs/tabOcr/tabOcr_B30.ui" line="38"/>
<source>dependencies.knnModel</source>
<translation>KNearest </translation>
</message>
<message>
<location filename="../../designer/tabs/tabOcr/tabOcr_B30.ui" line="62"/>
<source>phashDatabaseSelector.title</source>
<translation> PHash </translation>
<source>dependencies.b30KnnModel</source>
<translation>B30 KNearest </translation>
</message>
<message>
<location filename="../../designer/tabs/tabOcr/tabOcr_B30.ui" line="74"/>
<source>imageSelector.title</source>
<translation></translation>
<location filename="../../designer/tabs/tabOcr/tabOcr_B30.ui" line="86"/>
<source>dependencies.phashDatabase</source>
<translation> pHash </translation>
</message>
</context>
<context>

View File

@ -8,6 +8,7 @@
<file>images/icon.png</file>
<file>images/logo.png</file>
<file>images/jacket-placeholder.png</file>
<file>images/stepCalculator/stamina.png</file>
<file>images/stepCalculator/play.png</file>
<file>images/stepCalculator/memory-boost.png</file>