class ImportSlipDialog(QDialog, Ui_ImportSlipDlg): qr_data_available = Signal(str) qr_data_validated = Signal() json_data_available = Signal() OPERATION_PURCHASE = 1 OPERATION_RETURN = 2 QR_pattern = "^t=(.*)&s=(.*)&fn=(.*)&i=(.*)&fp=(.*)&n=(.*)$" timestamp_patterns = [ 'yyyyMMddTHHmm', 'yyyyMMddTHHmmss', 'yyyy-MM-ddTHH:mm', 'yyyy-MM-ddTHH:mm:ss' ] def __init__(self, parent): QDialog.__init__(self, parent=parent) self.setupUi(self) self.initUi() self.model = None self.delegate = [] self.CameraGroup.setVisible(False) self.cameraActive = False self.camera = None self.img_capture = None self.QR_data = '' self.slip_json = None self.slip_lines = None self.slipsAPI = SlipsTaxAPI() self.qr_data_available.connect(self.parseQRdata) self.LoadQRfromFileBtn.clicked.connect(self.loadFileQR) self.GetQRfromClipboardBtn.clicked.connect(self.readClipboardQR) self.GetQRfromCameraBtn.clicked.connect(self.readCameraQR) self.StopCameraBtn.clicked.connect(self.closeCamera) self.GetSlipBtn.clicked.connect(self.downloadSlipJSON) self.LoadJSONfromFileBtn.clicked.connect(self.loadFileSlipJSON) self.AddOperationBtn.clicked.connect(self.addOperation) self.ClearBtn.clicked.connect(self.clearSlipData) self.AssignCategoryBtn.clicked.connect(self.recognizeCategories) self.AssignCategoryBtn.setEnabled(dependency_present(['tensorflow'])) def closeEvent(self, arg__1): if self.cameraActive: self.closeCamera() def initUi(self): self.SlipAmount.setText('') self.FN.setText('') self.FD.setText('') self.FP.setText('') self.SlipType.setCurrentIndex(0) #------------------------------------------------------------------------------------------ # Loads graphics file and tries to read QR-code from it. # Assignes self.QR_data after successful read and emit signal qr_data_available @Slot() def loadFileQR(self): self.initUi() qr_file, _filter = \ QFileDialog.getOpenFileName(self, g_tr('ImportSlipDialog', "Select file with QR code"), ".", "JPEG images (*.jpg);;PNG images (*.png)") if qr_file: barcodes = pyzbar.decode(Image.open(qr_file), symbols=[pyzbar.ZBarSymbol.QRCODE]) if barcodes: self.qr_data_available.emit(barcodes[0].data.decode('utf-8')) else: logging.warning('ImportSlipDialog', "No QR codes were found in file") #------------------------------------------------------------------------------------------ # Qt operates with QImage class while pyzbar need PIL.Image as input # So, we first need to save QImage into the buffer and then read PIL.Image out from buffer # Returns: True if QR found, False if no QR found # Emits: qr_data_available(str: qr_data) if QR found def readImageQR(self, image): buffer = QBuffer() buffer.open(QBuffer.ReadWrite) image.save(buffer, "BMP") pillow_image = Image.open(io.BytesIO(buffer.data())) barcodes = pyzbar.decode(pillow_image, symbols=[pyzbar.ZBarSymbol.QRCODE]) if barcodes: self.qr_data_available.emit(barcodes[0].data.decode('utf-8')) return True else: return False @Slot() def readClipboardQR(self): self.initUi() if not self.readImageQR(QApplication.clipboard().image()): logging.warning('ImportSlipDialog', "No QR codes found in clipboard") @Slot() def readCameraQR(self): self.initUi() if len(QCameraInfo.availableCameras()) == 0: logging.warning( g_tr('ImportSlipDialog', "There are no cameras available")) return self.cameraActive = True self.CameraGroup.setVisible(True) self.SlipDataGroup.setVisible(False) camera_info = QCameraInfo.defaultCamera() logging.info( g_tr('ImportSlipDialog', "Read QR with camera: " + camera_info.deviceName())) self.camera = QCamera(camera_info) self.camera.errorOccurred.connect(self.onCameraError) self.img_capture = QCameraImageCapture(self.camera) self.img_capture.setCaptureDestination( QCameraImageCapture.CaptureToBuffer) self.img_capture.setBufferFormat(QVideoFrame.Format_RGB32) self.img_capture.error.connect(self.onCameraCaptureError) self.img_capture.readyForCaptureChanged.connect(self.onReadyForCapture) self.img_capture.imageAvailable.connect(self.onCameraImageReady) self.camera.setViewfinder(self.Viewfinder) self.camera.setCaptureMode(QCamera.CaptureStillImage) self.camera.start() @Slot() def closeCamera(self): self.camera.stop() self.camera.unload() self.CameraGroup.setVisible(False) self.SlipDataGroup.setVisible(True) self.img_capture = None self.camera = None self.cameraActive = False @Slot() def onCameraError(self, error): logging.error( g_tr( 'ImportSlipDialog', "Camera error: " + str(error) + " / " + self.camera.errorString())) @Slot() def onCameraCaptureError(self, _id, error, msg): logging.error( g_tr('ImportSlipDialog', "Camera error: " + str(error) + " / " + msg)) #---------------------------------------------------------------------- # This event happens once upon camera start - it triggers first capture # Consequent captures will be initiated after image processing in self.onCameraImageReady @Slot() def onReadyForCapture(self): self.camera.searchAndLock() self.img_capture.capture() self.camera.unlock() #---------------------------------------------------------------------- # Try to decode QR from captured frame # Close camera if decoded successfully otherwise try to capture again def onCameraImageReady(self, _id, captured_image): if self.readImageQR(captured_image.image()): self.closeCamera() else: QThread.sleep(1) self.camera.searchAndLock() self.img_capture.capture() self.camera.unlock() #----------------------------------------------------------------------------------------------- # Check if available QR data matches with self.QR_pattern # Emits qr_data_validated if match found. Otherwise shows warning message but allows to proceed @Slot() def parseQRdata(self, qr_data): self.QR_data = qr_data logging.info(g_tr('ImportSlipDialog', "QR: " + self.QR_data)) parts = re.match(self.QR_pattern, qr_data) if not parts: logging.warning( g_tr( 'ImportSlipDialog', "QR available but pattern isn't recognized: " + self.QR_data)) for timestamp_pattern in self.timestamp_patterns: datetime = QDateTime.fromString(parts.group(1), timestamp_pattern) if datetime.isValid(): self.SlipTimstamp.setDateTime(datetime) self.SlipAmount.setText(parts.group(2)) self.FN.setText(parts.group(3)) self.FD.setText(parts.group(4)) self.FP.setText(parts.group(5)) self.SlipType.setCurrentIndex(int(parts.group(6)) - 1) self.qr_data_validated.emit() def downloadSlipJSON(self): timestamp = self.SlipTimstamp.dateTime().toSecsSinceEpoch() attempt = 0 while True: result = self.slipsAPI.get_slip(timestamp, float(self.SlipAmount.text()), self.FN.text(), self.FD.text(), self.FP.text(), self.SlipType.currentIndex() + 1) if result != SlipsTaxAPI.Pending: break if attempt > 5: logging.warning( g_tr('ImportSlipDialog', "Max retry count exceeded.")) break attempt += 1 time.sleep(0.5) # wait half a second before next attempt if result == SlipsTaxAPI.Success: self.slip_json = self.slipsAPI.slip_json self.parseJSON() @Slot() def loadFileSlipJSON(self): json_file, _filter = \ QFileDialog.getOpenFileName(self, g_tr('ImportSlipDialog', "Select file with slip JSON data"), ".", "JSON files (*.json)") if json_file: with open(json_file) as f: self.slip_json = json.load(f) self.parseJSON() def parseJSON(self): # Slip data might be in a root element or in ticket/document/receipt if 'ticket' in self.slip_json: sub = self.slip_json['ticket'] if 'document' in sub: sub = sub['document'] if 'receipt' in sub: slip = sub['receipt'] else: logging.error( g_tr('ImportSlipDialog', "Can't find 'receipt' tag in json 'document'")) return else: logging.error( g_tr('ImportSlipDialog', "Can't find 'document' tag in json 'ticket'")) return else: slip = self.slip_json # Get operation type operation = 0 if 'operationType' in slip: operation = int(slip['operationType']) else: logging.error( g_tr('ImportSlipDialog', "Can't find 'operationType' tag in json 'ticket'")) return # Get shop name shop_name = '' if 'user' in slip: shop_name = self.SlipShopName.setText(slip['user']) if (not shop_name) and ('userInn' in slip): shop_name = self.slipsAPI.get_shop_name_by_inn(slip['userInn']) self.SlipShopName.setText(shop_name) peer_id = self.match_shop_name(self.SlipShopName.text()) if peer_id is not None: self.PeerEdit.selected_id = peer_id try: self.slip_lines = pd.DataFrame(slip['items']) except: return # Get date from timestamp if 'dateTime' in slip: slip_datetime = QDateTime() slip_datetime.setSecsSinceEpoch(int(slip['dateTime'])) self.SlipDateTime.setDateTime(slip_datetime) # Convert price to roubles self.slip_lines['price'] = self.slip_lines['price'] / 100 if operation == self.OPERATION_PURCHASE: self.slip_lines['sum'] = -self.slip_lines['sum'] / 100 elif operation == self.OPERATION_RETURN: self.slip_lines['sum'] = self.slip_lines['sum'] / 100 else: logging.error( g_tr('ImportSlipDialog', "Unknown operation type ") + f"{operation}") return # Use quantity if it differs from 1 unit value self.slip_lines.loc[self.slip_lines['quantity'] != 1, 'name'] = \ self.slip_lines.agg('{0[name]} ({0[quantity]:g} x {0[price]:.2f})'.format, axis=1) # Assign empty category self.slip_lines['category'] = 0 self.slip_lines['confidence'] = 1 # Assign empty tags self.slip_lines['tag'] = None self.slip_lines = self.slip_lines[[ 'name', 'category', 'confidence', 'tag', 'sum' ]] self.model = PandasLinesModel(self.slip_lines) self.LinesTableView.setModel(self.model) self.delegate = SlipLinesDelegate(self.LinesTableView) for column in range(self.model.columnCount()): if column == 0: self.LinesTableView.horizontalHeader().setSectionResizeMode( column, QHeaderView.Stretch) elif column == 1: self.LinesTableView.setColumnWidth(column, 200) elif column == 2: self.LinesTableView.setColumnHidden(column, True) else: self.LinesTableView.setColumnWidth(column, 100) self.LinesTableView.setItemDelegateForColumn(column, self.delegate) font = self.LinesTableView.horizontalHeader().font() font.setBold(True) self.LinesTableView.horizontalHeader().setFont(font) self.LinesTableView.show() def addOperation(self): if self.AccountEdit.selected_id == 0: logging.warning( g_tr('ImportSlipDialog', "Not possible to import slip: no account set for import")) return if self.PeerEdit.selected_id == 0: logging.warning( g_tr( 'ImportSlipDialog', "Not possible to import slip: can't import: no peer set for import" )) return if self.slip_lines[self.slip_lines['category'] == 0].shape[0] != 0: logging.warning( g_tr( 'ImportSlipDialog', "Not possible to import slip: some categories are not set") ) return query = executeSQL( "INSERT INTO actions (timestamp, account_id, peer_id) " "VALUES (:timestamp, :account_id, :peer_id)", [(":timestamp", self.SlipDateTime.dateTime().toSecsSinceEpoch()), (":account_id", self.AccountEdit.selected_id), (":peer_id", self.PeerEdit.selected_id)]) pid = query.lastInsertId() # update mappings _ = executeSQL( "INSERT INTO map_peer (value, mapped_to) VALUES (:peer_name, :peer_id)", [(":peer_name", self.SlipShopName.text()), (":peer_id", self.PeerEdit.selected_id)]) for index, row in self.slip_lines.iterrows(): _ = executeSQL( "INSERT INTO action_details (pid, category_id, tag_id, amount, note) " "VALUES (:pid, :category_id, :tag_id, :amount, :note)", [(":pid", pid), (":category_id", row['category']), (":tag_id", row['tag']), (":amount", row['sum']), (":note", row['name'])]) # update mappings _ = executeSQL( "INSERT INTO map_category (value, mapped_to) VALUES (:item_name, :category_id)", [(":item_name", row['name']), (":category_id", row['category'])], commit=True) self.clearSlipData() def clearSlipData(self): self.QR_data = '' self.slip_json = None self.slip_lines = None self.LinesTableView.setModel(None) self.initUi() def match_shop_name(self, shop_name): return readSQL("SELECT mapped_to FROM map_peer WHERE value=:shop_name", [(":shop_name", shop_name)]) @Slot() def recognizeCategories(self): self.slip_lines['category'], self.slip_lines['confidence'] = \ recognize_categories(self.slip_lines['name'].tolist()) self.model.dataChanged.emit(None, None) # refresh full view
class CameraMainWin(QtWidgets.QMainWindow, ui_cameraWin.Ui_MainWindow): """程序的主窗体逻辑""" def __init__(self, img_rec: ImageRecognition, res_wid: ResultWid, sig_res_wid: SingleResultWid): super(CameraMainWin, self).__init__() self.setupUi(self) # 设置图像处理实例和跳转实例 self.img_rec = img_rec self.res_wid = res_wid self.sig_res_wid = sig_res_wid # camera设置 self.camera = QCamera() self.camera.setCaptureMode(QCamera.CaptureViewfinder) self.cameraOpened = False # 设置相机打开状态为未打开 # 设置取景器分辨率 view_finder_settings = QCameraViewfinderSettings() self.camera.setViewfinderSettings(view_finder_settings) # 初始化取景器 self.viewCamera = QtMultimediaWidgets.QCameraViewfinder(self) self.camera.setViewfinder(self.viewCamera) self.cameraLayout.addWidget(self.viewCamera) # 取景器放置到预留的布局中 # 设置图像捕获 self.capImg = QCameraImageCapture(self.camera) self.capImg.setCaptureDestination(QCameraImageCapture.CaptureToBuffer) self.capImg.imageCaptured.connect(self._process_captured_image) # 绑定按钮槽函数 self.cameraButton.clicked.connect(self.switch_camera) self.captureButton.clicked.connect(self.take_pic) self.loadButton.clicked.connect(self.load_file) self.reconitionButton.clicked.connect(self.load_path) self.chg_mod_Button.clicked.connect(self.change_model) def switch_camera(self): """槽函数,开关摄像头""" if not self.cameraOpened: self.camera.start() self.cameraOpened = True self.cameraButton.setText("关闭摄像头") else: self.camera.stop() self.cameraOpened = False self.cameraButton.setText("打开摄像头") def take_pic(self): """槽函数,捕获摄像头图像""" self.capImg.capture() def _process_captured_image(self, _, img): """ 图像捕获时执行 打开识别结果窗体并反馈预测结果 :param img: 摄像头捕获到的QImage格式图片 :return: """ self.sig_res_wid.show() self.sig_res_wid.image_predict(img) def load_file(self): """槽函数,打开单个图片文件识别并弹窗反馈""" img_path, _ = QFileDialog.getOpenFileName( self, '选择图片', 'Images', 'Image files(*.jpg *.gif *.png)') if img_path == '': return else: q_image = QImage(img_path) self.sig_res_wid.show() self.sig_res_wid.image_predict(q_image) def load_path(self): """槽函数,打开图片文件夹识别并弹窗反馈""" dir_path = QFileDialog.getExistingDirectory(self, '选择图片', 'Images') if dir_path == '': return else: self.img_rec.set_images_path(dir_path) self.res_wid.show() self.res_wid.image_predict() def change_model(self): """槽函数,打开模型文件夹识别并弹窗反馈""" mod_path, _ = QFileDialog.getOpenFileName(self, '选择模型文件', 'Models/') if mod_path == '': return else: self.img_rec.change_model(mod_path)