end0tknr's kipple - web写経開発

太宰府天満宮の狛犬って、妙にカワイイ

svgelements for pythonでsvgをparseし、shapely用に変換し表示

https://end0tknr.hateblo.jp/entry/20221107/1667831772

先日の上記entryの別版です。 先日のものは 素朴な svg parserでしたが、 その後、svgelements for python を見つけましたので、置き換えました。

以下の通りです

# -*- coding: utf-8 -*-

import io
import matplotlib.pyplot as plt
import numpy             as np
import re
import sys
import svgelements      # https://pypi.org/project/svgelements/
# svgelements & shapely 共に Polygon クラスがある為
# importによる名前空間の汚染には、要注意
from shapely.geometry   import Polygon

from matplotlib.path        import Path
from matplotlib.patches     import PathPatch
from matplotlib.collections import PatchCollection

def main():
    # with io.StringIO(svg_string) as f:
    #     svg = svgelements.SVG.parse(f)

    svg_file = sys.argv[1]
    print( svg_file )
    svg = svgelements.SVG.parse(svg_file,
                                ppi= 1/0.0393701 # 1 inch = 25.4 mm
                                )
    elms = svg.elements()
    polygons = []
    for elm in elms:
        if type(elm) == svgelements.svgelements.Group:
            continue
        
        if type(elm) in [svgelements.svgelements.Text,
                         svgelements.svgelements.Path ]:
            print("WARNING not applicable for ",type(elm),
                  file=sys.stderr)
            continue

        if type(elm) == svgelements.svgelements.Rect:
            polygons.append( rect_to_shapely(elm) )
            continue
        
        if type(elm) == svgelements.svgelements.Polygon:
            # if not elm.values["class"] in ["outer","convex"]:
            #    continue
            polygons.append( polygon_to_shapely(elm) )
            continue
        
    fig, ax = plt.subplots()
    
    for polygon in polygons:
        plot_polygon(ax, polygon,
                     facecolor='lightgray',
                     edgecolor='black',
                     alpha=0.2)
    plt.show()

def rect_to_shapely( elm:svgelements.svgelements.Rect ):
    pathds =  re.split("[, ]", elm.d() )
    return pathds_to_shapely( pathds )

def polygon_to_shapely( elm:svgelements.svgelements.Polygon ):
    pathds =  re.split("[, ]", elm.d() )
    return pathds_to_shapely( pathds )

def pathds_to_shapely( pathds ):
    holes = []
    holes_tmp = []

    while len(pathds):
        if pathds[0] in ["M","L"]:
            pathds.pop(0)
            continue
        if pathds[0] == "Z":
            pathds.pop(0)
            holes.append( holes_tmp )
            holes_tmp = []
            continue
        
        co_x = float( pathds.pop(0) )
        co_y = float( pathds.pop(0) )
        holes_tmp.append( (co_x, co_y) )
        
    # 念の為 座標群:dが Zで終わらない場合にも備える
    if len( holes_tmp ):
        holes.append( holes_tmp )

    shell = holes.pop(0)
    polygon = Polygon( shell=shell, holes=holes)
    return polygon
    

# Plots a Polygon to pyplot `ax`
# cf. https://stackoverflow.com/questions/55522395
def plot_polygon(ax, poly, **kwargs):
    path = Path.make_compound_path(
        Path(np.asarray(poly.exterior.coords)[:, :2]),
        *[Path(np.asarray(ring.coords)[:, :2]) for ring in poly.interiors])

    patch = PathPatch(path, **kwargs)
    collection = PatchCollection([patch], **kwargs)
    
    ax.add_collection(collection, autolim=True)
    ax.autoscale_view()
    return collection

if __name__ == '__main__':
    main()

pyclipper for python による多角形の切り抜き

先日までのentryでは、shapely for python で polygonを操作していましたが、pyclipper もあるらしい。

import pyclipper

