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

View File

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

View File

@ -79,6 +79,12 @@
</item> </item>
<item row="0" column="1"> <item row="0" column="1">
<widget class="QStackedWidget" name="options_roisStackedWidget"> <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"> <property name="currentIndex">
<number>0</number> <number>0</number>
</property> </property>
@ -148,6 +154,12 @@
</item> </item>
<item row="1" column="1"> <item row="1" column="1">
<widget class="QStackedWidget" name="options_maskerStackedWidget"> <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"> <property name="currentIndex">
<number>0</number> <number>0</number>
</property> </property>

View File

@ -76,6 +76,11 @@ class Ui_TabOcr_Device(object):
self.options_roisStackedWidget = QStackedWidget(self.options_preciseControlWidget) self.options_roisStackedWidget = QStackedWidget(self.options_preciseControlWidget)
self.options_roisStackedWidget.setObjectName(u"options_roisStackedWidget") 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 = QWidget()
self.page.setObjectName(u"page") self.page.setObjectName(u"page")
self.verticalLayout_2 = QVBoxLayout(self.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 = QStackedWidget(self.options_preciseControlWidget)
self.options_maskerStackedWidget.setObjectName(u"options_maskerStackedWidget") 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 = QWidget()
self.page_3.setObjectName(u"page_3") self.page_3.setObjectName(u"page_3")
self.verticalLayout_5 = QVBoxLayout(self.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 = OcrQueue(TabOcr_Device)
self.ocrQueue.setObjectName(u"ocrQueue") self.ocrQueue.setObjectName(u"ocrQueue")
sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) sizePolicy1 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0) sizePolicy1.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0) sizePolicy1.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.ocrQueue.sizePolicy().hasHeightForWidth()) sizePolicy1.setHeightForWidth(self.ocrQueue.sizePolicy().hasHeightForWidth())
self.ocrQueue.setSizePolicy(sizePolicy) self.ocrQueue.setSizePolicy(sizePolicy1)
self.verticalLayout_3.addWidget(self.ocrQueue) 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 import sys
from functools import cached_property from functools import cached_property
from pathlib import Path 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 PySide6.QtCore import QFile
from .singleton import Singleton from .singleton import Singleton
@ -45,3 +46,41 @@ class Data(metaclass=Singleton):
@property @property
def arcaeaPath(self): def arcaeaPath(self):
return self.dataPath / "Arcaea" 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.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): class TextSegmentDelegate(QStyledItemDelegate):
@ -15,6 +22,28 @@ class TextSegmentDelegate(QStyledItemDelegate):
GradientWrapperRole = TextRole + 3 GradientWrapperRole = TextRole + 3
FontRole = TextRole + 20 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( def getTextSegments(
self, index: QModelIndex, option self, index: QModelIndex, option
) -> list[ ) -> list[
@ -50,14 +79,32 @@ class TextSegmentDelegate(QStyledItemDelegate):
height += lineHeight + self.VerticalPadding height += lineHeight + self.VerticalPadding
return QSize(width, height) 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( def paint(
self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex
): ):
self.initStyleOption(option, index) self.initStyleOption(option, index)
# draw text only
baseX = option.rect.x() + self.HorizontalPadding baseX = self.baseX(option, index)
baseY = option.rect.y() + self.VerticalPadding baseY = self.baseY(option, index)
maxWidth = option.rect.width() - (2 * self.HorizontalPadding) maxWidth = self.textMaxWidth(option, index)
fm: QFontMetrics = option.fontMetrics fm: QFontMetrics = option.fontMetrics
painter.save() painter.save()
for line in self.getTextSegments(index, option): for line in self.getTextSegments(index, option):
@ -69,8 +116,7 @@ class TextSegmentDelegate(QStyledItemDelegate):
# elide text, get font values # elide text, get font values
text = textFrag[self.TextRole] text = textFrag[self.TextRole]
fragMaxWidth = maxWidth - (lineBaseX - baseX) fragMaxWidth = maxWidth - (lineBaseX - baseX)
font = textFrag.get(self.FontRole) if font := textFrag.get(self.FontRole):
if font:
painter.setFont(font) painter.setFont(font)
_fm = QFontMetrics(font) _fm = QFontMetrics(font)
else: else:
@ -116,37 +162,3 @@ class TextSegmentDelegate(QStyledItemDelegate):
def super_styledItemDelegate_paint(self, painter, option, index): def super_styledItemDelegate_paint(self, painter, option, index):
return super().paint(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 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.QtCore import QModelIndex, Qt, Signal
from PySide6.QtGui import QColor from PySide6.QtGui import QColor, QPainter, QPixmap
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QFrame, QFrame,
QHBoxLayout, QHBoxLayout,
@ -13,6 +14,7 @@ from PySide6.QtWidgets import (
QWidget, QWidget,
) )
from ui.extends.shared.data import Data
from ui.implements.components.chartSelector import ChartSelector from ui.implements.components.chartSelector import ChartSelector
from ..utils import keepWidgetInScreen from ..utils import keepWidgetInScreen
@ -86,51 +88,139 @@ class ChartDelegate(TextSegmentDelegate):
def getChart(self, index: QModelIndex) -> Chart | None: def getChart(self, index: QModelIndex) -> Chart | None:
return 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): def getTextSegments(self, index: QModelIndex, option):
chart = self.getChart(index) chart = self.getChart(index)
if not isinstance(chart, Chart): song = self.getSong(index)
difficulty = self.getDifficulty(index)
chartValid = isinstance(chart, Chart)
songValid = isinstance(song, Song)
difficultyValid = isinstance(difficulty, Difficulty)
if not chartValid and not songValid:
return [ return [
[{self.TextRole: "Chart Invalid", self.ColorRole: QColor("#ff0000")}] [
{
self.TextRole: "Chart/Song not set",
self.ColorRole: QColor("#ff0000"),
}
]
] ]
chartConstantString = ( # get texts
f"{chart.constant / 10:.1f}" if chartValid:
if chart.constant is not None and chart.constant > 0 title = chart.title
else "?" 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 [ return [
[ [
{self.TextRole: f"{chart.title}"}, {self.TextRole: str(title)},
], ],
[ [
{ {
self.TextRole: f"{rating_class_to_text(chart.rating_class)} {chartConstantString}", self.TextRole: ratingText,
self.ColorRole: self.RatingClassColors[chart.rating_class], self.ColorRole: ratingClassColor,
}, },
], ],
[ [
{ {
self.TextRole: f"({chart.song_id}, {chart.set})", self.TextRole: descText,
self.ColorRole: option.widget.palette().placeholderText().color(), self.ColorRole: option.widget.palette().placeholderText().color(),
}, },
], ],
] ]
def paintWarningBackground(self, index: QModelIndex) -> bool: def sizeHint(self, option, index):
return True size = super().sizeHint(option, index)
size.setWidth(size.width() + self.HorizontalPadding + size.height())
return size
def paint(self, painter, option, index): 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 = "" 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) super().paint(painter, option, index)
def checkIsEditor(self, val): def checkIsEditor(self, val):

View File

@ -1,16 +1,20 @@
import logging import logging
import cv2 import cv2
import numpy as np
from arcaea_offline_ocr.b30.chieri.v4.ocr import ChieriBotV4Ocr from arcaea_offline_ocr.b30.chieri.v4.ocr import ChieriBotV4Ocr
from arcaea_offline_ocr.phash_db import ImagePHashDatabase from arcaea_offline_ocr.phash_db import ImagePhashDatabase
from arcaea_offline_ocr.sift_db import SIFTDatabase
from arcaea_offline_ocr.utils import imread_unicode from arcaea_offline_ocr.utils import imread_unicode
from PIL import Image
from PySide6.QtCore import Signal, Slot 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.designer.tabs.tabOcr.tabOcr_B30_ui import Ui_TabOcr_B30
from ui.extends.components.ocrQueue import OcrQueueModel 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.language import LanguageChangeEventFilter
from ui.extends.shared.settings import ( from ui.extends.shared.settings import (
B30_KNN_MODEL_FILE, B30_KNN_MODEL_FILE,
@ -36,97 +40,119 @@ class TabOcr_B30(Ui_TabOcr_B30, QWidget):
self.b30TypeComboBox.setCurrentIndex(0) self.b30TypeComboBox.setCurrentIndex(0)
self.b30TypeComboBox.setEnabled(False) self.b30TypeComboBox.setEnabled(False)
self.imageSelector.filesSelected.connect(self.imageSelected) self.dependencies_knnModelSelector.filesSelected.connect(self.knnModelSelected)
self.knnModelSelector.filesSelected.connect(self.knnModelSelected) self.dependencies_b30KnnModelSelector.filesSelected.connect(
self.b30KnnModelSelector.filesSelected.connect(self.b30KnnModelSelected) self.b30KnnModelSelected
self.phashDatabaseSelector.filesSelected.connect(self.phashDatabaseSelected) )
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.knnModel = None
self.b30KnnModel = None self.b30KnnModel = None
# self.siftDatabase = None
self.phashDatabase = None self.phashDatabase = None
self.ocr = None self.ocr = None
self.tryPrepareOcr.connect(self.prepareOcr)
logger.info("Applying settings...") logger.info("Applying settings...")
self.knnModelSelector.connectSettings(KNN_MODEL_FILE) self.dependencies_knnModelSelector.connectSettings(KNN_MODEL_FILE)
self.b30KnnModelSelector.connectSettings(B30_KNN_MODEL_FILE) self.dependencies_b30KnnModelSelector.connectSettings(B30_KNN_MODEL_FILE)
self.phashDatabaseSelector.connectSettings(PHASH_DATABASE_FILE) self.dependencies_phashDatabaseSelector.connectSettings(PHASH_DATABASE_FILE)
self.ocrQueueModel = OcrQueueModel(self) self.ocrQueueModel = OcrQueueModel(self)
self.ocrQueue.setModel(self.ocrQueueModel) self.ocrQueue.setModel(self.ocrQueueModel)
def imageSelected(self): # def imageSelected(self):
if selectedFiles := self.imageSelector.selectedFiles(): # if selectedFiles := self.imageSelector.selectedFiles():
imagePath = selectedFiles[0] # imagePath = selectedFiles[0]
self.imagePath = imagePath # self.imagePath = imagePath
self.img = imread_unicode(imagePath) # self.img = imread_unicode(imagePath)
self.tryPrepareOcr.emit() # self.tryPrepareOcr.emit()
def knnModelSelected(self): def knnModelSelected(self):
if selectedFiles := self.knnModelSelector.selectedFiles(): try:
knnModelPath = selectedFiles[0] filePath = self.dependencies_knnModelSelector.selectedFiles()[0]
self.knnModel = cv2.ml.KNearest.load(knnModelPath) self.knnModel = cv2.ml.KNearest.load(filePath)
self.tryPrepareOcr.emit() except Exception:
self.knnModel = None
logger.exception("Error loading knn model:")
finally:
self.dependencies_knnModelStatusLabel.setText(
getCv2StatModelStatusText(self.knnModel)
)
def b30KnnModelSelected(self): def b30KnnModelSelected(self):
if selectedFiles := self.b30KnnModelSelector.selectedFiles(): try:
b30KnnModelPath = selectedFiles[0] filePath = self.dependencies_b30KnnModelSelector.selectedFiles()[0]
self.b30KnnModel = cv2.ml.KNearest.load(b30KnnModelPath) self.b30KnnModel = cv2.ml.KNearest.load(filePath)
self.tryPrepareOcr.emit() except Exception:
self.b30KnnModel = None
logger.exception("Error loading b30 knn model:")
finally:
self.dependencies_b30KnnModelStatusLabel.setText(
getCv2StatModelStatusText(self.b30KnnModel)
)
def phashDatabaseSelected(self): def phashDatabaseSelected(self):
if selectedFiles := self.phashDatabaseSelector.selectedFiles(): try:
phashDatabasePath = selectedFiles[0] filePath = self.dependencies_phashDatabaseSelector.selectedFiles()[0]
self.phashDatabase = ImagePHashDatabase(phashDatabasePath) self.phashDatabase = ImagePhashDatabase(filePath)
self.tryPrepareOcr.emit() 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() b30Type = self.b30TypeComboBox.currentData()
if not b30Type: 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 return
if b30Type == "chieri_v4": imagePath, _ = QFileDialog.getOpenFileName(
if ( self, None, "", "Image Files (*.png *.jpg *.jpeg *.bmp *.webp);;*"
not self.imagePath )
or not self.knnModel
or not self.b30KnnModel
or not self.phashDatabase
):
return
self.ocrQueueModel.clear() if not imagePath:
return
ocr = ChieriBotV4Ocr(self.knnModel, self.b30KnnModel, self.phashDatabase) self.ocrQueueModel.clear()
ocr.set_factor(self.img)
self.ocr = ocr
roi = ocr.rois img = imread_unicode(imagePath, cv2.IMREAD_COLOR)
for component in roi.components(self.img): ocr = ChieriBotV4Ocr(self.knnModel, self.b30KnnModel, self.phashDatabase)
qImage = cv2BgrMatToQImage(component.copy()) ocr.set_factor(img)
self.ocrQueueModel.addItem(qImage) self.ocr = ocr
roi = ocr.rois
for component in roi.components(img):
qImage = Image.fromarray(component.copy()).toqimage()
self.ocrQueueModel.addItem(qImage)
self.ocrQueue.resizeTableView() self.ocrQueue.resizeTableView()
@Slot() @Slot()
def on_ocr_startButton_clicked(self): def on_ocr_startButton_clicked(self):
if ( if not self.ocr:
not self.imagePath
or not self.knnModel
or not self.b30KnnModel
or not self.phashDatabase
):
return return
for row in range(self.ocrQueueModel.rowCount()): for row in range(self.ocrQueueModel.rowCount()):
index = self.ocrQueueModel.index(row, 0) index = self.ocrQueueModel.index(row, 0)
qImage = index.data(OcrQueueModel.ImageQImageRole) qImage = index.data(OcrQueueModel.ImageQImageRole)
cv2Mat = qImageToCvMatBgr(qImage) cv2Mat = np.array(Image.fromqimage(qImage))
runnable = ChieriV4OcrRunnable(self.ocr, cv2Mat) runnable = ChieriV4OcrRunnable(self.ocr, cv2Mat)
self.ocrQueueModel.setData(index, runnable, OcrQueueModel.OcrRunnableRole) self.ocrQueueModel.setData(index, runnable, OcrQueueModel.OcrRunnableRole)
self.ocrQueueModel.setData( 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> <translation>B30 Image Type</translation>
</message> </message>
<message> <message>
<location filename="../../designer/tabs/tabOcr/tabOcr_B30.ui" line="34"/> <location filename="../../designer/tabs/tabOcr/tabOcr_B30.ui" line="32"/>
<source>knnModelSelector.title</source> <source>dependencies.title</source>
<translation>Select KNearest Model</translation> <translation>OCR Dependencies</translation>
</message> </message>
<message> <message>
<location filename="../../designer/tabs/tabOcr/tabOcr_B30.ui" line="46"/> <location filename="../../designer/tabs/tabOcr/tabOcr_B30.ui" line="38"/>
<source>b30KnnModelSelector.title</source> <source>dependencies.knnModel</source>
<translation>Select B30 Specialized KNearest Model</translation> <translation>KNearest model</translation>
</message> </message>
<message> <message>
<location filename="../../designer/tabs/tabOcr/tabOcr_B30.ui" line="62"/> <location filename="../../designer/tabs/tabOcr/tabOcr_B30.ui" line="62"/>
<source>phashDatabaseSelector.title</source> <source>dependencies.b30KnnModel</source>
<translation>Select Image PHash Database</translation> <translation>B30 KNearest model</translation>
</message> </message>
<message> <message>
<location filename="../../designer/tabs/tabOcr/tabOcr_B30.ui" line="74"/> <location filename="../../designer/tabs/tabOcr/tabOcr_B30.ui" line="86"/>
<source>imageSelector.title</source> <source>dependencies.phashDatabase</source>
<translation>Select Image</translation> <translation>Image pHash database</translation>
</message> </message>
</context> </context>
<context> <context>

View File

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

View File

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