monoからは、結構すぐ移植できました。
とりあえず動けばいいという感じで、まぁ汚いコードですけどね…
以下に並べたvector2d.py(実行権限を削っておいたほうがいいと思います、プラグインロードが無意味に長くなると思うので)とperspective_grid.py(こっちは実行権限が必要) を、~/.gimp-2.8/plug-ins/に保存してgimpを起動すれば、メニューのPython-fu/layer/にperspective-grid-layerが出来ているはずなので、パスツールで任意の4点を時計回りに設定して動かすとグリッドが出来る…と思います。たぶん。
ちなみに、ベジェ曲線は無視されアンカーポイントだけが利用されるので、マウス操作をしくじって多少曲線になってしまっても問題なし。
グリッドはレイヤとして作成されるので、半透明にしたり上書きすることも可能!
なお、元となる四角形を修正するわけではなく、無理なパースは無理な分割になるので、視覚的に違和感のある場合は、無理な四角形が元になっているのだな…と諦めてください。
多分バグ山盛りだと思いますが、ぼちぼち直していく予定…。
おっと!アフィリエイトだ!!いや、この本持ってないですけど(汗 割と評価が高かったもので…
さて、スクリプトです。
vector2d.py
#!/usr/bin/env python # -*- coding: UTF-8 -*- import math class vector2d: def __init__(s,x=0.0,y=0.0): s.x=x s.y=y def __repr__(self): return "vector (%f , %f)" % (self.x , self.y) def __add__(s,other): return vector2d( s.x+other.x, s.y+other.y ) def __iadd__(s,other): s.x+=other.x s.y+=other.y return s def __sub__(s,other): return vector2d( s.x-other.x, s.y-other.y ) def __isub__(s,other): s.x-=other.x s.y-=other.y return s def dot(s,other): return s.x*other.x + s.y*other.y def cross(s,other): """ in math,there is no cross-product in 2D vector. cross-product can be in only 3D vector. but for convinience,I created cross products thats treat 2d vector as virtually 3d vector by adding fixed Z axis value(i.e, z=1) and this method returns only culculated Z value if this Z value is NEGATIVE, the 'other' vector locates ... -> counter-clockwise from this vector @ screen-coordinate(or right-handed-system) -> clockwise from this vector @ ordinary-coordinate(left-handed-system) """ return s.x*other.y-other.x*s.y def __mul__(s,other): if type(other)==float: return vector2d( s.x*other, s.y*other ) elif type(other)==int: other = float(other) return vector2d( s.x*other, s.y*other ) else: try: return s.dot(other) except: pass try: # assume to matrix3.otherwise,exception should be raised. return s.mult_matrix(other) except: raise TypeError def __imul__(s,other): if type(other)==float: s.x*=other s.y*=other elif type(other)==int: other = float(other) s.x*=other s.y*=other else: # dot product cannot be applied to vector itself # because output is scalar. try: # assume to matrix3.otherwise,exception should be raised. s.mult_matrix_self(other) except: raise TypeError return s def __div__(s,other): if type(other)==float: return vector2d( s.x/other, s.y/other ) def get_scalar(s): return math.sqrt((s.x*s.x) + (s.y * s.y)) def get_angle(s,other): """ get the angle of 'self' with the 'other' in RADIAN. for example, when you want to get angle from a base-vector 'bvec' assigned (0.0 , 1.0) to a direction-vector 'dvec' (-128.0,128.0), (offcouse you MUST normalize direction-vector previously) you can get angle with following code: angle = bvec.get_angle(dvec) if the 'other' vector locates in counter-clock wise from 'self' vector, return value should be negative. """ cz = s.cross(other) dp = s.dot(other) if dp<=-1.0: return math.pi elif dp >= 1.0: return 0.0 try: if cz > 0.0: return -math.acos(dp) else: return math.acos(dp) except: print("vec s.x,s.y %f,%f / other %f,%f / dot %f " % (s.x,s.y,other.x,other.y,s.dot(other))) def get_angle_abs(s,other): """ get the angle of 'self' with the 'other' in RADIAN. this method returns simply angle of two normalized vector with absolute value. so,each vector2d objects should be normalized preceding calling this method. """ dp = s.dot(other) if dp <= -1.0: return math.pi elif dp >= 1.0: return 0.0 try: return math.acos(dp) except: print("vec s.x,s.y %f,%f / other %f,%f / dot %f " % (s.x,s.y,other.x,other.y,s.dot(other))) def get_degree(s,other): """ get the angle of 'self' with the 'other' in degree. this method is a wrapper conversion method of 'get_angle()'. """ return s.get_angle(other)*57.295779513082323 def get_normalized(s): sc= s.get_scalar() if sc!=0.0: return vector2d(s.x / sc,s.y / sc) def normalize(s): # deprecated,remained for compatibility return s.get_normalized() def normalize_self(s): sc= s.get_scalar() if sc!=0.0: s.x /= sc s.y /= sc def rotate(s,angle): """ this rotation is clockwize.in RADIAN(0.0 to pi*2) """ return vector2d( s.x * math.cos(angle) + s.y * math.sin(angle), -s.x * math.sin(angle) + s.y * math.cos(angle)) def rotate_self(s,angle): """ this rotation is clockwize.in RADIAN(0.0 to pi*2) """ nx=s.x * math.cos(angle) + s.y * math.sin(angle) ny=-s.x * math.sin(angle) + s.y * math.cos(angle) s.x=nx s.y=ny def tuple(self): return (self.x,self.y) def set(self,aX,aY): self.x=aX self.y=aY def is_zero(self): return ((self.x==0.0) and (self.y==0.0)) @staticmethod def sub(out,a,b): out.x=a.x-b.x out.y=a.y-b.y return out @staticmethod def add(out,a,b): out.x=a.x+b.x out.y=a.y+b.y return out @staticmethod def mul(out,a,b): out.x=a.x*b out.y=a.y*b return out @staticmethod def cross(a,b): return a.x*b.y - b.x*a.y @staticmethod def dot(a,b): return a.x*a.x + a.y*a.y @staticmethod def normalize(out,s): sc= s.get_scalar() if sc!=0.0: out.x=s.x / sc out.y=s.y / sc else: raise ValueError @staticmethod def rotate(out,s,angle): """ this rotation is clockwize.in RADIAN(0.0 to pi*2) """ out.x=s.x * math.cos(angle) + s.y * math.sin(angle) out.y=-s.x * math.sin(angle) + s.y * math.cos(angle) return out
perspective_grid.py
#!/usr/bin/env python # -*- coding: UTF-8 -*- #[license] GPLv3 #[plugin] #[name] perspective-grid-layer #[desc] #透視法に基づくグリッドの描かれたレイヤを選択されたパスに基づいて生成 #[version] #0.1 初期リリース #[end] # このプログラムはGPLライセンスver3で公開します。 # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You may have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # # Copyright 2014 dothiko(http://dothiko.hatenablog.com/) from gimpfu import * from vector2d import * import random class Perspective_grid: def __init__(self,image,thick_size,mid_size,thin_size): self.BD=vector2d() self.AB=vector2d() self.BC=vector2d() self.AC=vector2d() self.img=image self.id=random.random() * 2147483647 self.sizes=(thick_size,mid_size,thin_size) def main(self,raw_stroke,recursive_max): margin=32 # the gridpt contains 4 lists,they are list of vectors to represent grid startpoints # that index should be... # # A---0---B # | | # 3 c 1 # | | # D---2---C # # the each number of ridges is the index of grid-point-list(self.gridpt). self.m_gridpt=([],[],[],[]) self.m_basevectors=[] for i in range(0,4): cv=vector2d() cv.x=raw_stroke[i*6+2] cv.y=raw_stroke[i*6+3] self.m_basevectors.append(cv) # culculate min-max coordinate of assigned vectors # and use them for layer dimension. min_x=min(self.m_basevectors,key=lambda v:v.x).x min_y=min(self.m_basevectors,key=lambda v:v.y).y max_x=max(self.m_basevectors,key=lambda v:v.x).x max_y=max(self.m_basevectors,key=lambda v:v.y).y width=int(max_x - min_x)+margin*2 height=int(max_y - min_y)+margin*2 top=int(min_y-margin) left=int(min_x-margin) # setup vanising points self.m_vp=(vector2d(),vector2d()) self.get_crossed_point(self.m_vp[0], self.m_basevectors[1], self.m_basevectors[0], self.m_basevectors[2], self.m_basevectors[3]); self.get_crossed_point(self.m_vp[1], self.m_basevectors[2], self.m_basevectors[0], self.m_basevectors[3], self.m_basevectors[1]); self.m_recursive_max=recursive_max self.divide_plane() layer = gimp.Layer(self.img, "perspective grid layer", width, height, RGBA_IMAGE, 100, NORMAL_MODE) self.img.add_layer(layer,0) layer.set_offsets(left,top) self.draw_grid(layer) def get_crossed_point(self,ov,a,b,c,d): """ get crossed point from vectors. Arguments: ov -- output vector, to consider recycle and low memory usage. a,b,c,d -- the vectors Returns: the cross-product of vectors,which indicates whether the crossed point actually crossed when that value is positive.when negative,they are not crossed but to be crossed. """ vector2d.sub(self.BD,d,b); vector2d.sub(self.AB,b,a); vector2d.sub(self.BC,c,b); vector2d.sub(self.AC,c,a); cross_AB=vector2d.cross(self.BD,self.AB); cross_BC=vector2d.cross(self.BD,self.BC); try: ratio=cross_AB / (cross_AB + cross_BC); except ZeroDivisionError: # vanising point dose not exist ov.x=None ov.y=None return None ov.x=a.x + ratio*(self.AC.x); ov.y=a.y + ratio*(self.AC.y); return ratio; def divide_plane(self): self.divide_recursive(self.m_vp[0], self.m_gridpt[0],self.m_gridpt[2], self.m_basevectors[0],self.m_basevectors[1], self.m_basevectors[2],self.m_basevectors[3],0); self.divide_recursive(self.m_vp[1], self.m_gridpt[1],self.m_gridpt[3], self.m_basevectors[1],self.m_basevectors[2], self.m_basevectors[3],self.m_basevectors[0],0); # sort all points from base-vector tv=vector2d() def sort_key_A(cv): # get distance from corner "A" vector2d.sub(tv,cv,self.m_basevectors[0]) return tv.get_scalar() def sort_key_D(cv): # get distance from corner "D" vector2d.sub(tv,cv,self.m_basevectors[3]) return tv.get_scalar() def sort_key_B(cv): # get distance from corner "B" vector2d.sub(tv,cv,self.m_basevectors[1]) return tv.get_scalar() self.m_gridpt[0].sort(key=sort_key_A) self.m_gridpt[1].sort(key=sort_key_B) self.m_gridpt[2].sort(key=sort_key_D) self.m_gridpt[3].sort(key=sort_key_A) def divide_recursive(self,vp,gridpt1,gridpt2,a,b,c,d,depth): # divide_recursive(self,vp,ref List<Vector2D> gridpt1,ref List<Vector2D> gridpt2,Vector2D a,Vector2D b,Vector2D c,Vector2D d,int depth=0) if(depth > self.m_recursive_max): return; self.divide_single_plane(vp, gridpt1,gridpt2, a,b, c,d); hab=gridpt1[-1]; hcd=gridpt2[-1]; self.divide_recursive(vp,gridpt1,gridpt2, a,hab, hcd,d,depth+1); self.divide_recursive(vp,gridpt1,gridpt2, hab,b, c,hcd,depth+1); def divide_line(self,array,targetpt,vp,a,b): curpt=vector2d(); self.get_crossed_point(curpt, vp, a, targetpt, b); array.append(curpt); return curpt; def divide_single_plane(self,vp,array1,array2,a,b,c,d): # static protected void divide_single_plane(self,Vector2D vp,ref List<Vector2D> array1,ref List<Vector2D> array2,Vector2D a,Vector2D b,Vector2D c,Vector2D d) center=vector2d(); self.get_crossed_point(center, a, b, c, d); if vp.x==None: # this means...vanising point does not exist!!(the target ridges are palallel!) nvp=a-d nvp.normalize_self() nvp+=center self.divide_line(array1,center,nvp,a,b); self.divide_line(array2,center,nvp,c,d); else: self.divide_line(array1,center,vp,a,b); self.divide_line(array2,center,vp,c,d); return center @staticmethod def generate_stroke_list(srcarray): """ generate gimp-stroke array from srcarray. Arguments: srcarray -- the array of vector Returns: generated list """ retlst=[] for cv in srcarray: i=0 while i<3: retlst.append(cv.x) retlst.append(cv.y) i+=1 return retlst @staticmethod def get_control_point_count(stroke): return len(stroke)/6 def draw_grid(self,layer): pathes=[] def generate_pathobj(dst_size,force): if self.sizes[0]==dst_size and force==False: return pathes[0] else: cp=pdb.gimp_vectors_new(self.img,"pg_path_%d_%08x" % (dst_size,self.id)) pdb.gimp_image_insert_vectors(self.img,cp,None,0) return cp for i in range(0,3): pathes.append(generate_pathobj(self.sizes[i],i==0)) # setup grid base rectangle ts=Perspective_grid.generate_stroke_list(self.m_basevectors) # setup grid lines def append_CAC_point(ss,cv): for r in range(0,3):# to satisfy 'CAC' control point format ss.append(cv.x) ss.append(cv.y) # the original rectangle ss=[] for i in range(0,4):# m_basevectors index append_CAC_point(ss,self.m_basevectors[i]) sid=pdb.gimp_vectors_stroke_new_from_points(pathes[0],0, len(ss),ss,True) # inner grid for i in range(0,2):# m_gridpt index for t in range(0,len(self.m_gridpt[i])): ss=[] append_CAC_point(ss,self.m_gridpt[i][t]) append_CAC_point(ss,self.m_gridpt[i+2][t]) if t != int(len(self.m_gridpt[i]) / 2): sid=pdb.gimp_vectors_stroke_new_from_points(pathes[2],0, len(ss),ss,0) else: sid=pdb.gimp_vectors_stroke_new_from_points(pathes[1],0, len(ss),ss,0) print 'mid-center!' #Perspective_grid.get_control_point_count(ss)*2,ss,0) # save current contexts pdb.gimp_context_push() # now,draw them. #pdb.gimp_context_set_paint_method("gimp-ink") #pdb.gimp_context_set_ink_blob_type(0) # CIRCLE pdb.gimp_context_set_paint_method("gimp-paintbrush") bname="brush_%08x" % self.id pdb.gimp_brush_new(bname) pdb.gimp_brush_set_shape(bname,0)# 0 Round brush 1=square brush pdb.gimp_context_set_brush(bname) pdb.gimp_context_set_dynamics("Dynamics Off") pdb.gimp_brush_set_radius(bname,self.sizes[0]*2) # i dont know really how it works... pdb.gimp_brush_set_hardness(bname,0.7)# blurness pdb.gimp_brush_set_spacing(bname,5) def draw_single_path(dpath): if len(dpath.strokes)==0 or len(dpath.strokes[0].points[0])==0: pass else: pdb.gimp_edit_stroke_vectors(layer,dpath) # deleting path pdb.gimp_image_remove_vectors(self.img,dpath) for i in range(0,3): #pdb.gimp_context_set_ink_size(self.sizes[i]/2.0) pdb.gimp_context_set_brush_size(self.sizes[i]) draw_single_path(pathes[i]) pdb.gimp_brush_delete(bname) # do not forget to pop context. pdb.gimp_context_pop() def draw_stroke(self,stroke): pass def python_fu_perspective_grid_func(a_img,a_drawable,sample_arg=True,divcount=2,thickness=4,same_thickness=False,delete_srcpath=False): # check the current active vector is suitable for later operations. v=pdb.gimp_image_get_active_vectors(a_img) if v==None: #pdb.gimp_message("please select a rectangular path") pdb.gimp_message("矩形パスを選択してください。") return if len(v.strokes)!=1: #pdb.gimp_message("the stroke should be a rectangle,and only one at path.") pdb.gimp_message("このスクリプトは、ひとつの矩形だけを持つパスにしか対応しません") return ctl_cnt=len(v.strokes[0].points[0])/6 if ctl_cnt!=4: #pdb.gimp_message("the stroke should be a rectangle.this stroke of path has %d control points." % ctl_cnt) pdb.gimp_message("このパスは %d つの制御点を持ち、矩形ではありません。" % ctl_cnt) return if same_thickness: thick_size=mid_size=thin_size=thickness else: thick_size=thickness mid_size=thick_size*0.75 thin_size=mid_size*0.5 # start of groping undoable operations try: pdb.gimp_image_undo_group_start(a_img) p=Perspective_grid(a_img,thick_size,mid_size,thin_size) p.main(v.strokes[0].points[0],divcount) if delete_srcpath: pdb.gimp_image_remove_vectors(a_img,v) except Exception,e: print(str(e)) import traceback print(traceback.format_exc()) # end of grouping undoable operations finally: pdb.gimp_image_undo_group_end(a_img) register( "python_fu_perspective_grid_func", "perspective-grid-layer", "creating perspective grid layer from a rectangular-formed path.", "dothiko", "kakukaku world", "dec 2014", "<Image>/Python-Fu/layer/perspective-grid-layer", "RGB*,GRAY*", [ (PF_OPTION,"divcount","分割数",1,("4分割(2x2)","16分割(4x4)","64分割(8x8)","256分割(16x16)")), (PF_BOOL,"delete_srcpath","対象パスを消去する",True), (PF_ADJUSTMENT,"thickness","描線の太さ",4,(1,8,1)), (PF_BOOL,"same_thickness","全て同じ太さにする",False), ], [], python_fu_perspective_grid_func) main()