from matplotlib.path        import Path
from matplotlib.patches     import PathPatch
from matplotlib.collections import PatchCollection
from shapely.geometry       import Polygon
from xml.dom                import minidom
import matplotlib.pyplot as plt
import numpy             as np

def main():
    clip = [[190, 210], [240, 210], [240, 130], [190, 130]]
    subj = [
        [[180, 200], [260, 200], [260, 150], [180, 150]],
        [[215, 160], [230, 190], [200, 190]] ]
    
    pc = pyclipper.Pyclipper()
    pc.AddPath(clip,  pyclipper.PT_CLIP,    True)
    pc.AddPaths(subj, pyclipper.PT_SUBJECT, True)

    solution = pc.Execute(pyclipper.CT_INTERSECTION,
                          pyclipper.PFT_EVENODD,
                          pyclipper.PFT_EVENODD)
    polygons = []

    fig, ax = plt.subplots()
    polygons.append( Polygon( shell=clip ) )
    polygons.append( Polygon( shell=subj[0], holes=[subj[1]] ) )
    for polygon in polygons:
        plot_polygon(ax, polygon,
                     facecolor='lightgray',
                     edgecolor='black',
                     alpha=0.2)
    polygon = Polygon( shell=solution[0], holes=[solution[1]] )
    plot_polygon(ax, polygon,
                 facecolor='None',
                 edgecolor='red')
    plt.show()

# Plots a Polygon to pyplot `ax`
# cf. https://stackoverflow.com/questions/55522395
def plot_polygon(ax, poly, **kwargs):
    path = Path.make_compound_path(
        Path(np.asarray(poly.exterior.coords)[:, :2]),
        *[Path(np.asarray(ring.coords)[:, :2]) for ring in poly.interiors])

    patch = PathPatch(path, **kwargs)
    collection = PatchCollection([patch], **kwargs)
    
    ax.add_collection(collection, autolim=True)
    ax.autoscale_view()
    return collection


if __name__ == '__main__':
    main()

↑こうかくと、↓こう表示されます

pythonでsvgをparseし、shapely用に変換し表示

先程のentryの続きです。

# -*- coding: utf-8 -*-

from matplotlib.path        import Path
from matplotlib.patches     import PathPatch
from matplotlib.collections import PatchCollection
from shapely.geometry       import Polygon
from xml.dom                import minidom
import matplotlib.pyplot as plt
import numpy             as np

def main():
    # file_path = "simple.svg"
    file_path = "room.svg"

    doc = minidom.parse( file_path )
    polygons = []
    
    for path in doc.getElementsByTagName('path'):
        if path.getAttribute('class') != "outer":
            continue

        pathds = path.getAttribute('d').strip().split()
        
        for i, pathd in enumerate( pathds ):
            if "." in pathd:
                pathds[i] = float( pathd )

        shell, holes = conv_pathd_to_shapely( pathds )

        polygons.append( Polygon( shell=shell, holes=holes) )

    fig, ax = plt.subplots()
    
    for polygon in polygons:
        plot_polygon(ax, polygon, facecolor='lightblue', edgecolor='red')
        
    plt.show()
    
def conv_pathd_to_shapely( pathds ):
    holes = []
    holes_tmp = []
    
    while len(pathds):
        if pathds[0] in ["M","L"]:
            pathds.pop(0)
            continue
        if pathds[0] == "Z":
            pathds.pop(0)
            holes.append( holes_tmp )
            holes_tmp = []
            continue
        
        # co_x = pathds.pop(0)
        # co_y = pathds.pop(0)
        co_y = pathds.pop(0)
        co_x = pathds.pop(0)
        co_x, co_y = conv_affine_rotate( co_x, co_y )
        holes_tmp.append( (co_x, co_y) )

    shell = holes.pop(0)
    return shell, holes

# cf https://end0tknr.hateblo.jp/entry/20220923/1663900289
def conv_affine_rotate( org_x, org_y ):

    # アフィン変換行列 180度回転
    affine = np.array([[-1, 0, 0],
                       [ 0,-1, 0],
                       [ 0, 0, 1]])
    org_co = np.array([[org_x],
                       [org_y],
                       [  1  ]])
    new_co = affine.dot(org_co)
    
    return new_co[0],new_co[1]
    

