From 3cc94ae9d27864f7370e2ace4f4ef1b989c01d33 Mon Sep 17 00:00:00 2001 From: gaojian Date: Wed, 30 May 2018 17:34:56 +0800 Subject: [PATCH 01/10] solve the bug that label information could not display --- imagepy/tools/Measure/angle2_tol.py | 28 +++++++++--------- imagepy/tools/Measure/angle_tol.py | 36 ++++++++++++----------- imagepy/tools/Measure/area_tol.py | 42 ++++++++++++++------------- imagepy/tools/Measure/distance_tol.py | 27 ++++++++--------- 4 files changed, 70 insertions(+), 63 deletions(-) diff --git a/imagepy/tools/Measure/angle2_tol.py b/imagepy/tools/Measure/angle2_tol.py index d548570a..caf3e165 100644 --- a/imagepy/tools/Measure/angle2_tol.py +++ b/imagepy/tools/Measure/angle2_tol.py @@ -19,13 +19,13 @@ class Angle: def __init__(self, body=None): self.body = body if body!=None else [] self.buf = [] - + def addline(self): line = self.buf if len(line)!=2 or line[0] !=line[-1]: self.body.append(line) self.buf = [] - + def snap(self, x, y, lim): minl, idx = 1000, None for i in self.body: @@ -33,20 +33,22 @@ def snap(self, x, y, lim): d = (j[0]-x)**2+(j[1]-y)**2 if d < minl:minl,idx = d,(i, i.index(j)) return idx if minl**0.52: self.body.append(line) self.buf = [] - + def snap(self, x, y, lim): minl, idx = 1000, None for i in self.body: @@ -32,21 +32,23 @@ def snap(self, x, y, lim): d = (j[0]-x)**2+(j[1]-y)**2 if d < minl:minl,idx = d,(i, i.index(j)) return idx if minl**0.5 Date: Wed, 6 Jun 2018 10:23:03 +0800 Subject: [PATCH 02/10] add white top hat function to erect small bright noises --- imagepy/menus/Process/Binary/binary_plgs.py | 30 +++++++++++++++------ 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/imagepy/menus/Process/Binary/binary_plgs.py b/imagepy/menus/Process/Binary/binary_plgs.py index bff609b5..e4a28015 100644 --- a/imagepy/menus/Process/Binary/binary_plgs.py +++ b/imagepy/menus/Process/Binary/binary_plgs.py @@ -9,7 +9,21 @@ import scipy.ndimage as ndimg import numpy as np from imagepy.core.engine import Filter -from skimage.morphology import convex_hull_object +from skimage.morphology import convex_hull_object, white_tophat,black_tophat +from skimage.morphology import disk + +class WhiteTophat(Filter): + """Closing: derived from imagepy.core.engine.Filter """ + title = 'WhiteTophat' + note = ['8-bit', 'auto_msk', 'auto_snap','preview'] + para = {'disk_Size':3} + view = [(int, 'disk_Size', (1,100), 0, 'width', 'pix')] + + def run(self, ips, snap, img, para = None): + selem = disk(para['disk_Size']); + white_tophat(snap, selem, out=img) + + class Closing(Filter): """Closing: derived from imagepy.core.engine.Filter """ @@ -23,7 +37,7 @@ def run(self, ips, snap, img, para = None): strc = np.ones((para['h'], para['w']), dtype=np.uint8) ndimg.binary_closing(snap, strc, output=img) img *= 255 - + class Opening(Filter): """Opening: derived from imagepy.core.engine.Filter """ title = 'Binary Opening' @@ -36,7 +50,7 @@ def run(self, ips, snap, img, para = None): strc = np.ones((para['h'], para['w']), dtype=np.uint8) ndimg.binary_opening(snap, strc, output=img) img *= 255 - + class Dilation(Filter): """Dilation: derived from imagepy.core.engine.Filter """ title = 'Binary Dilation' @@ -49,7 +63,7 @@ def run(self, ips, snap, img, para = None): strc = np.ones((para['h'], para['w']), dtype=np.uint8) ndimg.binary_dilation(snap, strc, output=img) img *= 255 - + class Erosion(Filter): """Erosion: derived from imagepy.core.engine.Filter """ title = 'Binary Erosion' @@ -62,7 +76,7 @@ def run(self, ips, snap, img, para = None): strc = np.ones((para['h'], para['w']), dtype=np.uint8) ndimg.binary_erosion(snap, strc, output=img) img *= 255 - + class Outline(Filter): """Outline: derived from imagepy.core.engine.Filter """ title = 'Binary Outline' @@ -72,7 +86,7 @@ def run(self, ips, snap, img, para = None): ndimg.binary_dilation(snap, output=img) img *= 255 img -= snap - + class FillHoles(Filter): """FillHoles: derived from imagepy.core.engine.Filter """ title = 'Fill Holes' @@ -89,6 +103,6 @@ class Convex(Filter): #process def run(self, ips, snap, img, para = None): img[convex_hull_object(snap)] = 255 - -plgs = [Dilation, Erosion, '-', Closing, Opening, '-', Outline, FillHoles, Convex] \ No newline at end of file + +plgs = [Dilation, Erosion, '-', Closing, Opening, '-', WhiteTophat,Outline, FillHoles, Convex] From 2336c1bb7771d42bd77b104a43aa01727dfadcd2 Mon Sep 17 00:00:00 2001 From: gaojian Date: Thu, 7 Jun 2018 14:40:46 +0800 Subject: [PATCH 03/10] solve the surf point display error. --- imagepy/core/loader/loader.py | 51 +++++++++++--------- imagepy/menus/Plugins/Manager/plgtree_wgt.py | 44 ++++++++--------- imagepy/menus/Plugins/Surf/surf_plg.py | 13 +++-- imagepy/menus/Process/Binary/binary_plgs.py | 17 +++++-- imagepy/ui/mainframe.py | 20 ++++---- 5 files changed, 83 insertions(+), 62 deletions(-) diff --git a/imagepy/core/loader/loader.py b/imagepy/core/loader/loader.py index 22afe7a3..75c8d8d0 100644 --- a/imagepy/core/loader/loader.py +++ b/imagepy/core/loader/loader.py @@ -21,9 +21,9 @@ def getpath(root, path): def extend_plugins(path, lst, err): rst = [] for i in lst: - if isinstance(i, tuple) or i=='-': + if isinstance(i, tuple) or i=='-': rst.append(i) - + elif i[-3:] == '.mc': pt = os.path.join(root_dir,path) f = open(pt+'/'+i, 'r', 'utf-8') @@ -46,7 +46,7 @@ def extend_plugins(path, lst, err): rst.extend([j if j=='-' else Widget(j) for j in plg.wgts]) for p in plg.wgts: if not isinstance(p, str):WidgetsManager.add(p) - else: + else: rst.append(Widget(plg.Plugin)) WidgetsManager.add(plg.Plugin) except Exception as e: @@ -60,14 +60,14 @@ def extend_plugins(path, lst, err): rst.extend([j for j in plg.plgs]) for p in plg.plgs: if not isinstance(p, str):PluginsManager.add(p) - else: + else: rst.append(plg.Plugin) PluginsManager.add(plg.Plugin) except Exception as e: err.append((path, i, sys.exc_info()[1])) return rst - + def sort_plugins(catlog, lst): rst = [] for i in catlog: @@ -78,7 +78,7 @@ def sort_plugins(catlog, lst): rst.append(j) rst.extend(lst) return rst - + def build_plugins(path, err=False): root = err in (True, False) if root: sta, err = err, [] @@ -86,6 +86,8 @@ def build_plugins(path, err=False): cont = os.listdir(os.path.join(root_dir, path)) for i in cont: subp = os.path.join(path,i) + print('build_plugins in :{}'.format(subp)) + if os.path.isdir(os.path.join(root_dir, subp)): sub = build_plugins(subp, err) if len(sub)!=0:subtree.append(sub) @@ -94,20 +96,23 @@ def build_plugins(path, err=False): elif i[-3:] in ('.mc', '.md'): subtree.append(i) if len(subtree)==0:return [] - + rpath = path.replace('/', '.').replace('\\','.') #rpath = rpath[rpath.index('imagepy.'):] pg = __import__('imagepy.'+rpath,'','',['']) pg.title = os.path.basename(path) if hasattr(pg, 'catlog'): subtree = sort_plugins(pg.catlog, subtree) + + print('path:{}'.format(path)) + subtree = extend_plugins(path, subtree, err) - + if root and sta and len(err)>0: IPy.write('some plugin may be not loaded, but not affect otheres!') for i in err: IPy.write('>>> %-50s%-20s%s'%i) - return (pg, subtree) - + return (pg, subtree) + def extend_tools(path, lst, err): rst = [] for i in lst: @@ -116,23 +121,23 @@ def extend_tools(path, lst, err): f = open(pt+'/'+i) cmds = f.readlines() f.close() - rst.append((Macros(i[:-3], [getpath(pt, i) for i in cmds]), + rst.append((Macros(i[:-3], [getpath(pt, i) for i in cmds]), os.path.join(root_dir, path)+'/'+i[:-3]+'.gif')) else: try: rpath = path.replace('/', '.').replace('\\','.') #rpath = rpath[rpath.index('imagepy.'):] - + plg = __import__('imagepy.'+rpath+'.'+i,'','',['']) - if hasattr(plg, 'plgs'): + if hasattr(plg, 'plgs'): for i,j in plg.plgs: rst.append((i, path+'/'+j)) - else: rst.append((plg.Plugin, + else: rst.append((plg.Plugin, os.path.join(root_dir, path)+'/'+i.split('_')[0]+'.gif')) except Exception as e: err.append((path, i, sys.exc_info()[1])) for i in rst:ToolsManager.add(i[0]) return rst - + def sort_tools(catlog, lst): rst = [] for i in catlog: @@ -143,7 +148,7 @@ def sort_tools(catlog, lst): rst.append(j) rst.extend(lst) return rst - + def build_tools(path, err=False): root = err in (True, False) if root: sta, err = err, [] @@ -167,12 +172,12 @@ def build_tools(path, err=False): pg.title = os.path.basename(path) if hasattr(pg, 'catlog'): subtree = sort_tools(pg.catlog, subtree) - if not root:subtree = extend_tools(path, subtree, err) - elif sta and len(err)>0: + if not root:subtree = extend_tools(path, subtree, err) + elif sta and len(err)>0: IPy.write('tools not loaded:') for i in err: IPy.write('>>> %-50s%-20s%s'%i) return (pg, subtree) - + def extend_widgets(path, lst, err): rst = [] for i in lst: @@ -185,7 +190,7 @@ def extend_widgets(path, lst, err): err.append((path, i, sys.exc_info()[1])) for i in rst:WidgetsManager.add(i) return rst - + def sort_widgets(catlog, lst): rst = [] for i in catlog: @@ -196,7 +201,7 @@ def sort_widgets(catlog, lst): rst.append(j) rst.extend(lst) return rst - + def build_widgets(path, err=False): root = err in (True, False) if root: sta, err = err, [] @@ -219,8 +224,8 @@ def build_widgets(path, err=False): if hasattr(pg, 'catlog'): subtree = sort_widgets(pg.catlog, subtree) if not root: - subtree = extend_widgets(path, subtree, err) - elif sta and len(err)>0: + subtree = extend_widgets(path, subtree, err) + elif sta and len(err)>0: IPy.write('widgets not loaded:') for i in err: IPy.write('>>> %-50s%-20s%s'%i) return (pg, subtree) diff --git a/imagepy/menus/Plugins/Manager/plgtree_wgt.py b/imagepy/menus/Plugins/Manager/plgtree_wgt.py index df8c1147..b604258e 100644 --- a/imagepy/menus/Plugins/Manager/plgtree_wgt.py +++ b/imagepy/menus/Plugins/Manager/plgtree_wgt.py @@ -15,51 +15,51 @@ class Plugin ( wx.Panel ): title = 'Plugin Tree View' single = None def __init__( self, parent ): - wx.Panel.__init__ ( self, parent, id = wx.ID_ANY, - pos = wx.DefaultPosition, size = wx.Size( 500,300 ), + wx.Panel.__init__ ( self, parent, id = wx.ID_ANY, + pos = wx.DefaultPosition, size = wx.Size( 500,300 ), style = wx.DEFAULT_FRAME_STYLE|wx.TAB_TRAVERSAL ) bSizer1 = wx.BoxSizer( wx.HORIZONTAL ) - - self.tre_plugins = wx.TreeCtrl( self, wx.ID_ANY, wx.DefaultPosition, + + self.tre_plugins = wx.TreeCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TR_DEFAULT_STYLE ) self.tre_plugins.SetMinSize( wx.Size( 200,-1 ) ) - + bSizer1.Add( self.tre_plugins, 0, wx.ALL|wx.EXPAND, 5 ) bSizer3 = wx.BoxSizer( wx.VERTICAL ) bSizer4 = wx.BoxSizer( wx.HORIZONTAL ) - + self.m_staticText2 = wx.StaticText( self, wx.ID_ANY, "Plugin Infomation:", wx.DefaultPosition, wx.DefaultSize, 0 ) self.m_staticText2.Wrap( -1 ) bSizer4.Add( self.m_staticText2, 0, wx.ALL, 5 ) - - self.m_staticText3 = wx.StaticText( self, wx.ID_ANY, "[SourceCode]", + + self.m_staticText3 = wx.StaticText( self, wx.ID_ANY, "[SourceCode]", wx.DefaultPosition, wx.DefaultSize, 0 ) self.m_staticText3.Wrap( -1 ) self.m_staticText3.SetForegroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_HIGHLIGHT ) ) - + bSizer4.Add( self.m_staticText3, 0, wx.ALL, 5 ) bSizer3.Add( bSizer4, 0, wx.EXPAND, 5 ) - - self.txt_info = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, + + self.txt_info = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_MULTILINE ) bSizer3.Add( self.txt_info, 1, wx.ALL|wx.EXPAND, 5 ) - - + + bSizer1.Add( bSizer3, 1, wx.EXPAND, 5 ) self.SetSizer( bSizer1 ) self.Layout() - + self.Centre( wx.BOTH ) - + # Connect Events self.tre_plugins.Bind( wx.EVT_TREE_ITEM_ACTIVATED, self.on_run ) self.tre_plugins.Bind( wx.EVT_TREE_SEL_CHANGED, self.on_select ) self.m_staticText3.Bind( wx.EVT_LEFT_DOWN, self.on_source ) self.plg = None self.load() - + def addnode(self, parent, data): for i in data: if i=='-':continue @@ -70,17 +70,17 @@ def addnode(self, parent, data): else: item = self.tre_plugins.AppendItem(parent, i.title) self.tre_plugins.SetItemData(item, i) - + def load(self): data = loader.build_plugins('menus') root = self.tre_plugins.AddRoot('Plugins') self.addnode(root, data[1]) - + # Virtual event handlers, overide them in your derived class def on_run( self, event ): plg = self.tre_plugins.GetItemPyData(event.GetItem()) if hasattr(plg, 'start'):plg().start() - + def on_select( self, event ): plg = self.tre_plugins.GetItemData(event.GetItem()) print(type(plg)) @@ -88,10 +88,10 @@ def on_select( self, event ): self.plg = plg if plg.__doc__!=None: self.txt_info.SetValue(plg.__doc__) - elif hasattr(plg, '__module__'): + elif hasattr(plg, '__module__'): self.txt_info.SetValue("plugin at {}".format(plg.__module__)) else: self.txt_info.SetValue("package at {}".format(plg.__name__)) - + def on_source(self, event): ## TODO: should it be absolute path ? filename = self.plg.__module__.replace('.','/')+'.py' @@ -99,4 +99,4 @@ def on_source(self, event): root = os.path.split(root_dir)[0] filename=os.path.join(root,filename) #print(filename) - EditorFrame(filename=filename).Show() \ No newline at end of file + EditorFrame(filename=filename).Show() diff --git a/imagepy/menus/Plugins/Surf/surf_plg.py b/imagepy/menus/Plugins/Surf/surf_plg.py index 5582a042..532b7242 100644 --- a/imagepy/menus/Plugins/Surf/surf_plg.py +++ b/imagepy/menus/Plugins/Surf/surf_plg.py @@ -13,7 +13,8 @@ def __init__(self, feats): def draw(self, dc, f, **key): for i in self.feats: - dc.DrawCircle(f(i.pt), 3) + # print('i.pt:{},{}'.format(type(f(i.pt)),i.pt)) + dc.DrawCircle(f(i.pt[0], i.pt[1]), 3) class Surf(Filter): title = 'Surf Detect' @@ -125,18 +126,24 @@ def filter_matches(self, kp1, kp2, matches, ratio = 0.75): #process def run(self, ips, imgs, para = None): + ips1 = ImageManager.get(para['img1']) ips2 = ImageManager.get(para['img2']) detector = CVSURF(hessianThreshold=para['thr'], nOctaves=para['oct'], nOctaveLayers=para['int'], upright=para['upright'],extended=para['ext']) + kps1, feats1 = detector.detectAndCompute(ips1.img, None) kps2, feats2 = detector.detectAndCompute(ips2.img, None) + dim, std = {'None':0, 'Affine':6, 'Homo':8}[para['trans']], para['std']/100.0 + style = para['style']=='Blue/Yellow' + idx, msk, m = Matcher(dim, std).filter(kps1,feats1,kps2,feats2) picker1 = Pick(kps1, kps2, idx, msk, ips1, ips2, True, style) picker2 = Pick(kps1, kps2, idx, msk, ips1, ips2, False, style) + ips1.tool, ips1.mark = picker1, picker1 ips2.tool, ips2.mark = picker2, picker2 if para['log']:self.log(kps1, kps2, msk, m, dim) @@ -153,7 +160,7 @@ def log(self, pts1, pts2, msk, v, dim): sb.append('%15.4f%15.4f%15.4f'%tuple(v.A1[3:6])) row = [0,0,1] if dim==6 else list(v[-2:])+[1] sb.append('%15.4f%15.4f%15.4f'%tuple(row)) - + cont = '\n'.join(sb) IPy.write(cont, 'Surf') @@ -174,4 +181,4 @@ def log(self, pts1, pts2, msk, v, dim): lt = np.array(sorted(lt)) matcher = Matcher(8, 3) - idx, msk, m = matcher.filter(pts,des,pts,des) \ No newline at end of file + idx, msk, m = matcher.filter(pts,des,pts,des) diff --git a/imagepy/menus/Process/Binary/binary_plgs.py b/imagepy/menus/Process/Binary/binary_plgs.py index e4a28015..157840b2 100644 --- a/imagepy/menus/Process/Binary/binary_plgs.py +++ b/imagepy/menus/Process/Binary/binary_plgs.py @@ -12,9 +12,9 @@ from skimage.morphology import convex_hull_object, white_tophat,black_tophat from skimage.morphology import disk -class WhiteTophat(Filter): - """Closing: derived from imagepy.core.engine.Filter """ - title = 'WhiteTophat' +class WhiteTopHat(Filter): + """WhiteTopHat: derived from imagepy.core.engine.Filter """ + title = 'WhiteTopHat' note = ['8-bit', 'auto_msk', 'auto_snap','preview'] para = {'disk_Size':3} view = [(int, 'disk_Size', (1,100), 0, 'width', 'pix')] @@ -23,7 +23,16 @@ def run(self, ips, snap, img, para = None): selem = disk(para['disk_Size']); white_tophat(snap, selem, out=img) +class BlackDownHat(Filter): + """BlackDownHat: derived from imagepy.core.engine.Filter """ + title = 'BlackDownHat' + note = ['8-bit', 'auto_msk', 'auto_snap','preview'] + para = {'disk_Size':3} + view = [(int, 'disk_Size', (1,100), 0, 'width', 'pix')] + def run(self, ips, snap, img, para = None): + selem = disk(para['disk_Size']); + black_tophat(snap, selem, out=img) class Closing(Filter): """Closing: derived from imagepy.core.engine.Filter """ @@ -105,4 +114,4 @@ def run(self, ips, snap, img, para = None): img[convex_hull_object(snap)] = 255 -plgs = [Dilation, Erosion, '-', Closing, Opening, '-', WhiteTophat,Outline, FillHoles, Convex] +plgs = [Dilation, Erosion, '-', Closing, Opening, '-', WhiteTopHat, BlackDownHat, '-',Outline, FillHoles, Convex] diff --git a/imagepy/ui/mainframe.py b/imagepy/ui/mainframe.py index 5fb74410..73656c61 100644 --- a/imagepy/ui/mainframe.py +++ b/imagepy/ui/mainframe.py @@ -24,8 +24,8 @@ def OnDropFiles(self, x, y, path): class ImagePy(wx.Frame): def __init__( self, parent ): - wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = 'ImagePy', - size = wx.Size(-1,-1), pos = wx.DefaultPosition, + wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = 'ImagePy', + size = wx.Size(-1,-1), pos = wx.DefaultPosition, style = wx.RESIZE_BORDER|wx.DEFAULT_FRAME_STYLE|wx.TAB_TRAVERSAL ) self.auimgr = aui.AuiManager() @@ -37,7 +37,7 @@ def __init__( self, parent ): self.SetIcon(wx.Icon(logopath, wx.BITMAP_TYPE_ICO)) IPy.curapp = self self.SetSizeHints( wx.Size(900,700) if IPy.uimode() == 'ipy' else wx.Size( 600,-1 )) - + self.menubar = pluginloader.buildMenuBarByPath(self, 'menus', 'plugins', None, True) self.SetMenuBar( self.menubar ) @@ -46,7 +46,7 @@ def __init__( self, parent ): #sizer = wx.BoxSizer(wx.VERTICAL) self.toolbar = toolsloader.build_tools(self, 'tools', 'plugins', None, True) - + print(IPy.uimode()) if IPy.uimode()=='ipy': self.load_aui() else: self.load_ijui() @@ -94,7 +94,7 @@ def load_aui(self): self.widgets = widgetsloader.build_widgets(self, 'widgets', 'plugins') self.auimgr.AddPane( self.widgets, wx.aui.AuiPaneInfo() .Right().Caption('Widgets') .PinButton( True ) .Dock().Resizable().FloatingSize( wx.DefaultSize ).MinSize( wx.Size( 266,-1 ) ) .Layer( 10 ) ) - + self.canvasnb = CanvasNoteBook( self) self.auimgr.AddPane( self.canvasnb, wx.aui.AuiPaneInfo() .Center() .CaptionVisible( False ).PinButton( True ).Dock() .PaneBorder( False ).Resizable().FloatingSize( wx.DefaultSize ). BottomDockable( True ).TopDockable( False ) @@ -102,19 +102,19 @@ def load_aui(self): self.tablenb = TableNoteBook( self) self.auimgr.AddPane( self.tablenb, wx.aui.AuiPaneInfo() .Bottom() .CaptionVisible( True ).PinButton( True ).Dock().Hide() - .MaximizeButton( True ).Resizable().FloatingSize((800, 600)).BestSize(( 120,120 )). Caption('Tables') . + .MaximizeButton( True ).Resizable().FloatingSize((800, 600)).BestSize(( 120,120 )). Caption('Tables') . BottomDockable( True ).TopDockable( False ).LeftDockable( True ).RightDockable( True ) ) - #self.canvasnb.Bind( wx.aui.EVT_AUINOTEBOOK_PAGE_CHANGED, self.on_pagevalid) + #self.canvasnb.Bind( wx.aui.EVT_AUINOTEBOOK_PAGE_CHANGED, self.on_pagevalid) def load_ijui(self): self.auimgr.AddPane(self.toolbar, wx.aui.AuiPaneInfo() .Top() .CaptionVisible( False ).PinButton( True ) - .PaneBorder( False ).Dock().Resizable().FloatingSize( wx.DefaultSize ).DockFixed( True ) + .PaneBorder( False ).Dock().Resizable().FloatingSize( wx.DefaultSize ).DockFixed( True ) .BottomDockable( False ).TopDockable( False ).LeftDockable( False ).RightDockable( False ) .MinSize(wx.Size(-1, 32)). Layer( 10 ) ) self.widgets = widgetsloader.build_widgets(self, 'widgets', 'plugins') self.auimgr.AddPane( self.widgets, wx.aui.AuiPaneInfo() .Right().Caption('Widgets') .PinButton( True ) .Float().Resizable().FloatingSize( wx.DefaultSize ).MinSize( wx.Size( 266,-1 ) ).Hide() .Layer( 10 ) ) - + def load_dev(self): return self.devpan = wx.aui.AuiNotebook( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.aui.AUI_NB_DEFAULT_STYLE ) @@ -157,7 +157,7 @@ def hold(self): else: v = max([(i[0]+1)*100.0/i[1] for i in arr]) wx.CallAfter(self.set_progress, v) - except: + except: pass def set_info(self, value): self.txt_info.SetLabel(value) From 744484dc39f567f7321049a182f489e968f3e0a5 Mon Sep 17 00:00:00 2001 From: gaojian Date: Fri, 8 Jun 2018 19:07:17 +0800 Subject: [PATCH 04/10] Test Project for Image Stitching Add SURF and ORB test code for image stitching --- imagepy/T1.jpg | Bin 0 -> 42474 bytes imagepy/T2.jpg | Bin 0 -> 33221 bytes imagepy/menus/Plugins/Surf/Orb Demo.mc | 7 + imagepy/menus/Plugins/Surf/Surf Demo.mc | 7 +- imagepy/menus/Plugins/Surf/__init__.py | 2 +- imagepy/menus/Plugins/Surf/orb_plg.py | 240 ++++++++++++++++++++++++ imagepy/menus/Plugins/Surf/surf_plg.py | 96 +++++++--- 7 files changed, 326 insertions(+), 26 deletions(-) create mode 100644 imagepy/T1.jpg create mode 100644 imagepy/T2.jpg create mode 100644 imagepy/menus/Plugins/Surf/Orb Demo.mc create mode 100644 imagepy/menus/Plugins/Surf/orb_plg.py diff --git a/imagepy/T1.jpg b/imagepy/T1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5f34d74fa2fd74cbc49d4e7be1faca7604133386 GIT binary patch literal 42474 zcmb@sbyQqS(>JWGstPaz z0DuI5VjKV{9Kl%t68HlE;6m^}6i`?p`$s8K9t(GOXD}83 zxVUtbfV> zcL3Yk#>)zByat@z(%Q?)1&%)g0F=+l-P0QY5DwsUT0d`hIEKNs1n@lJ3c@knZ*2EB z7Wj=V|Hi-d#PihAl7{n`!@C#F!ot%I05E>*MdfQ{2bY5p4==-UwRUiYvlha!n3cVS zHN5jz;XJG^E^cs4iU!Awmj6SJ-T%NA7MB0Yw6L)I7yoSwye7P183!+SXN!P8AO8Q$ zT%3L2{q-l&!gDlxPX#Ub8}E0&Ik_qS#&F$1L#~!;>Trw$$1@Iai~fVR?R^w<;rKWI zwUw7Nye|O)_>@DlwvbVVV=_3V@p00X{0|nh@Pwm3{9vO%dmVi^hRX-Ldb(--M?N^v z)mcpiPX8?@-rh#>A6#VPC94gm!*u`$+B(Q7!ZCad!7UEniaKx%*8x1_>!7Cw#~5(D z%`&}1eYGv_92O>CjwDB_dt=k`ctZigu;TWzDB;3YT_dj|2-3|C>$&$KzhU+W*u&_J&V3I2~>a6mYaq`aO@~?LzU~y)}N@2-git1*ibh zfDAwd_`n}azzuK&9L5it58$8vE|CK)08hXUumRZqL;0tM{+|+8c&-fu0*8ProX6|m zdQyK%Z2(_5{r+F--*wpl+drkge`+`Z+wc}#pd?T+s4!Foo=dlT!9R8)L0;_PTfA@+FyvP2Bbx?Y^*1S+Ds3=^2xb9E_C<(v@<$+rw z2^ELi#tWzZx7YsZ;eYz`x9&dwXpPK)3`QnImOy6w-!@_}WAOf~&2QVN{)g3n>t*?` zCDQ-)$Nz5azaP(eB&Es!zD=im50?aTe?H@kmZ z)B4lbPH-C>{^8-M;@IW5`w#y=mRi6|f4Bd8Y~X9g+s5A;J|6*TH}?Qf2RnOjDoOZm zZ9}E#YQ@e<#l^u1-$j4#{l9qt;7se!o(>|I_%F@E8GgQS0RW2Be`#*;{h~GB|sg}0Sp0C zct6_%E^w{=fFK|Yhz8<-WFQ^L1`2>;paQ4?J^;-?JJ1ab0HeTHU=~<}&y_7;A2*!Tv4wa-f+4Yx3`h~A7SaYE>1oI+ z>Faj6@j32&M4PlP3=dgHK9;^=51)GFzz%CFG5s495 z5JeHy5iJnC5u*{a5UUY85vLHh5U-I?ktmV4kmQhzkX(_%kus3pA$1^4A?+aDB4Z&x zK^8(*N47=|Ku$s~L2g5yK;A;WMZrd4KoLdJL2*P0LwSQzi_(v>gmQ+8ib{hjh^mR| zfEtSW2K7Da5b7H04H^y_GnzD-37QXDB3e0G7usKFXXxnY^ym`kM(Ez?iRhK+z340G z*BH1M>==p|))*leIT%eCQy52>$e2$sB{5Ag12Ho&8!#s@53rE2=&_`+EU<#HaBZT=h2YZSO5Gc$oxB!c3w`;zyEC(ob?gibpC; zYDF4L`hj$T3{1vMrbYIgte9+!?1G$vT%O#WJd3=S{D6XhLW07P;x$DF#V#c-r5L3H zMO!6#Sr4D|Z+G4##!KNyG@6d3{;su-3S(HX@VJs67^ zXP9720!&U!c}!oK!OXnO_RKlV6D(jBJ{AX-JeEmT1Xe*-SJoodc{VgQNj6`$Dz-It z0(KSlaQ0^Qqo;IFjh?1F9en!0!OP*yQNppviNmSH8P3_pdCJAiWyO`lHN%b0Eyo?g z-NJps!@^_3lh5;w7nfI+H-@)|_nwcR&y%l~Z8{=!4KtVK!kW;djD6L>NSDL`p<9L}^4VMBj?8icyJ~ixrBkic^bQh!=^k zOVCPKOO#3MNHR$}N>)oANpVPdN;OJdNefB8knWO!$jHmY%8bk6$m+^w%dW`L$l1wN z$sNn{%0HLyQb15pR!C8pS0q=oQmj-wR^nHBq1300rmU@;t-P+ntm3KCrV3V7QcY7` zQhTE2s@ALys4J?csV{3VXn1IR(u8ShXy#~cYjJ7?X$@-QYMW`l)4tS^(n-=;)MeE5 z(e2U0)HBhm)VtD`(NEQ1HF#?9!eG>p#L&U8#R$G?JI zWBa@Mj|DIXyb9O}lnX3<4t;L*yeEh{C@g60h2)F3!JuHv;GPiLkjRj&Q2Ee`FqAN- zurJ{p;i=*05&98rk>rt~ksDF+QB~0x(Vo%sF~TtgFQG3TUXH)wewFp=A=Wx}B#t94 zBknHVDt;t^GvQ6bW1?;1coJVyeljfCHF+*YJf$KPJ2fzMBTY4}={42s*w^RjX6eHj zJQ)R4b+h@<_NafV!lI6b2y~?xBo648SuPq=ih%5M2=ur6W zt^C`jBKo3?Vpy?n@pg$`$zZ8KX=NEnSzOtDxoi1qg;qshC4Xh*JF<64Rp2V0s@-an z>aR62H7&JlwQuVP>f+u5?|t6y)tlGPeNg_;(;(1L`|-)g+(z8S*d|a@K+|!vee-II zVasHzQfp6}a9iUi_D|*Q)a^N+@joYZpmaobJaz_lUUa#49dz4uZ}gb;EcP1o&h%;a zjrS|}4-Lo-^bSf6b`FUSwGRsow~h#mG>`I)HjVL)HGbj!(m2jH-Za5K(ehRBYulvA zWXF{FRQI&Z^uUbb%;>Dz?Bty8+_!m?`PFaM-+uh%{MX5X@51e3=n}$G>@wDJ#tPX= z(RZfrb*p@xvtJo55fD zzjki@?hxL7Kl#xS|GnA`}dzKf>$I1Up zAXczRXiWrNR6xvJ{FOw#!1T?)0A6-k?6me(eFiL88H%q1EAuk%7_PvF2nzIshcPoYmuPv^|o&hpM7 z%+1eNeDnQFVga@=2%lfPOFPRcE5a)q-xF4aSNGQn*0nYu8+Dtyn>Smpw^_G4ceHjk ze?;v*-QC=)*mphPKDaonJF@zT{j>2{<9Pcd=#=nu;4JCf^}^!P7Cw(QfA!z9KR*5$ ze_GIcup@*E@&j6r5DGIu6h&e~WF-~z+31`V9srS-fWe(+#&EIu>IWFu8|oOz7>k?8Ju@=(GfOvrZ_#5pV%2ZmXp?LE+)m$~!vW3V!11e7 zn{%a0j%$)zxVxu^iKm1YrPr5@UUtjOg?ic=yKMT+YQU-kk z+dyEDBq$4X0wECg6t;_4ie!)c6!`{a1hoLo4_ym`3ljtL66-JQE}Tl-G`tu1b_804 zqC`x@_{4W4KS-y@I>>7&iYarbGN@nEB+*9G`986rmt~-0xMmzfkfCU!^h9Y@`HhOYDxzwyT7kHufAj3KX{edBIgL#U&K6X4Y49OfeJ z3U%#wi*lEAzxJr|G=!homEP*!=RR+IMSNHMLjB48KL?lx5(F+huL_EIVGzt1d>PUk zniggkE)&5VNfd<|4Md;EY`pyPsxCG@&LW;C0Wo1A@qJQkvQvtDDo*M^nsM5}>%er$ z45`eQSxDKjIh%RJ`CJ8pg@SK+i=GxUmQa@xmtmKqR3KMk!e^3bwRufUZB5;gbaGXT(k0sul30MI}O03AU998(K`?sgP;ue$a-NQm;@{o)`N(EsDzk|_zj5~$r-5?84+0@xdQnY ziYm%mlq*y<)Cx2xnk8B<`V;h5=tmej80~ORCj|?@^1#}}HpHI5k;D0nD}dXCCxq9F zuZ6!s;6n%_%pjsCY9Ur5{z3ARl#_IWtbp8-f`#Ima)2tG+M0%k7Dl^B*YG5X-kCv} zk%bAJ>6m$zA2~8PKg<6$IO-n1Rh%m22y|$pcR;a#|`d51fZuN zC6FyVB2Wa60<43fU+15b}$Vw z$FbzFy0FEtJ8`6OMsT%pm*Jl2FZ?6|W`a>d4|bkY(s39?P{6bdCu z49aDy2I@o_2U;mQBD(!2pXrksY#F7Po-pIEAh9B_A+cjT#o@r_MB=>R`oTTVGs4@= z*Udj7uqb#cj3L4ysww6m?k(XdX)k3UEiWS|%OfWsFRx&x7_9VGc~Iq8jZ|G$!wG(- zz1GguN!NX?SE%1zeBCsMyqP<_jUd6;g<6kC{CiSL-rx~P6 zX9{Nt=LqKs7BIbq77dj6mXVf!uC%PeuU@Y0e?RfzqLH&XsCBd*vqPrKv!|?YcTj5h z!x-my?$_n1^O=*moo^cpKbFqFgVsR9-254T zFnsl6!=p`XfEb_&kB7bhGJyBM7_bLIhR23fL7t#Dpf1oJm;fvT_6C=M7a-UWMMxy1 z3vv&S%mhN)pmzw82(bwBFeX?SY#C7uu?PtX$qi`+Sr9o71q_dc%%aMmenJyO>qgf= z-^K{Xq{i&RGQqmUPR3!w`GV_;hmKc)FNMEE5I{&lI8KyEY)K+T%1nkwc1ylY(L$L@ zt zlWT`;ALfYSRP3Va2DmqSx_MLh^!oV(SU*1sE(r|?n+v~-1VB1Q&Zz{7*b5Zij3-}8^7l{{lmCBTjRA^MrROwWIt(B{5t!Mm@`4Oq{ zMbll&t2X*iUq6R;@^$U@iv57CS)so2mB*q54s<6aC9X1GvGMyB;nNOOy=zB{N07x#p0#?<>i&{)$X;)^~85-s{}gJU|`{9;zQfkGhZVeviMGEjK(~2~Qw2DR}sC z19sm$i0pLF6@$t6k@$tSG?rbapK$r91-~0S#{{1~u?)Tqlgn2gf_xpdQ$1VUH z2}A)3gMerNFg6H+4SMW@A1?q10UmsWr+=71Ubvo^CXea3Ej`1LQ3wc$h)JH%GcYnS z^YHTV3kV8H%gD;fD<~>y>*(s~!^5st);6|w_709--afv5{sDmzkx|hxFJHx`roB$j z$b6HPT~u6BT2@|B`R+r*$Hu1Sme#hO-oE~U!J*-isp*;7x%qE@Ev&6?Y;JAu{Mg+) zIXyeSxV*Z)`Ssf_5d19tC;e^N|FR1kZWkB|g+LL1+XVvq!5;`V6oHx(h9jwoXyJiN z!xe^vCzbNHz8jgATk9C#(sL4pfR1PF$;ofi{#f>ZXIS|EC(Hgd?7wy`!UN;rKM4W` zLl7Vk2m%ZNo?uAuh$;+*gaTjW@PzW8g!U(4{7#tvrboCC5L^Zn3PptfW1%9WV*S5P zkIV223i0CtfCl$?!PpRN_!)Iu2L8NvV2zu5V$hVyu|(x>ZHOOARXKsUYcxs0`HbFX z3&>8FcML-&cU-_1b?ovi6TGNGC8mw_WTeG6XHvo6&KtI-$03B&NRyBqI!=#s%BNQQ z`IVI@>$9*1RE`;#!o0MqRRm0BO<(C#a+GbdYn6DIqg_;q8EWP?+RMEY*wwB!#B1%A z#RDHfXsWACJm#uP3bnu_BCCp~ba87-QLukeF8gl36ouyOF*9rw`U{qSlL`pioxifW75y$mOJK0lKX zW#WTHx&l=%{ntOt1fJ-vy9)5;NZ7MrT+oCHp%gxG4b@Hf8K3B2V6~m(K0-#~l6!51 zdx02>y&^fZz)X!=oGH$E*l%<|>n#9bC^d+`f_<0~9)Ni0VQ-qSgD4tdv2s04fx4w# zJEa;glLhznBGwyhD^o{>WwY??90dob@n6s_fynnUHqg0}3~U}8^sBe^o1>DvwSQ@m ze~~oBzm>X^JPrvJ0nx3n8q|t^EQ_66STUjG()Tzs-+4JWrtUWIxzSc-O&$Lv=8*c9 z{SoNdnxj9-s9PV6b8=z+dY`GTfL|<4lHp0}zcsX zsGAEoGgV$dS92y!uja_Sy*Y7jN;ZQ@M~EvoT8|j|LY5}u-R3|>ODFO5S+5u{l~}W` zTo1;<)~^yy_N_SJlH+pcl2i3p@0m#aHJ=ou>Sp`PEED#jVWq@H#HRY=HDV^(n*;;F zY|VnKnY9mv?>5W66icbGzreoG#ed^^Bpqti#T#Jrtkssd%*yL{f+jvTDe#x(=NByR zb_5^4-q>BbUMIP{_0MNf$9Ez;mf@>*&O{v5g<7b{V98Gx)_wadbh+}dz*42CXJ4u@ zvge5;uJqtR)6zEWrgx711@~%8O_=0%^wR&nWFV==(BDftYoNgYyU?fTM0S8w*~%Uu zJKKHc6(RV#I=KFcfl7oa=el~5IPx6>%9kqt*u~^(0tTFV%v{Pev4SXnv7B3Dv+L15 zy#@8~AdyGF+Trr!6$WA!k-&`vBJqVNf|70S;Ih5PP?g1d!CHNZ&vUK^87_{KtUJzm zDZ|_B+=Bw-RYhoy2Q1l0x=|`1%{}Hw^$Gj{N?Q6fsa8wcaM8V0AYayw#rnvoJ%*+< zc5@_eP^p>Zz1XY_0UBrjvqa5&2RwV`N^)jNlKP>F4IkPX0|`ugenvA^?MM@ycUeEb zi19jm&Bev`DN|seE9sXiwJs0st23o?%5$_Tm2e2cN)zS1!UjJZo+9IIhD9UcE2fQz zaor%P^Ol6pV9V!HMC#5G-6T1x$$GvTqM~4yA@zr))d{Y@QCo3Hqcf5h5S!&v;hw;k zEL(`G|7sg&w(>sts;H0j-F~7#dD+%883QIqJp&-1KjsrrvsWl3Jk`v`-3=#aHb zv~PWDS$E{<3;^OI7CH#8Pd1*C`e*hF?L_;ap4b&Rw0)>)cglK_Es%+Gq$5rzsce%& z_92lo`_;(E_BC4TT=nkCup9igaL@l~-g)X_NR}ag9!t&2;KB0R{wzLFpLU}&Hk+_# z0{z=}|8T+WmOQIiVJB;@ky95kK-jKzy?tU;Jcjo>W|uSX*4TU0jQE?d*Ds0>PTSrk zwY^h7;qtG`6n8Yk_HWu5Y@>)+AGQ8CefJ3P5wSko%r^iso@RZ8VNzK|n)sa;X?bmGNBZpt(1#6#JXv-GL;+wG+`J`n@M+&K- z`SKxw$B#f{*I*TBzl+qI?l6il!>%emfC*< zg#)qCoy4W!mF4wT<#+#Yan_ynGesYb*|vgv(VC=|T6G@*quUa;^R88tpg9zdh}Str zbO(vTs|5p;gm(tl4b|U_Wt*kn$MStjl#pzEBd0$sC6yY~dqxqwTUXS<|`}3K+iL=5pQ}viGG7BvSuyC zVNq4z@01~F;#xSCmi<(G_tG*lxHa<(iCqisI9-vVI2{G&~&PbpstgP51JBA z-$|CP^m}J=4E)@BZ<(m$sz)w~NDFHb4e41yo%(38PHr@^czCG?USP?}B}hOyBr*lj zdK_lwbQ78(x0Bk7D3d?r74MOhmcIX-z+NolKxO=wXLh~3d+~gI`FWon{hIbrw7kXBjAG_B zU>t(pY3h`4%y3A1D|AhA`)r14);y{);BKDa_&w`z`jP|?)C-FlTOP`%FR0fks&RQ? zcE{Ox61zVr^av#KvWVjL$ep5d;u8cQWtAxE6bER8!!RZ@d_f-BBlmgjnCmYeXrK0} z>JjmL;nti^B{imdtxP-UUBi(zW%T{`$z+Fqw^Yt)8v z4*A!i5>DD;v?KH+bJ6{Rubs|6qr{L}rkwA@OZNB!eIuZHmn&8Xsj>=2PgijBqlPK9 ze=xszy*NoC;*C!s>+}ei+U?UylDf-+kzSi)G~R?&J_6lJJPGkU1xRX?h%m#M_{&uk zzk}Xe!ytw0@)O-V;SJ=%=csW3ThT<8*srZO(U;W_@vAG9=Igi4ig(1BUND|!PNhx2 zkDSS!N8p3Q88`Ri_ax`J3JaY&ME#vb< zXm}^%(}UzrUoT&7*B=T!?O8T9IFap0DVK0PY(!G+&+Aj)DW+2Aj~Lk2z1>at3u;bs zdv)o3{N%8$V&?o2c>m^f05-xwx7w}}K(!jqoiZ7t670Alx&9NuOul?K^Q{HNSqmjzRxYNm5Z$9qzBVbckde~7}X|xwu!uVsnh?`aB6#o@-%yei# zU7HwT5dFzOk_*Ax^$DE4FCN~8+cAexvGqitQP(B$hoEUBmZciZg)F+Z=-D$XhNwOVI);! zMv2Pcj0`P1Uyw`EFwPs@Fq`j z+;NX0pjsg3>s+jYN7LJ)n*@Sq-)Qpdhe+%2^#naRzs^SH9%DL6xB7r4#+bXvJah>( z0%xPnzbIQt_cR?Ort@bdB9$?o^HjFqZ!uVW03^~zmVU#PK= zoBDIxdG_Ija0MT)~^u%W-0vWfU3fEW}nlqbs zt+HN9q?C)}@H1rp*UbINab{mTEYSK1;3&}eGOwbAc2QhhuLsFjcK-V0E_$p=!#vL0`OC$%wzk^b5Qjb*(gO{{vbgk(e=xlm zxSqc3xn@{TP*{5QBRlVa^FD7~obA&qYId(yYq}RlEGL(OL6S`3oRDw2jUVS0NlSt# zy<)R0R%M(N#jG#HwRnCmigyPSov3us9oL0#8`3xM)!Kd>E-je7vnx)?oV30JF-CW_H>Jv$g_6YpMadbkbnOYI&Dd#mkCADSP!Y^vJ5^fsj za&bHywjk=)a#6c!V;4u+&7Y!p7jasGRq=qWnoXO2t5$f=h0buN^1YIG_RXNhjdr9t zg}s72S4QT~k4LV7^9MfdRF4!DUjF<#ZYtF(~`iJhSPQ|F+ei+2R z-MXcA;{1LGIbMipt7DzVtpXKG@yWeVF_yzaYAwaj-Bc z{y25la?;W0Qn9byT`W?p)NE_4@`R81H^$`V4?cp$)ZQCa{eytPLug%lv z?Ze)6G|qJIb!bIE*TKByC!}43W|-|#ZG2_b{C%mFHVXBKpQg20faTMZ@dQ z7ll7gA3ae9Ch6J<;{~1+=?MrRWQOH;COi+0JMmhln`f&UEm~Mv63=+QRBfv5rX7`R zRVRID9MYhkbcbkDCA&63DAD8_n4a_3vym>M1%)LM9P9~}T-^PLeY0{3ifi@bsYgH| z;p6q;&+5ow9!xCltwZ5U7}?<{ZlZudsgD!33ueB^(_8r7v_)3c%NkqRs`_>Y(IjDU`Z4oNSF!jkr86I?giy>A2x?qlO`_d&zT`U0(PjRpG~*-^`|!sMhK&Fo)~ar z$gAM*-tDW(`d*5)9#8a>>kp(#d9xuDHD!x)n(k>tCVoRA3oiN`p(GUf^{BI%;0zKD zan(M;Ks>FcM7_40?iC8+<2$Lm6<~}<6huf%G>G8=&owO<} zLQe;62OqCq$wz z-`WUPWY(!QvMDN5Wf>FlkZ^S;_9 zFd^A)#H8?7@4d3^HJGhAbcHPq(uH!YxVJygPv>KKbFR_Lb8xFKSa=<$c!Hli<8A!0 zy@_>rTh8-S@>=wZ1aFk}6xq-h2ughUMeXtfArucJ{v>-Nqk6Mz0d?&C#no!H=2m$V z?3g1}@sxo_chno-k^GNPxDWAOGxiE0fhbQYy&Ks83fIX8SEKubh)dI zH#TGC9)Zw}n>objXXTSV5ZY7;S4F|w{6-@eW~lvu!4Vx<{YJ!|6O&T^0E50u@KsbK z7XR=(T}T4q{PgY#vP}ZOKy26bpoZ1Dk zmm;ZAaIxscx6rXJEVZD|pT!zy*>XFK#j_72E-9y4U6^&#benbLRHFA<5Vz`-6#9IR z4g?p}HQbeb=~#z5N}WIxs-Ikgo{2Cs{bgvqC)k*`G?LxZNlN!|?fj~WRJ)WUz`ryISK_lm{JP3Y} zGi&QxpDGnr(^szsHq2VTzbB}kFMviBD<~V8sf4}2i=yd7r4}y@ZVTxaytB2OZY)3& zE8wXoj{NQj1aRVX`n=jGo7d zLNV`g26w))dS2RCl4-ec8M*69;hUhx#^ zJMM9$Y%j_Q<*cKZ=f}0LN~zx4unKB;T*XMvkYU#~CR}hjcTH`kuE@8N_Pvjwd+v_P zL7S*vzx(AmLein|iJ4q832$`Olh4)Wm69LHmyw@H3{M1}R`AvYIt6%(ic+`OydF_c zO`_L18b2*h{5Ih!oV~p{S=QK^`d!DiIh{~|rNvIHxcRdSURzd6L)!^MzZh83{ z^9Y3Pmph==7%aY&X~<&T0ENNmOzsjg=%t8o*DKkL)slKRN~e@) zp6s~C_^AZ7)@dBO4SvVp=hNDwpgzt@WPH{>@j*ekMEW{hjNhtrjHP<219WZw=}VQ- zE4uV-m80=t3=WAO@{P~))7B1IlwN5qEJLginnKlU9gnmm*J@k`P2a03b+r}0*i7+N zB;JoP9mJoLXHa^JU(>E7Qa^xQGO!NgPK1S^G*wQJ>3RAQ7q*{Q&Z>LpuAzMnsDaX1 z);oU;WrU=KnF&8k<L&?zm z>gIXColG5M*R<=t5tR;nB$<(=lRU~_Tt}`=K5u0i99Dllz$CMKE=bAEC#T5Z#{0`Z zNO!G;#*n>H*q;j|yrnx3T%yTe8#TU+nIP`)^i{CX@qE7-+K+rf-v$LXyB0-|A@*O< zbapXREeVJtdsB zxU?1YV%pn`;Cf`oa9TupZJ#frzoupP=!)JcNc}hA`D;Cpy*iq)CbBPKrms;m~MO z+64XD8-hVjKIC84V;VUeiY>)}h1(5`O)KhGP+uPDEZ6Xy*yRGn;C`X`@^V zihgB_tX_+>=FB`(LnajzEyQ&ld9gg^nC;)hQ^cn_*~O6WoUS>KZ8r=uSnjoTjJKj0 zLz`p`)+4d-{S=TlQlGKLTh)jB~$C-_rLMLM@3suf%1i#$&U?$~Cv>UntjRJ^4h5=zL3QV8vh?Ycyr7F>SVCk?l(DoT+}b z5|Xd?<}fXfh18oVHm*IzmpTf$scHa7O;IfU#xMZxsN&8M(oZN%7oD$64L<^f>2*&} zk`8Y-5<*lnRZuOQ$Z`v4m?WLOiS0yeW2EGi6z;BO$#@UGi4S}quUzbLHKt4$cHKx| zhE0n@oox);CSLjN+3OUm@me-?6V6}HZ&=SOizKWGUlLi>Tkq=SNQT{|vYosS_v|~m ztP@iwHI*iFH;&XzzzZSpr~IkKpWq`;mj#+|ExxFx8&w*tUNBU)5>|Br4^$UYyg-+& zGBR;&%9rMfLL)lbC=DR>uhOze;sJya%#JI{PuT7UW&_?FLeAbQkcje4$V88hWAfMq zZbS@0TJQGO9WKL%x_2sH9tje;Dh_Wx&pz3x{w`>HR=_r&_JwATP)#b+Dk+eiC&?## zVfs+>lbUYvCK`64s8eCa>4<=#$bB!_R_nb}kN1tu?P0!yYhA6PdEx3wlB3^~s{AaE zce|<)w5|*O#M;*v?{M~oxL&h%pFB)%oYy@GSW4@RIC2DWH@uvjNp*+G*%R;8)a`;p zk^yzn0Y&j>mWiXX_@$8{KXBiJ?fY7ib>d4U4$c&%RFXNgXz#$hgXB@9uo4^pZfeDS zJ+x8!JQpTC%=U;+f}6Ns7}8#NXQUdv9QbzZmlpA}tFn&ZkmoRy!q9$8%h!&G3IiS; z-c^Z?jMdp0V`(QpcZSe6qmB2zrmy*F2>FZ?Eg9Bo%O=*cg5Z94o!F7u)yyQXlR>xr5zlS1 ziyixEShSY3O`EQf1=Z*V5S{)pDRu=E6vJQ+d$S2ZUydDL`Uy-{b z8tr?F<6bApvs|+5$Juo2VJk=j>H?y&ffL~!47;Wz^(4l-uPW(U3`NGGfMoLY7?p}a z>HZgW;@#qrVzl#A^H#GTf3W?e3154wI7KyNMd5fbG(dXv3;T<=G*0Q)5qJCJ?-63; zCk1_mya`S{6!fi4v#ZM9oK-G%37On-RANi$iF))l6HyLbTBP&3prYAc*9ev*g;?V5(hzO&w=pX^%Y>Ka@*|I@dGCVL8df zn=V;2_{ZcCyIsQD8R}E^^+&(}ami}hGk{%l{bCll_T9N{wr2;)?qq zSe8+TLSQH5>o5J3l{@w8s7krXmF4B70d*SVEE60|a-)v5`;H)Eg5MG_el#CiqTUhh7uSFL*Ud35SJt-o1CgK}?Ttvds3xB$QjFqP%WI z;GQw@DZYY&aq&ko?|$pijXXK((DwvKRjKlG@17?47%~_r_aXGcn2Uf!qf>Lvaf6F$)FgcWXr#E0U0k`?o2$<~6{`c<-D(+O#!U%| zd+%BwbgFsAFGOhlH?!F3EcUZMbf7UjYDdN|UER!CN4M%!x8D07sS;3&s_RpbOcp00+R6vT_RTo+*weVw z;WS|gk~?J_Z|B|Dq4d(osMPL%p7%o2$E|GW4e==dP(d)o1ujHeUC(({@@zS-KibJp zI`(p{7H5ht8E--9ZIE@6TKZTf?+;VR+{b0s<@*i`EcZ`d-|?0rUN=`7?1dv+yZ^i` z!aPz66}sZxVmCMzsPmo;GAq1Kv{xPWO(A+Z6qpiwT-24%n=h?*y*(?fsD! z#;#iTcfB`rN5zE!1knl)+R5DU8w^v89}#0Ww43e{$?OFGiG-GNRWnDi3;Ua) z!N7BH3^|h`vHaLc!`i-~4~SGIV$7EKNm8d83(Bfw>Hf*Cn2dGxqe9aP7Z}5qgGmd3>8Is~-BLI>ODa;<6N|pT3(|WrG&| zl$+~d>loR2)rz;Y;;)wG*6&Va);$VzM^v=|6nC?9eO^_J#p6`b+FxTQ3k$u82kMLF_`zHQ>W!BJ+{frfYKh_B#HxS#nV5BfPn3GPw|bzT_ja612;=4*pDK@6 z$w~Pe>*cFxKgyHZjB(xirq(ld*kX5d7eZ1|8cE%6$S*o7~6Z4efwC($1L^5nh7ojGn!TP+%^E)NugKb(|A z>I|}GobM^FnJVNf`pd3kZ+n`RFI^^}b12GfwWtKNX^nCqk$ivN%clv!d=7_2yO;Gp zn_c9jk#fe7>9HOG94pV}ib110hvSJs%2(NFm{vtjAB_G=_u;@1soxGy=)QD5`JO*J z=9tfN@?@pbgS|ZX3P-NrNG2`%fqe99?nXsTIup@e(vP2A3s=#N=rH2*syE^0gj)Np z&)SY4tyi@Js>Vvq)C`}a`7BysDU!7FX;YnfKO-}m^Z8B$j?dff`%9XW$E?I0WX~+0 z4PmGY0`oIX1r3;eISHEH^K;gzH|H1a4LAx$f@uQwZ#JDpj$c%49gS|kNSgih2)tYY zO_lR4B)w!Ue8YZPBV^}RXEd%J_Z<@+&}Mn2^K4iUr3`6BhVJ|(U10~2NVIB$GU^d% zppW!!od0>Zc&Nwq-gI}q*f}{D-&${IH+gS0S>k8*uehEE;rdw=}&@e^^A`}O{}vjfNLjZ!PVnquR#Co`M{T6^L( zx*#i>J9-|BaS^xvX7pE;HGbgGX3ymwktrfjIGNY7Gs zdA$mE$-C-WIvp2{#mR##XMJSzg)68P3Zo7;(c)i5kj004&1c`tQ9{0T#ZhJ^chstz zin4TYU1@PXo50RxG)FAlHD002UZL}QvvF0q_|Dqu8k^Dy>!Jzp#IE6y0m*i-`J)&8y(j@u0PmO{VI8ii${kw*PEv* zBrH6be6~5`IR60aS0d0{OB*Cnu`CE29`*HFXtskOWCP9z9R3wybc;1&qc{L}?^2`* zpE>ENt=J;kn^rOgX)*yIa0jQgL-t)(?e6B5)=x4;AHaDz1Jb^iO*+>(Y?J=+=}}K- zWgEw}1`7?`)8}Dswmf6QR+`qEYaPN{^Bc2maKQZ4#p?ba)|*hezk70g`5DeS44n6` zsG2JUL-HvV$LpzXmh*XP0Kw}-sxihkcRqD3R;^}^GbxnyLymFHQYS2F@SmNTQC)(P z0rkx{v7r4z;9NR|lOPYRLMr9vsdIMi9nHK@d25f8Jd;$E;8R)w>t72-`;9?8Q9qkj z#9jcfavYb)1E3x2E8#~sxA{Nv%@68pZ9H)SxO+&gBrcp}w$bwO?^4KjzJ|t?phKr# zBgLGkWf}CYgHW;;R;?6+A-eu`&FGrsnvSC!Z3mXGzYV{4BepSJW~Soa%4wIDMI-`n zKN@#4dz}1UAF+v9%w*v6&TB<9ttu$Yjcf@$PHUEf#Mc> zY65_BoaaA{Hkn6FKAERR*^U$$!S(2Cd3Kc?SCh-*3)vUh%Q-W_Y+BtAO zWe4*V5Ydq=Hr{^bjech8S0J^=}wI+I%x(bJUfMiS;bC z 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 literal 0 HcmV?d00001 diff --git a/imagepy/menus/Plugins/Surf/Orb Demo.mc b/imagepy/menus/Plugins/Surf/Orb Demo.mc new file mode 100644 index 00000000..02ed662d --- /dev/null +++ b/imagepy/menus/Plugins/Surf/Orb Demo.mc @@ -0,0 +1,7 @@ +Open>{'path':'./T1.jpg'} +Open>{'path':'./T2.jpg'} +Orb Matcher>{'int': 4, 'upright': False, 'img2': u'T2', 'img1': u'T1', 'std': 1, 'style': 'Blue/Yellow', 'log': True, 'thr': 2000, 'ext': False, 'trans': 'Homo', 'oct': 3} + +#Open Url>{'url': u'http://data.imagepy.org/testdata/box.png'} +#Open Url>{'url': u'http://data.imagepy.org/testdata/box_in_scene.png'} +#Orb Matcher>{'int': 4, 'upright': False, 'img2': u'box', 'img1': u'box_in_scene', 'std': 1, 'style': 'Blue/Yellow', 'log': True, 'thr': 2000, 'ext': False, 'trans': 'Homo', 'oct': 3} diff --git a/imagepy/menus/Plugins/Surf/Surf Demo.mc b/imagepy/menus/Plugins/Surf/Surf Demo.mc index 51869842..fc01f041 100644 --- a/imagepy/menus/Plugins/Surf/Surf Demo.mc +++ b/imagepy/menus/Plugins/Surf/Surf Demo.mc @@ -1,3 +1,4 @@ -Open Url>{'url': u'http://data.imagepy.org/testdata/box.png'} -Open Url>{'url': u'http://data.imagepy.org/testdata/box_in_scene.png'} -Surf Matcher>{'int': 4, 'upright': False, 'img2': u'box_in_scene', 'img1': u'box', 'std': 1, 'style': 'Blue/Yellow', 'log': True, 'thr': 2000, 'ext': False, 'trans': 'Homo', 'oct': 3} +Open>{'path':'./T1.jpg'} +Open>{'path':'./T2.jpg'} + +Surf Matcher>{'int': 4, 'upright': False, 'img2': u'T1', 'img1': u'T2', 'std': 1, 'style': 'Blue/Yellow', 'log': True, 'thr': 2000, 'ext': False, 'trans': 'Homo', 'oct': 3} diff --git a/imagepy/menus/Plugins/Surf/__init__.py b/imagepy/menus/Plugins/Surf/__init__.py index 39f3f347..39f9a5e3 100644 --- a/imagepy/menus/Plugins/Surf/__init__.py +++ b/imagepy/menus/Plugins/Surf/__init__.py @@ -1 +1 @@ -catlog = ['surf_plg', '-', 'Surf Demo'] \ No newline at end of file +catlog = ['surf_plg', 'Surf Demo','-','orb_plg','Orb Demo'] diff --git a/imagepy/menus/Plugins/Surf/orb_plg.py b/imagepy/menus/Plugins/Surf/orb_plg.py new file mode 100644 index 00000000..0a423d67 --- /dev/null +++ b/imagepy/menus/Plugins/Surf/orb_plg.py @@ -0,0 +1,240 @@ +import cv2, wx +from imagepy.core.engine import Filter, Simple, Tool +from imagepy.core.manager import ImageManager +from .matcher import Matcher +import numpy as np +from imagepy import IPy + +from skimage.color import rgb2gray,gray2rgb +from skimage.feature import ( match_descriptors, corner_harris, corner_peaks, ORB, plot_matches) +from skimage.measure import ransac +from skimage.transform import warp +from skimage.transform import SimilarityTransform +from skimage.transform import FundamentalMatrixTransform +from skimage.transform import ProjectiveTransform + + +CVSURF = cv2.xfeatures2d.SURF_create if cv2.__version__[0] =="3" else cv2.SURF + +class OrbFeatMark: + def __init__(self, feats): + self.feats = feats + + def draw(self, dc, f, **key): + for i in self.feats: + # print('i.pt:{},{}'.format(type(f(i.pt)),i.pt)) + dc.DrawCircle(f(i[1], i[0]), 3) + +class Orb(Filter): + title = 'ORB Detect' + note = ['all', 'not-slice'] + + para = {'Num':1000} + view = [ + (int, 'Num', (500,2000), 0, 'orb descriptor num', '1-1000')] + + def run(self, ips, snap, img, para): + descriptor_extractor = ORB(n_keypoints=para['Num']) + + grayImg = rgb2gray(img) + descriptor_extractor.detect_and_extract(grayImg) + keypoints1 = descriptor_extractor.keypoints + descriptors1 = descriptor_extractor.descriptors + + ips.orb_keypoint = keypoints1 + ips.orb_descriptors = descriptors1 + ips.mark = OrbFeatMark(keypoints1) + + IPy.write("Detect completed, {} ORB points found!".format(len(keypoints1)), 'Surf') + +class Pick(Tool): + title = 'Key Point Pick Tool' + def __init__(self, pts1, pts2, pair, msk, ips1, ips2, host, style): + self.pts1, self.pts2 = pts1, pts2 + self.ips1, self.ips2 = ips1, ips2 + self.pair, self.msk = pair, msk + self.cur, self.host = -1, host + self.pts = self.pts1 if host else self.pts2 + self.style = style + + def nearest(self, x, y): + mind, mini = 1000, -1 + for i1, i2 in self.pair: + i = i1 if self.host else i2 + d = np.sqrt((x-self.pts[i].pt[0])**2+(y-self.pts[i].pt[1])**2) + if d Date: Sat, 9 Jun 2018 11:11:32 +0800 Subject: [PATCH 05/10] Purify the code and Generate transform matrix for the table --- imagepy/menus/Plugins/Surf/orb_plg.py | 30 ++++++++++++++++++--------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/imagepy/menus/Plugins/Surf/orb_plg.py b/imagepy/menus/Plugins/Surf/orb_plg.py index 0a423d67..e6a9ebf4 100644 --- a/imagepy/menus/Plugins/Surf/orb_plg.py +++ b/imagepy/menus/Plugins/Surf/orb_plg.py @@ -13,6 +13,7 @@ from skimage.transform import FundamentalMatrixTransform from skimage.transform import ProjectiveTransform +import pandas as pd CVSURF = cv2.xfeatures2d.SURF_create if cv2.__version__[0] =="3" else cv2.SURF @@ -98,8 +99,7 @@ class OrbMatch(Simple): note = ['all'] #parameter - para = {'img1':'','img2':'','upright':False, 'log':False, - 'oct':3, 'int':4, 'thr':1000} + para = {'img1':'','img2':'','upright':False, 'log':False, 'int':4} def load(self, ips): titles = ImageManager.get_titles() @@ -110,9 +110,7 @@ def load(self, ips): (list, 'img2', titles, str, 'image2', ''), ('lab', None, ''), ('lab', None, '====== parameter about the surf ======'), - (int, 'oct', (0,5), 0, 'octaves', ''), - (int, 'int', (0,5), 0, 'intervals', ''), - (int, 'thr', (500,2000), 0, 'threshold', '1-100')] + (int, 'int', (0,5), 0, 'order', '')] return True # def filter_matches(self, kp1, kp2, matches, ratio = 0.75): @@ -181,18 +179,26 @@ def run(self, ips, imgs, para = None): offset = SimilarityTransform(translation=-corner_min) # Warp pano0 to pano1 using 3rd order interpolation - img1_ = warp(ips1.img, offset.inverse, order = 3, + img1_ = warp(ips1.img, offset.inverse, order = para['int'], output_shape=output_shape, cval=0) img1_mask = (img1_ != 0) img1_[~img1_mask] = 0 - img2_ = warp(ips2.img, (model + offset).inverse, + img2_ = warp(ips2.img, (model + offset).inverse, order = para['int'], output_shape=output_shape, cval=0) img2_[img1_mask] = 0 test = (img1_+img2_) + print('model:{}'.format(model._inv_matrix)) + print('(model + offset):{}'.format((model + offset)._inv_matrix)) + + dim = 3 + self.log(keypoints1, keypoints2, matches12, (model + offset)._inv_matrix.flatten(), dim) + # print('offset:{}'.format(offset)) + + test *= 255 img1_ *= 255 img2_ *= 255 @@ -202,17 +208,21 @@ def run(self, ips, imgs, para = None): title = 'Orb Stitched image' IPy.show_img([test.astype(np.uint8)], title) + + titles=['x','y','z'] + IPy.show_table(pd.DataFrame((model + offset)._inv_matrix, columns=titles), ips.title+'-region') + # cv2.imwrite('orb.jpg',test) # print surf result to the Name 'Surf' Console def log(self, pts1, pts2, msk, v, dim): sb = [] sb.append('Image1:{} points detected!'.format(len(pts1))) sb.append('Image2:{} points detected!\r\n'.format(len(pts2))) - sb.append('Matched Point:{0}/{1}\r\n'.format(msk.sum(),len(msk))) + sb.append('Matched Point:{0}/{1}\r\n'.format(len(msk),len(pts2))) if dim == 0: return sb.append('Transformation:') - sb.append('%15.4f%15.4f%15.4f'%tuple(v.A1[:3])) - sb.append('%15.4f%15.4f%15.4f'%tuple(v.A1[3:6])) + sb.append('%15.4f%15.4f%15.4f'%tuple(v[:3])) + sb.append('%15.4f%15.4f%15.4f'%tuple(v[3:6])) row = [0,0,1] if dim==6 else list(v[-2:])+[1] sb.append('%15.4f%15.4f%15.4f'%tuple(row)) From caa94181d575a2532e54c106e04ed61f08deb311 Mon Sep 17 00:00:00 2001 From: gaojian Date: Sat, 9 Jun 2018 11:54:22 +0800 Subject: [PATCH 06/10] Purify the code and generate transform matrix for ORB-based image Purify the code and generate transform matrix for ORB-based image --- imagepy/menus/Plugins/Surf/orb_plg.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/imagepy/menus/Plugins/Surf/orb_plg.py b/imagepy/menus/Plugins/Surf/orb_plg.py index e6a9ebf4..109d9b6c 100644 --- a/imagepy/menus/Plugins/Surf/orb_plg.py +++ b/imagepy/menus/Plugins/Surf/orb_plg.py @@ -99,7 +99,7 @@ class OrbMatch(Simple): note = ['all'] #parameter - para = {'img1':'','img2':'','upright':False, 'log':False, 'int':4} + para = {'img1':'','img2':'', 'log':False, 'int':4, 'num':200} def load(self, ips): titles = ImageManager.get_titles() @@ -109,8 +109,10 @@ def load(self, ips): (list, 'img1', titles, str, 'image1', ''), (list, 'img2', titles, str, 'image2', ''), ('lab', None, ''), - ('lab', None, '====== parameter about the surf ======'), - (int, 'int', (0,5), 0, 'order', '')] + ('lab', None, '====== parameter about the Orb ======'), + (int, 'int', (0,5), 3, 'order', ''), + (int, 'num', (50,1000), 200, 'Descriptor_Num', '') + ] return True # def filter_matches(self, kp1, kp2, matches, ratio = 0.75): @@ -134,7 +136,8 @@ def run(self, ips, imgs, para = None): grayImg1 = rgb2gray(ips1.img) grayImg2 = rgb2gray(ips2.img) - descriptor_extractor = ORB(n_keypoints=200) + print('desciptor num:{}'.format(para['num'])) + descriptor_extractor = ORB(n_keypoints=int(para['num'])) descriptor_extractor.detect_and_extract(grayImg1) keypoints1 = descriptor_extractor.keypoints @@ -179,13 +182,13 @@ def run(self, ips, imgs, para = None): offset = SimilarityTransform(translation=-corner_min) # Warp pano0 to pano1 using 3rd order interpolation - img1_ = warp(ips1.img, offset.inverse, order = para['int'], + img1_ = warp(ips1.img, offset.inverse, order = int(para['int']), output_shape=output_shape, cval=0) img1_mask = (img1_ != 0) img1_[~img1_mask] = 0 - img2_ = warp(ips2.img, (model + offset).inverse, order = para['int'], + img2_ = warp(ips2.img, (model + offset).inverse, order = int(para['int']), output_shape=output_shape, cval=0) img2_[img1_mask] = 0 @@ -223,8 +226,10 @@ def log(self, pts1, pts2, msk, v, dim): sb.append('Transformation:') sb.append('%15.4f%15.4f%15.4f'%tuple(v[:3])) sb.append('%15.4f%15.4f%15.4f'%tuple(v[3:6])) - row = [0,0,1] if dim==6 else list(v[-2:])+[1] - sb.append('%15.4f%15.4f%15.4f'%tuple(row)) + sb.append('%15.4f%15.4f%15.4f'%tuple(v[6:9])) + + # row = [0,0,1] if dim==6 else list(v[-2:])+[1] + # sb.append('%15.4f%15.4f%15.4f'%tuple(row)) cont = '\n'.join(sb) IPy.write(cont, 'Surf') From 8875e6c15bcd76700a3d4fe4407f729879cc604d Mon Sep 17 00:00:00 2001 From: gaojian Date: Tue, 12 Jun 2018 14:00:25 +0800 Subject: [PATCH 07/10] Change SURF and ORB threshold for better image registration.And Incresease text font for better Visualisation --- imagepy/menus/Plugins/Surf/Orb Demo.mc | 2 +- imagepy/menus/Plugins/Surf/orb_plg.py | 6 +++--- imagepy/menus/Plugins/Surf/surf_plg.py | 6 ++++-- imagepy/tools/Measure/distance_tol.py | 7 ++++++- imagepy/tools/Measure/setting.py | 2 +- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/imagepy/menus/Plugins/Surf/Orb Demo.mc b/imagepy/menus/Plugins/Surf/Orb Demo.mc index 02ed662d..73fedb27 100644 --- a/imagepy/menus/Plugins/Surf/Orb Demo.mc +++ b/imagepy/menus/Plugins/Surf/Orb Demo.mc @@ -1,6 +1,6 @@ Open>{'path':'./T1.jpg'} Open>{'path':'./T2.jpg'} -Orb Matcher>{'int': 4, 'upright': False, 'img2': u'T2', 'img1': u'T1', 'std': 1, 'style': 'Blue/Yellow', 'log': True, 'thr': 2000, 'ext': False, 'trans': 'Homo', 'oct': 3} +Orb Matcher>{'int': 3, 'upright': False, 'img2': u'T2', 'img1': u'T1', 'num': 200} #Open Url>{'url': u'http://data.imagepy.org/testdata/box.png'} #Open Url>{'url': u'http://data.imagepy.org/testdata/box_in_scene.png'} diff --git a/imagepy/menus/Plugins/Surf/orb_plg.py b/imagepy/menus/Plugins/Surf/orb_plg.py index 109d9b6c..0df0177e 100644 --- a/imagepy/menus/Plugins/Surf/orb_plg.py +++ b/imagepy/menus/Plugins/Surf/orb_plg.py @@ -46,7 +46,7 @@ def run(self, ips, snap, img, para): ips.orb_descriptors = descriptors1 ips.mark = OrbFeatMark(keypoints1) - IPy.write("Detect completed, {} ORB points found!".format(len(keypoints1)), 'Surf') + IPy.write("Detect completed, {} ORB points found!".format(len(keypoints1)), 'Orb') class Pick(Tool): title = 'Key Point Pick Tool' @@ -152,9 +152,10 @@ def run(self, ips, imgs, para = None): src = keypoints2[matches12[:, 1]][:, ::-1] dst = keypoints1[matches12[:, 0]][:, ::-1] + model, inliers = ransac((src, dst), ProjectiveTransform, min_samples=8, - residual_threshold=1, max_trials=5000) + residual_threshold=0.5, max_trials=5000) r, c = grayImg1.shape[:2] @@ -201,7 +202,6 @@ def run(self, ips, imgs, para = None): self.log(keypoints1, keypoints2, matches12, (model + offset)._inv_matrix.flatten(), dim) # print('offset:{}'.format(offset)) - test *= 255 img1_ *= 255 img2_ *= 255 diff --git a/imagepy/menus/Plugins/Surf/surf_plg.py b/imagepy/menus/Plugins/Surf/surf_plg.py index 3e0f7064..e8c7b526 100644 --- a/imagepy/menus/Plugins/Surf/surf_plg.py +++ b/imagepy/menus/Plugins/Surf/surf_plg.py @@ -163,8 +163,10 @@ def run(self, ips, imgs, para = None): # print('newPnt1:{}'.format(len(newPnt1))) # print('newPnt2:{}'.format(len(newPnt2))) - - H, _ = cv2.findHomography(tempPnt1, tempPnt2, cv2.RANSAC, 5.0) + # 第四个参数取值范围在 1 到 10 , 绝一个点对的阈值。原图像的点经过变换后点与目标图像上对应点的误差 + # 超过误差就认为是 outlier + # 返回值中 H 为变换矩阵。mask是掩模,online的点 + H, _ = cv2.findHomography(tempPnt1, tempPnt2, cv2.RANSAC, 1.0) # print('H type:{}'.format(type(H))) # print('H shape:{}'.format(H.shape)) # print('H :{}'.format(H)) diff --git a/imagepy/tools/Measure/distance_tol.py b/imagepy/tools/Measure/distance_tol.py index 6121e38c..687c1671 100644 --- a/imagepy/tools/Measure/distance_tol.py +++ b/imagepy/tools/Measure/distance_tol.py @@ -43,7 +43,8 @@ def draged(self, ox, oy, nx, ny, i): def draw(self, dc, f, **key): dc.SetPen(wx.Pen(Setting['color'], width=1, style=wx.SOLID)) dc.SetTextForeground(Setting['tcolor']) - font = wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False) + + font = wx.Font(15, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False) dc.SetFont(font) if self.buf: dc.DrawLines([f(*i) for i in self.buf]) @@ -57,6 +58,10 @@ def draw(self, dc, f, **key): dis = norm((pts[:-1]-pts[1:]), axis=1) unit = 1 if self.unit is None else self.unit[0] for i,j in zip(dis, mid): + if j[0]>6: + j-=5 + if i <=0: + continue dc.DrawText('%.2f'%(i*unit), f(*j)) def report(self, title): diff --git a/imagepy/tools/Measure/setting.py b/imagepy/tools/Measure/setting.py index a31dbbe9..a804c05d 100644 --- a/imagepy/tools/Measure/setting.py +++ b/imagepy/tools/Measure/setting.py @@ -1 +1 @@ -Setting = {'color':(255,255,0), 'tcolor':(255,255,255), 'width':1} \ No newline at end of file +Setting = {'color':(255,255,0), 'tcolor':(255,0,255), 'bcolor':(255,255,255),'width':1} From b3d2122ee5cbde862c96b3542023f3104e68f9b3 Mon Sep 17 00:00:00 2001 From: gaojian Date: Thu, 21 Jun 2018 17:17:04 +0800 Subject: [PATCH 08/10] Add delta version image stitching module Delta Version for image Stitching module listed in Plugins/Match/ --- imagepy/core/engine/simple.py | 14 +- imagepy/menus/Plugins/Match/__init__.py | 1 + imagepy/menus/Plugins/Match/basic.py | 56 ++++ imagepy/menus/Plugins/Match/matcher.py | 85 ++++++ imagepy/menus/Plugins/Match/orb_plgs.py | 265 +++++++++++++++++ imagepy/menus/Plugins/Match/sift_plgs.py | 247 ++++++++++++++++ imagepy/menus/Plugins/Match/surf_plg.py | 223 ++++++++++++++ imagepy/menus/Plugins/Match/trans_plgs.py | 41 +++ imagepy/menus/Plugins/Surf/__init__.py | 2 +- imagepy/menus/Plugins/Surf/imgStitch_plg.py | 307 ++++++++++++++++++++ imagepy/menus/Plugins/Surf/surf_plg.py | 13 +- 11 files changed, 1239 insertions(+), 15 deletions(-) create mode 100755 imagepy/menus/Plugins/Match/__init__.py create mode 100755 imagepy/menus/Plugins/Match/basic.py create mode 100644 imagepy/menus/Plugins/Match/matcher.py create mode 100755 imagepy/menus/Plugins/Match/orb_plgs.py create mode 100755 imagepy/menus/Plugins/Match/sift_plgs.py create mode 100755 imagepy/menus/Plugins/Match/surf_plg.py create mode 100755 imagepy/menus/Plugins/Match/trans_plgs.py create mode 100644 imagepy/menus/Plugins/Surf/imgStitch_plg.py diff --git a/imagepy/core/engine/simple.py b/imagepy/core/engine/simple.py index b989ea09..faa6894f 100644 --- a/imagepy/core/engine/simple.py +++ b/imagepy/core/engine/simple.py @@ -24,12 +24,12 @@ def __init__(self, ips=None): print('simple start') self.ips = IPy.get_ips() if ips==None else ips self.dialog = None - + def progress(self, i, n): self.prgs = (i, n) def load(self, ips):return True - + def preview(self, ips, para):pass def show(self, temp=ParaDialog): @@ -41,16 +41,16 @@ def show(self, temp=ParaDialog): self.dialog.on_ok = lambda : self.ok(self.ips) self.dialog.on_cancel = lambda : self.cancel(self.ips) self.dialog.Show() - + def run(self, ips, imgs, para = None):pass - + def cancel(self, ips):pass def ok(self, ips, para=None, callafter=None): if para == None: para = self.para if IPy.uimode() == 'no': self.runasyn(ips, ips.imgs, para, callafter) - else: threading.Thread(target = self.runasyn, + else: threading.Thread(target = self.runasyn, args = (ips, ips.imgs, para, callafter)).start() win = WidgetsManager.getref('Macros Recorder') if win!=None: win.write('{}>{}'.format(self.title, para)) @@ -99,7 +99,7 @@ def check(self, ips): IPy.alert('stack3d required!') return False return True - + def start(self, para=None, callback=None): #print self.title, para if not self.check(self.ips):return @@ -118,4 +118,4 @@ def start(self, para=None, callback=None): self.ok(self.ips, para, callback) else:self.cancel(self.ips) if not self.dialog is None: self.dialog.Destroy() - else: self.show() \ No newline at end of file + else: self.show() diff --git a/imagepy/menus/Plugins/Match/__init__.py b/imagepy/menus/Plugins/Match/__init__.py new file mode 100755 index 00000000..deef7095 --- /dev/null +++ b/imagepy/menus/Plugins/Match/__init__.py @@ -0,0 +1 @@ +catlog = ['trans_plgs', '-', 'surf_plgs', 'sift_plgs', 'orb_plgs'] \ No newline at end of file diff --git a/imagepy/menus/Plugins/Match/basic.py b/imagepy/menus/Plugins/Match/basic.py new file mode 100755 index 00000000..938a3346 --- /dev/null +++ b/imagepy/menus/Plugins/Match/basic.py @@ -0,0 +1,56 @@ +from imagepy.core.engine import Tool +# 用于绘制特征集 +class FeatMark: + def __init__(self, feats): + self.feats = feats + + def draw(self, dc, f, **key): + for i in self.feats: + dc.DrawCircle(f(*i), 3) + +# 用于双图特征集交互 +class Pick(Tool): + title = 'Key Point Pick Tool' + def __init__(self, pts1, pts2, pair, msk, ips1, ips2, host, style): + self.pts1, self.pts2 = pts1, pts2 + self.ips1, self.ips2 = ips1, ips2 + self.pair, self.msk = pair, msk + self.cur, self.host = -1, host + self.pts = self.pts1 if host else self.pts2 + self.style = style + + def nearest(self, x, y): + mind, mini = 1000, -1 + for i1, i2 in self.pair: + i = i1 if self.host else i2 + d = np.sqrt((x-self.pts[i,0])**2+(y-self.pts[i,1])**2) + if d d + + def accept(self, v1, v2): + L = v2 + Dl = np.mat(np.diag(np.ones(2)))*self.std**2 + T = self.getT(v1.A1,v2.A1) + CX = (self.Dk.I + T.T * Dl.I * T).I + CL = CX * T .T* Dl.I + CV = np.mat(np.diag(np.ones(self.dim))) - CX * T.T * Dl.I * T + self.V = CL * L + CV * self.V + self.Dk = CV * self.Dk * CV.T + CL * Dl * CL.T + + def normalrize(self, pts): + o = pts.mean(axis=0) + l = norm(pts-o, axis=1).mean() + pts[:] = (pts-o)/l + + def filter(self, kpt1, feat1, kpt2, feat2): + kpt1 = np.array([(k.pt[0],k.pt[1]) for k in kpt1]) + kpt2 = np.array([(k.pt[0],k.pt[1]) for k in kpt2]) + self.normalrize(kpt1), self.normalrize(kpt2) + idx = self.match(feat1, feat2) + if self.dim == 0: + return idx, np.ones(len(idx), dtype=np.bool), 1 + mask = [] + for i1, i2 in idx: + v1 = np.mat(kpt1[i1]) + v2 = np.mat(kpt2[i2]) + if self.test(v1, v2): + self.accept(v1.T,v2.T) + mask.append(True) + else: mask.append(False) + mask = np.array(mask) + #print mask + return idx, mask, self.V + + def getTrans(self): + result = np.eye(3) + result[:2] = self.V.reshape((2,3)) + return result + + def checkV(self): + trans = self.getTrans()[:2,:2] + axis = norm(trans,axis=0) + return norm(axis-1)< 0.5 \ No newline at end of file diff --git a/imagepy/menus/Plugins/Match/orb_plgs.py b/imagepy/menus/Plugins/Match/orb_plgs.py new file mode 100755 index 00000000..86266a23 --- /dev/null +++ b/imagepy/menus/Plugins/Match/orb_plgs.py @@ -0,0 +1,265 @@ +# 仿照surf_plgs +import cv2, wx +from imagepy.core.engine import Filter, Simple, Tool +from imagepy.core.manager import ImageManager +from .matcher import Matcher +import numpy as np +from imagepy import IPy + +from skimage.color import rgb2gray,gray2rgb +from skimage.feature import ( match_descriptors, corner_harris, corner_peaks, ORB, plot_matches) +from skimage.measure import ransac +from skimage.transform import warp +from skimage.transform import SimilarityTransform +from skimage.transform import FundamentalMatrixTransform +from skimage.transform import ProjectiveTransform + +import pandas as pd + + +class OrbFeatMark: + def __init__(self, feats): + self.feats = feats + + def draw(self, dc, f, **key): + for i in self.feats: + # print('i.pt:{},{}'.format(type(f(i.pt)),i.pt)) + dc.DrawCircle(f(i[0], i[1]), 3) + +class Orb(Filter): + title = 'ORB Detect' + note = ['all', 'not-slice'] + + para = {'Num':1000} + view = [ + (int, 'Num', (500,2000), 0, 'orb descriptor num', '1-1000')] + + def run(self, ips, snap, img, para): + descriptor_extractor = ORB(n_keypoints=para['Num']) + + grayImg = rgb2gray(img) + descriptor_extractor.detect_and_extract(grayImg) + keypoints1 = descriptor_extractor.keypoints + descriptors1 = descriptor_extractor.descriptors + + ips.orb_keypoint = keypoints1[::-1] + ips.orb_descriptors = descriptors1 + ips.mark = OrbFeatMark(keypoints1) + + IPy.write("Detect completed, {} ORB points found!".format(len(keypoints1)), 'Orb') + +class OrbPick(Tool): + title = 'Key Point Pick Tool' + def __init__(self, pts1, pts2, pair, msk, ips1, ips2, host, style): + self.pts1, self.pts2 = pts1, pts2 + self.ips1, self.ips2 = ips1, ips2 + self.pair, self.msk = pair, msk + self.cur, self.host = -1, host + self.pts = self.pts1 if host else self.pts2 + self.style = style + + def nearest(self, x, y): + mind, mini = 1000, -1 + for i1, i2 in self.pair: + i = i1 if self.host else i2 + d = np.sqrt((x-self.pts[i][0])**2+(y-self.pts[i][1])**2) + + # d = np.sqrt((x-self.pts[i].pt[0])**2+(y-self.pts[i].pt[1])**2) + if d1:img = img[::para['dsample'], ::para['dsample']] + + detector = cv2.xfeatures2d.SIFT_create() if cv2.__version__[0] =="3" else cv2.SIFT() + + kps = detector.detect(img, None) + ips.sift_keypoint = kps + skps = np.array([i.pt for i in kps])*para['dsample'] + ips.mark = FeatMark(skps) + IPy.write("Detect completed, {} points found!".format(len(kps)), 'SIFT') + +class Pick(Tool): + title = 'Key Point Pick Tool' + def __init__(self, pts1, pts2, pair, msk, ips1, ips2, host, style): + self.pts1, self.pts2 = pts1, pts2 + self.ips1, self.ips2 = ips1, ips2 + self.pair, self.msk = pair, msk + self.cur, self.host = -1, host + self.pts = self.pts1 if host else self.pts2 + self.style = style + + def nearest(self, x, y): + mind, mini = 1000, -1 + for i1, i2 in self.pair: + i = i1 if self.host else i2 + d = np.sqrt((x-self.pts[i].pt[0])**2+(y-self.pts[i].pt[1])**2) + if d1:img = img[::para['dsample'], ::para['dsample']] + detector = CVSURF(hessianThreshold=para['thr'], nOctaves=para['oct'], + nOctaveLayers=para['int'], upright=para['upright'],extended=para['ext']) + kps = detector.detect(img) + skps = np.array([i.pt for i in kps])*para['dsample'] + ips.mark = FeatMark(skps) + + # if para['tab'], show the table + if not para['tab']:return + feats = detector.compute(img, kps)[1] + columns=['Point-X','Point-Y']+['feat%d'%(i+1) for i in range(feats.shape[1])] + vs = pd.DataFrame(np.hstack((skps, feats*10000)), columns=columns) + IPy.show_table(vs, ips.title+'-SurfFeats') + +class Pick(Tool): + title = 'Key Point Pick Tool' + def __init__(self, pts1, pts2, pair, msk, ips1, ips2, host, style): + self.pts1, self.pts2 = pts1, pts2 + self.ips1, self.ips2 = ips1, ips2 + self.pair, self.msk = pair, msk + self.cur, self.host = -1, host + self.pts = self.pts1 if host else self.pts2 + self.style = style + + def nearest(self, x, y): + mind, mini = 1000, -1 + for i1, i2 in self.pair: + i = i1 if self.host else i2 + d = np.sqrt((x-self.pts[i].pt[0])**2+(y-self.pts[i].pt[1])**2) + if d Date: Sun, 30 Sep 2018 10:18:55 +0800 Subject: [PATCH 10/10] add measure line delete function add measure line delete function --- imagepy/core/engine/tool.py | 291 +++++++++++- imagepy/core/engine/toolFormal.py | 237 ++++++++++ imagepy/menus/Plugins/Manager/plgtree_wgt.py | 12 +- imagepy/menus/Plugins/Match/surf_plg.py | 1 + imagepy/menus/Plugins/New/demo_simple.py | 2 +- imagepy/menus/Plugins/New/new_plg.py | 23 +- imagepy/menus/Plugins/Surf/imgStitch_plg.py | 82 ---- imagepy/menus/Plugins/Surf/orb_plg.py | 12 - imagepy/menus/Process/Math/math_plgs.py | 4 +- imagepy/menus/Table/Statistic/del_poly_plg.py | 26 + imagepy/popUpMenu.py | 152 ++++++ imagepy/test.py | 74 +++ imagepy/tools/Measure/distance_tol.py | 447 +++++++++++++----- imagepy/tools/Measure/distance_tol_BackUp.py | 232 +++++++++ imagepy/tools/Measure/setting.py | 2 +- testPanda.py | 39 ++ 16 files changed, 1394 insertions(+), 242 deletions(-) create mode 100644 imagepy/core/engine/toolFormal.py create mode 100644 imagepy/menus/Table/Statistic/del_poly_plg.py create mode 100644 imagepy/popUpMenu.py create mode 100644 imagepy/test.py create mode 100644 imagepy/tools/Measure/distance_tol_BackUp.py create mode 100644 testPanda.py diff --git a/imagepy/core/engine/tool.py b/imagepy/core/engine/tool.py index 2a3ea7b8..d9c82c33 100644 --- a/imagepy/core/engine/tool.py +++ b/imagepy/core/engine/tool.py @@ -6,10 +6,58 @@ from ... import IPy from ...core.manager import ToolsManager +from wx import * +import wx, platform +# import wx, platform + +menu_titles = [ "Open", + "Properties", + "Rename", + "Delete" ] + +menu_title_by_id = {} +for title in menu_titles: + # menu_title_by_id[ wxNewId() ] = title + menu_title_by_id[ wx.NewId() ] = title + +list_title = "files" +list_items = [ "binding.py", + "clipboard.py", + "config.py", + "debug.py", + "dialog.py", + "dispatch.py", + "error.py", ] + class Tool: title = 'Tool' view, para = None, None - + list = None + + def popUpInit(self, **key): + box = wx.BoxSizer(wx.VERTICAL) + + # Make and layout the controls + fs = key['canvas'].GetFont().GetPointSize() + bf = wx.Font(fs+4, wx.SWISS, wx.NORMAL, wx.BOLD) + nf = wx.Font(fs+2, wx.SWISS, wx.NORMAL, wx.NORMAL) + + t = wx.StaticText(key['canvas'], -1, "PopupMenu") + t.SetFont(bf) + + box.Add(t, 0, wx.CENTER|wx.ALL, 5) + box.Add(wx.StaticLine(key['canvas'], -1), 0, wx.EXPAND) + box.Add((10,20)) + + # text = 'hello' + # t = wx.StaticText(key['canvas'], -1, text) + # t.SetFont(nf) + # box.Add(t, 0, wx.CENTER|wx.ALL, 5) + t.Bind(wx.EVT_CONTEXT_MENU, self.OnContextMenu) + + key['canvas'].SetSizer(box) + key['canvas'].Bind(wx.EVT_CONTEXT_MENU, self.OnContextMenu) + def show(self): if self.view == None:return rst = IPy.get_para(self.title, self.view, self.para) @@ -31,5 +79,246 @@ def mouse_up(self, ips, x, y, btn, **key): pass def mouse_move(self, ips, x, y, btn, **key): pass def mouse_wheel(self, ips, x, y, d, **key): pass + def OnContextMenu(self, **key): + print("OnContextMenu\n") + + # only do this part the first time so the events are only bound once + # + # Yet another anternate way to do IDs. Some prefer them up top to + # avoid clutter, some prefer them close to the object of interest + # for clarity. + if not hasattr(self, "popupID1"): + self.popupID1 = wx.NewId() + self.popupID2 = wx.NewId() + self.popupID3 = wx.NewId() + # self.popupID4 = wx.NewId() + # self.popupID5 = wx.NewId() + # self.popupID6 = wx.NewId() + # self.popupID7 = wx.NewId() + # self.popupID8 = wx.NewId() + # self.popupID9 = wx.NewId() + + self.Bind(wx.EVT_MENU, self.OnPopupOne, id=self.popupID1) + self.Bind(wx.EVT_MENU, self.OnPopupTwo, id=self.popupID2) + self.Bind(wx.EVT_MENU, self.OnPopupThree, id=self.popupID3) + # self.Bind(wx.EVT_MENU, self.OnPopupFour, id=self.popupID4) + # self.Bind(wx.EVT_MENU, self.OnPopupFive, id=self.popupID5) + # self.Bind(wx.EVT_MENU, self.OnPopupSix, id=self.popupID6) + # self.Bind(wx.EVT_MENU, self.OnPopupSeven, id=self.popupID7) + # self.Bind(wx.EVT_MENU, self.OnPopupEight, id=self.popupID8) + # self.Bind(wx.EVT_MENU, self.OnPopupNine, id=self.popupID9) + + # make a menu + menu = wx.Menu() + # Show how to put an icon in the menu + item = wx.MenuItem(menu, self.popupID1,"One") + # bmp = images.Smiles.GetBitmap() + # item.SetBitmap(bmp) + menu.AppendItem(item) + # add some other items + menu.Append(self.popupID2, "Two") + menu.Append(self.popupID3, "Three") + # menu.Append(self.popupID4, "Four") + # menu.Append(self.popupID5, "Five") + # menu.Append(self.popupID6, "Six") + # make a submenu + # sm = wx.Menu() + # sm.Append(self.popupID8, "sub item 1") + # sm.Append(self.popupID9, "sub item 1") + # menu.AppendMenu(self.popupID7, "Test Submenu", sm) + + + # Popup the menu. If an item is selected then its handler + # will be called before PopupMenu returns. + key['canvas'].PopupMenu(menu) + menu.Destroy() + + def OnPopupOne(self): + print("Popup one\n") + + def OnPopupTwo(self): + print("Popup two\n") + + def OnPopupThree(self): + print("Popup three\n") + + def dropmenue(self, ips, x, y, btn, list_items, **key): + # build listF + if not self.list: + list_title = 'test' + self.list = wx.ListCtrl( key['canvas'],pos=(x,y), size=(1, 10),style=wx.LC_REPORT) + self.list.InsertColumn( 0, list_title ) + + + for i, x in enumerate(list_items): + self.list.InsertStringItem(0,x) + self.list.EnsureVisible(i) + + ### 1. Register source's EVT_s to invoke launcher. ### + EVT_LIST_ITEM_RIGHT_CLICK( self.list, -1, self.RightClickCb ) + # clear variables + self.list_item_clicked = None + + # # show & run + key['canvas'].Show(1) + + # def dropmenue(self, ips, x, y, btn, list_items, **key): + + box = wx.BoxSizer(wx.VERTICAL) + + # Make and layout the controls + fs = key['canvas'].GetFont().GetPointSize() + bf = wx.Font(fs+4, wx.SWISS, wx.NORMAL, wx.BOLD) + nf = wx.Font(fs+2, wx.SWISS, wx.NORMAL, wx.NORMAL) + + t = wx.StaticText(key['canvas'], -1, "PopupMenu") + t.SetFont(bf) + box.Add(t, 0, wx.CENTER|wx.ALL, 5) + + box.Add(wx.StaticLine(key['canvas'], -1), 0, wx.EXPAND) + box.Add((10,20)) + + text = 'PopUp Menu' + t = wx.StaticText(key['canvas'], -1, text) + t.SetFont(nf) + box.Add(t, 0, wx.CENTER|wx.ALL, 5) + t.Bind(wx.EVT_CONTEXT_MENU, self.OnContextMenu) + + self.SetSizer(box) + + self.Bind(wx.EVT_CONTEXT_MENU, self.OnContextMenu) + # return 1 + def RightClickCb( self, event ): + # record what was clicked + self.list_item_clicked = right_click_context = event.GetText() + + ### 2. Launcher creates wxMenu. ### + menu = wx.Menu() + for (id,title) in menu_title_by_id.items(): + ### 3. Launcher packs menu with Append. ### + menu.Append( id, title ) + ### 4. Launcher registers menu handlers with EVT_MENU, on the menu. ### + EVT_MENU( menu, id, self.MenuSelectionCb ) + + ### 5. Launcher displays menu with call to PopupMenu, invoked on the source component, passing event's GetPoint. ### + key['canvas'].PopupMenu( menu, event.GetPoint() ) + menu.Destroy() # destroy to avoid mem leak + + def MenuSelectionCb( self, event ): + # do something + operation = menu_title_by_id[ event.GetId() ] + target = self.list_item_clicked + print( 'Perform "%(operation)s" on "%(target)s."' % vars()) + def OnContextMenu(self, event): + self.log.WriteText("OnContextMenu\n") + + # only do this part the first time so the events are only bound once + # + # Yet another anternate way to do IDs. Some prefer them up top to + # avoid clutter, some prefer them close to the object of interest + # for clarity. + if not hasattr(key['canvas'], "popupID1"): + self.popupID1 = wx.NewId() + self.popupID2 = wx.NewId() + self.popupID3 = wx.NewId() + self.popupID4 = wx.NewId() + self.popupID5 = wx.NewId() + self.popupID6 = wx.NewId() + self.popupID7 = wx.NewId() + self.popupID8 = wx.NewId() + self.popupID9 = wx.NewId() + + self.Bind(wx.EVT_MENU, self.OnPopupOne, id=self.popupID1) + self.Bind(wx.EVT_MENU, self.OnPopupTwo, id=self.popupID2) + self.Bind(wx.EVT_MENU, self.OnPopupThree, id=self.popupID3) + self.Bind(wx.EVT_MENU, self.OnPopupFour, id=self.popupID4) + self.Bind(wx.EVT_MENU, self.OnPopupFive, id=self.popupID5) + self.Bind(wx.EVT_MENU, self.OnPopupSix, id=self.popupID6) + self.Bind(wx.EVT_MENU, self.OnPopupSeven, id=self.popupID7) + self.Bind(wx.EVT_MENU, self.OnPopupEight, id=self.popupID8) + self.Bind(wx.EVT_MENU, self.OnPopupNine, id=self.popupID9) + + # make a menu + menu = wx.Menu() + # Show how to put an icon in the menu + item = wx.MenuItem(menu, self.popupID1,"One") + bmp = images.Smiles.GetBitmap() + item.SetBitmap(bmp) + menu.AppendItem(item) + # add some other items + menu.Append(self.popupID2, "Two") + menu.Append(self.popupID3, "Three") + menu.Append(self.popupID4, "Four") + menu.Append(self.popupID5, "Five") + menu.Append(self.popupID6, "Six") + # make a submenu + sm = wx.Menu() + sm.Append(self.popupID8, "sub item 1") + sm.Append(self.popupID9, "sub item 1") + menu.AppendMenu(self.popupID7, "Test Submenu", sm) + + + # Popup the menu. If an item is selected then its handler + # will be called before PopupMenu returns. + self.PopupMenu(menu) + menu.Destroy() + + def OnContextMenu(self, event): + self.log.WriteText("OnContextMenu\n") + # only do this part the first time so the events are only bound once + # + # Yet another anternate way to do IDs. Some prefer them up top to + # avoid clutter, some prefer them close to the object of interest + # for clarity. + if not hasattr(self, "popupID1"): + self.popupID1 = wx.NewId() + self.popupID2 = wx.NewId() + self.popupID3 = wx.NewId() + self.popupID4 = wx.NewId() + self.popupID5 = wx.NewId() + self.popupID6 = wx.NewId() + self.popupID7 = wx.NewId() + self.popupID8 = wx.NewId() + self.popupID9 = wx.NewId() + + self.Bind(wx.EVT_MENU, self.OnPopupOne, id=self.popupID1) + self.Bind(wx.EVT_MENU, self.OnPopupTwo, id=self.popupID2) + self.Bind(wx.EVT_MENU, self.OnPopupThree, id=self.popupID3) + self.Bind(wx.EVT_MENU, self.OnPopupFour, id=self.popupID4) + self.Bind(wx.EVT_MENU, self.OnPopupFive, id=self.popupID5) + self.Bind(wx.EVT_MENU, self.OnPopupSix, id=self.popupID6) + self.Bind(wx.EVT_MENU, self.OnPopupSeven, id=self.popupID7) + self.Bind(wx.EVT_MENU, self.OnPopupEight, id=self.popupID8) + self.Bind(wx.EVT_MENU, self.OnPopupNine, id=self.popupID9) + + # make a menu + menu = wx.Menu() + # Show how to put an icon in the menu + item = wx.MenuItem(menu, self.popupID1,"One") + bmp = images.Smiles.GetBitmap() + item.SetBitmap(bmp) + menu.AppendItem(item) + # add some other items + menu.Append(self.popupID2, "Two") + menu.Append(self.popupID3, "Three") + menu.Append(self.popupID4, "Four") + menu.Append(self.popupID5, "Five") + menu.Append(self.popupID6, "Six") + # make a submenu + sm = wx.Menu() + sm.Append(self.popupID8, "sub item 1") + sm.Append(self.popupID9, "sub item 1") + menu.AppendMenu(self.popupID7, "Test Submenu", sm) + + + # Popup the menu. If an item is selected then its handler + # will be called before PopupMenu returns. + self.PopupMenu(menu) + menu.Destroy() + def OnPopupOne(self, event): + self.log.WriteText("Popup one\n") + + +# dropmenue方法,传入一个[(title, function), ...] \ No newline at end of file diff --git a/imagepy/core/engine/toolFormal.py b/imagepy/core/engine/toolFormal.py new file mode 100644 index 00000000..d1515d3b --- /dev/null +++ b/imagepy/core/engine/toolFormal.py @@ -0,0 +1,237 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat Dec 3 03:55:51 2016 +@author: yxl +""" +from ... import IPy +from ...core.manager import ToolsManager + +from wx import * +import wx, platform +# import wx, platform + +menu_titles = [ "Open", + "Properties", + "Rename", + "Delete" ] + +menu_title_by_id = {} +for title in menu_titles: + # menu_title_by_id[ wxNewId() ] = title + menu_title_by_id[ wx.NewId() ] = title + +list_title = "files" +list_items = [ "binding.py", + "clipboard.py", + "config.py", + "debug.py", + "dialog.py", + "dispatch.py", + "error.py", ] + +class Tool: + title = 'Tool' + view, para = None, None + list = None + + def show(self): + if self.view == None:return + rst = IPy.get_para(self.title, self.view, self.para) + if rst!=None : self.config() + + def config(self):pass + def load(self):pass + def switch(self):pass + + def start(self): + ips = IPy.get_ips() + if not ips is None and not ips.tool is None: + ips.tool = None + ips.update = True + ToolsManager.set(self) + + + def mouse_down(self, ips, x, y, btn, **key): pass + def mouse_up(self, ips, x, y, btn, **key): pass + def mouse_move(self, ips, x, y, btn, **key): pass + def mouse_wheel(self, ips, x, y, d, **key): pass + # def dropmenue(self, ips, x, y, btn, list_items, **key): + # # build listF + # if not self.list: + # list_title = 'test' + # self.list = wx.ListCtrl( key['canvas'],pos=(x,y), size=(1, 10),style=wx.LC_REPORT) + # self.list.InsertColumn( 0, list_title ) + + + # for i, x in enumerate(list_items): + # self.list.InsertStringItem(0,x) + # self.list.EnsureVisible(i) + + # ### 1. Register source's EVT_s to invoke launcher. ### + # EVT_LIST_ITEM_RIGHT_CLICK( self.list, -1, self.RightClickCb ) + # # clear variables + # self.list_item_clicked = None + + # # # show & run + # key['canvas'].Show(1) + + def dropmenue(self, ips, x, y, btn, list_items, **key): + + box = wx.BoxSizer(wx.VERTICAL) + + # Make and layout the controls + fs = key['canvas'].GetFont().GetPointSize() + bf = wx.Font(fs+4, wx.SWISS, wx.NORMAL, wx.BOLD) + nf = wx.Font(fs+2, wx.SWISS, wx.NORMAL, wx.NORMAL) + + t = wx.StaticText(key['canvas'], -1, "PopupMenu") + t.SetFont(bf) + box.Add(t, 0, wx.CENTER|wx.ALL, 5) + + box.Add(wx.StaticLine(key['canvas'], -1), 0, wx.EXPAND) + box.Add((10,20)) + + text = 'PopUp Menu' + t = wx.StaticText(key['canvas'], -1, text) + t.SetFont(nf) + box.Add(t, 0, wx.CENTER|wx.ALL, 5) + t.Bind(wx.EVT_CONTEXT_MENU, self.OnContextMenu) + + self.SetSizer(box) + + self.Bind(wx.EVT_CONTEXT_MENU, self.OnContextMenu) + # return 1 + def RightClickCb( self, event ): + # record what was clicked + self.list_item_clicked = right_click_context = event.GetText() + + ### 2. Launcher creates wxMenu. ### + menu = wx.Menu() + for (id,title) in menu_title_by_id.items(): + ### 3. Launcher packs menu with Append. ### + menu.Append( id, title ) + ### 4. Launcher registers menu handlers with EVT_MENU, on the menu. ### + EVT_MENU( menu, id, self.MenuSelectionCb ) + + ### 5. Launcher displays menu with call to PopupMenu, invoked on the source component, passing event's GetPoint. ### + key['canvas'].PopupMenu( menu, event.GetPoint() ) + menu.Destroy() # destroy to avoid mem leak + + def MenuSelectionCb( self, event ): + # do something + operation = menu_title_by_id[ event.GetId() ] + target = self.list_item_clicked + print( 'Perform "%(operation)s" on "%(target)s."' % vars()) + def OnContextMenu(self, event): + self.log.WriteText("OnContextMenu\n") + + # only do this part the first time so the events are only bound once + # + # Yet another anternate way to do IDs. Some prefer them up top to + # avoid clutter, some prefer them close to the object of interest + # for clarity. + if not hasattr(key['canvas'], "popupID1"): + self.popupID1 = wx.NewId() + self.popupID2 = wx.NewId() + self.popupID3 = wx.NewId() + self.popupID4 = wx.NewId() + self.popupID5 = wx.NewId() + self.popupID6 = wx.NewId() + self.popupID7 = wx.NewId() + self.popupID8 = wx.NewId() + self.popupID9 = wx.NewId() + + self.Bind(wx.EVT_MENU, self.OnPopupOne, id=self.popupID1) + self.Bind(wx.EVT_MENU, self.OnPopupTwo, id=self.popupID2) + self.Bind(wx.EVT_MENU, self.OnPopupThree, id=self.popupID3) + self.Bind(wx.EVT_MENU, self.OnPopupFour, id=self.popupID4) + self.Bind(wx.EVT_MENU, self.OnPopupFive, id=self.popupID5) + self.Bind(wx.EVT_MENU, self.OnPopupSix, id=self.popupID6) + self.Bind(wx.EVT_MENU, self.OnPopupSeven, id=self.popupID7) + self.Bind(wx.EVT_MENU, self.OnPopupEight, id=self.popupID8) + self.Bind(wx.EVT_MENU, self.OnPopupNine, id=self.popupID9) + + # make a menu + menu = wx.Menu() + # Show how to put an icon in the menu + item = wx.MenuItem(menu, self.popupID1,"One") + bmp = images.Smiles.GetBitmap() + item.SetBitmap(bmp) + menu.AppendItem(item) + # add some other items + menu.Append(self.popupID2, "Two") + menu.Append(self.popupID3, "Three") + menu.Append(self.popupID4, "Four") + menu.Append(self.popupID5, "Five") + menu.Append(self.popupID6, "Six") + # make a submenu + sm = wx.Menu() + sm.Append(self.popupID8, "sub item 1") + sm.Append(self.popupID9, "sub item 1") + menu.AppendMenu(self.popupID7, "Test Submenu", sm) + + + # Popup the menu. If an item is selected then its handler + # will be called before PopupMenu returns. + self.PopupMenu(menu) + menu.Destroy() + + def OnContextMenu(self, event): + self.log.WriteText("OnContextMenu\n") + # only do this part the first time so the events are only bound once + # + # Yet another anternate way to do IDs. Some prefer them up top to + # avoid clutter, some prefer them close to the object of interest + # for clarity. + if not hasattr(self, "popupID1"): + self.popupID1 = wx.NewId() + self.popupID2 = wx.NewId() + self.popupID3 = wx.NewId() + self.popupID4 = wx.NewId() + self.popupID5 = wx.NewId() + self.popupID6 = wx.NewId() + self.popupID7 = wx.NewId() + self.popupID8 = wx.NewId() + self.popupID9 = wx.NewId() + + self.Bind(wx.EVT_MENU, self.OnPopupOne, id=self.popupID1) + self.Bind(wx.EVT_MENU, self.OnPopupTwo, id=self.popupID2) + self.Bind(wx.EVT_MENU, self.OnPopupThree, id=self.popupID3) + self.Bind(wx.EVT_MENU, self.OnPopupFour, id=self.popupID4) + self.Bind(wx.EVT_MENU, self.OnPopupFive, id=self.popupID5) + self.Bind(wx.EVT_MENU, self.OnPopupSix, id=self.popupID6) + self.Bind(wx.EVT_MENU, self.OnPopupSeven, id=self.popupID7) + self.Bind(wx.EVT_MENU, self.OnPopupEight, id=self.popupID8) + self.Bind(wx.EVT_MENU, self.OnPopupNine, id=self.popupID9) + + # make a menu + menu = wx.Menu() + # Show how to put an icon in the menu + item = wx.MenuItem(menu, self.popupID1,"One") + bmp = images.Smiles.GetBitmap() + item.SetBitmap(bmp) + menu.AppendItem(item) + # add some other items + menu.Append(self.popupID2, "Two") + menu.Append(self.popupID3, "Three") + menu.Append(self.popupID4, "Four") + menu.Append(self.popupID5, "Five") + menu.Append(self.popupID6, "Six") + # make a submenu + sm = wx.Menu() + sm.Append(self.popupID8, "sub item 1") + sm.Append(self.popupID9, "sub item 1") + menu.AppendMenu(self.popupID7, "Test Submenu", sm) + + + # Popup the menu. If an item is selected then its handler + # will be called before PopupMenu returns. + self.PopupMenu(menu) + menu.Destroy() + def OnPopupOne(self, event): + self.log.WriteText("Popup one\n") + + +# dropmenue方法,传入一个[(title, function), ...] + + \ No newline at end of file diff --git a/imagepy/menus/Plugins/Manager/plgtree_wgt.py b/imagepy/menus/Plugins/Manager/plgtree_wgt.py index 78b70bc4..6fdf0706 100644 --- a/imagepy/menus/Plugins/Manager/plgtree_wgt.py +++ b/imagepy/menus/Plugins/Manager/plgtree_wgt.py @@ -23,13 +23,8 @@ def __init__( self, parent ): self.tre_plugins = wx.TreeCtrl( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TR_DEFAULT_STYLE ) -<<<<<<< HEAD - self.tre_plugins.SetMinSize( wx.Size( 200,-1 ) ) - -======= + # self.tre_plugins.SetMinSize( wx.Size( 200,-1 ) ) self.tre_plugins.SetMinSize( wx.Size( 300,-1 ) ) - ->>>>>>> Image-Py/master bSizer1.Add( self.tre_plugins, 0, wx.ALL|wx.EXPAND, 5 ) bSizer3 = wx.BoxSizer( wx.VERTICAL ) bSizer4 = wx.BoxSizer( wx.HORIZONTAL ) @@ -93,13 +88,8 @@ def load(self): keydata[name].extend(j[1]) else: datas[1].append(j) root = self.tre_plugins.AddRoot('Plugins') -<<<<<<< HEAD - self.addnode(root, data[1]) -======= self.addnode(root, datas[1]) - ->>>>>>> Image-Py/master # Virtual event handlers, overide them in your derived class def on_run( self, event ): plg = self.tre_plugins.GetItemPyData(event.GetItem()) diff --git a/imagepy/menus/Plugins/Match/surf_plg.py b/imagepy/menus/Plugins/Match/surf_plg.py index 4487048b..cbfab262 100755 --- a/imagepy/menus/Plugins/Match/surf_plg.py +++ b/imagepy/menus/Plugins/Match/surf_plg.py @@ -137,6 +137,7 @@ def run(self, ips, imgs, para = None): style = para['style']=='Blue/Yellow' idx, msk, m = Matcher(dim, std).filter(kps1,feats1,kps2,feats2) + picker1 = Pick(kps1, kps2, idx, msk, ips1, ips2, True, style) picker2 = Pick(kps1, kps2, idx, msk, ips1, ips2, False, style) diff --git a/imagepy/menus/Plugins/New/demo_simple.py b/imagepy/menus/Plugins/New/demo_simple.py index f43cac67..b8cc27e3 100644 --- a/imagepy/menus/Plugins/New/demo_simple.py +++ b/imagepy/menus/Plugins/New/demo_simple.py @@ -11,6 +11,6 @@ class Plugin(Simple): # increase the current def run(self, ips, imgs, para = None): if ips.cur +#

