mirror of https://github.com/NaN-tic/nanscan.git
Implemented DataMatrix analyzer.
Other architecture enhancements involving generalization of analyzers to create new once more easily, improved spawn() function to allow it to be run in several simultaneous threads.
This commit is contained in:
parent
327542e74d
commit
ec3b13fac2
|
@ -0,0 +1,57 @@
|
|||
# coding=iso-8859-1
|
||||
# Copyright (C) 2008 by Albert Cervera i Areny
|
||||
# albert@nan-tic.com
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the
|
||||
# Free Software Foundation, Inc.,
|
||||
# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
|
||||
import subprocess
|
||||
import tempfile
|
||||
import codecs
|
||||
import os
|
||||
|
||||
class Analyzer:
|
||||
analyzers = {}
|
||||
|
||||
def __init__(self):
|
||||
self.boxes = []
|
||||
|
||||
@staticmethod
|
||||
def registerAnalyzer(name, analyzer):
|
||||
Analyzer.analyzers[name] = analyzer
|
||||
|
||||
@staticmethod
|
||||
def unregisterAnalyzer(name):
|
||||
del Analyzer.analyzers[name]
|
||||
|
||||
@staticmethod
|
||||
def create(name):
|
||||
return Analyzer.analyzers[name]()
|
||||
|
||||
def scan(self, image):
|
||||
pass
|
||||
|
||||
def textInRegion(self, region):
|
||||
pass
|
||||
|
||||
def featureRectInRegion(self, region):
|
||||
pass
|
||||
|
||||
# Spawn process and return STDOUT
|
||||
def spawn(self, command, *args):
|
||||
command = [ command ] + list( args )
|
||||
process = subprocess.Popen( command , stdout=subprocess.PIPE )
|
||||
content = process.communicate()[0]
|
||||
return content
|
|
@ -23,6 +23,7 @@ from string import lower
|
|||
import os
|
||||
import tempfile
|
||||
from temporaryfile import *
|
||||
from analyzer import *
|
||||
|
||||
class Box:
|
||||
def __init__(self):
|
||||
|
@ -30,25 +31,10 @@ class Box:
|
|||
self.type = None
|
||||
self.position = None
|
||||
|
||||
class Barcode:
|
||||
class Barcode(Analyzer):
|
||||
def __init__(self):
|
||||
self.boxes = []
|
||||
|
||||
# Spawn process and return STDOUT
|
||||
def spawn(self, command, *args):
|
||||
(fd,filename) = tempfile.mkstemp()
|
||||
previousFd = os.dup(1)
|
||||
os.dup2(fd, 1)
|
||||
p = os.spawnlp(os.P_WAIT, command, command, *args)
|
||||
os.dup2(previousFd, 1)
|
||||
os.close(fd)
|
||||
os.close(previousFd)
|
||||
f = open( filename )
|
||||
content = f.read()
|
||||
f.close()
|
||||
os.unlink( filename )
|
||||
return content
|
||||
|
||||
def parseBardecodeOutput(self, content):
|
||||
# Sample output "818043376500 [type: ean13 at: (1798,936)]"
|
||||
for line in content.splitlines():
|
||||
|
@ -101,3 +87,4 @@ class Barcode:
|
|||
self.parseBardecodeOutput( content )
|
||||
self.printBoxes()
|
||||
|
||||
Analyzer.registerAnalyzer( 'barcode', Barcode )
|
||||
|
|
|
@ -0,0 +1,146 @@
|
|||
# Copyright (C) 2008 by Albert Cervera i Areny
|
||||
# albert@nan-tic.com
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the
|
||||
# Free Software Foundation, Inc.,
|
||||
# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
|
||||
from PyQt4.QtCore import *
|
||||
from PyQt4.QtGui import *
|
||||
# Do not import everything as Template is defined in string too
|
||||
from string import lower
|
||||
import os
|
||||
import tempfile
|
||||
from temporaryfile import *
|
||||
from analyzer import *
|
||||
|
||||
class Box:
|
||||
def __init__(self):
|
||||
self.text = None
|
||||
self.position = None
|
||||
self.size = None
|
||||
self.dataCodewords = None
|
||||
self.errorCodewordsd = None
|
||||
self.dataRegions = None
|
||||
self.interleavedBlocks = None
|
||||
self.rotationAngle = None
|
||||
self.box = None
|
||||
|
||||
class DataMatrix(Analyzer):
|
||||
def __init__(self):
|
||||
self.boxes = []
|
||||
|
||||
# Spawn process and return STDOUT
|
||||
|
||||
def outputTextToPoint(self, text):
|
||||
pos = text.strip('(').strip(')').split(',')
|
||||
x = float(pos[0]) / self.dotsPerMillimeterX
|
||||
y = float(pos[1]) / self.dotsPerMillimeterY
|
||||
return QPointF( x, y )
|
||||
|
||||
def parseOutput(self, content):
|
||||
# Each datamatrix is a line of the output
|
||||
nextText = False
|
||||
box = None
|
||||
lines = content.splitlines()
|
||||
for x in xrange(len(lines)):
|
||||
line = lines[x]
|
||||
if not box and line == ('-' * 50):
|
||||
continue
|
||||
|
||||
if not box:
|
||||
box = Box()
|
||||
self.boxes.append( box )
|
||||
|
||||
if nextText:
|
||||
box.text = line
|
||||
nextText = False
|
||||
box = None
|
||||
continue
|
||||
if line == ('-' * 50):
|
||||
nextText = True
|
||||
continue
|
||||
|
||||
key, value = line.split(':')
|
||||
value = value.strip()
|
||||
if 'Matrix Size' in key:
|
||||
box.size = value
|
||||
elif 'Data Codewords' in key:
|
||||
box.dataCodewords = value
|
||||
elif 'Error Codewords' in key:
|
||||
box.errorCodewords = value
|
||||
elif 'Data Regions' in key:
|
||||
box.dataRegions = value
|
||||
elif 'Interleaved Blocks' in key:
|
||||
box.interleavedBlocks = value
|
||||
elif 'Rotation Angle' in key:
|
||||
box.rotationAngle = value
|
||||
elif 'Corner 0' in key:
|
||||
box.corner0 = self.outputTextToPoint( value )
|
||||
elif 'Corner 1' in key:
|
||||
box.corner1 = self.outputTextToPoint( value )
|
||||
elif 'Corner 2' in key:
|
||||
box.corner2 = self.outputTextToPoint( value )
|
||||
elif 'Corner 3' in key:
|
||||
box.corner3 = self.outputTextToPoint( value )
|
||||
r1 = QRectF( box.corner0, box.corner1 )
|
||||
r2 = QRectF( box.corner2, box.corner3 )
|
||||
box.box = r1.united( r2 )
|
||||
|
||||
def printBoxes(self):
|
||||
for x in self.boxes:
|
||||
print "Text: '%s'; Position: %f, %f; Size: %f, %f;" % (x.text, x.box.x(), x.box.y(), x.box.width(), x.box.height() )
|
||||
|
||||
## @brief Returns all data matrix values concatenated for a given region of the image.
|
||||
def textInRegion(self, region):
|
||||
texts = []
|
||||
for x in self.boxes:
|
||||
if region.intersects(x.box):
|
||||
texts.append( unicode(x.text) )
|
||||
|
||||
# Always return unicode strings
|
||||
return u''.join( texts )
|
||||
|
||||
## @brief Returns the bounding rectangle of the text returned by textInRegion for
|
||||
# the given region.
|
||||
def featureRectInRegion(self, region):
|
||||
rect = QRectF()
|
||||
for x in self.boxes:
|
||||
if region.intersects(x.box):
|
||||
rect = rect.united( x.box )
|
||||
return rect
|
||||
|
||||
## @brief Scans the given image (QImage) looking for barcodes.
|
||||
def scan(self, image):
|
||||
# Clean boxes so scan() can be called more than once
|
||||
self.boxes = []
|
||||
|
||||
# Obtain image resolution
|
||||
image = QImage( image )
|
||||
self.dotsPerMillimeterX = float( image.dotsPerMeterX() ) / 1000.0
|
||||
self.dotsPerMillimeterY = float( image.dotsPerMeterY() ) / 1000.0
|
||||
|
||||
file = TemporaryFile.create()
|
||||
image.save( file, 'PNG' )
|
||||
command = 'dmtxread'
|
||||
content = self.spawn( command, '-n', '-v', file )
|
||||
self.parseOutput( content )
|
||||
self.printBoxes()
|
||||
|
||||
Analyzer.registerAnalyzer( 'dataMatrix', DataMatrix )
|
||||
|
||||
if __name__ == '__main__':
|
||||
d = DataMatrix()
|
||||
d.scan( '/tmp/ex3.png' )
|
||||
|
|
@ -21,6 +21,7 @@ class Document:
|
|||
self.name = ''
|
||||
self.template = None
|
||||
self.boxes = []
|
||||
self.formatedText = None
|
||||
|
||||
def addBox(self, box):
|
||||
self.boxes.append( box )
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
# Free Software Foundation, Inc.,
|
||||
# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
|
||||
import levenshtein
|
||||
|
||||
class Levenshtein:
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -25,6 +25,7 @@ import shutil
|
|||
import math
|
||||
|
||||
from temporaryfile import *
|
||||
from analyzer import *
|
||||
|
||||
from gamera.core import *
|
||||
from PyQt4.QtCore import *
|
||||
|
@ -45,14 +46,25 @@ def boxComparison(x, y):
|
|||
|
||||
## @breif This class allows using an OCR and provides several convenient functions
|
||||
# regarding text and image processing such as deskewing or obtaining formated text.
|
||||
class Ocr:
|
||||
class Ocr(Analyzer):
|
||||
file = ""
|
||||
|
||||
## @brief Uses tesseract to recognize text of the current image.
|
||||
def ocr(self):
|
||||
def tesseract(self):
|
||||
directory = tempfile.mkdtemp()
|
||||
path = os.path.join( directory, 'tesseract' )
|
||||
os.spawnlp(os.P_WAIT, 'tesseract', 'tesseract', self.file, path, '-l', 'spa', 'batch.nochop', 'makebox' )
|
||||
self.spawn( 'tesseract', self.file, path, '-l', 'spa', 'batch.nochop', 'makebox' )
|
||||
f=codecs.open(path + '.txt', 'r', 'utf-8')
|
||||
content = f.read()
|
||||
f.close()
|
||||
shutil.rmtree(directory, True)
|
||||
return content
|
||||
|
||||
## @brief Uses cuneiform to recognize text of the current image.
|
||||
def cuneiform(self):
|
||||
directory = tempfile.mkdtemp()
|
||||
path = os.path.join( directory, 'cuneiform' )
|
||||
os.spawnlpe(os.P_WAIT, '/home/albert/d/git/cuneiform/bin/cuneiform', '/home/albert/d/git/cuneiform/bin/cuneiform', self.file, path, '-l', 'spa', 'batch.nochop', {'LD_LIBRARY_PATH': '/home/albert/d/git/cuneiform/lib'} )
|
||||
f=codecs.open(path + '.txt', 'r', 'utf-8')
|
||||
content = f.read()
|
||||
f.close()
|
||||
|
@ -130,7 +142,7 @@ class Ocr:
|
|||
self.file = TemporaryFile.create('.tif')
|
||||
self.convertToGrayScale(image, self.file)
|
||||
|
||||
txt = lower( self.ocr() )
|
||||
txt = lower( self.tesseract() )
|
||||
|
||||
self.boxes = self.parseTesseractOutput(txt)
|
||||
|
||||
|
@ -316,6 +328,8 @@ class Ocr:
|
|||
if slope > 0.001:
|
||||
self.deskewOnce( self, region )
|
||||
|
||||
Analyzer.registerAnalyzer( 'text', Ocr )
|
||||
|
||||
## @brief Initializes OCR functions that need to be executed once before the library
|
||||
# can work. Currently only initiates Gamera which is not being used by now.
|
||||
def initOcrSystem():
|
||||
|
|
|
@ -20,7 +20,9 @@
|
|||
from PyQt4.QtCore import *
|
||||
from barcode import *
|
||||
from ocr import *
|
||||
from datamatrix import *
|
||||
|
||||
from analyzer import *
|
||||
from template import *
|
||||
from document import *
|
||||
from trigram import *
|
||||
|
@ -39,63 +41,64 @@ class Analyze(QThread):
|
|||
def run(self):
|
||||
self.analyzer.scan( self.image )
|
||||
|
||||
|
||||
class Recognizer(QObject):
|
||||
def __init__(self, parent=None):
|
||||
QObject.__init__(self, parent)
|
||||
self.barcode = Barcode()
|
||||
self.ocr = Ocr()
|
||||
self.analyzers = {}
|
||||
for x in Analyzer.analyzers:
|
||||
self.analyzers[x] = Analyzer.create(x)
|
||||
self.ocr = self.analyzers['text']
|
||||
self.image = None
|
||||
self.threads = []
|
||||
|
||||
## @brief Returns the text of a given region of the image.
|
||||
def textInRegion(self, region, type=None):
|
||||
if type == 'barcode':
|
||||
return self.barcode.textInRegion( region )
|
||||
elif type == 'text':
|
||||
return self.ocr.textInRegion( region )
|
||||
if type in self.analyzers.keys():
|
||||
return self.analyzers[type].textInRegion( region )
|
||||
else:
|
||||
return None
|
||||
|
||||
## @brief Returns the bounding rectangle of the text returned by textInRegion for
|
||||
# the given region.
|
||||
def featureRectInRegion(self, region, type=None):
|
||||
if type == 'barcode':
|
||||
return self.barcode.featureRectInRegion( region )
|
||||
elif type == 'text':
|
||||
return self.ocr.featureRectInRegion( region )
|
||||
if type in self.analyzers:
|
||||
return self.analyzers[type].featureRectInRegion( region )
|
||||
else:
|
||||
return None
|
||||
|
||||
def boxes(self, type):
|
||||
if type == 'barcode':
|
||||
return self.barcode.boxes
|
||||
elif type == 'text':
|
||||
return self.ocr.boxes
|
||||
if type in self.analyzers:
|
||||
return self.analyzers[type].boxes
|
||||
else:
|
||||
return None
|
||||
|
||||
def analyzersAvailable(self):
|
||||
return ['barcode', 'text']
|
||||
return self.analyzers.keys()
|
||||
|
||||
# Synchronous
|
||||
def recognize(self, image):
|
||||
self.image = image
|
||||
self.barcode.scan( image )
|
||||
self.ocr.scan( image )
|
||||
for analyzer in self.analyzers.values():
|
||||
analyzer.scan( image )
|
||||
#self.barcode.scan( image )
|
||||
#self.ocr.scan( image )
|
||||
|
||||
## @brief Asynchronous: Starts analyzers in background threads. Emits finished() at the end
|
||||
def startRecognition(self, image):
|
||||
self.image = image
|
||||
self.ocrThread = Analyze( self.ocr, image, self )
|
||||
self.barcodeThread = Analyze( self.barcode, image, self )
|
||||
self.connect( self.ocrThread, SIGNAL('finished()'), self.recognitionFinished )
|
||||
self.connect( self.barcodeThread, SIGNAL('finished()'), self.recognitionFinished )
|
||||
self.ocrThread.start()
|
||||
self.barcodeThread.start()
|
||||
self.threads = []
|
||||
for analyzer in self.analyzers.values():
|
||||
thread = Analyze( analyzer, image, self )
|
||||
self.connect( thread, SIGNAL('finished()'), self.recognitionFinished )
|
||||
self.threads.append( thread )
|
||||
thread.start()
|
||||
|
||||
def recognitionFinished(self):
|
||||
if self.ocrThread.isFinished() and self.barcodeThread.isFinished():
|
||||
self.emit( SIGNAL('finished()') )
|
||||
for thread in self.threads:
|
||||
if thread.isRunning():
|
||||
return
|
||||
self.emit( SIGNAL('finished()') )
|
||||
self.threads = []
|
||||
|
||||
def filter(self, value, filterType):
|
||||
numeric = '0123456789'
|
||||
|
@ -246,6 +249,7 @@ class Recognizer(QObject):
|
|||
return QPoint( 0, 0 )
|
||||
|
||||
lines = self.ocr.textLinesWithSpaces()
|
||||
print "FORMATED: ", self.ocr.formatedText().encode( 'ascii', 'replace' )
|
||||
|
||||
# Create a default translator only once
|
||||
translator = Translator()
|
||||
|
@ -257,7 +261,7 @@ class Recognizer(QObject):
|
|||
continue
|
||||
|
||||
templateBoxText = templateBox.text.strip()
|
||||
templateBox.ranges = Range.extractAllRangesFromDocument(lines, len(templateBoxText))
|
||||
templateBox.ranges = Range.extractAllRangesFromDocument( lines, len(templateBoxText), templateBox.featureRect.width() + 2 )
|
||||
for ran in templateBox.ranges:
|
||||
text = ran.text()
|
||||
#value = Hamming.hamming( text, templateBoxText, translator )
|
||||
|
@ -402,7 +406,7 @@ class Range:
|
|||
## @brief Returns a list with all possible ranges of size length of the
|
||||
# given document
|
||||
@staticmethod
|
||||
def extractAllRangesFromDocument(lines, length):
|
||||
def extractAllRangesFromDocument(lines, length, width=0):
|
||||
if length <= 0:
|
||||
return []
|
||||
ranges = []
|
||||
|
@ -413,6 +417,9 @@ class Range:
|
|||
ran.pos = 0
|
||||
ran.length = len(lines[line])
|
||||
ran.document = lines
|
||||
#if width:
|
||||
# while ran.rect().width() > width:
|
||||
# ran.length -= 1
|
||||
ranges.append( ran )
|
||||
continue
|
||||
for pos in range(len(lines[line]) - length + 1):
|
||||
|
@ -421,6 +428,9 @@ class Range:
|
|||
ran.pos = pos
|
||||
ran.length = length
|
||||
ran.document = lines
|
||||
#if width:
|
||||
# while ran.rect().width() > width:
|
||||
# ran.length -= 1
|
||||
ranges.append( ran )
|
||||
return ranges
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ class Template(QObject):
|
|||
self.emit(SIGNAL('boxRemoved(PyQt_PyObject)'), box)
|
||||
|
||||
class TemplateBox:
|
||||
recognizers = ['text', 'barcode']
|
||||
recognizers = ['text', 'barcode', 'dataMatrix']
|
||||
types = ['matcher','input']
|
||||
filters = ['none','numeric','alphabetic','alphanumeric']
|
||||
|
||||
|
|
|
@ -183,6 +183,13 @@ class DocumentScene(QGraphicsScene):
|
|||
circle.setBrush( self._circleItemBrush )
|
||||
self._imageBoxes.addToGroup( circle )
|
||||
|
||||
for i in self.recognizer.boxes('dataMatrix'):
|
||||
#rect = QGraphicsRectItem( i.box, self._imageBoxes )
|
||||
rect = QGraphicsRectItem( self.mapRectFromRecognizer( i.box ), self._imageBoxes )
|
||||
rect.setPen( self._boxItemPen )
|
||||
rect.setBrush( self._boxItemBrush )
|
||||
self._imageBoxes.addToGroup( rect )
|
||||
|
||||
self.setImageBoxesVisible( self._imageBoxesVisible )
|
||||
self._imageBoxes.setZValue( 2 )
|
||||
|
||||
|
@ -512,7 +519,7 @@ class MainWindow(QMainWindow):
|
|||
|
||||
def recognizerChanged(self, recognizer):
|
||||
rect = self.uiTool.box.rect
|
||||
self.uiTool.setText( self.scene.recognizer.textInRegion( rect, recognizer ) )
|
||||
self.uiTool.setText( self.scene.recognizer.textInRegion( rect, unicode(recognizer) ) )
|
||||
|
||||
def newTemplateBox(self, rect):
|
||||
# Creating and adding the box to the template
|
||||
|
@ -644,7 +651,6 @@ class MainWindow(QMainWindow):
|
|||
if not self.login():
|
||||
return
|
||||
|
||||
print "DDDDD: ", Rpc.session
|
||||
dialog = OpenTemplateDialog(self)
|
||||
if dialog.exec_() == QDialog.Rejected:
|
||||
return
|
||||
|
|
Loading…
Reference in New Issue