# Plots a Polygon to pyplot `ax`
# cf. https://stackoverflow.com/questions/55522395
def plot_polygon(ax, poly, **kwargs):
    path = Path.make_compound_path(
        Path(np.asarray(poly.exterior.coords)[:, :2]),
        *[Path(np.asarray(ring.coords)[:, :2]) for ring in poly.interiors])

    patch = PathPatch(path, **kwargs)
    collection = PatchCollection([patch], **kwargs)
    
    ax.add_collection(collection, autolim=True)
    ax.autoscale_view()
    return collection

if __name__ == '__main__':
    main()

↑こう書くと、先程のentryで表示したsvgを以下のように shapely用に変換できます。

ちなみに、svgのsrcは以下をご確認ください。

Export-Paper-Model-from-Blender による Meshの自動展開(平面化, Unfold, Unwrap)

以下のようなSVGの平面を作成します

Step1 Export Paper Model アドオンのinstall

「menuバー → Edit → Preferences」で ダイアログを表示後、 addon画面で「Export Paper Model」をインストールして下さい。

ただし、私の場合

c:/Program Files/Blender Foundation/Blender 3.3/3.3/scripts/addons/io_export_paper_model.py

の 513~514行目にある以下をコメントアウトしました。

for point in points:
    point.rotate(rot)

このコメントアウトを行わない場合、ネスティング処理?の影響で、 以下のように平面が斜めに配置されます。

Step2 展開対象オブジェクトの選択と、オートスケールOFF

Step3 エクスポート実行

「menuバー → File → Export → Paper Model」で ダイアログを表示後、 export条件を少々、指定して完了です。

参考URL

pythonによる GP(Genetic Programming、遺伝的プログラミング)

pythonによる GA(Genetic Algorithm、遺伝的アルゴリズム) - end0tknr's kipple - web写経開発

先程のentryにあるGAとは別に、今度は、GPを以下のurlから写経します

真の関数である

 \large{ x^4 + x^3 + x^2 + x +  1  範囲: -1 ≦ x ≦ 1}

と出力する値が等しい式(木構造)を探索します。

from statistics import mean
from copy import deepcopy
import graphviz
import matplotlib.pyplot as plt
import random
import sys

POP_SIZE        = 60   # 1世代におけ個体数
MIN_DEPTH       = 2    # 初期の最小深さ
MAX_DEPTH       = 5    # 初期の最大深さ
GENERATIONS     = 250  # 最大世代数
TOURNAMENT_SIZE = 5    # トーナメント選択のsize
XO_RATE         = 0.8  # 交叉率 
PROB_MUTATION   = 0.2  # 突然変異率

# 真の関数
def target_func(x):
    return x**4 + x**3 + x**2 + x + 1

# 使用できる関数
def add(x, y): return x + y
def sub(x, y): return x - y
def mul(x, y): return x * y
FUNCTIONS = [add, sub, mul]

# nodeに使用できる記号
TERMINALS = ['x', -2, -1, 0, 1, 2]

