研究開発日誌

CG研究・開発のちょっとしたメモ書き

Pythonを使ったOBJファイルの入出力

2015-01-15

Pythonを使ったOBJファイルの入出力に関するTipsです.

CG技術の実装と数理の実装で使ったOBJファイルの入出力のPythonコードをまとめます.このプロジェクトでは,OBJファイルデータを読み取り,メッシュに変更を加えて別のOBJファイルデータとして出力するということをやっていました.出力ファイルは,MeshLab等のソフトウェアで確認することができます.

OBJモデルデータの読み込み

以下,Pythonを使ってOBJモデルを読み込むサンプルコードです.

def loadOBJ(fliePath):
    numVertices = 0
    numUVs = 0
    numNormals = 0
    numFaces = 0

    vertices = []
    uvs = []
    normals = []
    vertexColors = []
    faceVertIDs = []
    uvIDs = []
    normalIDs = []

    for line in open(fliePath, "r"):
        vals = line.split()

        if len(vals) == 0:
            continue

        if vals[0] == "v":
            v = map(float, vals[1:4])
            vertices.append(v)

            if len(vals) == 7:
                vc = map(float, vals[4:7])
                vertexColors.append(vc)

            numVertices += 1
        if vals[0] == "vt":
            vt = map(float, vals[1:3])
            uvs.append(vt)
            numUVs += 1
        if vals[0] == "vn":
            vn = map(float, vals[1:4])
            normals.append(vn)
            numNormals += 1
        if vals[0] == "f":
            fvID = []
            uvID = []
            nvID = []
            for f in vals[1:]:
                w = f.split("/")

                if numVertices > 0:
                    fvID.append(int(w[0])-1)

                if numUVs > 0:
                    uvID.append(int(w[1])-1)

                if numNormals > 0:
                    nvID.append(int(w[2])-1)

            faceVertIDs.append(fvID)
            uvIDs.append(uvID)
            normalIDs.append(nvID)

            numFaces += 1

    print "numVertices: ", numVertices
    print "numUVs: ", numUVs
    print "numNormals: ", numNormals
    print "numFaces: ", numFaces

    return vertices, uvs, normals, faceVertIDs, uvIDs, normalIDs, vertexColors

取得できるデータ

  • 頂点座標データ: vertices (numVertices x 3)
  • テクスチャ座標データ:uv (numUVs x 3)
  • 法線データ: normals (numNormals x 3)
  • 面を構成する頂点のID: faceVertIDs (numFaces x 3 or 4)
  • 面頂点と対応したテクスチャ座標ID: uvIDs (numFaces x 3 or 4)
  • 面を構成する法線のID: normalIDs (numFaces x 3 or 4)
  • 頂点カラーデータ: vertexColors (numVertices x 3)

OBJモデルデータの書き込み

以下,Pythonを使ってOBJモデルを出力するサンプルコードです.

def saveOBJ(filePath, vertices, uvs, normals, faceVertIDs, uvIDs, normalIDs, vertexColors):
    f_out = open(filePath, 'w')

    f_out.write("####\n")
    f_out.write("#\n")
    f_out.write("# Vertices: %s\n" %(len(vertices)))
    f_out.write("# Faces: %s\n" %(len( faceVertIDs)))
    f_out.write("#\n")
    f_out.write("####\n")


    for vi, v in enumerate( vertices ):
        vStr = "v %s %s %s"  %(v[0], v[1], v[2])
        if len( vertexColors) > 0:
            color = vertexColors[vi]
            vStr += " %s %s %s" %(color[0], color[1], color[2])
        vStr += "\n"
        f_out.write(vStr)
    f_out.write("# %s vertices\n\n"  %(len(vertices)))

    for uv in uvs:
        uvStr =  "vt %s %s\n"  %(uv[0], uv[1])
        f_out.write(uvStr)
    f_out.write("# %s uvs\n\n"  %(len(uvs)))

    for n in normals:
        nStr =  "vn %s %s %s\n"  %(n[0], n[1], n[2])
        f_out.write(nStr)
    f_out.write("# %s normals\n\n"  %(len(normals)))

    for fi, fvID in enumerate( faceVertIDs ):
        fStr = "f"
        for fvi, fvIDi in enumerate( fvID ):
            fStr += " %s" %( fvIDi + 1 )

            if len(uvIDs) > 0:
                fStr += "/%s" %( uvIDs[fi][fvi] + 1 )
            if len(normalIDs) > 0:
                fStr += "/%s" %( normalIDs[fi][fvi] + 1 )

        fStr += "\n"
        f_out.write(fStr)
    f_out.write("# %s faces\n\n"  %( len( faceVertIDs)) )

    f_out.write("# End of File\n")
    f_out.close()

出力できるデータ

読み込みで取得できるデータと揃えてあります. - 頂点座標データ: vertices (numVertices x 3) - テクスチャ座標データ:uv (numUVs x 3) - 法線データ: normals (numNormals x 3) - 面を構成する頂点のID: faceVertIDs (numFaces x 3 or 4) - 面頂点と対応したテクスチャ座標ID: uvIDs (numFaces x 3 or 4) - 面を構成する法線のID: normalIDs (numFaces x 3 or 4) - 頂点カラーデータ: vertexColors (numVertices x 3)

OBJモデルデータの変更

実際に処理を行う時は,数値計算ライブラリnumpyを利用し,頂点座標データをnumpy.array型に変更して各種処理を行っています.例えば,以下の例では,頂点座標にノイズを加えて保存しています.

import numpy as np
import random

def noise(P, sigma = 0.02):
    xRange = np.max(P[:,0]) - np.min(P[:,0])
    yRange = np.max(P[:,1]) - np.min(P[:,1])
    zRange = np.max(P[:,2]) - np.min(P[:,2])

    noiseScale = 0.5 * np.linalg.norm( np.array([xRange, yRange, zRange]) )

    P_new = np.array( P )
    numVertices = len(P)
    for i in range(numVertices):
        P_new[i][0] += noiseScale * random.normalvariate(0,sigma)
        P_new[i][1] += noiseScale * random.normalvariate(0,sigma)
        P_new[i][2] += noiseScale * random.normalvariate(0,sigma)

    return P_new

vertices, uvs, normals, faceVertIDs, uvIDs, normalIDs, vertexColors = loadOBJ('InputMesh.obj')
P = np.array(vertices)
P_noise = noise(P)
saveOBJ('NoiseMesh.obj', P_noise, uvs, normals, faceVertIDs, uvIDs, normalIDs, vertexColors)

先ほどのsaveOBJ関数は,np.arrayのサイズが対応していれば,特に変換しなくてもそのまま使えます.