├── pladecode.py ├── pla ├── pla_config.py ├── pla_image.py ├── qtui │ ├── pladecgridview.py │ ├── main.ui │ └── mainwin.py ├── pla.py ├── pla_plane.py └── pla_group.py ├── readme.md └── LICENSE /pladecode.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | if __name__ == "__main__": 3 | from pla.qtui import mainwin 4 | mainwin.main() 5 | -------------------------------------------------------------------------------- /pla/pla_config.py: -------------------------------------------------------------------------------- 1 | box_colour = (0x00, 0x00, 0xff) 2 | sel_colour = (0xFF, 0xFF, 0xFF) 3 | grp_colour = (0x00, 0xFF, 0x00) 4 | pln_colour = (0xFF, 0xFF, 0x00) 5 | dat_colour = (0xFF, 0xFF, 0xFF) 6 | man_colour = (0x00, 0xFF, 0x00) 7 | exc_colour = (0xFF, 0x00, 0x00) 8 | cfm_colour = (0xFF, 0xFF, 0x00) 9 | font_sz = 0.3 -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # pladecode 2 | 3 | Tool for decoding mask programmed PLAs from die shots 4 | 5 | ## Disclaimer 6 | The code of this tool is still in very active development and will be cleaned up once all 7 | core features have been implemented 8 | 9 | ## Contributors 10 | 11 | The project was started by Peter Bosch, and includes small amounts of (mostly UI) code from 12 | rompar (https://github.com/AdamLaurie/rompar) by Adam Laurie et al. 13 | 14 | ## Usage 15 | 16 | For now, the tool is not considered complete or fit for end users. When it does reach that 17 | point, I will add usage information here. 18 | ![Example image](https://home.strw.leidenuniv.nl/~pbosch/pladec_x4l_and.png) 19 | -------------------------------------------------------------------------------- /pla/pla_image.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from enum import Enum 3 | import numpy 4 | import cv2 as cv 5 | 6 | class ChannelMode(Enum): 7 | RED = 0 8 | GREEN = 1 9 | BLUE = 2 10 | LUMINOSITY = 3 11 | 12 | class pla_image: 13 | 14 | def __init__( self, pixels, bgr=False): 15 | if not bgr: 16 | self.pixels = cv.cvtColor(pixels,cv.COLOR_RGB2BGR) 17 | else: 18 | self.pixels = pixels 19 | self.select_channel(ChannelMode.LUMINOSITY) 20 | 21 | def select_channel( self, mode ): 22 | self.channel_mode = mode 23 | if self.channel_mode == ChannelMode.LUMINOSITY: 24 | self.mono = cv.cvtColor( self.pixels, cv.COLOR_BGR2GRAY ) 25 | elif self.channel_mode == ChannelMode.RED: 26 | self.mono = self.pixels[::,::,2] 27 | elif self.channel_mode == ChannelMode.GREEN: 28 | self.mono = self.pixels[::,::,1] 29 | elif self.channel_mode == ChannelMode.BLUE: 30 | self.mono = self.pixels[::,::,0] 31 | else: 32 | raise IndexError 33 | 34 | def getvalue(self, coord): 35 | return self.pixels[coord.x,coord.y] 36 | 37 | def to_rgb(self): 38 | return cv.cvtColor( self.pixels, cv.COLOR_BGR2RGB ) 39 | 40 | def mono_to_bgr(self): 41 | return cv.cvtColor( self.mono, cv.COLOR_GRAY2BGR ) 42 | 43 | def width(self): 44 | return numpy.shape(self.pixels)[1] 45 | 46 | def height(self): 47 | return numpy.shape(self.pixels)[0] -------------------------------------------------------------------------------- /pla/qtui/pladecgridview.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import Qt, QPointF, pyqtSignal 2 | from PyQt5 import QtWidgets, QtCore 3 | 4 | 5 | class PlaDecGridView(QtWidgets.QGraphicsView): 6 | """""" 7 | 8 | """Signal sends the QPointF(x,y) position of Left mouse clicks on the 9 | scene, as well as the keyboard modifiers.""" 10 | sceneLeftClicked = pyqtSignal(QPointF, int, name='sceneLeftClicked') 11 | 12 | """Signal sends the QPointF(x,y) position of Right mouse clicks on the 13 | scene, as well as the keyboard modifiers.""" 14 | sceneRightClicked = pyqtSignal(QPointF, int, name='sceneRightClicked') 15 | 16 | def mousePressEvent(self, event): 17 | """Dispatch a signal when right and left clicks occur in the display region""" 18 | if event.button() == Qt.LeftButton: 19 | qimg_xy = self.mapToScene(event.pos()) 20 | if 0 <= qimg_xy.x() < self.scene().width() and\ 21 | 0 <= qimg_xy.y() < self.scene().height(): 22 | self.sceneLeftClicked.emit(qimg_xy, event.modifiers()) 23 | return 24 | elif event.button() == Qt.RightButton: 25 | qimg_xy = self.mapToScene(event.pos()) 26 | if 0 <= qimg_xy.x() < self.scene().width() and\ 27 | 0 <= qimg_xy.y() < self.scene().height(): 28 | self.sceneRightClicked.emit(qimg_xy, event.modifiers()) 29 | return 30 | 31 | super(PlaDecGridView, self).mousePressEvent(event) 32 | 33 | def wheelEvent(self, event): 34 | if event.modifiers() & QtCore.Qt.ControlModifier: 35 | if event.angleDelta().y() > 0: 36 | self.scale(1.25, 1.25) 37 | else: 38 | self.scale(0.8, 0.8) 39 | else: 40 | super().wheelEvent(event) 41 | 42 | def zoomToFit(self): 43 | self.fitInView(self.scene().sceneRect(), QtCore.Qt.KeepAspectRatio) 44 | 45 | def resizeEvent(self, QResizeEvent): 46 | super().resizeEvent(QResizeEvent) 47 | #self.zoomToFit() -------------------------------------------------------------------------------- /pla/pla.py: -------------------------------------------------------------------------------- 1 | import cv2 as cv 2 | import numpy 3 | 4 | from pla import pla_plane 5 | from pla.pla_image import pla_image 6 | 7 | 8 | class pla: 9 | image = ... # type: pla_image 10 | planes = [] 11 | 12 | def __init__(self, name, path): 13 | self.name = name 14 | self.img_path = path 15 | self.load_img() 16 | self.serialize_path = None 17 | 18 | def load_img(self): 19 | self.image = pla_image( cv.imread(str(self.img_path), cv.IMREAD_COLOR), bgr=True ) 20 | def render(self, mono=False, highlight=None, **kwargs ): 21 | target = None 22 | if mono: 23 | target = self.image.mono_to_bgr() 24 | else: 25 | target = self.image.pixels 26 | target = numpy.copy(target) 27 | for p in self.planes: 28 | p.overlay(target, highlight=highlight, **kwargs) 29 | return pla_image(target, bgr=True) 30 | 31 | def children(self): 32 | return self.planes 33 | 34 | def add_plane(self): 35 | g = pla_plane.pla_plane(self, "Plane %i"%len(self.planes)) 36 | self.planes.append(g) 37 | return g 38 | 39 | def parent(self): 40 | return None 41 | 42 | def base_coord(self): 43 | return numpy.array([0,0]) 44 | 45 | 46 | def serialize(self): 47 | dict = {} 48 | dict["name"] = self.name 49 | dict["image"] = self.img_path 50 | p = [] 51 | for v in self.planes: 52 | p.append(v.serialize()) 53 | dict["planes"] = p 54 | return dict 55 | 56 | def _deserialize(self,dict): 57 | self.planes = [] 58 | p = dict["planes"] 59 | for v in p: 60 | self.planes.append(pla_plane.pla_plane.deserialize(self,v)) 61 | 62 | @classmethod 63 | def deserialize(cls, dict): 64 | p = pla(dict["name"],dict["image"]) 65 | p._deserialize(dict) 66 | return p 67 | 68 | def get_render_item(self): 69 | return self 70 | 71 | def plane_report(self): 72 | out = "Plane report for "+self.name+"\n\n" 73 | for p in self.planes: 74 | out += p.cell_report() 75 | return out 76 | 77 | def gen_sim(self): 78 | out = "/* "+self.name+" simulator */\n/*generated by pladecode 0.1 by Peter Bosch */\n" 79 | 80 | for p in self.planes: 81 | out += p.generate_c() 82 | a = [] 83 | o = [] 84 | for p in self.planes: 85 | if p.is_and: 86 | a.append(p) 87 | else: 88 | o.append(p) 89 | args = [] 90 | loops = "" 91 | for op in o: 92 | on = self.name+"_"+op.name 93 | loops += """ 94 | for ( int j = 0; j < %s_groups; j++ ) 95 | output[j] = 0; 96 | """ % (on) 97 | for ap in a: 98 | args.append("uint32_t %s"%ap.name) 99 | an = self.name+"_"+ap.name 100 | loops += """ 101 | for ( int i = %s_offset; i < (%s_offset + %s_size); i++ ) { 102 | if ( %s_test( %s, i - %s_offset ) ) {"""%(an,an,an,an,ap.name,an) 103 | for op in o: 104 | on = self.name+"_"+op.name 105 | loops += """ 106 | if ( i >= %s_offset && i < (%s_offset + %s_size) ) { 107 | for ( int j = 0; j < %s_groups; j++ ) 108 | output[j] |= %s[i][j]; 109 | } 110 | """ % (on,on,on,on,on) 111 | loops += """ 112 | } 113 | } 114 | """ 115 | args = ", ".join(args) 116 | func=""" 117 | void %s_eval( uint32_t *output, %s ) { 118 | %s 119 | } 120 | """%(self.name,args,loops) 121 | return out+func -------------------------------------------------------------------------------- /pla/pla_plane.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | import cv2 as cv 3 | 4 | from pla import pla_image, pla_config, pla 5 | from pla.pla_group import pla_group 6 | 7 | class pla_plane: 8 | pla = ... #type pla.pla 9 | image = ... # type: pla_image.pla_image 10 | 11 | MINTERM_NOCARE = 0 12 | MINTERM_LOW = 1 13 | MINTERM_HIGH = 2 14 | 15 | def __init__(self, pla, name): 16 | self.pla = pla 17 | self.name = name 18 | self.rows = 0 19 | self.cols = 0 20 | self.cells = numpy.zeros( (self.rows,self.cols) ) 21 | self.groups = [] 22 | self.region_base = numpy.array([0,0]) 23 | self.region_size = numpy.array([10,10]) 24 | self.cells_overlap = False 25 | self.cells_unset = True 26 | self.unset_cells = self.cells 27 | self.input_count = 0 28 | self.output_count = 0 29 | self.minterms = None 30 | self.minterms_conflict = False 31 | self.is_and = False 32 | self.horiz_inputs = False 33 | self.input_pol = False 34 | self.minterms_valid = False 35 | self.cell_values_valid = False 36 | self.outputs_valid = False 37 | self.int_offset = 0 38 | 39 | def invalidate_cells(self): 40 | self.cell_values_valid = False 41 | self.invalidate_minterms() 42 | self.invalidate_outputs() 43 | 44 | def invalidate_minterms(self): 45 | self.minterms_valid = False 46 | 47 | def invalidate_outputs(self): 48 | self.outputs_valid = False 49 | 50 | def ensure_minterms(self): 51 | if not self.is_and: 52 | return 53 | if self.minterms_valid: 54 | return 55 | self.ensure_cells() 56 | c = self.cells 57 | if self.horiz_inputs: 58 | c = c.transpose() 59 | s = numpy.shape(c) 60 | self.input_count = s[0] // 2 61 | self.output_count = s[1] 62 | self.minterms = numpy.zeros((self.output_count, self.input_count), dtype="int") 63 | if self.input_pol: 64 | r1 = pla_plane.MINTERM_LOW 65 | r0 = pla_plane.MINTERM_HIGH 66 | else: 67 | r0 = pla_plane.MINTERM_LOW 68 | r1 = pla_plane.MINTERM_HIGH 69 | for i in range(0, self.input_count): 70 | self.minterms[::,i] = r0 * c[i*2,::] + r1 * c[i*2+1,::] 71 | self.minterms_conflict = numpy.amax(self.minterms,axis=(0,1)) > pla_plane.MINTERM_HIGH 72 | self.minterms_valid = True 73 | #print("enum") 74 | #self.enumerate_inputs() 75 | #print("strip") 76 | #self.strip_underspecified() 77 | #print("done") 78 | 79 | def ensure_outputs(self): 80 | if self.is_and: 81 | return 82 | if self.outputs_valid: 83 | return 84 | self.ensure_cells() 85 | c = self.cells 86 | if self.horiz_inputs: 87 | c = c.transpose() 88 | self.outputs = c 89 | self.input_count = numpy.shape(c)[0] 90 | self.output_count = numpy.shape(c)[1] 91 | self.outputs_valid = True 92 | 93 | def generate_c_orplane(self): 94 | res = "" 95 | name = self.pla.name+"_"+self.name 96 | self.ensure_outputs() 97 | grsz = 32 98 | grc = (self.output_count + grsz - 1) // grsz 99 | arr = "uint32_t %s[%2i][%2i] = "%(name, self.input_count, grc) 100 | arrb = [] 101 | for i in self.outputs: 102 | arra = [] 103 | for j in range(0, grc): 104 | gnp = i[j*grsz:(j+1)*grsz] 105 | gi = pla_plane.np_to_int(gnp) 106 | arra.append("0x%08X"%gi) 107 | arra = "{ "+", ".join(arra) + " }" 108 | arrb.append(arra) 109 | arrb = "{\n\t" +",\n\t".join(arrb) +" };\n" 110 | res = """ 111 | %s 112 | #define %s_size (%i) 113 | #define %s_bits (%i) 114 | #define %s_offset (%i) 115 | #define %s_groups (%i) 116 | """%( arr+arrb, 117 | name, self.input_count, 118 | name, self.output_count, 119 | name, self.int_offset, 120 | name, grc) 121 | return res 122 | 123 | def generate_c_andplane(self): 124 | res = "" 125 | name = self.pla.name+"_"+self.name 126 | self.ensure_minterms() 127 | if self.input_count > 31: 128 | return "Not supported for input size > int32" 129 | mask = "" 130 | val = "" 131 | j = 0 132 | for ii,i in enumerate(self.minterms): 133 | mask += "0x%08X"%pla_plane.int_minterm(i,[1,2]) 134 | val += "0x%08X"%pla_plane.int_minterm(i,[2]) 135 | if ii == self.output_count - 1: 136 | pass 137 | elif (ii % 8) == 7: 138 | mask += ",\n\t" 139 | val += ",\n\t" 140 | else: 141 | mask += ", " 142 | val += ", " 143 | res = """ 144 | #define %s_offset (%i) 145 | #define %s_size (%i) 146 | uint32_t %s_mask[%2i] = {%s}; 147 | uint32_t %s_val [%2i] = {%s}; 148 | #define %s_test(v,i) ((v & %s_mask[i]) == %s_val[i]) 149 | """%(name,self.int_offset, 150 | name,self.output_count, 151 | name,self.output_count,mask, 152 | name,self.output_count,val, 153 | name,name,name) 154 | return res 155 | 156 | def generate_c(self): 157 | if self.is_and: 158 | return self.generate_c_andplane() 159 | else: 160 | return self.generate_c_orplane() 161 | 162 | def set_input_orientation(self, val): 163 | self.horiz_inputs = val 164 | self.invalidate_minterms() 165 | self.invalidate_outputs() 166 | 167 | def set_and_plane(self,val): 168 | self.is_and = val 169 | self.invalidate_minterms() 170 | 171 | def set_input_pol(self,val): 172 | self.input_pol = val 173 | self.invalidate_minterms() 174 | 175 | def set_region(self, base, size): 176 | self.region_base = base 177 | self.region_size = size 178 | 179 | def set_region_base(self, base): 180 | self.region_base = base 181 | 182 | def set_region_size(self, size): 183 | self.region_size = size 184 | 185 | def get_region_base(self): 186 | return self.region_base 187 | 188 | def get_region_size(self): 189 | return self.region_size 190 | 191 | def base_coord(self): 192 | return self.region_base 193 | 194 | def set_size(self, rows, cols): 195 | oldcells = self.cells 196 | if self.rows <= rows and self.cols <= cols: 197 | self.cells = numpy.zeros( (rows, cols) ) 198 | self.cells[0:self.rows,0:self.cols] = oldcells 199 | else: 200 | self.cells = oldcells[0:rows,0:cols] 201 | self.rows = rows 202 | self.cols = cols 203 | self.invalidate_cells() 204 | 205 | def add_group(self): 206 | g = pla_group(self, "Group %i"%len(self.groups)) 207 | self.groups.append(g) 208 | return g 209 | 210 | def overlay(self, target, highlight=None,**kwargs): 211 | if highlight == self: 212 | col = pla_config.sel_colour 213 | else: 214 | col = pla_config.grp_colour 215 | tl = self.region_base 216 | br = tl + self.region_size 217 | cv.rectangle(target,tuple(tl),tuple(br),col) 218 | 219 | def ensure_cells(self): 220 | if self.cell_values_valid or self.rows <= 0 or self.cols <= 0: 221 | return 222 | c = numpy.zeros_like(self.cells) 223 | for g in self.groups: 224 | rs = g.row_start 225 | cs = g.col_start 226 | b = g.get_data_bits() 227 | if b is None: 228 | continue 229 | bs = numpy.shape(b) 230 | re = rs + bs[0] 231 | ce = cs + bs[1] 232 | try: 233 | self.cells[rs:re,cs:ce] = b 234 | c[rs:re,cs:ce] += 1 235 | except: 236 | pass 237 | self.unset_cells = c 238 | self.cells_overlap = numpy.amax(c,axis=(0,1)) >= 2 239 | self.cells_unset = numpy.amin(c,axis=(0,1)) < 1 240 | self.cell_values_valid = True 241 | 242 | @classmethod 243 | def np_to_int(cls, a): 244 | i = 0 245 | for j,v in enumerate(a): 246 | if v: 247 | i |= 1 << j 248 | return i 249 | 250 | @classmethod 251 | def int_minterm(cls, mt, vm1): 252 | i = 0 253 | for j,v in enumerate(mt[::-1]): 254 | if v in vm1: 255 | i |= 1 << j 256 | return i 257 | 258 | @classmethod 259 | def str_minterm(cls, mt): 260 | s = "" 261 | for i in mt: 262 | if i == pla_plane.MINTERM_NOCARE: 263 | s+=" " 264 | elif i == pla_plane.MINTERM_HIGH: 265 | s+="1" 266 | elif i == pla_plane.MINTERM_LOW: 267 | s+="0" 268 | else: 269 | s+="X" 270 | i = 0 271 | for j,v in enumerate(mt[::-1]): 272 | if v==2: 273 | i |= 1 << j 274 | bl = int(numpy.ceil(len(mt) /4)) 275 | s+=(" 0x%%0%ix"%bl)%i 276 | return s 277 | 278 | @classmethod 279 | def is_compat(cls,a,b): 280 | return numpy.all((a|b) < 3) 281 | 282 | def enumerate_inputs(self, w = None, m = None): 283 | if w is None: 284 | w = numpy.zeros(self.input_count,dtype="int") 285 | self.input_list = set() 286 | if m is None: 287 | m = numpy.zeros(self.input_count,dtype="int") 288 | if not pla_plane.is_compat(w,m): 289 | return 290 | w = w | m 291 | if tuple(w.tolist()) in self.input_list: 292 | return 293 | self.input_list.add(tuple(w.tolist())) 294 | for i in range(0, self.output_count): 295 | self.enumerate_inputs(w,self.minterms[i,::]) 296 | 297 | @classmethod 298 | def is_subset(cls, a, b): 299 | return pla_plane.is_compat(a,b) and numpy.all((a-b) <= 0) 300 | 301 | def strip_underspecified(self): 302 | print("strip underspec") 303 | f = True 304 | us = set() 305 | while f: 306 | f = False 307 | l = [] 308 | print(len(self.input_list)) 309 | for ii in self.input_list: 310 | ia = numpy.array(ii) 311 | for j in self.input_list: 312 | if ii == j: 313 | continue 314 | ja = numpy.array(j) 315 | if pla_plane.is_subset(ia,ja): 316 | f = True 317 | l.append((ia,ja,ii)) 318 | break 319 | for t in l: 320 | ia = t[0] 321 | ja = t[1] 322 | ii = t[2] 323 | self.input_list.remove(ii) 324 | us.add(ii) 325 | da = ja - ia 326 | m = da == pla_plane.MINTERM_HIGH 327 | ml = len(m[m]) 328 | print(ml,ia) 329 | for i in range(0, ml): 330 | va = numpy.copy(ia) 331 | (va[m])[i] |= pla_plane.MINTERM_LOW 332 | t = tuple(va.tolist()) 333 | if t not in us: 334 | self.input_list.add(t) 335 | m = da == pla_plane.MINTERM_LOW 336 | ml = len(m[m]) 337 | for i in range(0, ml): 338 | va = numpy.copy(ia) 339 | (va[m])[i] |= pla_plane.MINTERM_HIGH 340 | t = tuple(va.tolist()) 341 | if t not in us: 342 | self.input_list.add(t) 343 | 344 | pass 345 | 346 | def cell_report(self): 347 | self.ensure_cells() 348 | rep = self.name 349 | rep += "rows: %3i columns:%3i overlap:%i incomplete: %i\n"%\ 350 | (self.rows,self.cols,int(self.cells_overlap),int(self.cells_unset)) 351 | rep += " " 352 | for j in range(0, self.cols): 353 | rep += "%i "%(j%10) 354 | rep+="\n" 355 | for i in range(0, self.rows): 356 | r = "%3i: "%i 357 | for j in range(0, self.cols): 358 | if self.cells[i,j]: 359 | r += "1 " 360 | else: 361 | r += " " 362 | rep += r.rstrip() + "\n" 363 | rep+="\n" 364 | if self.is_and: 365 | self.ensure_minterms() 366 | rep+="minterms conflict:%i\n"%int(self.minterms_conflict) 367 | for i in range(0,self.output_count): 368 | rep+="%3i: %s\n"%(i,pla_plane.str_minterm(self.minterms[i,::])) 369 | rep += self.generate_c() 370 | #for i in self.input_list: 371 | # rep+="ENN: %s\n"%(pla_plane.str_minterm(i)) 372 | return rep 373 | 374 | def cell_dump(self): 375 | self.ensure_cells() 376 | rep = "#name:%s rows: %3i columns:%3i overlap:%i incomplete: %i\n"%\ 377 | (self.name, self.rows,self.cols,int(self.cells_overlap),int(self.cells_unset)) 378 | for i in range(0, self.rows): 379 | r = "" 380 | for j in range(0, self.cols): 381 | if self.cells[i,j]: 382 | r += "1 " 383 | else: 384 | r += "0 " 385 | rep += r.rstrip() + "\n" 386 | rep+="\n" 387 | return rep 388 | 389 | def render(self, mono=False, highlight=None, **kwargs ): 390 | target = None 391 | if mono: 392 | target = self.pla.image.mono_to_bgr() 393 | else: 394 | target = self.pla.image.pixels 395 | ishl = highlight == self 396 | region_end = self.region_base + self.region_size 397 | target = numpy.copy(target[self.region_base[1]:region_end[1], self.region_base[0]:region_end[0]]) 398 | for g in self.groups: 399 | g.render(target, offset=-self.region_base, highlight=highlight, **kwargs) 400 | return pla_image.pla_image(target, bgr=True) 401 | 402 | def children(self): 403 | return self.groups 404 | 405 | def parent(self): 406 | return self.pla 407 | 408 | def serialize(self): 409 | dict = {} 410 | dict["name"] = self.name 411 | dict["rows"] = self.rows 412 | dict["cols"] = self.cols 413 | self.ensure_cells() 414 | dict["cells"] = self.cells.tolist() 415 | groups = [] 416 | for v in self.groups: 417 | groups.append(v.serialize()) 418 | dict["groups"] = groups 419 | dict["region_base"] = self.region_base.tolist() 420 | dict["region_size"] = self.region_size.tolist() 421 | dict["is_and"] = self.is_and 422 | dict["horiz_inputs"] = self.horiz_inputs 423 | dict["input_pol"] = self.input_pol 424 | dict["int_offset"] = self.int_offset 425 | 426 | return dict 427 | 428 | def _deserialize(self, dict): 429 | self.rows = dict["rows"] 430 | self.cols = dict["cols"] 431 | self.cells = numpy.array(dict["cells"]) 432 | groups = dict["groups"] 433 | for v in groups: 434 | self.groups.append(pla_group.deserialize(self,v)) 435 | self.region_base = numpy.array(dict["region_base"]) 436 | self.region_size = numpy.array(dict["region_size"]) 437 | if "is_and" in dict: 438 | self.is_and = dict["is_and"] 439 | self.horiz_inputs = dict["horiz_inputs"] 440 | self.input_pol = dict["input_pol"] 441 | if "int_offset" in dict: 442 | self.int_offset = dict["int_offset"] 443 | 444 | @classmethod 445 | def deserialize(cls, pla, dict): 446 | o = pla_plane(pla, dict["name"]) 447 | o._deserialize(dict) 448 | return o 449 | 450 | def get_render_item(self): 451 | return self -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /pla/pla_group.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | import cv2 as cv 3 | from pla import pla_config, pla_plane 4 | import time 5 | 6 | class pla_group: 7 | 8 | CELL_AUTO = -1 9 | CELL_MANUAL = 0 10 | CELL_CONFIRMED = 1 11 | CELL_EXCLUDED = 2 12 | 13 | plane = ... # type: pla_plane 14 | 15 | def __init__(self, plane, name): 16 | self.name = name 17 | self.plane = plane 18 | self.horizontal = False 19 | self.bit_horiz = False 20 | self.bit_rows = 0 21 | self.bit_cols = 0 22 | self.class_count = 4 23 | self.row_start = 0 24 | self.row_count = 1 25 | self.row_height = 5.0 26 | self.col_start = 0 27 | self.col_count = 1 28 | self.col_width = 5.0 29 | self.crop_top = 0 30 | self.crop_bottom = 0 31 | self.crop_left = 0 32 | self.crop_right = 0 33 | self.bittrim_start = 0 34 | self.bittrim_end = 0 35 | self.class_refs = None 36 | self.region_base = numpy.array([0, 0]) 37 | self.man_cells = None 38 | self.templated_refs = False 39 | self.trimmed_bits = None 40 | self.cell_grid_valid = False 41 | self.cell_pos_valid = False 42 | self.cell_value_valid = False 43 | self.bit_grid_valid = False 44 | self.bit_value_valid = False 45 | self.reference_valid = False 46 | 47 | def base_coord(self): 48 | return self.region_base 49 | 50 | def set_region_base(self, base): 51 | self.region_base = base 52 | self.invalidate_cell_pos() 53 | 54 | def get_region_base(self): 55 | return self.region_base 56 | 57 | def get_region_size(self): 58 | self.ensure_cell_pos() 59 | return self.region_size 60 | 61 | def set_crop(self, l, t, r, b): 62 | self.crop_top = t 63 | self.crop_left = l 64 | self.crop_bottom = b 65 | self.crop_right = r 66 | self.invalidate_cell_pos() 67 | 68 | def set_size(self, rows, cols): 69 | self.row_count = rows 70 | self.col_count = cols 71 | self.invalidate_cell_grid() 72 | self.invalidate_bit_grid() 73 | 74 | def set_start(self, rows, cols): 75 | self.row_start = rows 76 | self.col_start = cols 77 | self.plane.invalidate_cells() 78 | 79 | def set_cell_size(self, width, height): 80 | self.row_height = height 81 | self.col_width = width 82 | self.invalidate_cell_pos() 83 | 84 | def set_orientation(self,val): 85 | self.horizontal = val 86 | self.invalidate_cell_values() 87 | 88 | def set_bit_orientation(self,val): 89 | self.bit_horiz = val 90 | self.invalidate_bit_grid() 91 | 92 | def set_bit_trim(self,start,end): 93 | self.bittrim_start = start 94 | self.bittrim_end = end 95 | self.invalidate_bit_grid() 96 | 97 | def set_class_count(self,value): 98 | self.class_count = value 99 | self.invalidate_cell_values() 100 | 101 | def get_class_bits(self): 102 | return int(numpy.round(numpy.log2(self.class_count))) 103 | 104 | def get_bit_size(self): 105 | self.ensure_bit_grid() 106 | return (self.bit_rows, self.bit_cols) 107 | 108 | def get_data_bits(self): 109 | self.ensure_bit_values() 110 | return self.trimmed_bits 111 | 112 | def ensure_cell_grid(self): 113 | if self.cell_grid_valid: 114 | return 115 | self.auto_cells = numpy.zeros((self.row_count, self.col_count),dtype="int") 116 | if self.man_cells is None or numpy.shape(self.man_cells) != (self.row_count, self.col_count): #TODO: Lossless resize 117 | self.man_cells = numpy.zeros((self.row_count, self.col_count),dtype="int") - 1 118 | self.typ_cells = numpy.zeros((self.row_count, self.col_count),dtype="int") - 1 119 | self.cell_grid_valid = True 120 | 121 | def ensure_cell_pos(self): 122 | if self.cell_pos_valid: 123 | return 124 | self.row_top = numpy.floor(self.region_base[1] + numpy.arange( 0, self.row_count ) * self.row_height).astype("int") 125 | self.col_left = numpy.floor(self.region_base[0] + numpy.arange( 0, self.col_count ) * self.col_width).astype("int") 126 | self.row_bottom = numpy.ceil(self.row_top + self.row_height).astype("int") 127 | self.col_right = numpy.ceil(self.col_left + numpy.ceil(self.col_width)).astype("int") 128 | self.cell_left = self.col_left + self.crop_left 129 | self.cell_top = self.row_top + self.crop_top 130 | self.cell_right = self.col_right - self.crop_right 131 | self.cell_bottom = self.row_bottom - self.crop_bottom 132 | self.cell_x = ((self.cell_left + self.cell_right ) / 2).astype("int") 133 | self.cell_y = ((self.cell_top + self.cell_bottom) / 2).astype("int") 134 | self.cell_width = self.cell_right[0] - self.cell_left[0] 135 | self.cell_height = self.cell_bottom[0] - self.cell_top[0] 136 | self.region_size = numpy.array([int(self.col_width * self.col_count), int(self.row_height * self.row_count)]) 137 | self.cell_pos_valid = True 138 | 139 | def ensure_bit_grid(self): 140 | if self.bit_grid_valid: 141 | return 142 | self.bit_rows = self.row_count 143 | self.bit_cols = self.col_count 144 | if self.bit_horiz: 145 | self.bit_cols *= self.get_class_bits() 146 | else: 147 | self.bit_rows *= self.get_class_bits() 148 | self.auto_bits = numpy.zeros((self.bit_rows,self.bit_cols),dtype="int") 149 | self.bit_grid_valid = True 150 | self.bit_value_valid = True 151 | 152 | def invalidate_cell_grid(self): 153 | self.cell_grid_valid = False 154 | self.invalidate_cell_pos() 155 | self.invalidate_bit_grid() 156 | self.invalidate_cell_values() 157 | 158 | def invalidate_cell_pos(self): 159 | self.cell_pos_valid = False 160 | self.invalidate_cell_values() 161 | 162 | def invalidate_cell_values(self): 163 | self.cell_value_valid = False 164 | self.invalidate_bit_values() 165 | 166 | def invalidate_bit_grid(self): 167 | self.bit_grid_valid = False 168 | self.invalidate_bit_values() 169 | 170 | def invalidate_bit_values(self): 171 | self.bit_value_valid = False 172 | self.plane.invalidate_cells() 173 | 174 | def invalidate_reference(self): 175 | self.reference_valid = False 176 | if not self.templated_refs: 177 | self.invalidate_cell_values() 178 | 179 | def is_cell_ref(self, r, c): 180 | return self.is_cell_man(r,c) and not self.is_cell_exc(r,c) 181 | 182 | def convert_cells(self): 183 | self.ensure_cell_grid() 184 | self.typ_cells = numpy.zeros((self.row_count, self.col_count),dtype="int") 185 | for i in range(0, self.row_count): 186 | for j in range(0, self.col_count): 187 | if self.man_cells[i,j] == -1: 188 | self.typ_cells[i,j] = pla_group.CELL_AUTO 189 | elif self.exc_cells[i,j] != 0: 190 | self.typ_cells[i,j] = pla_group.CELL_EXCLUDED 191 | else: 192 | self.typ_cells[i,j] = pla_group.CELL_MANUAL 193 | self.exc_cells = None 194 | 195 | def is_cell_man(self, r, c): 196 | self.ensure_cell_grid() 197 | return self.typ_cells[r,c] == pla_group.CELL_EXCLUDED or self.typ_cells[r,c] == pla_group.CELL_MANUAL 198 | 199 | def is_cell_exc(self, r, c): 200 | self.ensure_cell_grid() 201 | return self.typ_cells[r,c] == pla_group.CELL_EXCLUDED 202 | 203 | def get_cell_class(self, r, c): 204 | self.ensure_cell_grid() 205 | v = self.man_cells[r,c] 206 | if v == -1: 207 | self.ensure_cell_values() 208 | v = self.auto_cells[r,c] 209 | return v 210 | 211 | def reset_cell_class(self, r, c): 212 | if self.is_cell_ref(r,c): 213 | self.invalidate_reference() 214 | self.man_cells[r, c] = -1 215 | self.typ_cells[r, c] = self.CELL_AUTO 216 | self.invalidate_bit_values() 217 | 218 | def set_cell_class(self, r, c, v): 219 | if (v != self.man_cells[r, c]) and not self.is_cell_exc(r,c): 220 | self.invalidate_reference() 221 | self.typ_cells[r, c] = self.CELL_MANUAL 222 | self.man_cells[r, c] = v 223 | self.invalidate_bit_values() 224 | 225 | def toggle_cell_exc(self, r, c): 226 | if self.typ_cells[r, c] == self.CELL_EXCLUDED: 227 | self.typ_cells[r, c] = self.CELL_MANUAL 228 | if self.is_cell_man(r, c): 229 | self.invalidate_reference() 230 | 231 | def set_cell_confirmed(self, r, c): 232 | if self.typ_cells[r, c] == self.CELL_AUTO: 233 | self.man_cells[r, c] = self.auto_cells[r, c] 234 | self.typ_cells[r, c] = self.CELL_CONFIRMED 235 | 236 | def set_row_confirmed(self, r): 237 | for i in range( self.col_count ): 238 | self.set_cell_confirmed( r, i ) 239 | 240 | # Image utility methods 241 | 242 | def get_cell_dim(self): 243 | self.ensure_cell_pos() 244 | if self.horizontal: 245 | return self.cell_width 246 | else: 247 | return self.cell_height 248 | 249 | def get_cell_coord(self,x,y): 250 | self.ensure_cell_pos() 251 | x -= self.region_base[0] 252 | y -= self.region_base[1] 253 | x //= self.col_width 254 | y //= self.row_height 255 | if x <0 or x >= self.col_count or y < 0 or y >= self.row_count: 256 | return None 257 | return (int(x),int(y)) 258 | 259 | def get_cell(self, r, c): 260 | self.ensure_cell_pos() 261 | return self.plane.pla.image.mono[self.cell_top[r]:self.cell_bottom[r],self.cell_left[c]:self.cell_right[c]] 262 | 263 | def get_cell_flattened(self, r, c): 264 | c = self.get_cell(r,c) 265 | if self.horizontal: 266 | c = numpy.mean(c, axis=0) 267 | else: 268 | c = numpy.mean(c, axis=1) 269 | n = c / numpy.max(c) 270 | return n 271 | 272 | # Reference generator methods 273 | 274 | def ensure_reference(self): 275 | if self.reference_valid: 276 | return 277 | if not self.templated_refs: 278 | self.ensure_cell_grid() 279 | n = [0]*self.class_count 280 | a = numpy.zeros((self.class_count,self.get_cell_dim())) 281 | for r in range(0, self.row_count): 282 | for c in range(0, self.col_count): 283 | if not self.is_cell_ref(r,c): 284 | continue 285 | v = self.man_cells[r,c] 286 | n[v] += 1 287 | a[v, ::] += self.get_cell_flattened(r,c) 288 | if numpy.any(n == 0): 289 | return 290 | for v in range(0, self.class_count): 291 | a[v] /= n[v] 292 | self.class_refs = a 293 | self.reference_valid = True 294 | 295 | # Classifier methods 296 | 297 | def score(self, a, b): 298 | """ 299 | 300 | :type a: numpy.ndarray 301 | :type b: numpy.ndarray 302 | """ 303 | return numpy.sum((a-b)**2.0) 304 | 305 | 306 | def score_class(self,r,c,i): 307 | self.ensure_reference() 308 | a = self.get_cell_flattened(r,c) 309 | b = self.class_refs[i] 310 | return self.score(a,b) 311 | 312 | def classify_chisq(self,r,c): 313 | return numpy.argmin(numpy.array([self.score_class(r,c,i) for i in range(0,self.class_count)])) 314 | 315 | def ensure_cell_values(self): 316 | if self.cell_value_valid: 317 | return 318 | if self.class_refs is None: 319 | return #TODO: Warn/error? 320 | print(self.name+" classify start") 321 | ts = time.time() 322 | try: 323 | for i in range(0, self.row_count): 324 | for j in range(0, self.col_count): 325 | self.auto_cells[i, j] = self.classify_chisq(i, j) 326 | except: 327 | print("FOO!") 328 | print(self.name+" classify done: %f"%(time.time()-ts)) 329 | self.cell_value_valid = True 330 | 331 | def ensure_bit_values(self): 332 | if self.bit_value_valid: 333 | return 334 | self.ensure_bit_grid() 335 | print(self.name+" do_binary") 336 | ts = time.time() 337 | bits = self.get_class_bits() 338 | for i in range(0, self.row_count): 339 | for j in range(0, self.col_count): 340 | v = self.get_cell_class(i,j) 341 | for k in range(0, bits): 342 | b = ((v >> k) & 1) != 0 343 | if self.bit_horiz: 344 | self.auto_bits[i,j*bits+k] = b 345 | else: 346 | self.auto_bits[i*bits+k,j] = b 347 | if self.bit_horiz: 348 | bs = self.auto_bits[::,self.bittrim_start:self.bit_cols-self.bittrim_end] 349 | else: 350 | bs = self.auto_bits[self.bittrim_start:self.bit_rows-self.bittrim_end,::] 351 | self.trimmed_bits = bs 352 | print(self.name+" do_binary done: %f"%(time.time()-ts)) 353 | self.bit_value_valid = True 354 | # Tree methods 355 | 356 | def children(self): 357 | return None 358 | 359 | def parent(self): 360 | return self.plane 361 | 362 | def get_render_item(self): 363 | return self.plane 364 | 365 | # IO methods 366 | 367 | def serialize(self, template_only=False): 368 | dict = {} 369 | if template_only: 370 | dict["template"] = True 371 | if not template_only: 372 | dict["name"] = self.name 373 | dict["row_start"] = self.row_start 374 | dict["row_count"] = self.row_count 375 | dict["row_height"] = self.row_height 376 | dict["col_start"] = self.col_start 377 | dict["col_count"] = self.col_count 378 | dict["col_width"] = self.col_width 379 | dict["crop"] = [self.crop_left, self.crop_top, self.crop_right, self.crop_bottom] 380 | if not template_only: 381 | dict["base"] = self.region_base.tolist() 382 | 383 | dict["bit_horiz"] = self.bit_horiz 384 | dict["bittrim_start"] = self.bittrim_start 385 | dict["bittrim_end"] = self.bittrim_end 386 | 387 | dict["horiz"] = self.horizontal 388 | dict["class_count"] = self.class_count 389 | dict["templated_refs"] = self.templated_refs 390 | 391 | # Reference 392 | self.ensure_reference() 393 | dict["class_refs"] = self.class_refs.tolist() 394 | # Cell grid 395 | self.ensure_cell_grid() 396 | if not template_only: 397 | dict["man_cells"] = self.man_cells.tolist() 398 | dict["typ_cells"] = self.typ_cells.tolist() 399 | return dict 400 | 401 | def _deserialize(self, dict,only_refs=False): 402 | template_only = "template" in dict 403 | if not template_only: 404 | self.name = dict["name"] 405 | self.region_base = numpy.array(dict["base"]) 406 | self.row_start = dict["row_start"] 407 | self.row_count = dict["row_count"] 408 | self.row_height = dict["row_height"] 409 | self.col_start = dict["col_start"] 410 | self.col_count = dict["col_count"] 411 | self.col_width = dict["col_width"] 412 | if "bittrim_start" in dict: 413 | self.bittrim_start = dict["bittrim_start"] 414 | self.bittrim_end = dict["bittrim_end"] 415 | if "bit_horiz" in dict: 416 | self.bit_horiz = dict["bit_horiz"] 417 | if "horiz" in dict: 418 | self.horizontal = dict["horiz"] 419 | if "class_count" in dict: 420 | self.class_count = dict["class_count"] 421 | if "man_cells" in dict and not template_only: 422 | self.man_cells = numpy.array(dict["man_cells"]) 423 | if "exc_cells" in dict and not template_only: 424 | self.exc_cells = numpy.array(dict["exc_cells"]) 425 | else: 426 | self.exc_cells = None 427 | if "typ_cells" in dict and not template_only: 428 | self.typ_cells = numpy.array(dict["typ_cells"]) 429 | if "templated_refs" in dict and not template_only: 430 | self.templated_refs = dict["templated_refs"] 431 | else: 432 | self.templated_refs = template_only 433 | if "class_refs" in dict: 434 | self.class_refs = numpy.array(dict["class_refs"]) 435 | self.crop_left,self.crop_top,self.crop_right,self.crop_bottom = dict["crop"] 436 | if self.exc_cells is not None: 437 | self.convert_cells() 438 | 439 | 440 | @classmethod 441 | def deserialize(cls, plane, dict): 442 | o = pla_group(plane, dict["name"]) 443 | o._deserialize(dict) 444 | return o 445 | 446 | # Template methods 447 | 448 | def to_template(self,name): 449 | dict = self.serialize(template_only=True) 450 | dict["name"] = name 451 | return dict 452 | 453 | def load_template(self,tmp): 454 | self.man_cells = None 455 | self._deserialize(tmp) 456 | 457 | def load_template_refs(self,dict): 458 | if "class_refs" in dict: 459 | self.templated_refs = True 460 | self.class_refs = numpy.array(dict["class_refs"]) 461 | self.class_count = dict["class_count"] 462 | self.invalidate_cell_values() 463 | return True 464 | return False 465 | 466 | # UI Methods 467 | 468 | def render(self, target, offset, highlight, boxes=True, values=True, **kwargs): 469 | self.ensure_cell_pos() 470 | if highlight == self: 471 | col = pla_config.sel_colour 472 | else: 473 | col = pla_config.grp_colour 474 | tl = self.region_base + offset 475 | br = tl + self.region_size 476 | cv.rectangle(target,tuple(tl),tuple(br),col) 477 | cv.circle(target,tuple(tl), 4, pla_config.grp_colour) 478 | if boxes: 479 | for i in range(0, self.row_count): 480 | for j in range(0, self.col_count): 481 | tl = (self.cell_left[j] + offset[0], self.cell_top[i] + offset[1]) 482 | br = (self.cell_right[j] + offset[0], self.cell_bottom[i] + offset[1]) 483 | cv.rectangle(target, tuple(tl), tuple(br), pla_config.box_colour) 484 | if values: 485 | for i in range(0, self.row_count): 486 | for j in range(0, self.col_count): 487 | tl = (self.cell_left[j] + offset[0], self.cell_bottom[i] + offset[1]) 488 | text="%i"%self.get_cell_class(i, j) 489 | typ = self.typ_cells[i,j] 490 | if typ == pla_group.CELL_AUTO: 491 | col = pla_config.dat_colour 492 | elif typ == pla_group.CELL_EXCLUDED: 493 | col = pla_config.exc_colour 494 | elif typ == pla_group.CELL_MANUAL: 495 | col = pla_config.man_colour 496 | elif typ == pla_group.CELL_CONFIRMED: 497 | col = pla_config.cfm_colour 498 | if text != "0" or self.is_cell_man(i,j): 499 | cv.putText( target, text, tl, cv.FONT_HERSHEY_SIMPLEX, pla_config.font_sz, col , thickness=1) 500 | 501 | def plot_ref(self, ax): 502 | self.ensure_reference() 503 | ax.set_ylim(0,1.) 504 | for v in range(0, self.class_count): 505 | ax.plot(numpy.arange(0,len(self.class_refs[v])),self.class_refs[v],label="Class %i"%v) 506 | ax.legend(loc="lower right") 507 | 508 | def class_score_info(self,r,c): 509 | n = "" 510 | for i in range(0,self.class_count): 511 | n += " %f"%self.score_class(r,c,i) 512 | return n 513 | 514 | def classify_dbg(self, r, c, ax): 515 | n = self.get_cell_flattened(r,c) 516 | ax.set_ylim(0,1.) 517 | ax.plot(numpy.arange(0,len(n)),n,label="Row %i Col %i %s"%(r,c,self.class_score_info(r,c))) 518 | ax.legend(loc="lower right") -------------------------------------------------------------------------------- /pla/qtui/main.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 965 10 | 1281 11 | 12 | 13 | 14 | PLA decoder 15 | 16 | 17 | 18 | :/mainicon_64x64.png:/mainicon_64x64.png 19 | 20 | 21 | 22 | 23 | 0 24 | 25 | 26 | 0 27 | 28 | 29 | 0 30 | 31 | 32 | 0 33 | 34 | 35 | 5 36 | 37 | 38 | 39 | 40 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 41 | 42 | 43 | QGraphicsView::NoDrag 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 0 53 | 0 54 | 965 55 | 23 56 | 57 | 58 | 59 | 60 | Fi&le 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | &Edit 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | &View 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | &Help 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable 112 | 113 | 114 | Pro&ject Tree 115 | 116 | 117 | 1 118 | 119 | 120 | 121 | 122 | 123 | 124 | Qt::CustomContextMenu 125 | 126 | 127 | QAbstractScrollArea::AdjustIgnored 128 | 129 | 130 | QAbstractItemView::EditKeyPressed 131 | 132 | 133 | true 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable 143 | 144 | 145 | &Item editor 146 | 147 | 148 | 2 149 | 150 | 151 | 152 | 153 | 154 | 155 | General 156 | 157 | 158 | false 159 | 160 | 161 | 162 | 163 | 164 | Name: 165 | 166 | 167 | 168 | 169 | 170 | 171 | true 172 | 173 | 174 | false 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | true 185 | 186 | 187 | Region 188 | 189 | 190 | 191 | 192 | 193 | px 194 | 195 | 196 | 100000 197 | 198 | 199 | 200 | 201 | 202 | 203 | px 204 | 205 | 206 | 100000 207 | 208 | 209 | 210 | 211 | 212 | 213 | Qt::Horizontal 214 | 215 | 216 | 217 | 40 218 | 20 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | Base: 227 | 228 | 229 | 230 | 231 | 232 | 233 | px 234 | 235 | 236 | 100000 237 | 238 | 239 | 240 | 241 | 242 | 243 | px 244 | 245 | 246 | 100000 247 | 248 | 249 | 250 | 251 | 252 | 253 | Size: 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | Plane 264 | 265 | 266 | 267 | 268 | 269 | 100000 270 | 271 | 272 | 273 | 274 | 275 | 276 | 100000 277 | 278 | 279 | 280 | 281 | 282 | 283 | Columns 284 | 285 | 286 | Qt::AlignCenter 287 | 288 | 289 | 290 | 291 | 292 | 293 | Rows 294 | 295 | 296 | Qt::AlignCenter 297 | 298 | 299 | 300 | 301 | 302 | 303 | Size: 304 | 305 | 306 | 307 | 308 | 309 | 310 | Qt::Horizontal 311 | 312 | 313 | 314 | 40 315 | 20 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | Vertical 325 | 326 | 327 | 328 | 329 | Horizontal 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | Is AND plane 338 | 339 | 340 | 341 | 342 | 343 | 344 | Inverted inputs 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | Internal offset: 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | Group 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 0.010000000000000 374 | 375 | 376 | 377 | 378 | 379 | 380 | Binary size: 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | Vertical 389 | 390 | 391 | 392 | 393 | Horizontal 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | Cell orientation: 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 512 415 | 416 | 417 | 418 | 419 | 420 | 421 | Rows 422 | 423 | 424 | Qt::AlignCenter 425 | 426 | 427 | 428 | 429 | 430 | 431 | Y 432 | 433 | 434 | Qt::AlignCenter 435 | 436 | 437 | 438 | 439 | 440 | 441 | Columns 442 | 443 | 444 | Qt::AlignCenter 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | Binary start: 455 | 456 | 457 | 458 | 459 | 460 | 461 | Cell padding: 462 | 463 | 464 | 465 | 466 | 467 | 468 | false 469 | 470 | 471 | 472 | 473 | 474 | 475 | Qt::Horizontal 476 | 477 | 478 | 479 | 480 | 481 | 482 | X 483 | 484 | 485 | Qt::AlignCenter 486 | 487 | 488 | 489 | 490 | 491 | 492 | Class orientation: 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 16384 503 | 504 | 505 | 506 | 507 | 508 | 509 | Rows 510 | 511 | 512 | Qt::AlignCenter 513 | 514 | 515 | 516 | 517 | 518 | 519 | Plot class reference 520 | 521 | 522 | 523 | 524 | 525 | 526 | false 527 | 528 | 529 | 530 | 531 | 532 | 533 | Qt::Horizontal 534 | 535 | 536 | 537 | 538 | 539 | 540 | 0.010000000000000 541 | 542 | 543 | 544 | 545 | 546 | 547 | Copy reference from template 548 | 549 | 550 | 551 | 552 | 553 | 554 | Class count: 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | Vertical 563 | 564 | 565 | 566 | 567 | Horizontal 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | Cell size: 576 | 577 | 578 | 579 | 580 | 581 | 582 | Qt::Horizontal 583 | 584 | 585 | 586 | 40 587 | 20 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | QFrame::Box 596 | 597 | 598 | QFrame::Sunken 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | Size: 620 | 621 | 622 | 623 | 624 | 625 | 626 | Use template reference 627 | 628 | 629 | 630 | 631 | 632 | 633 | Columns 634 | 635 | 636 | Qt::AlignCenter 637 | 638 | 639 | 640 | 641 | 642 | 643 | Discard bits: 644 | 645 | 646 | 647 | 648 | 649 | 650 | Start 651 | 652 | 653 | Qt::AlignCenter 654 | 655 | 656 | 657 | 658 | 659 | 660 | End 661 | 662 | 663 | Qt::AlignCenter 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | Qt::Vertical 674 | 675 | 676 | 677 | 20 678 | 40 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | &Cell plot 689 | 690 | 691 | 1 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | toolBar 704 | 705 | 706 | Qt::ToolButtonTextUnderIcon 707 | 708 | 709 | TopToolBarArea 710 | 711 | 712 | false 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | toolBar_2 721 | 722 | 723 | TopToolBarArea 724 | 725 | 726 | false 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | &Palette 740 | 741 | 742 | 1 743 | 744 | 745 | 746 | 747 | 748 | 749 | Group templates 750 | 751 | 752 | 753 | 754 | 755 | Create 756 | 757 | 758 | 759 | 760 | 761 | 762 | Delete 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | Manual cell class 776 | 777 | 778 | 779 | 780 | 781 | Current class: 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | Qt::Horizontal 792 | 793 | 794 | 795 | 40 796 | 20 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | Qt::Vertical 808 | 809 | 810 | 811 | 20 812 | 40 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | Te&xt output 823 | 824 | 825 | 8 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | Monospace 834 | 835 | 836 | 837 | true 838 | 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | &About 847 | 848 | 849 | 850 | 851 | Create &Plane 852 | 853 | 854 | P 855 | 856 | 857 | 858 | 859 | 860 | .. 861 | 862 | 863 | &Open... 864 | 865 | 866 | Ctrl+O 867 | 868 | 869 | 870 | 871 | 872 | .. 873 | 874 | 875 | Sa&ve As... 876 | 877 | 878 | Ctrl+Shift+S 879 | 880 | 881 | 882 | 883 | Create &Group 884 | 885 | 886 | G 887 | 888 | 889 | 890 | 891 | &New... 892 | 893 | 894 | 895 | 896 | &Save 897 | 898 | 899 | Ctrl+S 900 | 901 | 902 | 903 | 904 | &Quit 905 | 906 | 907 | 908 | 909 | Probe &Cell 910 | 911 | 912 | C 913 | 914 | 915 | 916 | 917 | true 918 | 919 | 920 | &Show cell boxes 921 | 922 | 923 | Q 924 | 925 | 926 | 927 | 928 | true 929 | 930 | 931 | S&how cell values 932 | 933 | 934 | W 935 | 936 | 937 | 938 | 939 | true 940 | 941 | 942 | &View monochrome 943 | 944 | 945 | 946 | 947 | &Clear plots 948 | 949 | 950 | E 951 | 952 | 953 | 954 | 955 | &Set manual cell class 956 | 957 | 958 | M 959 | 960 | 961 | 962 | 963 | &Exclude cell from ref. 964 | 965 | 966 | N 967 | 968 | 969 | 970 | 971 | &Idle mode 972 | 973 | 974 | Esc 975 | 976 | 977 | 978 | 979 | &Unset manual cell class 980 | 981 | 982 | 983 | 984 | &Export plane report... 985 | 986 | 987 | 988 | 989 | &Export plane dumps... 990 | 991 | 992 | 993 | 994 | Export sim. source (&C)... 995 | 996 | 997 | 998 | 999 | true 1000 | 1001 | 1002 | Show &zero cells 1003 | 1004 | 1005 | 1006 | 1007 | Confirm row 1008 | 1009 | 1010 | J 1011 | 1012 | 1013 | 1014 | 1015 | 1016 | PlaDecGridView 1017 | QGraphicsView 1018 |
pladecgridview.h
1019 |
1020 |
1021 | 1022 | 1023 |
1024 | -------------------------------------------------------------------------------- /pla/qtui/mainwin.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | import traceback 3 | import pathlib 4 | import json 5 | import cv2 as cv 6 | from PyQt5 import QtCore 7 | from PyQt5 import QtGui 8 | from PyQt5 import QtWidgets 9 | from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas 10 | from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar 11 | import matplotlib.pyplot as plt 12 | #from .about import RomparAboutDialog 13 | #from .findhexdialog import FindHexDialog 14 | 15 | # Parse the ui file once. 16 | import sys, os.path 17 | from PyQt5 import uic 18 | from PyQt5.QtCore import QModelIndex 19 | from PyQt5.QtGui import QStandardItemModel, QStandardItem 20 | import numpy 21 | from PyQt5.QtWidgets import QLineEdit, QInputDialog, QAction, QMenu, QFileDialog, QVBoxLayout 22 | 23 | from pla.pla import pla 24 | from pla.pla_group import pla_group 25 | from pla.pla_image import pla_image 26 | from pla.pla_plane import pla_plane 27 | 28 | thisdir = os.path.dirname(os.path.abspath(__file__)) 29 | sys.path.append(thisdir) # Needed to load ui 30 | MainUi, MainUiBase = uic.loadUiType(os.path.join(thisdir, 'main.ui')) 31 | del sys.path[-1] # Remove the now unnecessary path entry 32 | 33 | MODE_IDLE = 0 34 | MODE_CREATE_PLANE_1 = 1 35 | MODE_CREATE_PLANE_2 = 2 36 | MODE_CREATE_GROUP = 3 37 | MODE_PROBE_CELL = 4 38 | MODE_SET_CELL = 5 39 | MODE_RESET_CELL = 6 40 | MODE_EXC_CELL = 7 41 | MODE_CFM_ROW = 8 42 | 43 | class PlaDecMainWin(QtWidgets.QMainWindow): 44 | def __init__(self): 45 | super(PlaDecMainWin, self).__init__() 46 | self.ui = MainUi() 47 | self.ui.setupUi(self) 48 | 49 | # Create buffers to show Rompar image in UI. 50 | self.pixmapitem = QtWidgets.QGraphicsPixmapItem() 51 | self.scene = QtWidgets.QGraphicsScene() 52 | self.scene.addItem(self.pixmapitem) 53 | self.renderingItem = None 54 | self.ui.graphicsView.setScene(self.scene) 55 | self.ui.graphicsView.setAlignment(QtCore.Qt.AlignTop|QtCore.Qt.AlignLeft) 56 | self.templateListModel = QStandardItemModel() 57 | self.ui.groupTemplateList.setModel(self.templateListModel) 58 | self.ui.groupTemplateList.selectionModel().selectionChanged.connect(self.on_groupTemplateList_selectionChanged) 59 | self.templateListModel.itemChanged.connect(self.on_groupTemplateList_itemChanged) 60 | self.model = QStandardItemModel() 61 | self.ui.projectTree.setModel(self.model) 62 | self.ui.projectTree.selectionModel().selectionChanged.connect(self.on_projectTree_selectionChanged) 63 | self.model.itemChanged.connect(self.on_projectTree_itemChanged) 64 | self.plalist = [] 65 | #self.load("entrypt.pla") 66 | self.mode = MODE_IDLE 67 | self.figure = plt.figure() 68 | self.canvas = FigureCanvas(self.figure) 69 | self.toolbar = NavigationToolbar(self.canvas, self.ui.plotWidget) 70 | # set the layout 71 | layout = QVBoxLayout() 72 | layout.addWidget(self.toolbar) 73 | layout.addWidget(self.canvas) 74 | self.ui.plotWidget.setLayout(layout) 75 | self.group_templates = [{"name":"None","dontuse":True}] 76 | self.populateGroupTemplateList() 77 | self.currentGroupTemplate = None 78 | 79 | def populateTree(self, list=None, parent=None): 80 | if list is None: 81 | list = self.plalist 82 | if parent is None: 83 | self.model.clear() 84 | parent = self.model.invisibleRootItem() 85 | for i in list: 86 | item = QStandardItem(i.name) 87 | parent.appendRow(item) 88 | item.setData(i.name+"foo") 89 | ch = i.children() 90 | if ch is not None: 91 | self.populateTree(ch,item) 92 | self.ui.projectTree.expandAll() 93 | 94 | def populateGroupTemplateList(self): 95 | self.templateListModel.clear() 96 | parent = self.templateListModel.invisibleRootItem() 97 | for i in self.group_templates: 98 | item = QStandardItem(i["name"]) 99 | parent.appendRow(item) 100 | 101 | def createGroupTemplate(self): 102 | if not isinstance(self.selectedItem,pla_group): 103 | self.showTempStatus("Error: To create a group template, select a group!") 104 | return 105 | self.group_templates.append(self.currentGroup.to_template("Template %i"%len(self.group_templates))) 106 | self.populateGroupTemplateList() 107 | 108 | def deleteGroupTemplate(self): 109 | if self.currentGroupTemplate is None: 110 | self.showTempStatus("Error: No template selected") 111 | return 112 | self.group_templates.remove(self.currentGroupTemplate) 113 | 114 | @QtCore.pyqtSlot() 115 | def on_createGroupTmplBtn_clicked(self): 116 | self.createGroupTemplate() 117 | 118 | @QtCore.pyqtSlot() 119 | def on_deleteGroupTmplBtn_clicked(self): 120 | self.deleteGroupTemplate() 121 | 122 | @QtCore.pyqtSlot("QItemSelection") 123 | def on_groupTemplateList_selectionChanged(self, selected): 124 | """ 125 | 126 | :type selected: QItemSelection 127 | """ 128 | a = selected.indexes()[0] # type: QModelIndex 129 | item = self.group_templates[a.row()] 130 | print("select",item["name"]) 131 | if "dontuse" in item: 132 | item = None 133 | self.currentGroupTemplate = item 134 | 135 | @QtCore.pyqtSlot("QStandardItem*") 136 | def on_groupTemplateList_itemChanged(self, item): 137 | pitem = self.group_templates[item.index().row()] 138 | nt = item["name"] 139 | if nt == pitem.name: 140 | return 141 | print("changed: "+pitem["name"]+" to "+nt) 142 | pitem["name"] = nt 143 | print("done") 144 | 145 | def view_plaimage(self, img): 146 | # Do initial draw 147 | self.qImg = QtGui.QImage( img.to_rgb(), 148 | img.width(), 149 | img.height(), 150 | 3 * img.width(), 151 | QtGui.QImage.Format_RGB888) 152 | self.pixmapitem.setPixmap(QtGui.QPixmap(self.qImg)) 153 | self.scene.setSceneRect(self.scene.itemsBoundingRect()) 154 | 155 | def selectProjectItem(self,item): 156 | self.selectedItem = item 157 | if item is None: 158 | return 159 | if isinstance(item, pla): 160 | self.setCurrentPLA( item ) 161 | self.setRenderingItem( item ) 162 | elif isinstance(item, pla_plane): 163 | self.setCurrentPlane( item ) 164 | elif isinstance(item, pla_group): 165 | self.setCurrentGroup( item ) 166 | self.updateItemEdit() 167 | self.renderItem() 168 | 169 | def updateItemEdit(self): 170 | item = self.selectedItem 171 | self.ui.itemNameText.setText(item.name) 172 | region = False 173 | if isinstance(item, pla_plane): 174 | self.ui.planeGroupBox.setDisabled(False) 175 | self.ui.planeColsSpin.setValue(item.cols) 176 | self.ui.planeRowsSpin.setValue(item.rows) 177 | self.ui.planeAndCheckbox.setChecked(item.is_and) 178 | if item.horiz_inputs: 179 | self.ui.planeInputOrientationCombo.setCurrentIndex(1) 180 | else: 181 | self.ui.planeInputOrientationCombo.setCurrentIndex(0) 182 | self.ui.planeInputPolCheckbox.setChecked(item.input_pol) 183 | region = True 184 | else: 185 | self.ui.planeGroupBox.setDisabled(True) 186 | 187 | if isinstance(item, pla_group): 188 | self.ui.groupGroupBox.setDisabled(False) 189 | self.ui.regionSizeSpinW.setDisabled(True) 190 | self.ui.regionSizeSpinH.setDisabled(True) 191 | self.ui.groupStartColSpin.setValue(item.col_start) 192 | self.ui.groupStartRowSpin.setValue(item.row_start) 193 | self.ui.groupSizeColSpin.setValue(item.col_count) 194 | self.ui.groupSizeRowSpin.setValue(item.row_count) 195 | self.ui.cellSizeSpinW.setValue(item.col_width) 196 | self.ui.cellSizeSpinH.setValue(item.row_height) 197 | self.ui.cellCropSpinT.setValue(item.crop_top) 198 | self.ui.cellCropSpinB.setValue(item.crop_bottom) 199 | self.ui.cellCropSpinL.setValue(item.crop_left) 200 | self.ui.cellCropSpinR.setValue(item.crop_right) 201 | if item.horizontal: 202 | self.ui.cellOrientationCombo.setCurrentIndex(1) 203 | else: 204 | self.ui.cellOrientationCombo.setCurrentIndex(0) 205 | if item.bit_horiz: 206 | self.ui.bitOrientationCombo.setCurrentIndex(1) 207 | else: 208 | self.ui.bitOrientationCombo.setCurrentIndex(0) 209 | self.ui.classCountSpin.setValue(item.class_count) 210 | self.ui.templRefCheckbox.setChecked(item.templated_refs) 211 | self.ui.binSizeSpinC.setValue(item.bit_cols) 212 | self.ui.binSizeSpinR.setValue(item.bit_rows) 213 | self.ui.bitTrimSpinStart.setValue(item.bittrim_start) 214 | self.ui.bitTrimSpinEnd.setValue(item.bittrim_end) 215 | region = True 216 | else: 217 | self.ui.regionSizeSpinW.setDisabled(False) 218 | self.ui.regionSizeSpinH.setDisabled(False) 219 | self.ui.groupGroupBox.setDisabled(True) 220 | 221 | if region: 222 | self.ui.regionGroupBox.setEnabled(True) 223 | rbs = item.get_region_base() 224 | self.ui.regionBaseSpinX.setValue(rbs[0]) 225 | self.ui.regionBaseSpinY.setValue(rbs[1]) 226 | rsz = item.get_region_size() 227 | self.ui.regionSizeSpinW.setValue(rsz[0]) 228 | self.ui.regionSizeSpinH.setValue(rsz[1]) 229 | else: 230 | self.ui.regionGroupBox.setDisabled(True) 231 | 232 | @QtCore.pyqtSlot() 233 | def on_templRefCheckbox_clicked(self): 234 | self.selectedItem.templated_refs = self.selectedItem.templated_refs and self.ui.templRefCheckbox.isChecked() 235 | #TODO: Re-run classify 236 | self.updateItemEdit() 237 | self.renderItem() 238 | 239 | @QtCore.pyqtSlot() 240 | def on_planeInputPolCheckbox_clicked(self): 241 | self.selectedItem.set_input_pol(self.ui.planeInputPolCheckbox.isChecked()) 242 | self.updateItemEdit() 243 | self.renderItem() 244 | 245 | @QtCore.pyqtSlot() 246 | def on_planeAndCheckbox_clicked(self): 247 | self.selectedItem.set_and_plane(self.ui.planeAndCheckbox.isChecked()) 248 | self.updateItemEdit() 249 | self.renderItem() 250 | 251 | def on_cellCropSpin_changed(self, l,t,r,b): 252 | if l == self.selectedItem.crop_left and t == self.selectedItem.crop_top and \ 253 | r == self.selectedItem.crop_right and b == self.selectedItem.crop_bottom: 254 | return 255 | self.selectedItem.set_crop(l,t,r,b) 256 | self.updateItemEdit() 257 | self.renderItem() 258 | 259 | def on_bitTrimSpin_changed(self, start, end): 260 | if start == self.selectedItem.bittrim_start and end == self.selectedItem.bittrim_end: 261 | return 262 | self.selectedItem.set_bit_trim(start, end) 263 | self.updateItemEdit() 264 | self.renderItem() 265 | 266 | def on_cellSizeSpin_changed(self, w, h): 267 | if w == self.selectedItem.col_width and h == self.selectedItem.row_height: 268 | return 269 | self.selectedItem.set_cell_size(w, h) 270 | self.updateItemEdit() 271 | self.renderItem() 272 | 273 | def on_groupStartSpin_changed(self,rows,cols): 274 | if rows == self.selectedItem.row_start and cols == self.selectedItem.col_start: 275 | return 276 | self.selectedItem.set_start(rows,cols) 277 | self.updateItemEdit() 278 | self.renderItem() 279 | 280 | def on_groupSizeSpin_changed(self,rows,cols): 281 | if rows == self.selectedItem.row_count and cols == self.selectedItem.col_count: 282 | return 283 | self.selectedItem.set_size(rows,cols) 284 | self.updateItemEdit() 285 | self.renderItem() 286 | 287 | def on_planeSizeSpin_changed(self,rows,cols): 288 | if rows == self.selectedItem.rows and cols == self.selectedItem.cols: 289 | return 290 | self.selectedItem.set_size(rows,cols) 291 | self.updateItemEdit() 292 | self.renderItem() 293 | 294 | def on_regionBaseSpin_changed(self,val): 295 | if numpy.all(val == self.selectedItem.get_region_base()): 296 | return 297 | self.selectedItem.set_region_base(val) 298 | self.updateItemEdit() 299 | self.renderItem() 300 | 301 | def on_regionSizeSpin_changed(self, val): 302 | if numpy.all(val == self.selectedItem.get_region_size()): 303 | return 304 | self.selectedItem.set_region_size(val) 305 | self.updateItemEdit() 306 | self.renderItem() 307 | 308 | @QtCore.pyqtSlot("int") 309 | def on_regionBaseSpinX_valueChanged(self, value): 310 | val = numpy.array([ 311 | value, 312 | self.selectedItem.region_base[1] 313 | ]) 314 | self.on_regionBaseSpin_changed(val) 315 | 316 | @QtCore.pyqtSlot("int") 317 | def on_regionBaseSpinY_valueChanged(self, value): 318 | val = numpy.array([ 319 | self.selectedItem.region_base[0], 320 | value 321 | ]) 322 | self.on_regionBaseSpin_changed(val) 323 | 324 | @QtCore.pyqtSlot("int") 325 | def on_regionSizeSpinW_valueChanged(self, value): 326 | val = numpy.array([ 327 | value, 328 | self.selectedItem.get_region_size()[1] 329 | ]) 330 | self.on_regionSizeSpin_changed(val) 331 | 332 | @QtCore.pyqtSlot("int") 333 | def on_regionSizeSpinH_valueChanged(self, value): 334 | val = numpy.array([ 335 | self.selectedItem.get_region_size()[0], 336 | value 337 | ]) 338 | self.on_regionSizeSpin_changed(val) 339 | 340 | @QtCore.pyqtSlot("int") 341 | def on_planeRowsSpin_valueChanged(self, value): 342 | self.on_planeSizeSpin_changed(value,self.selectedItem.cols) 343 | 344 | @QtCore.pyqtSlot("int") 345 | def on_planeColsSpin_valueChanged(self, value): 346 | self.on_planeSizeSpin_changed(self.selectedItem.rows,value) 347 | 348 | @QtCore.pyqtSlot("int") 349 | def on_groupSizeRowSpin_valueChanged(self, value): 350 | self.on_groupSizeSpin_changed(value,self.selectedItem.col_count) 351 | 352 | @QtCore.pyqtSlot("int") 353 | def on_groupSizeColSpin_valueChanged(self, value): 354 | self.on_groupSizeSpin_changed(self.selectedItem.row_count,value) 355 | 356 | @QtCore.pyqtSlot("int") 357 | def on_groupStartRowSpin_valueChanged(self, value): 358 | self.on_groupStartSpin_changed(value,self.selectedItem.col_start) 359 | 360 | @QtCore.pyqtSlot("int") 361 | def on_groupStartColSpin_valueChanged(self, value): 362 | self.on_groupStartSpin_changed(self.selectedItem.row_start,value) 363 | 364 | @QtCore.pyqtSlot("double") 365 | def on_cellSizeSpinW_valueChanged(self, value): 366 | self.on_cellSizeSpin_changed(value,self.selectedItem.row_height) 367 | 368 | @QtCore.pyqtSlot("double") 369 | def on_cellSizeSpinH_valueChanged(self, value): 370 | self.on_cellSizeSpin_changed(self.selectedItem.col_width,value) 371 | 372 | @QtCore.pyqtSlot("int") 373 | def on_cellCropSpinL_valueChanged(self, value): 374 | self.on_cellCropSpin_changed(value,self.selectedItem.crop_top,self.selectedItem.crop_right,self.selectedItem.crop_bottom) 375 | 376 | @QtCore.pyqtSlot("int") 377 | def on_cellCropSpinT_valueChanged(self, value): 378 | self.on_cellCropSpin_changed(self.selectedItem.crop_left,value,self.selectedItem.crop_right,self.selectedItem.crop_bottom) 379 | 380 | @QtCore.pyqtSlot("int") 381 | def on_cellCropSpinR_valueChanged(self, value): 382 | self.on_cellCropSpin_changed(self.selectedItem.crop_left,self.selectedItem.crop_top,value,self.selectedItem.crop_bottom) 383 | 384 | @QtCore.pyqtSlot("int") 385 | def on_cellCropSpinB_valueChanged(self, value): 386 | self.on_cellCropSpin_changed(self.selectedItem.crop_left,self.selectedItem.crop_top,self.selectedItem.crop_right,value) 387 | 388 | @QtCore.pyqtSlot("int") 389 | def on_classCountSpin_valueChanged(self, value): 390 | self.currentGroup.set_class_count( value ) 391 | 392 | @QtCore.pyqtSlot("int") 393 | def on_cellOrientationCombo_currentIndexChanged(self,idx): 394 | self.currentGroup.set_orientation(idx == 1) 395 | self.renderItem() 396 | 397 | @QtCore.pyqtSlot("int") 398 | def on_bitOrientationCombo_currentIndexChanged(self,idx): 399 | self.currentGroup.set_bit_orientation(idx == 1) 400 | self.updateItemEdit() 401 | self.renderItem() 402 | 403 | @QtCore.pyqtSlot("int") 404 | def on_planeInputOrientationCombo_currentIndexChanged(self,idx): 405 | self.currentPlane.set_input_orientation(idx == 1) 406 | self.updateItemEdit() 407 | self.renderItem() 408 | 409 | @QtCore.pyqtSlot("int") 410 | def on_bitTrimSpinStart_valueChanged(self, value): 411 | self.on_bitTrimSpin_changed(value,self.selectedItem.bittrim_end) 412 | 413 | @QtCore.pyqtSlot("int") 414 | def on_bitTrimSpinEnd_valueChanged(self, value): 415 | self.on_bitTrimSpin_changed(self.selectedItem.bittrim_start,value) 416 | 417 | def setCurrentPLA(self,pla): 418 | self.currentPLA = pla 419 | self.currentPlane = None 420 | self.currentGroup = None 421 | 422 | def setCurrentPlane(self,plane): 423 | self.setCurrentPLA(plane.pla) 424 | self.currentPlane = plane 425 | self.currentGroup = None 426 | 427 | def setCurrentGroup(self, group): 428 | self.setCurrentPlane(group.plane) 429 | self.currentGroup = group 430 | 431 | def setRenderingItem(self,item): 432 | if item == self.renderingItem: 433 | return 434 | item = item.get_render_item() 435 | self.renderingItem = item 436 | self.renderItem() 437 | self.ui.graphicsView.zoomToFit() 438 | 439 | def renderItem(self): 440 | self.view_plaimage(self.renderingItem.render(highlight=self.selectedItem,boxes=self.ui.action_ShowCellBoxes.isChecked(),values=self.ui.action_ShowCellValues.isChecked())) 441 | if pla is not None: 442 | self.setWindowTitle("PLA decoder - %s"%self.getProjectItemName(self.renderingItem)) 443 | else: 444 | self.setWindowTitle("PLA decoder") 445 | if self.currentPlane is not None: 446 | self.ui.textOutputEdit.document().setPlainText(self.currentPlane.cell_report()) 447 | 448 | def showTempStatus(self, *msg): 449 | full_msg = " ".join((str(part) for part in msg)) 450 | print("Status:", repr(full_msg)) 451 | self.statusBar().showMessage(full_msg, 4000) 452 | 453 | def showModalStatus(self, *msg): 454 | full_msg = " ".join((str(part) for part in msg)) 455 | print("Status:", repr(full_msg)) 456 | self.statusBar().showMessage(full_msg) 457 | 458 | ############################################ 459 | # Slots for Mouse Events from Graphicsview # 460 | ############################################ 461 | 462 | @QtCore.pyqtSlot(QtCore.QPointF, int) 463 | def on_graphicsView_sceneLeftClicked(self, qimg_xy, keymods): 464 | if self.mode == MODE_IDLE: 465 | pass #TODO: Select item 466 | elif self.mode == MODE_CREATE_PLANE_1: 467 | self.modeParam1 = numpy.array([int(qimg_xy.x()),int(qimg_xy.y())]) 468 | self.showModalStatus("Click bottom-right corner of plane") 469 | self.mode = MODE_CREATE_PLANE_2 470 | elif self.mode == MODE_CREATE_PLANE_2: 471 | self.modeParam2 = numpy.array([int(qimg_xy.x()),int(qimg_xy.y())]) 472 | self.mode = MODE_IDLE 473 | self.showModalStatus("Idle") 474 | self.showTempStatus("Creating") 475 | self.currentPlane = self.currentPLA.add_plane() 476 | print(self.modeParam1) 477 | print(self.modeParam2) 478 | self.currentPlane.set_region( self.modeParam1, self.modeParam2 - self.modeParam1 ) 479 | self.selectProjectItem(self.currentPlane) 480 | self.populateTree() 481 | self.renderItem() 482 | elif self.mode == MODE_CREATE_GROUP: 483 | self.modeParam1 = numpy.array([int(qimg_xy.x()),int(qimg_xy.y())]) + self.renderingItem.base_coord() 484 | self.mode = MODE_IDLE 485 | self.showModalStatus("Idle") 486 | self.showTempStatus("Creating") 487 | self.currentGroup = self.currentPlane.add_group() 488 | print(self.modeParam1) 489 | self.currentGroup.set_region_base( self.modeParam1 ) 490 | if self.currentGroupTemplate is not None: 491 | self.currentGroup.load_template(self.currentGroupTemplate) 492 | self.selectProjectItem(self.currentGroup) 493 | self.populateTree() 494 | self.renderItem() 495 | elif self.mode == MODE_PROBE_CELL: 496 | self.modeParam1 = numpy.array([int(qimg_xy.x()),int(qimg_xy.y())]) + self.renderingItem.base_coord() 497 | t = self.currentGroup.get_cell_coord(self.modeParam1[0], self.modeParam1[1]) 498 | if t: 499 | x,y = t 500 | self.showTempStatus("Probe %i %i"%t) 501 | ax = self.figure.add_subplot(111) 502 | self.currentGroup.classify_dbg(y,x,ax) 503 | self.canvas.draw() 504 | elif self.mode == MODE_SET_CELL: 505 | self.modeParam1 = numpy.array([int(qimg_xy.x()),int(qimg_xy.y())]) + self.renderingItem.base_coord() 506 | t = self.currentGroup.get_cell_coord(self.modeParam1[0], self.modeParam1[1]) 507 | if t: 508 | x,y = t 509 | self.showTempStatus("Set cell %i %i"%t) 510 | self.currentGroup.set_cell_class(y,x,self.ui.classSetSpin.value()) 511 | self.renderItem() 512 | elif self.mode == MODE_RESET_CELL: 513 | self.modeParam1 = numpy.array([int(qimg_xy.x()),int(qimg_xy.y())]) + self.renderingItem.base_coord() 514 | t = self.currentGroup.get_cell_coord(self.modeParam1[0], self.modeParam1[1]) 515 | if t: 516 | x,y = t 517 | self.showTempStatus("Reset cell %i %i"%t) 518 | self.currentGroup.reset_cell_class(y,x) 519 | self.renderItem() 520 | elif self.mode == MODE_EXC_CELL: 521 | self.modeParam1 = numpy.array([int(qimg_xy.x()),int(qimg_xy.y())]) + self.renderingItem.base_coord() 522 | t = self.currentGroup.get_cell_coord(self.modeParam1[0], self.modeParam1[1]) 523 | if t: 524 | x,y = t 525 | self.showTempStatus("Excluded cell %i %i"%t) 526 | self.currentGroup.toggle_cell_exc(y,x) 527 | self.renderItem() 528 | elif self.mode == MODE_CFM_ROW: 529 | self.modeParam1 = numpy.array([int(qimg_xy.x()),int(qimg_xy.y())]) + self.renderingItem.base_coord() 530 | t = self.currentGroup.get_cell_coord(self.modeParam1[0], self.modeParam1[1]) 531 | if t: 532 | x,y = t 533 | self.showTempStatus("Confirmed row %i %i"%t) 534 | self.currentGroup.set_row_confirmed(y) 535 | self.renderItem() 536 | 537 | @QtCore.pyqtSlot() 538 | def on_copyTemplateRefPushButton_clicked(self): 539 | if self.currentGroupTemplate is None: 540 | self.showTempStatus("Cannot copy reference when no template is selected") 541 | return 542 | if not self.currentGroup.load_template_refs(self.currentGroupTemplate): 543 | self.showTempStatus("Failed to load template refs") 544 | else: 545 | self.showTempStatus("Loaded template refs") 546 | self.renderItem() 547 | self.updateItemEdit() 548 | 549 | 550 | 551 | @QtCore.pyqtSlot(QtCore.QPointF, int) 552 | def on_graphicsView_sceneRightClicked(self, qimg_xy, keymods): 553 | pass 554 | 555 | @QtCore.pyqtSlot() 556 | def on_action_CreatePlane_triggered(self): 557 | self.startCreatePlane() 558 | 559 | @QtCore.pyqtSlot() 560 | def on_action_CreateGroup_triggered(self): 561 | self.startCreateGroup() 562 | 563 | @QtCore.pyqtSlot() 564 | def on_action_ProbeCell_triggered(self): 565 | self.startProbeCell() 566 | 567 | @QtCore.pyqtSlot() 568 | def on_action_SetCell_triggered(self): 569 | self.startSetCell() 570 | 571 | @QtCore.pyqtSlot() 572 | def on_action_ResetCell_triggered(self): 573 | self.startResetCell() 574 | 575 | @QtCore.pyqtSlot() 576 | def on_action_ExcCell_triggered(self): 577 | self.startExcludeCell() 578 | 579 | @QtCore.pyqtSlot() 580 | def on_action_ConfirmRow_triggered(self): 581 | self.startConfirmRow() 582 | 583 | @QtCore.pyqtSlot() 584 | def on_action_IdleMode_triggered(self): 585 | self.mode = MODE_IDLE 586 | self.showModalStatus("Idle") 587 | 588 | def startCreatePlane(self): 589 | if not isinstance(self.selectedItem, pla): 590 | self.showTempStatus("Error: planes can only be added to a PLA") 591 | return 592 | self.showModalStatus("Click top-left corner of plane (Esc to cancel)") 593 | self.mode = MODE_CREATE_PLANE_1 594 | 595 | def startCreateGroup(self): 596 | if not isinstance(self.selectedItem, pla_plane): 597 | self.showTempStatus("Error: groups can only be added to a plane") 598 | return 599 | if not self.renderingItem == self.currentPlane: 600 | self.showTempStatus("Error: groups can only be added when the plane view is active") 601 | return 602 | self.showModalStatus("Click top-left corner of group (Esc to cancel)") 603 | self.mode = MODE_CREATE_GROUP 604 | 605 | def startProbeCell(self): 606 | if not isinstance(self.selectedItem, pla_group): 607 | self.showTempStatus("Error: cells can only be probed on a group") 608 | return 609 | if not self.renderingItem == self.currentPlane: 610 | self.showTempStatus("Error: groups can only be probed when the plane view is active") 611 | return 612 | self.showModalStatus("Click cell to probe (Esc to cancel)") 613 | self.mode = MODE_PROBE_CELL 614 | 615 | def startSetCell(self): 616 | if not isinstance(self.selectedItem, pla_group): 617 | self.showTempStatus("Error: cells can only be set on a group") 618 | return 619 | if not self.renderingItem == self.currentPlane: 620 | self.showTempStatus("Error: groups can only be set when the plane view is active") 621 | return 622 | self.showModalStatus("Click cells to set (Esc to cancel)") 623 | self.mode = MODE_SET_CELL 624 | 625 | def startResetCell(self): 626 | if not isinstance(self.selectedItem, pla_group): 627 | self.showTempStatus("Error: cells can only be reset on a group") 628 | return 629 | if not self.renderingItem == self.currentPlane: 630 | self.showTempStatus("Error: groups can only be reset when the plane view is active") 631 | return 632 | self.showModalStatus("Click cells to reset (Esc to cancel)") 633 | self.mode = MODE_RESET_CELL 634 | 635 | def startExcludeCell(self): 636 | if not isinstance(self.selectedItem, pla_group): 637 | self.showTempStatus("Error: cells can only be excluded on a group") 638 | return 639 | if not self.renderingItem == self.currentPlane: 640 | self.showTempStatus("Error: groups can only be excluded when the plane view is active") 641 | return 642 | self.showModalStatus("Click cells to exclude from reference (Esc to cancel)") 643 | self.mode = MODE_EXC_CELL 644 | 645 | def startConfirmRow(self): 646 | if not isinstance(self.selectedItem, pla_group): 647 | self.showTempStatus("Error: rows can only be confirmed on a group") 648 | return 649 | if not self.renderingItem == self.currentPlane: 650 | self.showTempStatus("Error: groups can only be confirmed when the plane view is active") 651 | return 652 | self.showModalStatus("Click cells to exclude from reference (Esc to cancel)") 653 | self.mode = MODE_CFM_ROW 654 | 655 | def modelIndexToArray(self, mi): 656 | arr = [] 657 | while mi.isValid(): 658 | arr.append(mi.row()) 659 | mi = mi.parent() 660 | return arr[::-1] 661 | 662 | def getProjectItem(self,mi): 663 | arr = self.modelIndexToArray(mi) 664 | item = self.plalist[arr[0]] 665 | for i in arr[1::]: 666 | item = item.children()[i] 667 | return item 668 | 669 | def getProjectItemName(self,i): 670 | arr = [] 671 | while i is not None: 672 | arr.append(i.name) 673 | i = i.parent() 674 | return "::".join(arr[::-1]) 675 | 676 | @QtCore.pyqtSlot("QItemSelection") 677 | def on_projectTree_selectionChanged(self, selected): 678 | """ 679 | 680 | :type selected: QItemSelection 681 | """ 682 | a = selected.indexes()[0] # type: QModelIndex 683 | item = self.getProjectItem(a) 684 | print("select",item) 685 | self.selectProjectItem(item) 686 | 687 | @QtCore.pyqtSlot("QStandardItem*") 688 | def on_projectTree_itemChanged(self, item): 689 | pitem = self.getProjectItem(item.index()) 690 | nt = item.text() 691 | if nt == pitem.name: 692 | return 693 | print("changed: "+pitem.name+" to "+nt) 694 | pitem.name = nt 695 | print("populate tree") 696 | #self.populateTree() #Caused segfault (concurrent modification) 697 | print("set current pla") 698 | if pitem == self.currentPLA: 699 | self.setCurrentPLA(pitem) 700 | self.setRenderingItem(pitem) 701 | print("done") 702 | 703 | @QtCore.pyqtSlot("QPoint") 704 | def on_projectTree_customContextMenuRequested(self,pt): 705 | idx = self.ui.projectTree.indexAt(pt) 706 | if not idx.isValid(): 707 | return 708 | item = self.getProjectItem(idx) 709 | gpt = self.ui.projectTree.mapToGlobal( pt ) 710 | ctx = QMenu(self) 711 | rename = ctx.addAction("&Rename") 712 | rename.triggered.connect(lambda : self.on_projectTreeMenu_rename(idx)) 713 | ctx.addSeparator() 714 | ctx.addAction("&Cancel") 715 | QMenu.exec(ctx, gpt) 716 | 717 | def on_projectTreeMenu_rename(self, idx): 718 | self.ui.projectTree.edit(idx) 719 | 720 | @QtCore.pyqtSlot("QModelIndex") 721 | def on_projectTree_doubleClicked(self): 722 | if self.selectedItem is None or self.selectedItem == self.renderingItem: 723 | return 724 | self.setRenderingItem(self.selectedItem) 725 | 726 | def handleFileSelection(self,tupl): 727 | path = tupl[0] # type: str 728 | filt = tupl[1] 729 | ext = filt.split(".")[1].split(")")[0] #TODO: Create a better version of this 730 | if ext == "*": 731 | ext = "" 732 | else: 733 | ext = "." + ext 734 | if path.endswith(ext): 735 | ext = "" 736 | return path+ext 737 | 738 | def load(self,path): 739 | fp = open(path, "r") 740 | _pla = pla.deserialize(json.load(fp)) 741 | self.plalist.append(_pla) 742 | fp.close() 743 | _pla.serialize_path = path 744 | self.populateTree() 745 | self.selectProjectItem( _pla ) 746 | 747 | def createpla(self,path): 748 | _pla = pla(path.split("/")[-1],path) 749 | self.plalist.append(_pla) 750 | self.populateTree() 751 | self.selectProjectItem( _pla ) 752 | 753 | @QtCore.pyqtSlot() 754 | def on_action_NewPLA_triggered(self): 755 | path = QFileDialog.getOpenFileName(None, 'Open image', '.', 'PNG images (*.png);;All files (*.*)') 756 | if path == ('',''): 757 | return 758 | path = self.handleFileSelection(path) 759 | self.createpla(path) 760 | 761 | @QtCore.pyqtSlot() 762 | def on_action_OpenFile_triggered(self): 763 | path = QFileDialog.getOpenFileName(None, 'Open file', '.', 'PLAdec files (*.pla);;All files (*.*)') 764 | if path == ('',''): 765 | return 766 | path = self.handleFileSelection(path) 767 | self.load(path) 768 | 769 | @QtCore.pyqtSlot() 770 | def on_action_SaveFileAs_triggered(self): 771 | if not self.currentPLA: 772 | self.showTempStatus("Nothing to save") 773 | path = QFileDialog.getSaveFileName(None, 'Save %s'%self.currentPLA.name, '.', 'PLAdec files (*.pla);;All files (*.*)') 774 | if path == ('',''): 775 | return 776 | path = self.handleFileSelection(path) 777 | print(path) 778 | fp = open(path,"w") 779 | json.dump(self.currentPLA.serialize(),fp, indent=4) 780 | fp.close() 781 | self.currentPLA.serialize_path = path 782 | 783 | @QtCore.pyqtSlot() 784 | def on_action_ExportPlaneReport_triggered(self): 785 | if not self.currentPLA: 786 | self.showTempStatus("Nothing to save") 787 | path = QFileDialog.getSaveFileName(None, 'Generate plane report for %s'%self.currentPLA.name, '.', 'Text files (*.txt);;All files (*.*)') 788 | if path == ('',''): 789 | return 790 | path = self.handleFileSelection(path) 791 | print(path) 792 | fp = open(path,"w") 793 | fp.write(self.currentPLA.plane_report()) 794 | fp.close() 795 | 796 | @QtCore.pyqtSlot() 797 | def on_action_ExportPlaneDumps_triggered(self): 798 | if not self.currentPLA: 799 | self.showTempStatus("Nothing to save") 800 | path = QFileDialog.getSaveFileName(None, 'Generate plane dumps for %s'%self.currentPLA.name, '.', 'Text files (*.txt);;All files (*.*)') 801 | if path == ('',''): 802 | return 803 | path = self.handleFileSelection(path) 804 | basepath = path[:-4] 805 | print(path) 806 | for p in self.currentPLA.planes: 807 | fp = open(basepath+"_"+p.name.replace(' ','_')+".txt","w") 808 | fp.write(p.cell_dump()) 809 | fp.close() 810 | 811 | @QtCore.pyqtSlot() 812 | def on_action_ExportC_triggered(self): 813 | if not self.currentPLA: 814 | self.showTempStatus("Nothing to save") 815 | path = QFileDialog.getSaveFileName(None, 'Generate simulator for %s'%self.currentPLA.name, '.', 'C source code (*.c);;All files (*.*)') 816 | if path == ('',''): 817 | return 818 | path = self.handleFileSelection(path) 819 | print(path) 820 | fp = open(path,"w") 821 | fp.write(self.currentPLA.gen_sim()) 822 | fp.close() 823 | 824 | @QtCore.pyqtSlot() 825 | def on_action_ShowCellBoxes_triggered(self): 826 | self.renderItem() 827 | @QtCore.pyqtSlot() 828 | def on_action_ShowCellValues_triggered(self): 829 | self.renderItem() 830 | 831 | @QtCore.pyqtSlot() 832 | def on_action_SaveFile_triggered(self): 833 | if not self.currentPLA: 834 | self.showTempStatus("Nothing to save") 835 | if not self.currentPLA.serialize_path: 836 | self.on_action_SaveFileAs_triggered() 837 | return 838 | fp = open(self.currentPLA.serialize_path,"w") 839 | json.dump(self.currentPLA.serialize(),fp, indent=4) 840 | fp.close() 841 | 842 | @QtCore.pyqtSlot() 843 | def on_action_ClearPlot_triggered(self): 844 | self.figure.clear() 845 | self.canvas.draw() 846 | 847 | @QtCore.pyqtSlot() 848 | def on_plotRefButton_clicked(self): 849 | self.figure.clear() 850 | ax = self.figure.add_subplot(111) 851 | self.currentGroup.plot_ref(ax) 852 | self.canvas.draw() 853 | 854 | 855 | def run(app): 856 | import argparse 857 | parser = argparse.ArgumentParser(description='') 858 | window = PlaDecMainWin() 859 | window.show() 860 | 861 | return app.exec_() # Start the event loop. 862 | 863 | def main(): 864 | import sys 865 | 866 | # Initialize the QApplication object, and free it last. 867 | # Not having this in a different function than other QT 868 | # objects can cause segmentation faults as app is freed 869 | # before the QEidgets. 870 | app = QtWidgets.QApplication(sys.argv) 871 | 872 | # Allow Ctrl-C to interrupt QT by scheduling GIL unlocks. 873 | timer = QtCore.QTimer() 874 | timer.start(500) 875 | timer.timeout.connect(lambda: None) # Let the interpreter run. 876 | 877 | sys.exit(run(app)) 878 | 879 | if __name__ == "__main__": 880 | main() 881 | --------------------------------------------------------------------------------