PopupMenu

+# """ + text + """ +# +# """ + + + +if __name__ == '__main__': + import sys,os + import run + run.main(['', os.path.basename(sys.argv[0])] + sys.argv[1:]) diff --git a/imagepy/test.py b/imagepy/test.py new file mode 100644 index 00000000..80d048c5 --- /dev/null +++ b/imagepy/test.py @@ -0,0 +1,74 @@ +# from wxPython.wx import * +from wx import * +import wx, platform +# import wx, platform + +menu_titles = [ "Open", + "Properties", + "Rename", + "Delete" ] + +menu_title_by_id = {} +for title in menu_titles: + # menu_title_by_id[ wxNewId() ] = title + menu_title_by_id[ wx.NewId() ] = title + + + +list_title = "files" +list_items = [ "binding.py", + "clipboard.py", + "config.py", + "debug.py", + "dialog.py", + "dispatch.py", + "error.py", ] + + + +class App( wx.PySimpleApp ): + def OnInit( self ): + # build frame + frame = wx.Frame(None, -1, "Hello from wxPython") + self.frame = frame # we'll use in RightClickCb + + # build listF + # list = wx.ListCtrl( frame, -1, style=wxLC_REPORT ) + list = wx.ListCtrl( frame, -1, style=wx.LC_REPORT) + list.InsertColumn( 0, list_title ) + for x in list_items: list.InsertStringItem(0,x) + + ### 1. Register source's EVT_s to invoke launcher. ### + EVT_LIST_ITEM_RIGHT_CLICK( list, -1, self.RightClickCb ) + + # clear variables + self.list_item_clicked = None + + # show & run + frame.Show(1) + return 1 + + def RightClickCb( self, event ): + # record what was clicked + self.list_item_clicked = right_click_context = event.GetText() + + ### 2. Launcher creates wxMenu. ### + menu = wx.Menu() + for (id,title) in menu_title_by_id.items(): + ### 3. Launcher packs menu with Append. ### + menu.Append( id, title ) + ### 4. Launcher registers menu handlers with EVT_MENU, on the menu. ### + EVT_MENU( menu, id, self.MenuSelectionCb ) + + ### 5. Launcher displays menu with call to PopupMenu, invoked on the source component, passing event's GetPoint. ### + self.frame.PopupMenu( menu, event.GetPoint() ) + menu.Destroy() # destroy to avoid mem leak + + def MenuSelectionCb( self, event ): + # do something + operation = menu_title_by_id[ event.GetId() ] + target = self.list_item_clicked + print( 'Perform "%(operation)s" on "%(target)s."' % vars()) + +app = App() +app.MainLoop() \ No newline at end of file diff --git a/imagepy/tools/Measure/distance_tol.py b/imagepy/tools/Measure/distance_tol.py index 00b6f8cd..f4dfb097 100644 --- a/imagepy/tools/Measure/distance_tol.py +++ b/imagepy/tools/Measure/distance_tol.py @@ -13,136 +13,323 @@ from .setting import Setting from imagepy import IPy +import itertools +import six +import copy + class Distance: - """Define the distance class""" - dtype = 'distance' - def __init__(self, body=None, unit=None): - self.body = body if body!=None else [] - self.buf, self.unit = [], unit - - def addline(self): - line = self.buf - if len(line)!=2 or line[0] !=line[-1]: - self.body.append(line) - self.buf = [] - - def snap(self, x, y, lim): - minl, idx = 1000, None - for i in self.body: - for j in i: - d = (j[0]-x)**2+(j[1]-y)**2 - if d < minl:minl,idx = d,(i, i.index(j)) - return idx if minl**0.56: - j-=5 - if i <=0: - continue - dc.DrawText('%.2f'%(i * unit), f(*j)) - - font = wx.Font(20, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False) - dc.SetFont(font) - dc.DrawText('%.2f'%(tempDist * unit), f(midPnt[0],midPnt[1])) - - - def report(self, title): - rst = [] - for line in self.body: - pts = np.array(line) - dis = norm((pts[:-1]-pts[1:]), axis=1) - dis *= 1 if self.unit is None else self.unit[0] - rst.append(list(dis.round(2))) - lens = [len(i) for i in rst] - maxlen = max(lens) - fill = [[0]*(maxlen-i) for i in lens] - rst = [i+j for i,j in zip(rst, fill)] - titles = ['L{}'.format(i+1) for i in range(maxlen)] - IPy.show_table(pd.DataFrame(rst, columns=titles), title) + """Define the distance class""" + dtype = 'distance' + def __init__(self, body=None, unit=None): + self.body = body if body!=None else [] + self.buf, self.unit = [], unit + + def addline(self): + line = self.buf + self.body = [] + curLine = [] + if len(line)!=2 or line[0] !=line[-1]: + for i in line: + if i[0] !=-1 and i[1]!=-1: + curLine.append(i) + else: + self.body.append(curLine) + curLine=[] + def delLine(self, x, y): + lines = self.buf + + # 先将 线段 切块; + # 再 计算每个线段和 点击点的位置 + # 如果 点距离线段的距离 小于 0.3, 则删除这个点 + tempList = [] + for i in range(len(lines)): + tempList.append((lines[i][0]-x , lines[i][1]-y )) + + + absDist = [ (np.abs(i[0]) + np.abs(i[1])) for i in tempList ] + + pntPos = np.argmin(absDist) + + if absDist[pntPos] > 10: + return + + blockIndex = [ i for i,j in enumerate(lines) if j==(-1,-1) ] + + blockIndex.insert(0,0) + + blockIndex3 = [ i-pntPos for i in blockIndex] + + # print('blockIndex3:{}\n'.format(blockIndex3)) + + blockIndex2 = [ blockIndex3[i] * blockIndex3[i+1] <= 0 for i,j in enumerate(blockIndex3[:-1])] + + # print('blockIndex2:{}\n'.format(blockIndex2)) + + + curIndex = 0 + if blockIndex2: + curIndex = np.argmax(blockIndex2) + + # print('curIndex:{}\n'.format(curIndex)) + # print('before self.body:{}\n',self.body) + + if blockIndex2: + startIndexToDelete = blockIndex[curIndex] + else: + startIndexToDelete = -1 + + tempBody = copy.deepcopy(self.buf) + + # print('before tempBody:{}\n',tempBody) + # print('before self.body:{}\n',self.buf) + # print('startIndexToDelete:{}\n',startIndexToDelete) + + removedIndex = [] + for i, j in enumerate(self.buf): + if i >= startIndexToDelete and j[0] != -1: + removedIndex.append(i) + if self.buf[i+1][0] == -1: + removedIndex.append(i+1) + break + + curBody = [ y for i,y in enumerate(self.buf) if i not in removedIndex ] + + self.buf = curBody + self.addline() + + + def snap(self, x, y, lim): + minl, idx = 1000, None + for i in self.body: + for j in i: + d = (j[0]-x)**2+(j[1]-y)**2 + if d < minl:minl,idx = d,(i, i.index(j)) + + return idx if minl**0.51: + dc.DrawLines([f(*i) for i in curLine if i[0] != -1 ]) + curLine=[] + + self.unit = Setting['ratioRuler'] + + font = wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False) + dc.SetFont(font) + dc.SetTextForeground(Setting['tcolor']) + + for line in self.body: + tempDist = 0 + if len(line) <=1 or line[0] == -1 and line[-1]== -1: + continue + + dc.DrawLines([f(*i) for i in line ]) + + for i in line : dc.DrawCircle(f(*i),2) + + # if g_DebugMode == True: + # print('line:{}'.format(line)) + + pts = np.array(line) + mid = (pts[:-1]+pts[1:])/2 + midPnt = (pts[-1]+pts[0]+(20,20))/2 + + dis = norm((pts[:-1]-pts[1:]), axis=1) + unit = 1 if self.unit is None else self.unit + + for i,j in zip(dis, mid): + tempDist += i + if j[0]>6: + j-=5 + if i <=0: + continue + dc.DrawText('%.2f'%(i * unit), f(*j)) + + font = wx.Font(15, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False) + # dc.SetTextForeground(Setting['hcolor']) + dc.SetFont(font) + dc.DrawText('%.2f mm'%(tempDist * unit), f(midPnt[0],midPnt[1])) + + def report(self, title): + rst = [] + for line in self.body: + pts = np.array(line) + dis = norm((pts[:-1]-pts[1:]), axis=1) + dis *= 1 if self.unit is None else self.unit + rst.append(list(dis.round(2))) + lens = [len(i) for i in rst] + maxlen = max(lens) + fill = [[0]*(maxlen-i) for i in lens] + rst = [i+j for i,j in zip(rst, fill)] + titles = ['L{}'.format(i+1) for i in range(maxlen)] + + lineTitle = ['{}'.format(i+1) for i in range(len(self.body))] + newLine = [list(row) for row in six.moves.zip_longest(*rst, fillvalue=0.0)] + + IPy.show_table(pd.DataFrame(newLine, columns=lineTitle), title) + + def updateReport(self, title): + if not (IPy.get_tps()): + return + data = IPy.get_tps().data + tempbuf = [] + self.buf = [] + for indexs in range(len(data.columns.values.tolist())): + + tempIndex = int(data.columns[indexs]) + print('cur index:{}'.format(tempIndex)) + + tempbuf.append(self.body[tempIndex][:]) + print('tempbuf:{}'.format(self.body[tempIndex])) + tempbuf.append([(-1,-1)]) + + for i in range(len(tempbuf)): + for x in range(len(tempbuf[i])): + self.buf.append(tempbuf[i][x] ) + self.addline() class Plugin(Tool): - """Define the diatance class plugin with the event callback functions""" - title = 'Distance' - def __init__(self): - self.curobj = None - self.doing = False - self.odx,self.ody = 0, 0 - - def mouse_down(self, ips, x, y, btn, **key): - if key['ctrl'] and key['alt']: - if isinstance(ips.mark, Distance): - ips.mark.report(ips.title) - return - - lim = 5.0/key['canvas'].get_scale() - if btn==1: - if not self.doing: - if isinstance(ips.mark, Distance): - self.curobj = ips.mark.pick(x, y, lim) - if self.curobj!=None:return - - if not isinstance(ips.mark, Distance): - ips.mark = Distance(unit=ips.unit) - self.doing = True - elif key['shift']: - self.doing = True - else: ips.mark = None - if self.doing: - ips.mark.buf.append((x,y)) - self.curobj = (ips.mark.buf, -1) - self.odx, self.ody = x,y - - elif btn==3: - if self.doing: - ips.mark.buf.append((x,y)) - self.doing = False - ips.mark.addline() - ips.update = True - - def mouse_up(self, ips, x, y, btn, **key): - self.curobj = None - - def mouse_move(self, ips, x, y, btn, **key): - if not isinstance(ips.mark, Distance):return - lim = 5.0/key['canvas'].get_scale() - if btn==None: - self.cursor = wx.CURSOR_CROSS - if ips.mark.snap(x, y, lim)!=None: - self.cursor = wx.CURSOR_HAND - elif btn==1: - ips.mark.draged(self.odx, self.ody, x, y, self.curobj) - ips.update = True - self.odx, self.ody = x, y - - def mouse_wheel(self, ips, x, y, d, **key): - pass + """Define the diatance class plugin with the event callback functions""" + title = 'Distance' + def __init__(self): + + self.curobj = None + self.doing = False + self.odx,self.ody = 0, 0 + self.list_items = ['del', 'copy','move'] + self.popUpMenuInitialized = False + + def mouse_down(self, ips, x, y, btn, **key): + + if key['shift'] and isinstance(ips.mark, Distance): + print('updateReport!') + ips.mark.updateReport(ips.title) + + if key['ctrl'] and key['alt']: + # print('key mouse_down:{}'.format(key)) + # print('table in') + if isinstance(ips.mark, Distance): + ips.mark.report(ips.title) + return + + lim = 5.0/key['canvas'].get_scale() + if btn==1:# 按下了左键 + if not self.doing: + if isinstance(ips.mark, Distance): + self.curobj = ips.mark.pick(x, y, lim) + if not isinstance(ips.mark, Distance): + ips.mark = Distance(unit=ips.unit) + self.doing = True + print('A new Distance instance is added!') + elif key['shift']: + self.doing = True + else: + x = 1 + + if self.doing: + ips.mark.buf.append((x,y)) + self.curobj = (ips.mark.buf, -1) + self.odx, self.ody = x, y + + elif btn==3: ### btn == 3 代表是右键被按下; + # print('Right bottom is pushed!') + if self.doing: + # ips.mark.buf.append((x,y)) + ips.mark.buf.append((-1,-1)) + lineNum = [i for i,x in enumerate(ips.mark.buf) if x == (-1,-1)] + ips.mark.addline() + elif btn==2: + if key['shift']: + if isinstance(ips.mark, Distance): + self.curobj = ips.mark.pick(x, y, lim) + # ips.mark.buf.append((x,y)) + # self.odx, self.ody = x, y + ips.mark.delLine(x,y) + + # if not self.popUpMenuInitialized: + # self.popUpInit(**key) + # self.popUpMenuInitialized = True + # self.OnContextMenu(**key) + ips.update = True + + def mouse_up(self, ips, x, y, btn, **key): + print('mouse is up!') + + def mouse_move(self, ips, x, y, btn, **key): + if not isinstance(ips.mark, Distance):return + lim = 5.0/key['canvas'].get_scale() + if btn==None: + self.cursor = wx.CURSOR_CROSS + if ips.mark.snap(x, y, lim)!=None: + self.cursor = wx.CURSOR_HAND + elif btn==1: + if self.curobj is not None: + ips.mark.draged(self.odx, self.ody, x, y, self.curobj) + ips.update = True + self.odx, self.ody = x, y + + def mouse_wheel(self, ips, x, y, d, **key): + pass + + def OnContextMenu(self, **key): + print("OnContextMenu\n") + + # only do this part the first time so the events are only bound once + # + # Yet another anternate way to do IDs. Some prefer them up top to + # avoid clutter, some prefer them close to the object of interest + # for clarity. + if not hasattr(self, "popupID1"): + + self.popupID1 = wx.NewId() + self.popupID2 = wx.NewId() + self.popupID3 = wx.NewId() + + self.Bind(wx.EVT_MENU, self.OnPopupOne, id=self.popupID1) + self.Bind(wx.EVT_MENU, self.OnPopupTwo, id=self.popupID2) + self.Bind(wx.EVT_MENU, self.OnPopupThree, id=self.popupID3) + + # make a menu + menu = wx.Menu() + # Show how to put an icon in the menu + item = wx.MenuItem(menu, self.popupID1,"Del Current Line") + # bmp = images.Smiles.GetBitmap() + # item.SetBitmap(bmp) + menu.AppendItem(item) + # add some other items + menu.Append(self.popupID2, "Two") + menu.Append(self.popupID3, "Three") + + # Popup the menu. If an item is selected then its handler + # will be called before PopupMenu returns. + key['canvas'].PopupMenu(menu) + menu.Destroy() + + def OnPopupOne(self, event): + print("Popup one\n") + + + def OnPopupTwo(self, event): + print("Popup two\n") + + def OnPopupThree(self, event): + print("Popup three\n") diff --git a/imagepy/tools/Measure/distance_tol_BackUp.py b/imagepy/tools/Measure/distance_tol_BackUp.py new file mode 100644 index 00000000..a700a6a4 --- /dev/null +++ b/imagepy/tools/Measure/distance_tol_BackUp.py @@ -0,0 +1,232 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Feb 3 22:21:32 2017 + +@author: yxl +""" + +import wx +from imagepy.core.engine import Tool +import numpy as np +import pandas as pd +from numpy.linalg import norm +from .setting import Setting +from imagepy import IPy + +class Distance: + """Define the distance class""" + dtype = 'distance' + def __init__(self, body=None, unit=None): + self.body = body if body!=None else [] + self.buf, self.unit = [], unit + + def addline(self): + line = self.buf + self.body = [] + curLine = [] + if len(line)!=2 or line[0] !=line[-1]: + for i in line: + if i[0] !=-1 and i[1]!=-1: + curLine.append(i) + else: + self.body.append(curLine) + curLine=[] + + # self.buf = [] + + def snap(self, x, y, lim): + minl, idx = 1000, None + for i in self.body: + for j in i: + d = (j[0]-x)**2+(j[1]-y)**2 + if d < minl:minl,idx = d,(i, i.index(j)) + return idx if minl**0.51: + dc.DrawLines([f(*i) for i in curLine if i[0] != -1 ]) + curLine=[] + # dc.DrawLines([f(*i) for i in self.buf) + # for i in self.buf: + # if i[0]!=-1 and i[1] != -1: + # dc.DrawCircle(f(*i),2) + # print('current pixel ratio is:{}'.format(Setting['ratioRuler'])) + self.unit = Setting['ratioRuler'] + + print('body:{}'.format(len(self.body))) + for line in self.body: + tempDist = 0 + # print('line:{}'.format(line)) + if len(line) <=1 or line[0] == -1 and line[-1]== -1: + continue + dc.DrawLines([f(*i) for i in line ]) + + for i in line:dc.DrawCircle(f(*i),2) + pts = np.array(line) + mid = (pts[:-1]+pts[1:])/2 + + midPnt = np.median( pts, axis=1) + + dis = norm((pts[:-1]-pts[1:]), axis=1) + # unit = 1 if self.unit is None else self.unit[0] + unit = 1 if self.unit is None else self.unit + + for i,j in zip(dis, mid): + tempDist += i + if j[0]>6: + j-=5 + if i <=0: + continue + dc.DrawText('%.2f'%(i * unit), f(*j)) + + font = wx.Font(20, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False) + dc.SetFont(font) + dc.DrawText('%.2f mm'%(tempDist * unit), f(midPnt[0],midPnt[1])) + + def report(self, title): + rst = [] + for line in self.body: + pts = np.array(line) + dis = norm((pts[:-1]-pts[1:]), axis=1) + dis *= 1 if self.unit is None else self.unit + rst.append(list(dis.round(2))) + lens = [len(i) for i in rst] + maxlen = max(lens) + fill = [[0]*(maxlen-i) for i in lens] + rst = [i+j for i,j in zip(rst, fill)] + titles = ['L{}'.format(i+1) for i in range(maxlen)] + # searchTitle = [1, len(self.body)] + IPy.show_table(pd.DataFrame(rst, columns=titles), title) + def updateReport(self, title): + data = IPy.get_tps().data + tempbuf = [] + self.buf = [] + for indexs in data.index: + print('cur index:{}'.format(indexs)) + # temp = list(filter(lambda a: a != 0.0, data.loc[indexs].values[0:-1])) + tempbuf.append(self.body[indexs][:]) + print('tempbuf:{}'.format(self.body[indexs])) + tempbuf.append([(-1,-1)]) + for i in range(len(tempbuf)): + for x in range(len(tempbuf[i])): + self.buf.append(tempbuf[i][x] ) + + # self.buf = [ tempbuf[i][x] for x in len(tempbuf[i]) for i in len(tempbuf)] + print('buf:{}'.format(self.buf)) + + self.addline() + + # rst = [] + # for line in self.body: + # pts = np.array(line) + # dis = norm((pts[:-1]-pts[1:]), axis=1) + # dis *= 1 if self.unit is None else self.unit + # rst.append(list(dis.round(2))) + # lens = [len(i) for i in rst] + # maxlen = max(lens) + # fill = [[0]*(maxlen-i) for i in lens] + # rst = [i+j for i,j in zip(rst, fill)] + # titles = ['L{}'.format(i+1) for i in range(maxlen)] + # # searchTitle = [1, len(self.body)] + # IPy.show_table(pd.DataFrame(rst, columns=titles), title) + +class Plugin(Tool): + """Define the diatance class plugin with the event callback functions""" + title = 'Distance' + def __init__(self): + self.curobj = None + self.doing = False + self.odx,self.ody = 0, 0 + + def mouse_down(self, ips, x, y, btn, **key): + + if key['shift'] and isinstance(ips.mark, Distance): + ips.mark.updateReport(ips.title) + + if key['ctrl'] or key['alt']: + print('key mouse_down:{}'.format(key)) + print('table in') + if isinstance(ips.mark, Distance): + ips.mark.report(ips.title) + + return + + lim = 5.0/key['canvas'].get_scale() + if btn==1: + if not self.doing: + if isinstance(ips.mark, Distance): + self.curobj = ips.mark.pick(x, y, lim) + # if self.curobj!=None:return + if not isinstance(ips.mark, Distance): + ips.mark = Distance(unit=ips.unit) + self.doing = True + print('A new Distance instance is added!') + elif key['shift']: + self.doing = True + else: + x = 1 + # ips.mark = None + # if not isinstance(ips.mark, Distance): + # ips.mark = Distance(unit=ips.unit) + # self.doing = True + # print('A new Distance instance is added!') + # elif key['shift']: + # self.doing = True + # else: ips.mark = None + if self.doing: + ips.mark.buf.append((x,y)) + self.curobj = (ips.mark.buf, -1) + self.odx, self.ody = x,y + + elif btn==3: ### btn == 3 代表是右键被按下; + print('Right bottom is pushed!') + if self.doing: + ips.mark.buf.append((x,y)) + ips.mark.buf.append((-1,-1)) + lineNum = [i for i,x in enumerate(ips.mark.buf) if x == (-1,-1)] + # self.doing = False + ips.mark.addline() + ips.update = True + + def mouse_up(self, ips, x, y, btn, **key): + # self.curobj = None + print('mouse is up!') + + def mouse_move(self, ips, x, y, btn, **key): + if not isinstance(ips.mark, Distance):return + lim = 5.0/key['canvas'].get_scale() + if btn==None: + self.cursor = wx.CURSOR_CROSS + if ips.mark.snap(x, y, lim)!=None: + self.cursor = wx.CURSOR_HAND + elif btn==1: + if self.curobj is not None: + ips.mark.draged(self.odx, self.ody, x, y, self.curobj) + ips.update = True + self.odx, self.ody = x, y + + def mouse_wheel(self, ips, x, y, d, **key): + pass diff --git a/imagepy/tools/Measure/setting.py b/imagepy/tools/Measure/setting.py index a804c05d..5dd9f570 100644 --- a/imagepy/tools/Measure/setting.py +++ b/imagepy/tools/Measure/setting.py @@ -1 +1 @@ -Setting = {'color':(255,255,0), 'tcolor':(255,0,255), 'bcolor':(255,255,255),'width':1} +Setting = {'color':(255,255,0),'hcolor':(155,155,0), 'tcolor':(255,0,255), 'bcolor':(255,255,255),'width':1, 'ratioRuler':1.0} diff --git a/testPanda.py b/testPanda.py new file mode 100644 index 00000000..8f4076c1 --- /dev/null +++ b/testPanda.py @@ -0,0 +1,39 @@ +import pandas as pd +import numpy as np + +import itertools +import six + +df = pd.DataFrame(np.arange(12).reshape(3,4),columns=['0', '1', '2', '3']) + +x = df.drop(['2'], axis=1) +print('df {}'.format(df)) +# print('x {}'.format(x)) +print('index {}'.format(x.index)) + +dfList = df.T.values.tolist() + +print('dfList {}'.format(dfList)) +print('dfList 0 {}'.format(len(dfList))) + + +for index in range(len(x.columns.values.tolist())): + print(index) + print(x.columns[index]) + + print('data {} {}'.format(index, x.iloc[:,index].values.tolist())) + +print('data 2 {}'.format(df.iloc[:,2])) +# print('data -1 {}'.format(df.iloc[:,-1])) + +y = df.drop(columns=['B','C']) +print('y {}'.format(y)) +print('df {}'.format(df)) + + +x = [[1,2,3], [4,5,6, 6.1, 6.2, 6.3], [7,8,9]] +newLine = map(list,map(None,*x)) +newLine = [list(row) for row in six.moves.zip_longest(*x, fillvalue=0.0)] + +print('x {}'.format(x)) +print('newLine {}'.format(newLine)) \ No newline at end of file