def main():
    # 引数なしで seed()すると、乱数が都度変化
    random.seed()
    # 真の関数で data点生成
    target_dataset = make_target_dataset()
    # 個体群(木)生成
    population= init_population(POP_SIZE, MAX_DEPTH)
    
    best_of_run = None
    best_of_run_f = 0
    best_of_run_gen = 0

    fitnesses = []
    for i in range(POP_SIZE):
        fitnesses.append( fitness(population[i], target_dataset) )
        
    average_f = []
    max_f = []
    gen_l = []

    for gen in range(GENERATIONS):
        gen_l.append(gen + 1)
        nextgen_population=[]
        for i in range(POP_SIZE):
            parent1 = selection(population, fitnesses)
            parent2 = selection(population, fitnesses)
            parent1.crossover(parent2)  # 交叉
            parent1.mutation()          # 突然変異
            nextgen_population.append(parent1)
        population=nextgen_population

        fitnesses = []
        for i in range(POP_SIZE):
            fitnesses.append( fitness(population[i], target_dataset) )
            
        average_f.append(sum(fitnesses) / len(fitnesses))# for figure
        max_f.append(max(fitnesses))# for figure
        
        if max(fitnesses) > best_of_run_f:
            best_of_run_f = max(fitnesses)
            best_of_run_gen = gen
            best_of_run = deepcopy(population[fitnesses.index(max(fitnesses))])
            print("gen:", gen,
                  "best_of_run_f:", round(max(fitnesses),3))
            best_of_run.print_tree()
            
            # best_of_run.draw_tree(
            #     "gen:", gen,
            #     "best_of_run_f:", round(max(fitnesses),3) )
            
        if best_of_run_f == 1: break
    
    print("END OF RUN")
    print("best_of_run attained at gen:", best_of_run_gen,
          " fitness:", round(best_of_run_f,3) )
    
    best_of_run.print_tree()
    best_of_run.draw_tree(
        "best_of_run",
        "gen:" + str(best_of_run_gen) + \
        "has f:"+ str(round(best_of_run_f,3)) )

    print("len(gen_l):",len(gen_l),"len(max_f):",len(max_f) )
    
    show_evolve_graph(gen_l,max_f,average_f)
    
    
def show_evolve_graph(gen_l,max_f,average_f):
    plt.plot(gen_l, max_f,    label = "MAX")
    plt.plot(gen_l, average_f,label = "AVERAGE")
    plt.ylim(0,1)
    plt.xlim(0,)
    plt.title('FITNESS')
    plt.xlabel("EPOCH")
    plt.ylabel("VALUE")
    plt.legend()
    
    plt.show()
    
    
# 真の関数で 101個のdata点生成
def make_target_dataset():
    dataset = []
    for x in range(-100,101,2):
        x /= 100
        dataset.append([x, target_func(x)])
    return dataset

