def derotate(marks, center, tangent): """Return new landmark pts calculated by rotating tangent about center. """ pt_array = [] #print marks for pt in (marks.ulc, marks.urc, marks.lrc, marks.llc): pt2 = Point(pt.x, pt.y) #rotate relative to center pt2.x -= center.x pt2.y -= center.y ra_sin = math.sin(-tangent * math.pi / 180.) ra_cos = math.cos(-tangent * math.pi / 180.) #print "SIN %2.1f COS %2.1f" % (ra_sin,ra_cos) #print "pt3.x= %2.1f minus %2.1f" % (pt2.x*ra_cos, pt2.y*ra_sin) #print "pt3.y=%2.1f plus %2.1f" % (pt2.x*ra_sin,pt2.y*ra_cos) pt3 = Point(pt2.x * ra_cos - pt2.y * ra_sin, pt2.x * ra_sin + pt2.y * (ra_cos)) # restore original center offset pt3.x += center.x pt3.y += center.y # restore y increasing downwards #pt2.y = -pt2.y pt3.x = int(pt3.x) pt3.y = int(pt3.y) pt_array.append(pt3) return Landmarks(pt_array[0], pt_array[1], pt_array[2], pt_array[3])
def test_derotate(): lm = Landmarks(Point(0, 0), Point(1000, 0), Point(1000, 1000), Point(0, 1000)) center = Point(500, 500) retlm = derotate(lm, center, 45) # 45 degrees CCW print retlm return retlm
def find_landmarks(self, uli, uri, lri, lli): """ retrieve landmarks for Hart images Landmarks for the Hart Ballot will be the ulc, urc, lrc, llc (x,y) pairs marking the four corners of the main surrounding box.""" TOP = True BOT = False LEFT = True RIGHT = False lm = [] hline = scan_strips_for_horiz_line_y( uli, const.dpi, uli.size[0] - const.dpi / 2, #starting_x const.dpi / 4, #starting_y const.dpi / 2, #height_to_scan TOP) x, y = follow_hline_to_corner( uli, const.dpi, uli.size[0], #starting_x hline, #starting_y LEFT) lm.append(Point(x, y)) hline = scan_strips_for_horiz_line_y( uri, const.dpi, const.dpi / 2, #starting_x const.dpi / 4, #starting_y const.dpi / 2, #height to search TOP) x, y = follow_hline_to_corner( uri, const.dpi, const.dpi / 2, #startx hline, #hline RIGHT) lm.append(Point(x, y)) hline = scan_strips_for_horiz_line_y(lri, const.dpi, const.dpi / 2, const.dpi / 4, const.dpi / 2, BOT) x, y = follow_hline_to_corner(lri, const.dpi, const.dpi / 2, hline, RIGHT) lm.append(Point(x, y)) hline = scan_strips_for_horiz_line_y(lli, const.dpi, uli.size[0] - const.dpi / 2, const.dpi / 4, const.dpi / 2, BOT) x, y = follow_hline_to_corner(lli, const.dpi, uli.size[0] - const.dpi / 2, hline, LEFT) lm.append(Point(x, y)) landmarks = Landmarks(lm[0], lm[1], lm[2], lm[3]) return landmarks
def __init__(self, dpi, filename, landmarks=None, layout_id=None, precinct="?", vendor=None, flip=False): self.dpi = dpi self.filename = filename self.landmarks = landmarks self.layout_id = layout_id self.precinct = precinct self.use_tint_test = False self.use_wide_bounded_test = False self.median_tangent = None self.image = Image.open(filename).convert("RGB") if flip: self.image = self.image.rotate(180.) self.draw_image = Image.new(self.image.mode, self.image.size, (255, 255, 255)) self.draw = ImageDraw.Draw(self.draw_image) self.darkness_threshold = 208 self.vendor_initialization(vendor) self.median_tangent = get_tangent(self.image, dpi=self.dpi) self.image = self.image.rotate(-self.median_tangent * 360. / 6.283) self.image.save("/tmp/rotated.jpg") # need to adjust landmarks at this point # by applying rotation about center to them center = Point(self.image.size[0] / 2, self.image.size[1] / 2) if self.landmarks: self.landmarks = derotate(self.landmarks, center, -self.median_tangent * 360 / 6.28) contestboxlist = find_contest_boxes( self.image, self.dpi / 30, #skip_between_lines self.dpi / 6, #min_extent dpi, #min_line_break_length 50, #required_intensity_drop True, #search_forwards dpi=dpi) # Merge grey zone artifact boxes into larger boxes # and append merged to our main boxlist. contestboxlist = merge_artifacts(contestboxlist, self.dpi) # returned list consists of bbox image pairs contestlist = self.cleanup_trim_and_draw(contestboxlist) self.ballot_text_array = [] # eliminate duplicates via dictionary contestdict = {} for bbox, contest_image in contestlist: contestdict[tuple(bbox)] = contest_image # sort contest list by x, then by y contestlist = sorted( contestdict.keys(), key=lambda bbox: int(bbox[0]) * 100 + int(bbox[1])) for bbox in contestlist: #print bbox,contest_image box_text_array = self.process_box(bbox, contestdict[bbox]) self.ballot_text_array.extend(box_text_array)
def test_derotate(): lm = Landmarks(Point(0, 0), Point(1000, 0), Point(1000, 1000), Point(0, 1000)) center = Point(500, 500) retlm = derotate(lm, center, 45) # 45 degrees CCW print retlm return retlm if __name__ == "__main__": if len(sys.argv) < 3: print "usage find_intensity_changes.py dpi file layout_id" dpi = int(sys.argv[1]) #test_derotate() bt = BallotTemplate(dpi, sys.argv[2], landmarks=Landmarks(Point(212, 201), Point(2401, 237), Point(2335, 3996), Point(143, 3957)), layout_id='id1', precinct='P1', vendor='Hart') bt_out = open("/tmp/bt_out.xml", "w") bt_out.write(bt.__repr__()) bt_out.close() bt.draw_image.save("/tmp/viz%s.jpg" % (os.path.basename(sys.argv[2])))
def return_transformed(self,region): x,y = self.transform_coord(region.x,region.y) return Point(x,y)
def process_recursive(self, node, x, y): """Recursive walk through XML rooted at node. The process_recursive function walks an XML tree generating VOPAnalyze instances for each Vote node of the tree. """ if node.nodeType != Node.ELEMENT_NODE: return print source_line(), "node name: ", node.nodeName print source_line(), dumpdict(node.attributes) #pdb.set_trace() if node.nodeName == 'BallotSide': units = node.getAttribute('units') if units == '': self.units = 'pixels' elif units == 'pixels' or units == 'inches': self.units = units else: raise WalkerException("Ballot side specified unknown unit %s" % (units, )) self.side = node.getAttribute('side') self.layout_id = node.getAttribute('layout-id') # If the layout includes attributes # related to the target size, use them. # For missing attributes, use values from the config file. # TARGET HEIGHT th = node.getAttribute('target-height') if th == '': self.target_height = target_height else: self.target_height = float(th) # TARGET WIDTH tw = node.getAttribute('target-width') if tw == '': self.target_width = target_width else: self.target_width = float(tw) # PRECINCT precinct = node.getAttribute('precinct') if precinct == '': self.precinct = "NOTINTEMPLATE" else: self.precinct = precinct # PARTY party = node.getAttribute('party') if party == '': self.party = "NOTINTEMPLATE" else: self.party = party # TARGET HOTSPOT OFFSET X # (a target may begin visually before the area to be analyzed, # for example, it may consist of two printed arrow halves, # with the area to analyze centered between the two printed halves.) thox = node.getAttribute('target-hotspot-offset-x') if thox == '': self.target_hotspot_offset_x = target_hotspot_offset_x else: self.target_hotspot_offset_x = float(thox) # TARGET HOTSPOT OFFSET Y thoy = node.getAttribute('target-hotspot-offset-y') if thoy == '': self.target_hotspot_offset_y = target_hotspot_offset_y else: self.target_hotspot_offset_y = float(thoy) print source_line(), "target height: %s, width: %s" % ( self.target_height, self.target_width) elif node.nodeName == 'Landmarks': # Set landmarks from node, building transformer try: ulc_x = float(node.getAttribute('ulc-x')) ulc_y = float(node.getAttribute('ulc-y')) urc_x = float(node.getAttribute('urc-x')) urc_y = float(node.getAttribute('urc-y')) llc_x = float(node.getAttribute('llc-x')) llc_y = float(node.getAttribute('llc-y')) lrc_x = float(node.getAttribute('lrc-x')) lrc_y = float(node.getAttribute('lrc-y')) except ValueError: raise WalkerException( "Missing required attrib in Landmarks node of XML.") print source_line(), "Layout", ulc_x, ulc_y, urc_x, urc_y print source_line( ), "Image", self.landmarks.ulc, self.landmarks.urc self.transformer = Transformer( Point(ulc_x, ulc_y), #layout self.landmarks.ulc, #ballot Point(urc_x, urc_y), #layout self.landmarks.urc, #ballot Point(llc_x, llc_y), #layout self.landmarks.llc #ballot ) print source_line(), "Transformer", self.transformer elif node.nodeName == 'Box': #Deal with a box by: #(1) changing our accumulated starting x and y positions; #(2) outputting appropriate data, if any, for the box #(3) setting juris, contest, etc... depending on the box's # text attribute""" print source_line(), "old x,y =(%d, %d)" % (x, y) try: x = (x + float(node.getAttribute('x1'))) y = (y + float(node.getAttribute('y1'))) except ValueError: raise WalkerException( "Missing required attrib in Box node of XML.") text = node.getAttribute('text') print source_line(), "new x,y =(%d, %d)" % (x, y) if text.upper().startswith('CONTEST:'): self.contest = text[8:] elif text.upper().startswith('JURISDICTION:'): self.jurisdiction = text[13:] else: self.contest = text try: self.max_votes = node.getAttribute('max-votes') except: self.max_votes = 1 pass try: #print self.max_votes self.max_votes = int(self.max_votes) if (self.max_votes == 0): self.max_votes = 1 print "Max votes was zero, set to 1." #print self.max_votes except: raise WalkerException( "Failed to convert or receive max-votes attribute as integer." ) self.max_votes = 1 self.current_votes = 0 # save the box node, so we can go to all its votes # if we turn out to have an overvote self.current_box_node = node elif node.nodeName == 'Vote': # Deal with a vote by adding its coordinates to the existing # surrounding box coordinates and transforming the result # to get actual coordinates to pass to an analysis object. # That object should probably hold the accumulating results, # not the BSW. attrib_x = None attrib_y = None attrib_name = None #max_votes = 1; try: attrib_x = float(node.getAttribute('x1')) attrib_y = float(node.getAttribute('y1')) attrib_name = node.getAttribute('text') except ValueError: raise WalkerException( "Missing required attrib in Vote node of XML.") v_x, v_y = self.transformer.transform_coord((attrib_x + x), (attrib_y + y)) v_x2, v_y2 = self.transformer.transform_coord( (attrib_x + x + self.target_width), (attrib_y + y + self.target_height)) #v_x = int(round(.998*v_x)) v_y = int(round(v_y)) v_y2 = int(round(v_y2)) print source_line(), "(v_x=%d, vy=%d), (v_x2=%d, v_y2=%d)" % \ (v_x, v_y, v_x2, v_y2) if abs(v_y2 - v_y) > 100: self.logger.error("Unreasonable values: %s %s %s %s" % (v_x, v_y, v_x2, v_y2)) raise WalkerException("Unreasonable transformed values.") # Note that the material below is bypassed with an "if False"!!! # A class-specific fine adjustment routine to adjust the crop # the crop coordinates for vote areas may be passed in # as an initialization argument to the walker. Otherwise, # it uses a version suited for Hart target boxes. # This can be generalized to a pre-vote-statistics call, # a post-vote-statistics call, and similar optional calls # for pre and post contest boxes and pre and post the entire image. if False: try: #before_filename = "/tmp/before/BEFORE_%d_%d.jpg" % ( # int(round(v_x)),int(round(v_y))) #after_filename = "/tmp/after/AFTER_%d_%d.jpg" % ( # int(round(v_x)),int(round(v_y))) #self.image.crop((int(round(v_x)), # int(round(v_y)), # int(round(v_x2)), # int(round(v_y2)) # )).save(before_filename) self.logger.info( "Recentering calculated crop %d %d %d %d." % ( int(round(v_x)), int(round(v_y)), int(round(v_x2)), int(round(v_y2)), )) v_x, v_y, v_x2, v_y2 = self.ballot_class_vop_fine_adjust( self.logger, self.image, int(round(v_x)), int(round(v_y)), int(round(v_x2)), int(round(v_y2)), #x,y margin in pixels, stored in BSW self.mwp, self.mhp, #horiz and vert line thickness,stored in BSW, # currently hardcoded to 1/32" as pixels self.hlt, self.vlt, # target width and height in pixels,stored in BSW self.twp, self.thp) self.logger.info("Recentered crop %d %d %d %d." % ( int(round(v_x)), int(round(v_y)), int(round(v_x2)), int(round(v_y2)), )) #self.image.crop((int(round(v_x)), # int(round(v_y)), # int(round(v_x2)), # int(round(v_y2)) # )).save(after_filename) except RecenteringException as e: self.logger.warning( "Recentering failed, target crop unchanged.") self.logger.warning(e) except Exception as e: self.logger.warning( "Adjustment routine failed, ballot_class_vop_fine_adjust" ) #pdb.set_trace() self.logger.warning(e) # Provide margins to add to the bounding box # as arguments to VOPAnalyze; # the coordinates should continue to reflect # the exact boundaries of the vote op. mwp = int(round(const.margin_width_inches * const.dpi)) mhp = int(round(const.margin_height_inches * const.dpi)) vop = VOPAnalyze(int(round(v_x)), int(round(v_y)), int(round(v_x2)), int(round(v_y2)), v_margin=mhp, h_margin=mwp, image=self.image, image_filename=self.image_filename, side=self.side, layout_id=self.layout_id, jurisdiction=self.jurisdiction, contest=self.contest, choice=attrib_name, max_votes=self.max_votes, logger=self.logger) print source_line(), vop # if the vop was_voted, increment self.current_votes # if self.current_votes exceeds self.max_votes # for the moment, set the ambig flag on this vote op # it would be nice if we could walk back up to the box level # and flag every vote in the box as ambiguous or overvoted. if vop.voted: self.current_votes = self.current_votes + 1 if self.current_votes > self.max_votes: vop.ambiguous = True # do something to flag every vote in self.current_box_node; # meaning we have to buffer the vops # until each box is completed # We need to buffer all vops for a box, # then flush them to the main # results only when we return from recursion. #print "Appending to box_results" #print vop self.box_results.append(vop) #print "Box results now" #print self.box_results #self.results.append(vop) else: self.logger.info("Unhandled element %s" % (node.nodeName, )) for n in node.childNodes: self.process_recursive(n, x, y) # set vote nodes overvoted if we have too many votes in box, # clear the was_voted flag, set ambiguous flag, # then flush pending box results to the main results, if self.box_results and node.nodeName == 'Box': #print "Self current votes %d > self.max_votes %d" % (self.current_votes,self.max_votes) #pdb.set_trace() # Don't call it an overvote if you encounter a situation # where an overvote would be caused by a slight mark in comparison # with one or more heavier marks lowest_intensity = 255 highest_intensity = 0 intensity_range = 0 if self.current_votes > self.max_votes: for vop in self.box_results: # determine the darkest and lightest vop if vop.red_mean < lowest_intensity: lowest_intensity = vop.red_mean if vop.red_mean > highest_intensity: highest_intensity = vop.red_mean intensity_range = highest_intensity - lowest_intensity # commenting out next two for loops as failing, mjt 11/15/2015 #for vop in self.box_results: # if vop.red_mean > (lowest_intensity + (.9*intensity_range)): # vop.voted = False # vop.ambiguous = False # vop.overvoted = False # self.current_votes = self.current_votes - 1 # now, are any of the valid votes still ambiguous #for vop in self.box_results: # if vop.voted and (vop.red_mean < (lowest_intensity + (.5*intensity_range))): # vop.ambiguous = False # vop.overvoted = False for vop in self.box_results: if self.current_votes > self.max_votes: if vop.voted: vop.overvoted = True vop.ambiguous = True vop.voted = False #print "Copying from box_results to results" #print vop self.results.append(vop) #print "Clearing box_results" #print self.box_results self.box_results = [] return node
def find_front_landmarks(im): """find the left and right corners of the uppermost line""" iround = lambda a: int(round(float(a))) adj = lambda a: int(round(const.dpi * a)) width = adj(0.75) height = adj(0.75) # for testing, fall back to image argument if can't get from page # generate ulc, urc, lrc, llc coordinate pairs landmarks = [] # use corners of top and bottom lines in preference to circled-plus # as the circled plus are often missed due to clogging, etc... try: a, b, c, d = find_line(im, im.size[0] / 2, 100, threshold=64, black_sufficient=True) #self.log.debug("Top line coords (%d,%d)(%d,%d)" % (a,b,c,d)) except Exception: pass else: landmarks.append(Point(a, b)) landmarks.append(Point(c, d)) try: # changing search start from 1/3" above bottom to 1/14" above a, b, c, d = find_line(im, im.size[0] / 2, im.size[1] - adj(0.07), -adj(0.75), threshold=64, black_sufficient=True) #self.log.debug("Top line coords (%d,%d)(%d,%d)" % (a,b,c,d)) except Exception: pass else: landmarks.append(Point(c, d)) landmarks.append(Point(a, b)) try: x, y = landmarks[0].x, landmarks[0].y longdiff_a = landmarks[3].y - landmarks[0].y shortdiff_a = landmarks[3].x - landmarks[0].x hypot = math.sqrt(longdiff_a * longdiff_a + shortdiff_a * shortdiff_a) r_a = float(shortdiff_a) / float(longdiff_a) longdiff_b = landmarks[1].x - landmarks[0].x shortdiff_b = landmarks[0].y - landmarks[1].y hypot = math.sqrt(longdiff_b * longdiff_b + shortdiff_b * shortdiff_b) r_b = float(shortdiff_b) / float(longdiff_b) magnitude_r = min(abs(r_a), abs(r_b)) if r_a < 0. and r_b < 0.: sign_r = -1 else: sign_r = 1 r = magnitude_r * sign_r except IndexError: # page without landmarks; if this is a back page, it's ok raise Ballot.BallotException if abs(r) > 0.1: #self.log.info("Tangent is unreasonably high, at %f." % (r,)) print "Tangent is unreasonably high, at %f." % (r, ) #pdb.set_trace() # we depend on back landmarks being processed after front landmarks = Landmarks(landmarks[0], landmarks[1], landmarks[2], landmarks[3]) return landmarks