refactored the code
This commit is contained in:
28
coin.py
Executable file
28
coin.py
Executable file
@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
############################################################
|
||||||
|
### Project: coin
|
||||||
|
### File: __init__.py
|
||||||
|
### Description: main file of coin
|
||||||
|
### Version: 1.0
|
||||||
|
############################################################
|
||||||
|
import sys, os, glob
|
||||||
|
import PyQt6
|
||||||
|
import gui
|
||||||
|
import tess
|
||||||
|
|
||||||
|
def main():
|
||||||
|
model_names = tess.get_tess_model_names()
|
||||||
|
|
||||||
|
if len(model_names) == 0:
|
||||||
|
print("[ERR] No tesseract model found at", MODELS_PATH, file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("Available models:\n", model_names)
|
||||||
|
|
||||||
|
app = PyQt6.QtWidgets.QApplication(sys.argv)
|
||||||
|
main_window = gui.coin_gui(model_names)
|
||||||
|
main_window.show()
|
||||||
|
sys.exit(app.exec())
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
55
constants.py
Normal file
55
constants.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
############################################################
|
||||||
|
### Project: coin
|
||||||
|
### File: constants.py
|
||||||
|
### Description: defines all the constants
|
||||||
|
### Version: 1.0
|
||||||
|
############################################################
|
||||||
|
# Change this if tesseract or coin can't find any data
|
||||||
|
MODELS_PATH = '/usr/share/tessdata/'
|
||||||
|
|
||||||
|
# Sane values for scaling
|
||||||
|
SCALE_MIN = 0.2
|
||||||
|
SCALE_MAX = 5.0
|
||||||
|
SCALE_DEFAULT = 1.0
|
||||||
|
# How much (in decimal) to change the scale for every degree
|
||||||
|
SCALE_DELTA = 8 * 360 * 1
|
||||||
|
|
||||||
|
# Sane values for text recognition
|
||||||
|
WIDTH_MIN = 10
|
||||||
|
HEIGHT_MIN = 10
|
||||||
|
|
||||||
|
# Table info
|
||||||
|
TABLE_COL_CNT = 5
|
||||||
|
TABLE_COL_GET = 0
|
||||||
|
TABLE_COL_DEL = 1
|
||||||
|
TABLE_COL_UPC = 2
|
||||||
|
TABLE_COL_NAME = 3
|
||||||
|
TABLE_COL_PRICE = 4
|
||||||
|
TABLE_HEADERS = [" Get ", " Del ", "UPC", "Item Name", "Price"]
|
||||||
|
|
||||||
|
# Cookies for upc.py
|
||||||
|
COOKIE_DICT_LIST = [
|
||||||
|
{
|
||||||
|
"name": "PHPSESSID",
|
||||||
|
"value": "9n1ic479r1bteiv758gm9hk65p",
|
||||||
|
"path": "/",
|
||||||
|
"domain": "stocktrack.ca"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cf_chl_3",
|
||||||
|
"value": "1d706187484b25c",
|
||||||
|
"path": "/",
|
||||||
|
"domain": "stocktrack.ca"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cf_clearance",
|
||||||
|
"value": "Wp8tAMUKLdS3a4Y9AT09BIlZKx4x120uC1QzBQTUluQ-1710517775-1.0.1.1-hMEP8oeggZHBkylkwkQfi2p57H6zUUvGG40d_M4vGqOqg2Zh7wZsg6KrGl3XkDUn3mXAqyZrTqlQfd5pgHCZWQ",
|
||||||
|
"path": "/",
|
||||||
|
"domain": "stocktrack.ca"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fp",
|
||||||
|
"value": "26f4acb9b23415f921bba6977b68d55f",
|
||||||
|
"path": "/",
|
||||||
|
"domain": "stocktrack.ca"
|
||||||
|
}]
|
424
gui.py
424
gui.py
@ -1,5 +1,9 @@
|
|||||||
# This module handles the GUI of coin
|
############################################################
|
||||||
import pytesseract
|
### Project: coin
|
||||||
|
### File: gui.py
|
||||||
|
### Description: handles the GUI of coin
|
||||||
|
### Version: 1.0
|
||||||
|
############################################################
|
||||||
import cv2, os, sys
|
import cv2, os, sys
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import PyQt6
|
import PyQt6
|
||||||
@ -9,161 +13,86 @@ from PyQt6.QtWidgets import *
|
|||||||
from PyQt6 import uic
|
from PyQt6 import uic
|
||||||
from PyQt6 import QtCore, QtGui, QtWidgets
|
from PyQt6 import QtCore, QtGui, QtWidgets
|
||||||
import glob
|
import glob
|
||||||
import upc
|
from upc import *
|
||||||
from upc import get_name_from_upc
|
from constants import *
|
||||||
|
import tess
|
||||||
# [!!!] Change this if tesseract or coin can't find any data
|
|
||||||
MODELS_PATH = '/usr/share/tessdata/'
|
|
||||||
|
|
||||||
# [!!!] Sane values for scaling
|
|
||||||
SCALE_MIN = 0.3
|
|
||||||
SCALE_MAX = 4.0
|
|
||||||
# How much (in decimal) to change the scale for every degree
|
|
||||||
SCALE_DELTA = 8 * 360 * 1
|
|
||||||
|
|
||||||
# [!!!] Sane values for recognition
|
|
||||||
WIDTH_MIN = 10
|
|
||||||
HEIGHT_MIN = 10
|
|
||||||
|
|
||||||
# Column Information
|
|
||||||
TABLE_COL_CNT = 5
|
|
||||||
TABLE_COL_GET = 0
|
|
||||||
TABLE_COL_DEL = 1
|
|
||||||
TABLE_COL_UPC = 2
|
|
||||||
TABLE_COL_NAME = 3
|
|
||||||
TABLE_COL_PRICE = 4
|
|
||||||
|
|
||||||
# Automatically finds all available language models
|
|
||||||
models_list = glob.glob(MODELS_PATH + "*.traineddata")
|
|
||||||
model_names = []
|
|
||||||
for path in models_list:
|
|
||||||
base_name = os.path.basename(path)
|
|
||||||
base_name = os.path.splitext(base_name)[0]
|
|
||||||
model_names.append(base_name)
|
|
||||||
if len(model_names) == 0:
|
|
||||||
print("No tesseract models found at", MODELS_PATH, "!", file=sys.stderr)
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
class UPCWorker(QObject):
|
class UPCWorker(QObject):
|
||||||
finished = pyqtSignal()
|
finished = pyqtSignal()
|
||||||
update_name = pyqtSignal(list)
|
update_name = pyqtSignal(list)
|
||||||
|
|
||||||
def __init__(self, upcList):
|
|
||||||
super().__init__()
|
|
||||||
self.upcList = upcList
|
|
||||||
|
|
||||||
|
# requires: upc_list is a list of list of two elements with the
|
||||||
|
# first element being the UPC's row number and the second being a
|
||||||
|
# string, containing the UPC
|
||||||
|
def __init__(self, upc_lst):
|
||||||
|
super().__init__()
|
||||||
|
self.upc_lst = upc_lst
|
||||||
|
|
||||||
|
# get_items_name(): get the names of all items using
|
||||||
|
# get_name_from_upc from the list of upc_list. Emits a pair of
|
||||||
|
# values stored in a list, the first element is the row number
|
||||||
|
# and the second is the item's name
|
||||||
def get_items_name(self):
|
def get_items_name(self):
|
||||||
for upc in self.upcList:
|
for upc in self.upc_lst:
|
||||||
name = get_name_from_upc(upc[1])
|
name = get_name_from_upc(upc[1])
|
||||||
self.update_name.emit([upc[0], name])
|
self.update_name.emit([upc[0], name])
|
||||||
self.finished.emit()
|
self.finished.emit()
|
||||||
|
|
||||||
class coin_app(QtWidgets.QMainWindow):
|
class coin_gui(QtWidgets.QMainWindow):
|
||||||
def __init__(self):
|
# requires: model_names must be valid and not empty
|
||||||
|
def __init__(self, model_names):
|
||||||
|
print("Initializing GUI...")
|
||||||
|
|
||||||
QtWidgets.QMainWindow.__init__(self)
|
QtWidgets.QMainWindow.__init__(self)
|
||||||
self.ui = uic.loadUi('coin.ui', self)
|
self.ui = uic.loadUi('coin.ui', self)
|
||||||
self.image = None
|
|
||||||
|
|
||||||
self.ui.openButton.clicked.connect(self.open)
|
self.ui.openButton.clicked.connect(self.open_image)
|
||||||
self.ui.getNameButton.clicked.connect(self.get_all)
|
self.ui.getNameButton.clicked.connect(self.get_all)
|
||||||
self.ui.saveFileButton.clicked.connect(self.save_file)
|
self.ui.saveFileButton.clicked.connect(self.save_file)
|
||||||
self.ui.saveDbButton.clicked.connect(self.save_database)
|
self.ui.saveDbButton.clicked.connect(self.save_database)
|
||||||
|
|
||||||
self.rubberBand = QRubberBand(QRubberBand.Shape.Rectangle, self)
|
self.rubber_band = QRubberBand(QRubberBand.Shape.Rectangle, self)
|
||||||
self.ui.photo.setMouseTracking(True)
|
self.ui.photo.setMouseTracking(True)
|
||||||
self.ui.photo.installEventFilter(self)
|
self.ui.photo.installEventFilter(self)
|
||||||
self.ui.photo.setAlignment(PyQt6.QtCore.Qt.AlignmentFlag.AlignTop)
|
self.ui.photo.setAlignment(PyQt6.QtCore.Qt.AlignmentFlag.AlignTop)
|
||||||
|
self.image = None
|
||||||
self.model = model_names[0]
|
|
||||||
print("Model selected as:", self.model)
|
|
||||||
self.models.addItems(model_names)
|
|
||||||
self.models.currentTextChanged.connect(self.update_now)
|
|
||||||
self.models.setCurrentIndex(model_names.index(self.model))
|
|
||||||
|
|
||||||
self.scale = 1.0
|
self.scale = 1.0
|
||||||
|
|
||||||
self.items = None
|
self.items = None
|
||||||
|
|
||||||
self.upcCheckTimer = QTimer(self)
|
if len(model_names) == 0:
|
||||||
self.upcCheckTimer.timeout.connect(self.pushUPCToThread)
|
print("[ERR] No tesseract model given to coin_gui!", file=sys.stderr)
|
||||||
self.upcCheckTimer.start(1500)
|
sys.exit(1)
|
||||||
|
self.model_names = model_names
|
||||||
|
self.update_model(self.model_names[0])
|
||||||
|
self.models.addItems(self.model_names)
|
||||||
|
self.models.currentTextChanged.connect(self.update_model)
|
||||||
|
self.models.setCurrentIndex(self.model_names.index(self.model))
|
||||||
|
|
||||||
|
self.upc_chk_timer = QTimer(self)
|
||||||
|
self.upc_chk_timer.timeout.connect(self.push_upc_to_thread)
|
||||||
|
self.upc_chk_timer.start(1500)
|
||||||
print("Initialized timer!")
|
print("Initialized timer!")
|
||||||
|
|
||||||
self.upcThread = QThread()
|
self.upc_thread = None
|
||||||
self.upcWorker = None
|
self.upc_worker = None
|
||||||
self.upcCheckList = []
|
self.upc_chk_lst = []
|
||||||
|
|
||||||
def pushUPCToThread(self):
|
# open_image(): prompts to open an image, if an error occurrs, the
|
||||||
if len(self.upcCheckList) > 0 and self.upcWorker is None:
|
# image within the photo view will not be updated, otherwise, the
|
||||||
self.upcWorker = UPCWorker(self.upcCheckList)
|
# image will be updated and stored in self.pixmap
|
||||||
self.upcWorker.moveToThread(self.upcThread)
|
def open_image(self):
|
||||||
|
|
||||||
self.upcWorker.update_name.connect(self.updateItemName)
|
|
||||||
self.upcWorker.finished.connect(self.collectUPCWorker)
|
|
||||||
self.upcThread.finished.connect(self.collectUPCThread)
|
|
||||||
|
|
||||||
self.upcThread.started.connect(self.upcWorker.get_items_name)
|
|
||||||
self.upcThread.start()
|
|
||||||
|
|
||||||
print("Lookup thread started!")
|
|
||||||
|
|
||||||
self.upcCheckList = []
|
|
||||||
|
|
||||||
def collectUPCWorker(self):
|
|
||||||
self.upcWorker.deleteLater()
|
|
||||||
self.upcWorker = None
|
|
||||||
|
|
||||||
def collectUPCThread(self):
|
|
||||||
self.upcThread.deleteLater()
|
|
||||||
|
|
||||||
def updateItemName(self, name):
|
|
||||||
itemName = QStandardItem(name[1])
|
|
||||||
itemName.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
||||||
self.items.setItem(name[0], TABLE_COL_NAME, itemName)
|
|
||||||
self.itemsTable.resizeColumnToContents(TABLE_COL_NAME)
|
|
||||||
self.itemsTable.resizeRowToContents(name[0])
|
|
||||||
|
|
||||||
def update_now(self, value):
|
|
||||||
self.model = value
|
|
||||||
print("Model selected as:", self.model)
|
|
||||||
|
|
||||||
def set_table_header(self):
|
|
||||||
item = QStandardItem(" Get ")
|
|
||||||
item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
||||||
item.setEditable(False)
|
|
||||||
self.items.setItem(0, TABLE_COL_GET, item)
|
|
||||||
item = QStandardItem(" Del ")
|
|
||||||
item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
||||||
item.setEditable(False)
|
|
||||||
self.items.setItem(0, TABLE_COL_DEL, item)
|
|
||||||
item = QStandardItem("UPC")
|
|
||||||
item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
||||||
item.setEditable(False)
|
|
||||||
self.items.setItem(0, TABLE_COL_UPC, item)
|
|
||||||
item = QStandardItem("Item Name")
|
|
||||||
item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
||||||
item.setEditable(False)
|
|
||||||
self.items.setItem(0, TABLE_COL_NAME, item)
|
|
||||||
item = QStandardItem("Price")
|
|
||||||
item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
||||||
item.setEditable(False)
|
|
||||||
self.items.setItem(0, TABLE_COL_PRICE, item)
|
|
||||||
self.itemsTable.resizeColumnsToContents()
|
|
||||||
|
|
||||||
def open(self):
|
|
||||||
filename = QFileDialog.getOpenFileName(self, 'Select File')
|
filename = QFileDialog.getOpenFileName(self, 'Select File')
|
||||||
if filename[0] == "":
|
if filename[0] == "":
|
||||||
print("No file was selected or opened!")
|
print("No file was selected or opened!")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if self.items is not None:
|
|
||||||
self.items.deleteLater()
|
|
||||||
|
|
||||||
print("Opening file: ", filename[0])
|
print("Opening file: ", filename[0])
|
||||||
self.image = cv2.imread(str(filename[0]))
|
self.image = cv2.imread(str(filename[0]))
|
||||||
|
|
||||||
if self.image is None:
|
if self.image is None:
|
||||||
print("Error: image is empty!", file=sys.stderr)
|
print("Error: image is empty!", file=sys.stderr)
|
||||||
|
print("No file was opened!")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
frame = cv2.cvtColor(self.image, cv2.COLOR_BGR2RGB)
|
frame = cv2.cvtColor(self.image, cv2.COLOR_BGR2RGB)
|
||||||
@ -171,11 +100,12 @@ class coin_app(QtWidgets.QMainWindow):
|
|||||||
frame.strides[0], QImage.Format.Format_RGB888)
|
frame.strides[0], QImage.Format.Format_RGB888)
|
||||||
|
|
||||||
self.pixmap = QPixmap.fromImage(image)
|
self.pixmap = QPixmap.fromImage(image)
|
||||||
self.ui.photo.setPixmap(self.pixmap)
|
self.scale = SCALE_DEFAULT
|
||||||
self.scale = 1.0
|
self.update_scale()
|
||||||
print("Updated photo!")
|
|
||||||
print("Photo scale set to:", self.scale)
|
|
||||||
|
|
||||||
|
if self.items is not None:
|
||||||
|
self.items.deleteLater()
|
||||||
|
|
||||||
self.items = QStandardItemModel()
|
self.items = QStandardItemModel()
|
||||||
self.itemsTable.setModel(self.items)
|
self.itemsTable.setModel(self.items)
|
||||||
self.items.setColumnCount(TABLE_COL_CNT)
|
self.items.setColumnCount(TABLE_COL_CNT)
|
||||||
@ -187,6 +117,8 @@ class coin_app(QtWidgets.QMainWindow):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# save_file(): prompt to save a file, will write to the file in
|
||||||
|
# .csv format
|
||||||
def save_file(self):
|
def save_file(self):
|
||||||
filename = QFileDialog.getSaveFileName(self, 'Select File')
|
filename = QFileDialog.getSaveFileName(self, 'Select File')
|
||||||
|
|
||||||
@ -203,53 +135,107 @@ class coin_app(QtWidgets.QMainWindow):
|
|||||||
print(name, file=out, end=", ")
|
print(name, file=out, end=", ")
|
||||||
print(self.items.item(i,TABLE_COL_PRICE).text().replace("\n", " "),
|
print(self.items.item(i,TABLE_COL_PRICE).text().replace("\n", " "),
|
||||||
file=out)
|
file=out)
|
||||||
out.close()
|
|
||||||
|
|
||||||
|
out.close()
|
||||||
self.statusBar().showMessage("Saved to: " + filename[0])
|
self.statusBar().showMessage("Saved to: " + filename[0])
|
||||||
|
|
||||||
|
# save_database(): saves to a database
|
||||||
def save_database(self):
|
def save_database(self):
|
||||||
print("Not implemented!")
|
print("Not implemented!")
|
||||||
|
|
||||||
def scale_update(self):
|
# set_table_header(): sets the headers of the table when a new
|
||||||
|
# image is opened
|
||||||
|
def set_table_header(self):
|
||||||
|
for i in range(TABLE_COL_CNT):
|
||||||
|
item = QStandardItem(TABLE_HEADERS[i])
|
||||||
|
item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
item.setEditable(False)
|
||||||
|
self.items.setItem(0, i, item)
|
||||||
|
|
||||||
|
self.itemsTable.resizeColumnsToContents()
|
||||||
|
|
||||||
|
# update_scale(): update the image with the new scale
|
||||||
|
def update_scale(self):
|
||||||
self.ui.photo.setPixmap(self.pixmap.scaled(
|
self.ui.photo.setPixmap(self.pixmap.scaled(
|
||||||
int(self.scale * self.pixmap.width()),
|
int(self.scale * self.pixmap.width()),
|
||||||
int(self.scale * self.pixmap.height())))
|
int(self.scale * self.pixmap.height())))
|
||||||
print("Photo scale set to:", self.scale)
|
|
||||||
|
|
||||||
|
print("Photo scale set to:", self.scale)
|
||||||
self.statusBar().showMessage("Photo scale set to: " + str(self.scale))
|
self.statusBar().showMessage("Photo scale set to: " + str(self.scale))
|
||||||
|
|
||||||
def image_to_text(self, cropped_image):
|
# push_upc_to_thread(): runs a new thread when there are items in
|
||||||
gray = cv2.cvtColor(cropped_image, cv2.COLOR_BGR2GRAY)
|
# self.upc_chk_lst and self.upc_thread is available
|
||||||
gray = cv2.medianBlur(gray, 1)
|
def push_upc_to_thread(self):
|
||||||
crop = Image.fromarray(gray)
|
if len(self.upc_chk_lst) > 0 and self.upc_worker is None:
|
||||||
text = pytesseract.image_to_string(crop, lang=self.model).strip()
|
self.upc_thread = QThread()
|
||||||
print("Text selected:", text)
|
|
||||||
return text
|
self.upc_worker = UPCWorker(self.upc_chk_lst)
|
||||||
|
self.upc_worker.moveToThread(self.upc_thread)
|
||||||
|
|
||||||
def get_row(self):
|
self.upc_worker.update_name.connect(self.update_item_name)
|
||||||
button = self.sender()
|
self.upc_worker.finished.connect(self.upc_thread.quit)
|
||||||
row = self.itemsTable.indexAt(button.pos()).row()
|
self.upc_worker.finished.connect(self.delete_upc_worker)
|
||||||
upc = self.items.item(row, TABLE_COL_UPC).text()
|
self.upc_thread.finished.connect(self.delete_upc_thread)
|
||||||
self.upcCheckList.append([row, upc])
|
|
||||||
print("Started getting name of UPC:", upc)
|
|
||||||
self.statusBar().showMessage("Started getting name of UPC: " + upc)
|
|
||||||
self.pushUPCToThread()
|
|
||||||
|
|
||||||
|
self.upc_thread.started.connect(self.upc_worker.get_items_name)
|
||||||
|
self.upc_thread.start()
|
||||||
|
|
||||||
|
print("Lookup thread started!")
|
||||||
|
|
||||||
|
self.upc_chk_lst = []
|
||||||
|
|
||||||
|
# delete_upc_worker(): wrapper for self.upc_worker.deleteLater()
|
||||||
|
def delete_upc_worker(self):
|
||||||
|
self.upc_worker.deleteLater()
|
||||||
|
self.upc_worker = None
|
||||||
|
|
||||||
|
# delete_upc_thread(): wrapper for self.upc_thread.deleteLater()
|
||||||
|
def delete_upc_thread(self):
|
||||||
|
print("Lookup thread is finished!")
|
||||||
|
self.upc_thread.deleteLater()
|
||||||
|
|
||||||
|
# update_item_name(name): name is a two-element list with the
|
||||||
|
# first element being the row in which is item is located, and
|
||||||
|
# the second being a string containing the item's name
|
||||||
|
def update_item_name(self, name):
|
||||||
|
item = QStandardItem(name[1])
|
||||||
|
item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.items.setItem(name[0], TABLE_COL_NAME, item)
|
||||||
|
self.itemsTable.resizeColumnToContents(TABLE_COL_NAME)
|
||||||
|
self.itemsTable.resizeRowToContents(name[0])
|
||||||
|
|
||||||
|
# get_all(): get the names of items in every row that doesn't have
|
||||||
|
# a name in it
|
||||||
def get_all(self):
|
def get_all(self):
|
||||||
for i in range(1, self.items.rowCount()):
|
for i in range(1, self.items.rowCount()):
|
||||||
if self.items.item(i, TABLE_COL_NAME) is None or self.items.item(i, TABLE_COL_NAME).text() == "N/A":
|
if (self.items.item(i, TABLE_COL_NAME) is None or
|
||||||
|
self.items.item(i, TABLE_COL_NAME).text() == "N/A"):
|
||||||
upc = self.items.item(i, TABLE_COL_UPC).text()
|
upc = self.items.item(i, TABLE_COL_UPC).text()
|
||||||
print("Started getting name of UPC:", upc)
|
print("Started getting name of UPC:", upc)
|
||||||
self.statusBar().showMessage("Started getting name of UPC: " + upc)
|
self.statusBar().showMessage("Started getting name of UPC: " + upc)
|
||||||
self.upcCheckList.append([i, upc])
|
self.upc_chk_lst.append([i, upc])
|
||||||
self.pushUPCToThread()
|
self.push_upc_to_thread()
|
||||||
|
|
||||||
|
# get_item_at_row(): get the name of the item at the row where the
|
||||||
|
# "Get" button was clicked
|
||||||
|
def get_item_at_row(self):
|
||||||
|
button = self.sender()
|
||||||
|
row = self.itemsTable.indexAt(button.pos()).row()
|
||||||
|
upc = self.items.item(row, TABLE_COL_UPC).text()
|
||||||
|
self.upc_chk_lst.append([row, upc])
|
||||||
|
print("Started getting name of UPC:", upc)
|
||||||
|
self.statusBar().showMessage("Started getting name of UPC: " + upc)
|
||||||
|
self.push_upc_to_thread()
|
||||||
|
|
||||||
|
# del_row(): delete the row where the "Del" button was clicked
|
||||||
def del_row(self):
|
def del_row(self):
|
||||||
button = self.sender()
|
button = self.sender()
|
||||||
index = self.itemsTable.indexAt(button.pos())
|
index = self.itemsTable.indexAt(button.pos())
|
||||||
self.items.removeRow(index.row())
|
self.items.removeRow(index.row())
|
||||||
print("Removed row", index.row())
|
print("Removed row", index.row())
|
||||||
|
self.statusBar().showMessage("Removed row " + str(index.row()))
|
||||||
|
|
||||||
|
# populate_buttons(): put buttons on the table when creating a new row
|
||||||
def populate_buttons(self):
|
def populate_buttons(self):
|
||||||
del_button = QPushButton("Del")
|
del_button = QPushButton("Del")
|
||||||
self.itemsTable.setIndexWidget(
|
self.itemsTable.setIndexWidget(
|
||||||
@ -261,17 +247,22 @@ class coin_app(QtWidgets.QMainWindow):
|
|||||||
self.itemsTable.setIndexWidget(
|
self.itemsTable.setIndexWidget(
|
||||||
self.items.index(self.items.rowCount() - 1, TABLE_COL_GET),
|
self.items.index(self.items.rowCount() - 1, TABLE_COL_GET),
|
||||||
get_button)
|
get_button)
|
||||||
get_button.clicked.connect(self.get_row)
|
get_button.clicked.connect(self.get_item_at_row)
|
||||||
|
|
||||||
|
# update_model(): update self.model to the one selected
|
||||||
|
def update_model(self, value):
|
||||||
|
self.model = value
|
||||||
|
print("Model selected as:", self.model)
|
||||||
|
|
||||||
|
# add_upc(text): adds a new row containing text in the UPC column
|
||||||
def add_upc(self, text: str):
|
def add_upc(self, text: str):
|
||||||
if text == "":
|
if text == "":
|
||||||
print("No text was recognized, will not add a new item!")
|
print("No text was recognized, will not add a new item!")
|
||||||
return None
|
return None
|
||||||
self.statusBar().showMessage("Text selected: " + text)
|
|
||||||
item = QStandardItem(text)
|
item = QStandardItem(text)
|
||||||
item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
self.items.setItem(self.items.rowCount(), TABLE_COL_UPC, item)
|
self.items.setItem(self.items.rowCount(), TABLE_COL_UPC, item)
|
||||||
|
|
||||||
item = QStandardItem("N/A")
|
item = QStandardItem("N/A")
|
||||||
item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
self.items.setItem(self.items.rowCount() - 1, TABLE_COL_NAME, item)
|
self.items.setItem(self.items.rowCount() - 1, TABLE_COL_NAME, item)
|
||||||
@ -280,69 +271,92 @@ class coin_app(QtWidgets.QMainWindow):
|
|||||||
self.items.setItem(self.items.rowCount() - 1, TABLE_COL_PRICE, item)
|
self.items.setItem(self.items.rowCount() - 1, TABLE_COL_PRICE, item)
|
||||||
|
|
||||||
self.populate_buttons()
|
self.populate_buttons()
|
||||||
|
|
||||||
self.itemsTable.resizeColumnToContents(TABLE_COL_UPC)
|
self.itemsTable.resizeColumnToContents(TABLE_COL_UPC)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# eventFilter(source, event): handles a few things differently
|
||||||
def eventFilter(self, source, event):
|
def eventFilter(self, source, event):
|
||||||
# Handling the selection box
|
if (event.type() == QEvent.Type.MouseButtonPress and
|
||||||
if event.type() == QEvent.Type.MouseButtonPress and source is self.ui.photo:
|
source is self.ui.photo):
|
||||||
self.org = self.mapFromGlobal(event.globalPosition())
|
self.rubber_band_show(event)
|
||||||
self.top_left = event.position()
|
|
||||||
self.rubberBand.setGeometry(QRect(self.org.toPoint(), QSize()))
|
|
||||||
self.rubberBand.show()
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
elif event.type() == QEvent.Type.MouseMove and source is self.ui.photo:
|
elif (event.type() == QEvent.Type.MouseMove and
|
||||||
if self.rubberBand.isVisible():
|
source is self.ui.photo and self.rubber_band.isVisible()):
|
||||||
pos = self.mapFromGlobal(event.globalPosition()).toPoint()
|
self.rubber_band_redraw(event)
|
||||||
pos = QPoint(int(max(pos.x(), 0)),
|
|
||||||
int(max(pos.y(), 0)))
|
|
||||||
self.rubberBand.setGeometry(
|
|
||||||
QRect(self.org.toPoint(),
|
|
||||||
pos).normalized())
|
|
||||||
return True
|
|
||||||
|
|
||||||
elif event.type() == QEvent.Type.MouseButtonRelease and source is self.ui.photo:
|
|
||||||
pos = event.position()
|
|
||||||
self.top_left = QPoint(int(max(min(pos.x(), self.top_left.x()), 0)),
|
|
||||||
int(max(min(pos.y(), self.top_left.y()), 0)))
|
|
||||||
if self.rubberBand.isVisible():
|
|
||||||
self.rubberBand.hide()
|
|
||||||
rect = self.rubberBand.geometry()
|
|
||||||
self.x1 = int(self.top_left.x() / self.scale)
|
|
||||||
self.y1 = int(self.top_left.y() / self.scale)
|
|
||||||
width = rect.width() / self.scale
|
|
||||||
height = rect.height() / self.scale
|
|
||||||
self.x2 = int(self.x1 + width)
|
|
||||||
self.y2 = int(self.y1 + height)
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
if width >= WIDTH_MIN and height >= HEIGHT_MIN and self.image is not None:
|
|
||||||
print("Cropped image:", self.x1, self.y1, self.x2, self.y2)
|
|
||||||
self.crop = self.image[self.y1:self.y2, self.x1:self.x2]
|
|
||||||
# cv2.imwrite("cropped.png", self.crop)
|
|
||||||
self.text = self.image_to_text(self.crop)
|
|
||||||
self.add_upc(self.text)
|
|
||||||
##self.ui.textEdit()
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Resizing the photo
|
|
||||||
elif event.type() == QEvent.Type.Wheel and self.image is not None and source is self.ui.photo:
|
|
||||||
modifiers = QApplication.keyboardModifiers()
|
|
||||||
if modifiers == Qt.KeyboardModifier.ControlModifier:
|
|
||||||
self.scale = self.scale + event.angleDelta().y() / SCALE_DELTA
|
|
||||||
if self.scale < SCALE_MIN:
|
|
||||||
self.scale = SCALE_MIN
|
|
||||||
if self.scale > SCALE_MAX:
|
|
||||||
self.scale = SCALE_MAX
|
|
||||||
self.scale_update()
|
|
||||||
return True
|
|
||||||
elif event.type() == QEvent.Type.MouseButtonDblClick and source is self.ui.photo:
|
|
||||||
self.open()
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
elif (event.type() == QEvent.Type.MouseButtonRelease and
|
||||||
|
source is self.ui.photo and self.rubber_band.isVisible()):
|
||||||
|
return self.rubber_band_select(event)
|
||||||
|
|
||||||
|
elif (event.type() == QEvent.Type.Wheel and
|
||||||
|
self.image is not None and source is self.ui.photo and
|
||||||
|
QApplication.keyboardModifiers() ==
|
||||||
|
Qt.KeyboardModifier.ControlModifier):
|
||||||
|
self.image_resize(event)
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif (event.type() == QEvent.Type.MouseButtonDblClick and
|
||||||
|
source is self.ui.photo):
|
||||||
|
self.open_image()
|
||||||
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
app = QtWidgets.QApplication(sys.argv)
|
# rubber_band_show(event): shows the rubber band for selection
|
||||||
mainWindow = coin_app()
|
def rubber_band_show(self, event):
|
||||||
mainWindow.show()
|
self.org = self.mapFromGlobal(event.globalPosition())
|
||||||
sys.exit(app.exec())
|
self.top_left = event.position()
|
||||||
|
self.rubber_band.setGeometry(QRect(self.org.toPoint(), QSize()))
|
||||||
|
self.rubber_band.show()
|
||||||
|
|
||||||
|
# rubber_band_redraw(event): resizes the rubber band for selection
|
||||||
|
def rubber_band_redraw(self, event):
|
||||||
|
pos = self.mapFromGlobal(event.globalPosition()).toPoint()
|
||||||
|
pos = QPoint(int(max(pos.x(), 0)),
|
||||||
|
int(max(pos.y(), 0)))
|
||||||
|
self.rubber_band.setGeometry(
|
||||||
|
QRect(self.org.toPoint(),
|
||||||
|
pos).normalized())
|
||||||
|
|
||||||
|
# rubber_band_select(event): processes the rubber band for selection
|
||||||
|
def rubber_band_select(self, event):
|
||||||
|
pos = event.position()
|
||||||
|
self.top_left = QPoint(int(max(min(pos.x(), self.top_left.x()), 0)),
|
||||||
|
int(max(min(pos.y(), self.top_left.y()), 0)))
|
||||||
|
|
||||||
|
self.rubber_band.hide()
|
||||||
|
rect = self.rubber_band.geometry()
|
||||||
|
self.x1 = int(self.top_left.x() / self.scale)
|
||||||
|
self.y1 = int(self.top_left.y() / self.scale)
|
||||||
|
width = rect.width() / self.scale
|
||||||
|
height = rect.height() / self.scale
|
||||||
|
self.x2 = int(self.x1 + width)
|
||||||
|
self.y2 = int(self.y1 + height)
|
||||||
|
|
||||||
|
if (width >= WIDTH_MIN and height >= HEIGHT_MIN and
|
||||||
|
self.image is not None):
|
||||||
|
print("Cropped image:", self.x1, self.y1, self.x2, self.y2)
|
||||||
|
crop = self.image[self.y1:self.y2, self.x1:self.x2]
|
||||||
|
|
||||||
|
text = tess.image_to_text(self.model, crop)
|
||||||
|
print("Text selected:", text)
|
||||||
|
self.statusBar().showMessage("Text selected: " + text)
|
||||||
|
self.add_upc(text)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
# image_resize(event): resize the image when scrolling with Ctrl
|
||||||
|
def image_resize(self, event):
|
||||||
|
self.scale = self.scale + event.angleDelta().y() / SCALE_DELTA
|
||||||
|
|
||||||
|
if self.scale < SCALE_MIN:
|
||||||
|
self.scale = SCALE_MIN
|
||||||
|
|
||||||
|
if self.scale > SCALE_MAX:
|
||||||
|
self.scale = SCALE_MAX
|
||||||
|
|
||||||
|
self.update_scale()
|
||||||
|
31
tess.py
Normal file
31
tess.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
############################################################
|
||||||
|
### Project: coin
|
||||||
|
### File: tess.py
|
||||||
|
### Description: handles all pytesseract-related operations
|
||||||
|
### Version: 1.0
|
||||||
|
############################################################
|
||||||
|
import sys, os, glob
|
||||||
|
import cv2
|
||||||
|
from PIL import Image
|
||||||
|
import pytesseract
|
||||||
|
from constants import *
|
||||||
|
|
||||||
|
# get_tess_model_names(models_path): automatically finds all available
|
||||||
|
# language models in models_path
|
||||||
|
def get_tess_model_names(models_path=MODELS_PATH):
|
||||||
|
models_list = glob.glob(models_path + "*.traineddata")
|
||||||
|
model_names = []
|
||||||
|
for path in models_list:
|
||||||
|
base_name = os.path.basename(path)
|
||||||
|
base_name = os.path.splitext(base_name)[0]
|
||||||
|
model_names.append(base_name)
|
||||||
|
return model_names
|
||||||
|
|
||||||
|
# image_to_text(model, cropped_image): use the model with the name
|
||||||
|
# model to try to recognize the text in cropped_image
|
||||||
|
def image_to_text(model, cropped_image):
|
||||||
|
gray = cv2.cvtColor(cropped_image, cv2.COLOR_BGR2GRAY)
|
||||||
|
gray = cv2.medianBlur(gray, 1)
|
||||||
|
crop = Image.fromarray(gray)
|
||||||
|
text = pytesseract.image_to_string(crop, lang=model).strip()
|
||||||
|
return text
|
68
upc.py
68
upc.py
@ -1,10 +1,18 @@
|
|||||||
|
############################################################
|
||||||
|
### Project: coin
|
||||||
|
### File: upc.py
|
||||||
|
### Description: scapes the item's name by using the its UPC
|
||||||
|
### Version: 1.0
|
||||||
|
############################################################
|
||||||
import time
|
import time
|
||||||
from seleniumbase import Driver
|
from seleniumbase import Driver
|
||||||
# from selenium import webdriver
|
from constants import *
|
||||||
|
|
||||||
driver = Driver(uc=True)
|
driver = Driver(uc=True)
|
||||||
driver.implicitly_wait(5)
|
driver.implicitly_wait(5)
|
||||||
|
|
||||||
|
# get_name_from_upc(upc, wait_interval, max_tries): will try to get
|
||||||
|
# the item's name from stocktrack.ca.
|
||||||
def get_name_from_upc(upc: str, wait_interval=1, max_tries=30) -> str:
|
def get_name_from_upc(upc: str, wait_interval=1, max_tries=30) -> str:
|
||||||
url = "https://stocktrack.ca/wm/index.php?s=wm&upc=" + upc
|
url = "https://stocktrack.ca/wm/index.php?s=wm&upc=" + upc
|
||||||
driver.get(url)
|
driver.get(url)
|
||||||
@ -13,59 +21,39 @@ def get_name_from_upc(upc: str, wait_interval=1, max_tries=30) -> str:
|
|||||||
|
|
||||||
print("Removed leading 0's:", upc)
|
print("Removed leading 0's:", upc)
|
||||||
|
|
||||||
# Change the cookies here to match that of yours when you lookup
|
for cookie in COOKIE_DICT_LIST:
|
||||||
# any item on stocktrack.ca to bypass the Cloudflare checks
|
driver.add_cookie(cookie)
|
||||||
driver.add_cookie({
|
|
||||||
"name": "PHPSESSID",
|
|
||||||
"value": "9n1ic479r1bteiv758gm9hk65p",
|
|
||||||
"path": "/",
|
|
||||||
"domain": "stocktrack.ca"
|
|
||||||
})
|
|
||||||
driver.add_cookie({
|
|
||||||
"name": "cf_chl_3",
|
|
||||||
"value": "1d706187484b25c",
|
|
||||||
"path": "/",
|
|
||||||
"domain": "stocktrack.ca"
|
|
||||||
})
|
|
||||||
driver.add_cookie({
|
|
||||||
"name": "cf_clearance",
|
|
||||||
"value": "Wp8tAMUKLdS3a4Y9AT09BIlZKx4x120uC1QzBQTUluQ-1710517775-1.0.1.1-hMEP8oeggZHBkylkwkQfi2p57H6zUUvGG40d_M4vGqOqg2Zh7wZsg6KrGl3XkDUn3mXAqyZrTqlQfd5pgHCZWQ",
|
|
||||||
"path": "/",
|
|
||||||
"domain": "stocktrack.ca"
|
|
||||||
})
|
|
||||||
driver.add_cookie({
|
|
||||||
"name": "fp",
|
|
||||||
"value": "26f4acb9b23415f921bba6977b68d55f",
|
|
||||||
"path": "/",
|
|
||||||
"domain": "stocktrack.ca"
|
|
||||||
})
|
|
||||||
|
|
||||||
driver.refresh()
|
driver.refresh()
|
||||||
name = ""
|
name = ""
|
||||||
str_s = "target=\"_blank\">"
|
pattern_front = "target=\"_blank\">"
|
||||||
str_t = "<br>UPC: " + upc + "<br>"
|
pattern_back_upc = "<br>UPC: " + upc + "<br>"
|
||||||
str_tt = "</a><br>SKU:"
|
pattern_back_sku = "</a><br>SKU:"
|
||||||
times = 0
|
|
||||||
while times < max_tries:
|
|
||||||
if __debug__:
|
|
||||||
print("Iteration No. ", times)
|
|
||||||
|
|
||||||
times = times + 1
|
tries = 0
|
||||||
|
|
||||||
|
while tries < max_tries:
|
||||||
|
if __debug__:
|
||||||
|
print("Iteration No.", tries)
|
||||||
|
|
||||||
|
tries = tries + 1
|
||||||
|
|
||||||
time.sleep(wait_interval)
|
time.sleep(wait_interval)
|
||||||
page = str(driver.execute_script(
|
page = str(driver.execute_script(
|
||||||
"return document.getElementsByTagName('html')[0].innerHTML"))
|
"return document.getElementsByTagName('html')[0].innerHTML"))
|
||||||
t = page.find(str_t)
|
|
||||||
s = page.rfind(str_s, 0, t)
|
back_upc_idx = page.find(pattern_back_upc)
|
||||||
tt = page.rfind(str_tt, 0, t)
|
front_idx = page.rfind(pattern_front, 0, back_upc_idx)
|
||||||
|
back_sku_idx = page.rfind(pattern_back_sku, 0, back_upc_idx)
|
||||||
|
|
||||||
if __debug__:
|
if __debug__:
|
||||||
p = open("page.html", "w")
|
p = open("page.html", "w")
|
||||||
print(page, file=p)
|
print(page, file=p)
|
||||||
|
|
||||||
if t == -1 or s == -1:
|
if back_upc_idx == -1 or front_idx == -1:
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
name = page[s + len(str_s) : tt]
|
name = page[front_idx + len(pattern_front) : back_sku_idx]
|
||||||
break
|
break
|
||||||
|
|
||||||
return name.replace(" ", "\n")
|
return name.replace(" ", "\n")
|
||||||
|
Reference in New Issue
Block a user