class GPTree:
    def __init__(self):
        self.operator  = None
        self.left  = None
        self.right = None
        
    def node_label(self): # string label
        if self.operator in FUNCTIONS :
            # __name__ には add,sub,mulが入ります
            return self.operator.__name__

        return str(self.operator)
    
    def print_tree(self, prefix = ""):
        print("%s%s" % (prefix, self.node_label()) )
        if self.left:
            self.left.print_tree (prefix + "   ")
        if self.right:
            self.right.print_tree(prefix + "   ")

    def draw_tree(self, fname, footer):
        dot = [graphviz.Digraph(format='svg', filename=fname)]
        dot[0].attr(kw='graph', label = footer)
        count = [0]
        self.draw(dot, count)
        dot[0].view()
        
    def draw(self, dot, count):
        node_name = str(count[0])
        dot[0].node(node_name, self.node_label())
        if self.left:
            count[0] += 1
            dot[0].edge(node_name, str(count[0]))
            self.left.draw(dot, count)
        if self.right:
            count[0] += 1
            dot[0].edge(node_name, str(count[0]))
            self.right.draw(dot, count)

    # 木にある x に値を代入し、算出
    def compute_tree(self, x): 
        if self.operator in FUNCTIONS:
            return self.operator( self.left.compute_tree(x),
                              self.right.compute_tree(x) )
        elif self.operator == 'x':
            return x
        else:
            return self.operator

    # create random tree using either grow or full method
    def random_tree(self, grow, max_depth, depth = 0):
        if depth < MIN_DEPTH or (depth < max_depth and not grow): 
            self.operator = FUNCTIONS[random.randint(0, len(FUNCTIONS)-1)]
        elif depth >= max_depth:   
            self.operator = TERMINALS[random.randint(0, len(TERMINALS)-1)]
        else: # intermediate depth, grow
            if random.random() > 0.5: 
                self.operator = TERMINALS[random.randint(0, len(TERMINALS)-1)]
            else:
                self.operator = FUNCTIONS[random.randint(0, len(FUNCTIONS)-1)]
        if self.operator in FUNCTIONS:
            self.left = GPTree()
            self.left.random_tree(grow, max_depth, depth = depth + 1)
            self.right = GPTree()
            self.right.random_tree(grow, max_depth, depth = depth + 1)
            
    # 突然変異
    def mutation(self):
        if random.random() < PROB_MUTATION:
            self.random_tree(grow = True, max_depth = 2)
            return
        if self.left:
            self.left.mutation()
            return
        if self.right:
            self.right.mutation()

    # 交叉 ( 部分木の入れ替え? )
    def crossover(self, other):
        if random.random() >= XO_RATE:
            return
        
        # 2nd random subtree
        second = other.scan_tree([random.randint(1, other.tree_size())], None)
        # 2nd subtree "glued" inside 1st tree
        self.scan_tree([random.randint(1, self.tree_size())], second)

    def scan_tree(self, count, second):
        count[0] -= 1
        if count[0] <= 1:
            if not second:
                return self.build_subtree()

            self.operator  = second.operator
            self.left  = second.left
            self.right = second.right
            return None
        
        if self.left  and count[0] > 1:
            return self.left.scan_tree(count, second)
        if self.right and count[0] > 1:
            return self.right.scan_tree(count, second)
    
    def build_subtree(self):
        t = GPTree()
        t.operator = self.operator
        
        if self.left:
            t.left  = self.left.build_subtree()
        if self.right:
            t.right = self.right.build_subtree()
        return t

    def tree_size(self):
        if self.operator in TERMINALS: return 1
        
        l = self.left.tree_size()  if self.left  else 0
        r = self.right.tree_size() if self.right else 0
        return 1 + l + r

# 個体群の生成
def init_population(arg_pop_size, arg_max_depth):
    pop = []
    for md in range(3, arg_max_depth + 1):
        for i in range(int(arg_pop_size/6)):
            t = GPTree()
            t.random_tree(grow = True, max_depth = md) # grow
            pop.append(t) 
        for i in range(int(arg_pop_size/6)):
            t = GPTree()
            t.random_tree(grow = False, max_depth = md) # full
            pop.append(t)
    return pop

# 適応度 : 1 /(誤差の平均+1)
#   →誤差0での適応度:1、誤差:大の適応度:0。
#     「+1」は0除算エラーを避ける為
def fitness(individual, dataset):
    errors = []
    for ds in dataset:
        errors.append( abs(individual.compute_tree(ds[0]) - ds[1] ) )
        
    return 1 / ( mean(errors) +1 )

# トーナメント選択
def selection(population, fitnesses):
    tournament = []
    tournament_fitnesses = []
    # まずは、ランダムに TOURNAMENT_SIZE 個を選択
    for i in range(TOURNAMENT_SIZE):
        pop_id = random.randint(0, len(population)-1)
        tournament.append( pop_id )
        tournament_fitnesses.append( fitnesses[pop_id] )

    # その中から、適応度が最も大きいものを選択
    tmp_id = tournament_fitnesses.index( max(tournament_fitnesses) )
    return deepcopy(population[tournament[tmp_id]])

if __name__ == '__main__':
    main()

↑こう書くと、↓こう表示されます

gen:249has f:0.855 0 mul 1 sub 0->1 10 add 0->10 2 mul 1->2 9 -1 1->9 3 mul 2->3 6 sub 2->6 4 x 3->4 5 x 3->5 7 2 6->7 8 x 6->8 11 x 10->11 12 1 10->12

pythonによる GA(Genetic Algorithm、遺伝的アルゴリズム)

遺伝的アルゴリズムを上記urlから写経します。

 \large{ y = x^2 (0≦x≦1) }

の最大値を探索しています。

突然変異は実装していますが、交差は実装していません

