モブ沢工房

プログラミングとかLinux関連(特にOSSのグラフィックツール関連)とかレトロゲームとか3Dプリンタやら日曜大工等、色々。

gimp向けパースグリッドプラグイン、とりあえず完成〜

monoからは、結構すぐ移植できました。

f:id:dothiko:20141208210610j:plain

とりあえず動けばいいという感じで、まぁ汚いコードですけどね…

以下に並べた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()