import random
import copy
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation

# GA:Genetic Algorithm 遺伝的アルゴリズム による
# y = x^2 (0<x<1) の最大値探索
# cf. https://github.com/igenki/youtube_python/blob/main/SimpleGA.ipynb

chromosome_size = 10    # 染色体の個体数
epochs_size     = 100   # 進化する世代数
genes_size      = 8     # 遺伝子数
mutation_rate   = 0.05  # 突然変異率

def main():
    ga = SimpleGeneticAlgorithm()
    ga.evolve()
    ga.output_animation()

class SimpleGeneticAlgorithm():

    def __init__(self):
        self.chromosomes = [Chromosome() for i in range(chromosome_size)]

        # 出力する画像のsizeはinch指定
        self.fig = plt.figure(figsize=(5, 5))
        # アニメ出力する各コマ
        self.ims = []
    
    def evolve(self):
        for epoch in range(epochs_size+1):
            
            self.tournament()

            # 突然変異
            for chromosome in self.chromosomes:
                chromosome.mutate()

            # 進化経過のアニメ出力
            self.snapshot(epoch)

            # 進化経過のtext出力
            if epoch % 10 == 0:
                print("epoch:",epoch)
                for chro in self.chromosomes:
                    print("\t", chro.getFittness() )
                    
    # トーナメント選択
    def tournament(self):
        chro_next = []
        while len(chro_next) < chromosome_size:
            chro_tmp_1 = copy.deepcopy(
                self.chromosomes[random.randrange(chromosome_size)])
            chro_tmp_2 = copy.deepcopy(
                self.chromosomes[random.randrange(chromosome_size)])
            
            if chro_tmp_1.getFittness() > chro_tmp_2.getFittness():
                chro_next.append(chro_tmp_1)
            else:
                chro_next.append(chro_tmp_2)
                
        self.chromosomes = chro_next

    def snapshot(self, t):
        # linspace():等差数列作成
        im=plt.plot(np.linspace(0,1,100),
                    [n*n for n in np.linspace(0,1,100)],
                    color='black',
                    linestyle='solid')
        im += [plt.text(0,0.8,'epoch: {}'.format(t))]
        im += [plt.scatter(
            [chro.getVal() for chro in self.chromosomes],
            [chro.getFittness() for chro in self.chromosomes],c='r')]
        self.ims.append(im)

    def output_animation(self):
        ani = animation.ArtistAnimation(self.fig, self.ims)
        ani.save('./GeneticAlgorithm.gif', writer="pillow")
        # ani.save('./GeneticAlgorithm.mp4', writer="ffmpeg")

#染色体
class Chromosome:
    def __init__(self):
        self.gene = [random.randint(0,1) for i in range(genes_size)]
        
    # 突然変異
    def mutate(self):
        for i in range(genes_size):
            if random.random() < mutation_rate:
                self.gene[i] = 1 - self.gene[i]
                
    # 遺伝子型から表現型への発現
    #  -遺伝子型の例 0110001011     ( 2進数)
    #  -表現型の例   395/(1024-1)   (10 〃 )
    def getVal(self):
        value = 0.
        for g in self.gene:
            value *= 2
            value += g
        value = value / (2 ** genes_size - 1.0)
        return value
    
    # 適応度評価
    def getFittness(self):
        result = self.getVal()
        return result * result

if __name__ == '__main__':
    main()

↑こう書くと、↓こう表示されます

二次元座標系での回転行列 振り返り

blenderのaddonである Export-Paper-Model-from-Blender において fitting_matrix()として回転行列が実装されていたので、振り返り。(メモ)

Export-Paper-Model-from-Blender の unfolder.py で実装されている fitting_matrix(v1, v2)

Export-Paper-Model-from-Blender/unfolder.py at master · addam/Export-Paper-Model-from-Blender · GitHub

def fitting_matrix(v1, v2):
    """Get a matrix that rotates v1 to the same direction as v2"""
    return (1 / v1.length_squared) * M.Matrix((
        (v1.x*v2.x + v1.y*v2.y, v1.y*v2.x - v1.x*v2.y),
        (v1.x*v2.y - v1.y*v2.x, v1.x*v2.x + v1.y*v2.y)))

回転行列振り返り

まず、回転行列とは以下

 \large{
\begin{pmatrix} x' \\\ y' \end{pmatrix} =

\begin{pmatrix} cosθ -sinθ \\\ sinθ cosθ \end{pmatrix}
\begin{pmatrix} x \\\ y \end{pmatrix}

}

上記ある cosθ、sinθ は、内積外積から導入できますが、 その内積外積の定義は以下の通りです

 \large{ a・b = |a||b|cosθ }
 \large{ a✕b = |a||b|sinθ }

or

 \large{ a✕b = ax・by - ay・bx  }

参考url

電子帳簿保存法 オレオレ memo

参考url

対象と区分

区分 内容 適用
電子帳簿等保存 電子的に作成の帳簿/書類をデータのまま保存 任意
スキャナ保存 紙で受領/発行した書類を画像データで保存 任意
電子取引 電子的に受送信した取引情報をデータで保存 必須★

電子取引に該当するケース

  • EDI取引
  • インターネット等による取引
  • メールで取引情報を授受する取引 (添付ファイル含む)
  • インターネット上にサイトを設け、サイトを通じて取引情報を授受する取引

電子保存のルール

区分 内容
真実性の確保 タイムスタンプ付与★
関連書類の備付け システム利用法の分かるマニュアル
見読可能性の確保 モニタ/プリンタにより約内容が速やかに確認可
検索機能の確保 日付,金額,取引先での検索

blender python による objectの交錯判定は mathutils.bvhtree の overlap()

で、できるみたい。

そのうち試すかもしれないので、メモ

https://www.kabuku.co.jp/developers/blender2minecraft-by-python

https://blender.stackexchange.com/questions/253355/collision-detection

blender python で、Boolean モディファイア による objectの差分作成

import bpy
import copy
import math
import bmesh
import sys
from mathutils import Vector

def main():
    #全object削除
    remove_all_obj()

    bpy.ops.mesh.primitive_cube_add()
    bpy.ops.mesh.primitive_cube_add(location = (1,1,1))
    boolean("Cube")

def boolean(obj):
    bpy.ops.object.modifier_add(type='BOOLEAN')

    boolean = bpy.context.object.modifiers["Boolean"]
    boolean.operation = 'DIFFERENCE'
    boolean.object = bpy.data.objects[obj]
    bpy.ops.object.modifier_apply(modifier="Boolean")


def remove_all_obj():
    for col in bpy.data.collections:
        for item in col.objects:
            col.objects.unlink(item)
            bpy.data.objects.remove(item)

    for item in bpy.context.scene.collection.objects:
        bpy.context.scene.collection.objects.unlink(item)
        bpy.data.objects.remove(item)

    for item in bpy.data.meshes:
        bpy.data.meshes.remove(item)

    for item in bpy.data.materials:
        bpy.data.materials.remove(item)

        
if __name__ == '__main__':
    main()

↑こう書くと、↓こう表示されます

blender python で、meshにmirror modifier適用

import bpy
import copy
import math
import bmesh
import sys
from mathutils import Vector

def main():
    #全object削除
    remove_all_obj()
    #球追加
    new_obj = add_sphere()
    # mirror適用
    set_mirror_modifier( new_obj.name )

def set_mirror_modifier( obj_name ):

    # 一旦、全てを選択解除
    for ob in bpy.context.scene.objects:
        ob.select_set(False)

    ob = bpy.data.objects.get( obj_name )
    if not ob:
        return

    ob.select_set(True)
    bpy.context.view_layer.objects.active = ob
    bpy.ops.object.mode_set(mode='EDIT', toggle=False)

    meshdata = bmesh.from_edit_mesh(ob.data)
    meshdata.select_mode = {'VERT'} # 頂点選択mode化
    
    for vert in meshdata.verts:
        if vert.co.x < -0.001:
            vert.select_set(True)
        else:
            vert.select_set(False)

    bpy.ops.mesh.delete(type='VERT') # 選択頂点を削除

    
    
    bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
  
    # ミラー モディファイアを適用
    bpy.ops.object.modifier_add(type='MIRROR')
    # 作成したモディファイアを取得
    modifier = ob.modifiers['Mirror']
  
    modifier.use_axis[0] = True        # X軸でmirror
    modifier.use_axis[1] = False
    modifier.use_axis[2] = False
  
    # option設定
    modifier.use_mirror_merge = True
    modifier.use_clip = True
    modifier.use_mirror_vertex_groups = True
  
    # 結合距離
    modifier.merge_threshold = 0.001


def add_sphere():
    bpy.ops.mesh.primitive_uv_sphere_add(
        segments=32,
        ring_count=16,
        radius=1.0,
        location=(0.0, 0.0, 0.0),
        rotation=(0.0, 0.0, 0.0) )

    # 作成したobject参照を取得
    obj = bpy.context.view_layer.objects.active
    obj.name  = 'NewSphere'
    return obj
    

def remove_all_obj():
    for col in bpy.data.collections:
        for item in col.objects:
            col.objects.unlink(item)
            bpy.data.objects.remove(item)

    for item in bpy.context.scene.collection.objects:
        bpy.context.scene.collection.objects.unlink(item)
        bpy.data.objects.remove(item)

    for item in bpy.data.meshes:
        bpy.data.meshes.remove(item)

    for item in bpy.data.materials:
        bpy.data.materials.remove(item)


if __name__ == '__main__':
    main()

blender python で、meshの縁のedgeを選択

import bpy
import copy
import math
import bmesh
import sys
from mathutils import Vector

def main():
    select_end_edge( "Cube" )


# 縁の辺を選択
def select_end_edge( obj_name ) -> bool:

    # 一旦、全てを選択解除
    for ob in bpy.context.scene.objects:
        ob.select_set(False)

    ob = bpy.data.objects.get( obj_name )
    if not ob:
        return False

    ob.select_set(True)
    bpy.context.view_layer.objects.active = ob
    bpy.ops.object.mode_set(mode='EDIT', toggle=False)

    
    meshdata = bmesh.from_edit_mesh(ob.data)   # meshデータ取得
    meshdata.select_mode = {'EDGE'}            # 辺選択modeへ
    
    bpy.ops.mesh.select_all(action='DESELECT') # 一旦、選択解除し
    meshdata.select_flush_mode()               # 辺/面の選択状態を更新
    meshdata.verts.ensure_lookup_table()       # 更にtable更新

    # https://docs.blender.org/api/current/bmesh.types.html?highlight=bmedge#bmesh.types.BMEdge
    for edge in meshdata.edges:
        if edge.is_boundary == True:    # 面境界の辺の判定
            edge.select_set(True)

    meshdata.select_flush_mode()        # 辺/面の選択状態を更新
    selectob.data.update()              # objectデータ更新

    return True

if __name__ == '__main__':
    main()

blender python で、meshにある穴を補完 (面貼り)

import bpy
import copy
import math
import bmesh
import sys
from mathutils import Vector

def main():
    touchup_mesh_object( "Cube" )

def touchup_mesh_object( obj_name ):
    for ob in bpy.context.scene.objects: # 一旦、全選択を解除
        ob.select_set(False)

    ob = bpy.data.objects[obj_name]

    ob.select_set(True)
    bpy.context.view_layer.objects.active = ob

    bpy.ops.object.mode_set(mode='EDIT', toggle=False)

    # 頂点を全選択
    bpy.ops.mesh.select_all(action='SELECT')
    # 辺数=4で穴を埋める
    bpy.ops.mesh.fill_holes(sides=4)

    bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
    return ob

if __name__ == '__main__':
    main()