From 75c8068836f8d76ac3b80031f47899da1a63593a Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Wed, 12 Oct 2022 21:14:01 +0200 Subject: [PATCH 1/9] PoC GLB write --- opendm/gltf.py | 212 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 opendm/gltf.py diff --git a/opendm/gltf.py b/opendm/gltf.py new file mode 100644 index 00000000..824bd65b --- /dev/null +++ b/opendm/gltf.py @@ -0,0 +1,212 @@ +import os +import rasterio +import warnings +import numpy as np +import pygltflib + +warnings.filterwarnings("ignore", category=rasterio.errors.NotGeoreferencedWarning) + +def load_obj(obj_path, _info=print): + if not os.path.isfile(obj_path): + raise IOError("Cannot open %s" % obj_path) + + obj_base_path = os.path.dirname(os.path.abspath(obj_path)) + obj = { + 'materials': {}, + } + vertices = [] + uvs = [] + normals = [] + + faces = {} + current_material = "_" + + with open(obj_path) as f: + _info("Loading %s" % obj_path) + + for line in f: + if line.startswith("mtllib "): + # Materials + mtl_file = "".join(line.split()[1:]).strip() + obj['materials'].update(load_mtl(mtl_file, obj_base_path, _info=_info)) + elif line.startswith("v "): + # Vertices + vertices.append(list(map(float, line.split()[1:4]))) + elif line.startswith("vt "): + # UVs + uvs.append(list(map(float, line.split()[1:3]))) + elif line.startswith("vn "): + normals.append(list(map(float, line.split()[1:4]))) + elif line.startswith("usemtl "): + mtl_name = "".join(line.split()[1:]).strip() + if not mtl_name in obj['materials']: + raise Exception("%s material is missing" % mtl_name) + + current_material = mtl_name + elif line.startswith("f "): + if current_material not in faces: + faces[current_material] = [] + + a,b,c = line.split()[1:] + + if a.count("/") == 2: + av, at, an = map(int, a.split("/")[0:3]) + bv, bt, bn = map(int, b.split("/")[0:3]) + cv, ct, cn = map(int, c.split("/")[0:3]) + + faces[current_material].append((av - 1, bv - 1, cv - 1, at - 1, bt - 1, ct - 1, an - 1, bn - 1, cn - 1)) + else: + av, at = map(int, a.split("/")[0:2]) + bv, bt = map(int, b.split("/")[0:2]) + cv, ct = map(int, c.split("/")[0:2]) + faces[current_material].append((av - 1, bv - 1, cv - 1, at - 1, bt - 1, ct - 1)) + + obj['vertices'] = np.array(vertices, dtype=np.float32) + obj['uvs'] = np.array(uvs, dtype=np.float32) + obj['normals'] = np.array(normals, dtype=np.float32) + obj['faces'] = faces + + return obj + +def load_mtl(mtl_file, obj_base_path, _info=print): + mtl_file = os.path.join(obj_base_path, mtl_file) + + if not os.path.isfile(mtl_file): + raise IOError("Cannot open %s" % mtl_file) + + mats = {} + current_mtl = "" + + with open(mtl_file) as f: + for line in f: + if line.startswith("newmtl "): + current_mtl = "".join(line.split()[1:]).strip() + elif line.startswith("map_Kd ") and current_mtl: + map_kd_filename = "".join(line.split()[1:]).strip() + map_kd = os.path.join(obj_base_path, map_kd_filename) + if not os.path.isfile(map_kd): + raise IOError("Cannot open %s" % map_kd) + + _info("Loading %s" % map_kd_filename) + + mats[current_mtl] = True + # with rasterio.open(map_kd, 'r') as r: + # mats[current_mtl] = r.read() + return mats + + +def obj2glb(input_obj, output_glb, _info=print): + obj = load_obj(input_obj, _info=_info) + + vertices = obj['vertices'] + uvs = obj['uvs'] + + vertices_blob = vertices.tobytes() + uvs_blob = uvs.tobytes() + + faces = obj['faces']['material0000'] + # TODO: all faces + + faces = np.array(faces, dtype=np.uint32) + + #faces = faces[:2] # TODO REMOVE + #print(faces) + + indices = faces[:,0:3] + uv_indices = faces[:,3:6] + if faces.shape[1] == 9: + normal_indices = faces[:,6:9] + else: + normal_indices = None + + #faces_blob = faces.tobytes() + + indices_blob = indices.tobytes() + uv_indices_blob = uv_indices.tobytes() + + bin = vertices_blob + uvs_blob + indices_blob + uv_indices_blob + + gltf = pygltflib.GLTF2( + scene=0, + scenes=[pygltflib.Scene(nodes=[0])], + nodes=[pygltflib.Node(mesh=0)], + meshes=[ + pygltflib.Mesh( + primitives=[ + pygltflib.Primitive( + attributes=pygltflib.Attributes(POSITION=0, TEXCOORD_0=1), indices=2 + ) + ] + ) + ], + accessors=[ + pygltflib.Accessor( + bufferView=0, + componentType=pygltflib.FLOAT, + count=len(vertices), + type=pygltflib.VEC3, + max=vertices.max(axis=0).tolist(), + min=vertices.min(axis=0).tolist(), + ), + pygltflib.Accessor( + bufferView=1, + componentType=pygltflib.FLOAT, + count=len(uvs), + type=pygltflib.VEC2, + max=uvs.max(axis=0).tolist(), + min=uvs.min(axis=0).tolist(), + ), + pygltflib.Accessor( + bufferView=2, + componentType=pygltflib.UNSIGNED_INT, + count=len(indices), + type=pygltflib.SCALAR, + max=[int(indices.max())], + min=[int(indices.min())], + ), + pygltflib.Accessor( + bufferView=3, + componentType=pygltflib.UNSIGNED_INT, + count=len(uv_indices), + type=pygltflib.SCALAR, + max=[int(uv_indices.max())], + min=[int(uv_indices.min())], + ), + ], + bufferViews=[ + pygltflib.BufferView( + buffer=0, + byteLength=len(vertices_blob), + target=pygltflib.ARRAY_BUFFER, + ), + pygltflib.BufferView( + buffer=0, + byteOffset=len(vertices_blob), + byteLength=len(uvs_blob), + target=pygltflib.ARRAY_BUFFER, + ), + pygltflib.BufferView( + buffer=0, + byteOffset=len(vertices_blob) + len(uvs_blob), + byteLength=len(indices_blob), + target=pygltflib.ELEMENT_ARRAY_BUFFER, + ), + pygltflib.BufferView( + buffer=0, + byteOffset=len(vertices_blob) + len(uvs_blob) + len(indices_blob), + byteLength=len(uv_indices_blob), + target=pygltflib.ELEMENT_ARRAY_BUFFER, + ), + ], + buffers=[ + pygltflib.Buffer( + byteLength=len(bin) + ) + ], + ) + + gltf.set_binary_blob(bin) + + gltf.save(output_glb) + print("OK") + From cda8f227b11ec15bf5c8803d0cf9cba4419e5ebd Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Wed, 19 Oct 2022 22:01:21 -0400 Subject: [PATCH 2/9] PoC gltf file loads without errors --- opendm/gltf.py | 51 ++++++++++++++++++++++++++++++++++++------------ requirements.txt | 1 + 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/opendm/gltf.py b/opendm/gltf.py index 824bd65b..007a8d3b 100644 --- a/opendm/gltf.py +++ b/opendm/gltf.py @@ -1,5 +1,6 @@ import os import rasterio +from rasterio.io import MemoryFile import warnings import numpy as np import pygltflib @@ -61,6 +62,11 @@ def load_obj(obj_path, _info=print): cv, ct = map(int, c.split("/")[0:2]) faces[current_material].append((av - 1, bv - 1, cv - 1, at - 1, bt - 1, ct - 1)) + if len(vertices) > len(uvs): + # Pad with empty UV coordinates + add_uvs = len(vertices) - len(uvs) + uvs += [[0,0]] * add_uvs + obj['vertices'] = np.array(vertices, dtype=np.float32) obj['uvs'] = np.array(uvs, dtype=np.float32) obj['normals'] = np.array(normals, dtype=np.float32) @@ -89,9 +95,10 @@ def load_mtl(mtl_file, obj_base_path, _info=print): _info("Loading %s" % map_kd_filename) - mats[current_mtl] = True - # with rasterio.open(map_kd, 'r') as r: - # mats[current_mtl] = r.read() + with MemoryFile() as memfile: + with rasterio.open(map_kd, 'r') as r: + mats[current_mtl] = r.read() + # TODO: copy code from export-rgb (webodm) return mats @@ -100,22 +107,25 @@ def obj2glb(input_obj, output_glb, _info=print): vertices = obj['vertices'] uvs = obj['uvs'] + normals = obj['normals'] vertices_blob = vertices.tobytes() uvs_blob = uvs.tobytes() faces = obj['faces']['material0000'] + material = obj['materials']['material0000'] + print(material.shape) + print(material.) + exit(1) # TODO: all faces faces = np.array(faces, dtype=np.uint32) - #faces = faces[:2] # TODO REMOVE - #print(faces) - - indices = faces[:,0:3] - uv_indices = faces[:,3:6] + indices = faces[:,0:3].flatten() + uv_indices = faces[:,3:6].flatten() + if faces.shape[1] == 9: - normal_indices = faces[:,6:9] + normal_indices = faces[:,6:9].flatten() else: normal_indices = None @@ -124,7 +134,7 @@ def obj2glb(input_obj, output_glb, _info=print): indices_blob = indices.tobytes() uv_indices_blob = uv_indices.tobytes() - bin = vertices_blob + uvs_blob + indices_blob + uv_indices_blob + binary = vertices_blob + uvs_blob + indices_blob + uv_indices_blob gltf = pygltflib.GLTF2( scene=0, @@ -134,11 +144,20 @@ def obj2glb(input_obj, output_glb, _info=print): pygltflib.Mesh( primitives=[ pygltflib.Primitive( - attributes=pygltflib.Attributes(POSITION=0, TEXCOORD_0=1), indices=2 + attributes=pygltflib.Attributes(POSITION=0, TEXCOORD_0=1), indices=2, material=0 ) ] ) ], + materials=[ + + ], + textures=[ + pygltflib.Texture(source=0, sampler=0) + ], + images=[ + pygltflib.Image(bufferView=0, mimeType="image/png") # TODO: use JPG + ], accessors=[ pygltflib.Accessor( bufferView=0, @@ -197,15 +216,21 @@ def obj2glb(input_obj, output_glb, _info=print): byteLength=len(uv_indices_blob), target=pygltflib.ELEMENT_ARRAY_BUFFER, ), + pygltflib.BufferView( + buffer=0, + byteOffset=len(vertices_blob) + len(uvs_blob) + len(indices_blob) + len(uv_indices_blob), + byteLength=len(texture_blob), + target=pygltflib.ARRAY_BUFFER + ) ], buffers=[ pygltflib.Buffer( - byteLength=len(bin) + byteLength=len(binary) ) ], ) - gltf.set_binary_blob(bin) + gltf.set_binary_blob(binary) gltf.save(output_glb) print("OK") diff --git a/requirements.txt b/requirements.txt index c5441e9a..e70efd49 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,3 +31,4 @@ xmltodict==0.12.0 fpdf2==2.4.6 Shapely==1.7.1 onnxruntime==1.12.1 +pygltflib==1.15.3 \ No newline at end of file From 7be182b6d59a444403f23ee2513928e87286f2fd Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 20 Oct 2022 23:55:09 -0400 Subject: [PATCH 3/9] Single primitive rendering with textures --- opendm/gltf.py | 230 ++++++++++++++++++++++++------------------------- 1 file changed, 113 insertions(+), 117 deletions(-) diff --git a/opendm/gltf.py b/opendm/gltf.py index 007a8d3b..05fefdde 100644 --- a/opendm/gltf.py +++ b/opendm/gltf.py @@ -62,11 +62,6 @@ def load_obj(obj_path, _info=print): cv, ct = map(int, c.split("/")[0:2]) faces[current_material].append((av - 1, bv - 1, cv - 1, at - 1, bt - 1, ct - 1)) - if len(vertices) > len(uvs): - # Pad with empty UV coordinates - add_uvs = len(vertices) - len(uvs) - uvs += [[0,0]] * add_uvs - obj['vertices'] = np.array(vertices, dtype=np.float32) obj['uvs'] = np.array(uvs, dtype=np.float32) obj['normals'] = np.array(normals, dtype=np.float32) @@ -96,142 +91,143 @@ def load_mtl(mtl_file, obj_base_path, _info=print): _info("Loading %s" % map_kd_filename) with MemoryFile() as memfile: - with rasterio.open(map_kd, 'r') as r: - mats[current_mtl] = r.read() - # TODO: copy code from export-rgb (webodm) + with rasterio.open(map_kd, 'r') as src: + data = src.read() + with memfile.open(driver='JPEG', jpeg_quality=90, count=3, width=src.width, height=src.height, dtype=rasterio.dtypes.uint8) as dst: + for b in range(1, min(3, src.count) + 1): + # TODO: convert if uint16 or float + dst.write(data[b - 1], b) + memfile.seek(0) + mats[current_mtl] = memfile.read() return mats +def paddedBuffer(buf, boundary): + r = len(buf) % boundary + if r == 0: + return buf + pad = boundary - r + return buf + b'\x00' * pad def obj2glb(input_obj, output_glb, _info=print): + _info("Converting %s --> %s" % (input_obj, output_glb)) obj = load_obj(input_obj, _info=_info) - + vertices = obj['vertices'] uvs = obj['uvs'] + # Flip Y + uvs = (([0, 1] - (uvs * [0, 1])) + uvs * [1, 0]).astype(np.float32) normals = obj['normals'] - vertices_blob = vertices.tobytes() - uvs_blob = uvs.tobytes() + binary = b'' + accessors = [] + bufferViews = [] + primitives = [] + materials = [] + textures = [] + images = [] - faces = obj['faces']['material0000'] - material = obj['materials']['material0000'] - print(material.shape) - print(material.) - exit(1) - # TODO: all faces + bufOffset = 0 + def addBufferView(buf, target=None): + nonlocal bufferViews, bufOffset + bufferViews += [pygltflib.BufferView( + buffer=0, + byteOffset=bufOffset, + byteLength=len(buf), + target=target, + )] + bufOffset += len(buf) + return len(bufferViews) - 1 - faces = np.array(faces, dtype=np.uint32) + for material in obj['faces'].keys(): + faces = obj['faces'][material] + texture_blob = paddedBuffer(obj['materials'][material], 4) - indices = faces[:,0:3].flatten() - uv_indices = faces[:,3:6].flatten() + faces = np.array(faces, dtype=np.uint32) - if faces.shape[1] == 9: - normal_indices = faces[:,6:9].flatten() - else: - normal_indices = None + prim_vertices = vertices[faces[:,0:3].flatten()] + prim_uvs = uvs[faces[:,3:6].flatten()] - #faces_blob = faces.tobytes() + if faces.shape[1] == 9: + prim_normals = normals[faces[:,6:9].flatten()] + normals_blob = prim_normals.tobytes() + else: + prim_normals = None + normals_blob = None - indices_blob = indices.tobytes() - uv_indices_blob = uv_indices.tobytes() + vertices_blob = prim_vertices.tobytes() + uvs_blob = prim_uvs.tobytes() - binary = vertices_blob + uvs_blob + indices_blob + uv_indices_blob + binary += vertices_blob + uvs_blob + if normals_blob is not None: + binary += normals_blob + binary += texture_blob + + verticesBufferView = addBufferView(vertices_blob, pygltflib.ARRAY_BUFFER) + uvsBufferView = addBufferView(uvs_blob, pygltflib.ARRAY_BUFFER) + if normals_blob is not None: + normalsBufferView = addBufferView(normals_blob, pygltflib.ARRAY_BUFFER) + textureBufferView = addBufferView(texture_blob) + + accessors += [ + pygltflib.Accessor( + bufferView=verticesBufferView, + componentType=pygltflib.FLOAT, + count=len(prim_vertices), + type=pygltflib.VEC3, + max=prim_vertices.max(axis=0).tolist(), + min=prim_vertices.min(axis=0).tolist(), + ), + pygltflib.Accessor( + bufferView=uvsBufferView, + componentType=pygltflib.FLOAT, + count=len(prim_uvs), + type=pygltflib.VEC2, + max=prim_uvs.max(axis=0).tolist(), + min=prim_uvs.min(axis=0).tolist(), + ), + ] + + if prim_normals is not None: + accessors += [ + pygltflib.Accessor( + bufferView=normalsBufferView, + componentType=pygltflib.FLOAT, + count=len(prim_normals), + type=pygltflib.VEC3, + max=prim_normals.max(axis=0).tolist(), + min=prim_normals.min(axis=0).tolist(), + ) + ] + + images += [pygltflib.Image(bufferView=textureBufferView, mimeType="image/jpeg")] + textures += [pygltflib.Texture(source=len(images) - 1, sampler=0)] + materials += [pygltflib.Material(pbrMetallicRoughness=pygltflib.PbrMetallicRoughness(baseColorTexture=pygltflib.TextureInfo(index=0), metallicFactor=0, roughnessFactor=1), + alphaMode=pygltflib.MASK)] + primitives += [pygltflib.Primitive( + attributes=pygltflib.Attributes(POSITION=verticesBufferView, TEXCOORD_0=uvsBufferView), material=len(materials) - 1 + )] + if len(primitives) == 2: + break gltf = pygltflib.GLTF2( scene=0, scenes=[pygltflib.Scene(nodes=[0])], nodes=[pygltflib.Node(mesh=0)], - meshes=[ - pygltflib.Mesh( - primitives=[ - pygltflib.Primitive( - attributes=pygltflib.Attributes(POSITION=0, TEXCOORD_0=1), indices=2, material=0 - ) - ] - ) - ], - materials=[ - - ], - textures=[ - pygltflib.Texture(source=0, sampler=0) - ], - images=[ - pygltflib.Image(bufferView=0, mimeType="image/png") # TODO: use JPG - ], - accessors=[ - pygltflib.Accessor( - bufferView=0, - componentType=pygltflib.FLOAT, - count=len(vertices), - type=pygltflib.VEC3, - max=vertices.max(axis=0).tolist(), - min=vertices.min(axis=0).tolist(), - ), - pygltflib.Accessor( - bufferView=1, - componentType=pygltflib.FLOAT, - count=len(uvs), - type=pygltflib.VEC2, - max=uvs.max(axis=0).tolist(), - min=uvs.min(axis=0).tolist(), - ), - pygltflib.Accessor( - bufferView=2, - componentType=pygltflib.UNSIGNED_INT, - count=len(indices), - type=pygltflib.SCALAR, - max=[int(indices.max())], - min=[int(indices.min())], - ), - pygltflib.Accessor( - bufferView=3, - componentType=pygltflib.UNSIGNED_INT, - count=len(uv_indices), - type=pygltflib.SCALAR, - max=[int(uv_indices.max())], - min=[int(uv_indices.min())], - ), - ], - bufferViews=[ - pygltflib.BufferView( - buffer=0, - byteLength=len(vertices_blob), - target=pygltflib.ARRAY_BUFFER, - ), - pygltflib.BufferView( - buffer=0, - byteOffset=len(vertices_blob), - byteLength=len(uvs_blob), - target=pygltflib.ARRAY_BUFFER, - ), - pygltflib.BufferView( - buffer=0, - byteOffset=len(vertices_blob) + len(uvs_blob), - byteLength=len(indices_blob), - target=pygltflib.ELEMENT_ARRAY_BUFFER, - ), - pygltflib.BufferView( - buffer=0, - byteOffset=len(vertices_blob) + len(uvs_blob) + len(indices_blob), - byteLength=len(uv_indices_blob), - target=pygltflib.ELEMENT_ARRAY_BUFFER, - ), - pygltflib.BufferView( - buffer=0, - byteOffset=len(vertices_blob) + len(uvs_blob) + len(indices_blob) + len(uv_indices_blob), - byteLength=len(texture_blob), - target=pygltflib.ARRAY_BUFFER - ) - ], - buffers=[ - pygltflib.Buffer( - byteLength=len(binary) - ) - ], + meshes=[pygltflib.Mesh( + primitives=primitives + )], + materials=materials, + textures=textures, + samplers=[pygltflib.Sampler(magFilter=pygltflib.LINEAR, minFilter=pygltflib.LINEAR)], + images=images, + accessors=accessors, + bufferViews=bufferViews, + buffers=[pygltflib.Buffer(byteLength=len(binary))], ) gltf.set_binary_blob(binary) + _info("Writing...") gltf.save(output_glb) - print("OK") + _info("Wrote %s" % output_glb) From d620507c2562c167db7b9f2421d28cadbc77d5ae Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Fri, 21 Oct 2022 00:09:19 -0400 Subject: [PATCH 4/9] All materials rendering --- opendm/gltf.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/opendm/gltf.py b/opendm/gltf.py index 05fefdde..a349e528 100644 --- a/opendm/gltf.py +++ b/opendm/gltf.py @@ -140,8 +140,6 @@ def obj2glb(input_obj, output_glb, _info=print): for material in obj['faces'].keys(): faces = obj['faces'][material] - texture_blob = paddedBuffer(obj['materials'][material], 4) - faces = np.array(faces, dtype=np.uint32) prim_vertices = vertices[faces[:,0:3].flatten()] @@ -160,14 +158,13 @@ def obj2glb(input_obj, output_glb, _info=print): binary += vertices_blob + uvs_blob if normals_blob is not None: binary += normals_blob - binary += texture_blob - + verticesBufferView = addBufferView(vertices_blob, pygltflib.ARRAY_BUFFER) uvsBufferView = addBufferView(uvs_blob, pygltflib.ARRAY_BUFFER) + normalsBufferView = None if normals_blob is not None: normalsBufferView = addBufferView(normals_blob, pygltflib.ARRAY_BUFFER) - textureBufferView = addBufferView(texture_blob) - + accessors += [ pygltflib.Accessor( bufferView=verticesBufferView, @@ -199,15 +196,19 @@ def obj2glb(input_obj, output_glb, _info=print): ) ] + primitives += [pygltflib.Primitive( + attributes=pygltflib.Attributes(POSITION=verticesBufferView, TEXCOORD_0=uvsBufferView, NORMAL=normalsBufferView), material=len(primitives) + )] + + for material in obj['faces'].keys(): + texture_blob = paddedBuffer(obj['materials'][material], 4) + binary += texture_blob + textureBufferView = addBufferView(texture_blob) + images += [pygltflib.Image(bufferView=textureBufferView, mimeType="image/jpeg")] textures += [pygltflib.Texture(source=len(images) - 1, sampler=0)] - materials += [pygltflib.Material(pbrMetallicRoughness=pygltflib.PbrMetallicRoughness(baseColorTexture=pygltflib.TextureInfo(index=0), metallicFactor=0, roughnessFactor=1), + materials += [pygltflib.Material(pbrMetallicRoughness=pygltflib.PbrMetallicRoughness(baseColorTexture=pygltflib.TextureInfo(index=len(textures) - 1), metallicFactor=0, roughnessFactor=1), alphaMode=pygltflib.MASK)] - primitives += [pygltflib.Primitive( - attributes=pygltflib.Attributes(POSITION=verticesBufferView, TEXCOORD_0=uvsBufferView), material=len(materials) - 1 - )] - if len(primitives) == 2: - break gltf = pygltflib.GLTF2( scene=0, From 711a3f463de3d5c81ed62ed4dd14d835da0d6737 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Wed, 25 Jan 2023 13:08:59 -0500 Subject: [PATCH 5/9] Add draco compression --- SuperBuild/CMakeLists.txt | 11 +++++++++ opendm/config.py | 7 ++++++ opendm/gltf.py | 24 ++++++++++++++++++- opendm/types.py | 1 + stages/mvstex.py | 49 +++++++++++++++++++++++++-------------- 5 files changed, 73 insertions(+), 19 deletions(-) diff --git a/SuperBuild/CMakeLists.txt b/SuperBuild/CMakeLists.txt index af59a2e5..4d9435b5 100644 --- a/SuperBuild/CMakeLists.txt +++ b/SuperBuild/CMakeLists.txt @@ -245,3 +245,14 @@ externalproject_add(lastools SOURCE_DIR ${SB_SOURCE_DIR}/lastools CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH=${SB_INSTALL_DIR} ) + +externalproject_add(draco + GIT_REPOSITORY https://github.com/google/draco + GIT_SHALLOW ON + GIT_TAG 1.5.5 + PREFIX ${SB_BINARY_DIR}/draco + SOURCE_DIR ${SB_SOURCE_DIR}/draco + CMAKE_ARGS -DDRACO_TRANSCODER_SUPPORTED=ON + -DCMAKE_INSTALL_PREFIX:PATH=${SB_INSTALL_DIR} + ${WIN32_CMAKE_ARGS} +) diff --git a/opendm/config.py b/opendm/config.py index c7953fb7..3665ee47 100755 --- a/opendm/config.py +++ b/opendm/config.py @@ -461,6 +461,13 @@ def config(argv=None, parser=None): help=('Generate OBJs that have a single material and a single texture file instead of multiple ones. ' 'Default: %(default)s')) + parser.add_argument('--gltf', + action=StoreTrue, + nargs=0, + default=False, + help=('Generate single file Binary glTF (GLB) textured models. ' + 'Default: %(default)s')) + parser.add_argument('--gcp', metavar='', action=StoreValue, diff --git a/opendm/gltf.py b/opendm/gltf.py index a349e528..3652b449 100644 --- a/opendm/gltf.py +++ b/opendm/gltf.py @@ -4,6 +4,8 @@ from rasterio.io import MemoryFile import warnings import numpy as np import pygltflib +from opendm import system +from opendm import io warnings.filterwarnings("ignore", category=rasterio.errors.NotGeoreferencedWarning) @@ -108,7 +110,7 @@ def paddedBuffer(buf, boundary): pad = boundary - r return buf + b'\x00' * pad -def obj2glb(input_obj, output_glb, _info=print): +def obj2glb(input_obj, output_glb, rtc=(None, None), draco_compression=True, _info=print): _info("Converting %s --> %s" % (input_obj, output_glb)) obj = load_obj(input_obj, _info=_info) @@ -226,9 +228,29 @@ def obj2glb(input_obj, output_glb, _info=print): buffers=[pygltflib.Buffer(byteLength=len(binary))], ) + if rtc != (None, None) and len(rtc) >= 2: + gltf.extensionsUsed = ['CESIUM_RTC'] + gltf.extensions = { + 'CESIUM_RTC': { + 'center': [float(rtc[0]), float(rtc[1]), 0.0] + } + } + gltf.set_binary_blob(binary) _info("Writing...") gltf.save(output_glb) _info("Wrote %s" % output_glb) + if draco_compression: + _info("Compressing with draco") + try: + compressed_glb = io.related_file_path(output_glb, postfix="_compressed") + system.run('draco_transcoder -i "{}" -o "{}" -qt 16 -qp 16'.format(output_glb, compressed_glb)) + if os.path.isfile(compressed_glb) and os.path.isfile(output_glb): + os.remove(output_glb) + os.rename(compressed_glb, output_glb) + except Exception as e: + log.ODM_WARNING("Cannot compress GLB with draco: %s" % str(e)) + + diff --git a/opendm/types.py b/opendm/types.py index 06a10bc0..7c529faa 100644 --- a/opendm/types.py +++ b/opendm/types.py @@ -289,6 +289,7 @@ class ODM_Tree(object): # texturing self.odm_textured_model_obj = 'odm_textured_model_geo.obj' + self.odm_textured_model_glb = 'odm_textured_model_geo.glb' # odm_georeferencing self.odm_georeferencing_coords = os.path.join( diff --git a/stages/mvstex.py b/stages/mvstex.py index bd0d3acc..6170533a 100644 --- a/stages/mvstex.py +++ b/stages/mvstex.py @@ -8,6 +8,7 @@ from opendm import types from opendm.multispectral import get_primary_band_name from opendm.photo import find_largest_photo_dim from opendm.objpacker import obj_pack +from opendm.gltf import obj2glb class ODMMvsTexStage(types.ODM_Stage): def process(self, args, outputs): @@ -129,26 +130,38 @@ class ODMMvsTexStage(types.ODM_Stage): '{nadirMode} ' '{labelingFile} ' '{maxTextureSize} '.format(**kwargs)) - - # Single material? - if args.texturing_single_material and r['primary'] and (not r['nadir'] or args.skip_3dmodel): - log.ODM_INFO("Packing to single material") - packed_dir = os.path.join(r['out_dir'], 'packed') - if io.dir_exists(packed_dir): - log.ODM_INFO("Removing old packed directory {}".format(packed_dir)) - shutil.rmtree(packed_dir) - - try: - obj_pack(os.path.join(r['out_dir'], tree.odm_textured_model_obj), packed_dir, _info=log.ODM_INFO) + if r['primary'] and (not r['nadir'] or args.skip_3dmodel): + # GlTF? + if args.gltf: + log.ODM_INFO("Generating glTF Binary") + odm_textured_model_glb = os.path.join(r['out_dir'], tree.odm_textured_model_glb) + + try: + obj2glb(odm_textured_model_obj, odm_textured_model_glb, rtc=reconstruction.get_proj_offset(), _info=log.ODM_INFO) + except Exception as e: + log.ODM_WARNING(str(e)) + + # Single material? + if args.texturing_single_material: + log.ODM_INFO("Packing to single material") + + packed_dir = os.path.join(r['out_dir'], 'packed') + if io.dir_exists(packed_dir): + log.ODM_INFO("Removing old packed directory {}".format(packed_dir)) + shutil.rmtree(packed_dir) - # Move packed/* into texturing folder - system.delete_files(r['out_dir'], (".vec", )) - system.move_files(packed_dir, r['out_dir']) - if os.path.isdir(packed_dir): - os.rmdir(packed_dir) - except Exception as e: - log.ODM_WARNING(str(e)) + try: + obj_pack(os.path.join(r['out_dir'], tree.odm_textured_model_obj), packed_dir, _info=log.ODM_INFO) + + # Move packed/* into texturing folder + system.delete_files(r['out_dir'], (".vec", )) + system.move_files(packed_dir, r['out_dir']) + if os.path.isdir(packed_dir): + os.rmdir(packed_dir) + except Exception as e: + log.ODM_WARNING(str(e)) + # Backward compatibility: copy odm_textured_model_geo.mtl to odm_textured_model.mtl # for certain older WebODM clients which expect a odm_textured_model.mtl From 45a67cb24435a2401a6da4c1ba043d6ea16af0b2 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 26 Jan 2023 12:07:15 -0500 Subject: [PATCH 6/9] Add LRV, TS video format support --- opendm/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendm/context.py b/opendm/context.py index 7bafdd5d..5c4b7a5c 100644 --- a/opendm/context.py +++ b/opendm/context.py @@ -41,7 +41,7 @@ settings_path = os.path.join(root_path, 'settings.yaml') # Define supported image extensions supported_extensions = {'.jpg','.jpeg','.png', '.tif', '.tiff', '.bmp'} -supported_video_extensions = {'.mp4', '.mov'} +supported_video_extensions = {'.mp4', '.mov', '.lrv', '.ts'} # Define the number of cores num_cores = multiprocessing.cpu_count() From 3b11a371e45ca0debf2ceda7a2fcbfa278f7136f Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 26 Jan 2023 13:14:05 -0500 Subject: [PATCH 7/9] Handle 16/32 bit TIFFs in GLB generation --- opendm/gltf.py | 64 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/opendm/gltf.py b/opendm/gltf.py index 3652b449..e54969b2 100644 --- a/opendm/gltf.py +++ b/opendm/gltf.py @@ -69,8 +69,60 @@ def load_obj(obj_path, _info=print): obj['normals'] = np.array(normals, dtype=np.float32) obj['faces'] = faces + obj['materials'] = convert_materials_to_jpeg(obj['materials']) + return obj +def convert_materials_to_jpeg(materials): + + min_value = 0 + value_range = 0 + skip_conversion = False + + for mat in materials: + image = materials[mat] + + # Stop here, assuming all other materials are also uint8 + if image.dtype == np.uint8: + skip_conversion = True + break + + # Find common min/range values + try: + data_range = np.iinfo(image.dtype) + min_value = min(min_value, 0) + value_range = max(value_range, float(data_range.max) - float(data_range.min)) + except ValueError: + # For floats use the actual range of the image values + min_value = min(min_value, float(image.min())) + value_range = max(value_range, float(image.max()) - min_value) + + if value_range == 0: + value_range = 255 # Should never happen + + for mat in materials: + image = materials[mat] + + if not skip_conversion: + image = image.astype(np.float32) + image -= min_value + image *= 255.0 / value_range + np.around(image, out=image) + image[image > 255] = 255 + image[image < 0] = 0 + image = image.astype(np.uint8) + + with MemoryFile() as memfile: + bands, h, w = image.shape + bands = min(3, bands) + with memfile.open(driver='JPEG', jpeg_quality=90, count=bands, width=w, height=h, dtype=rasterio.dtypes.uint8) as dst: + for b in range(1, min(3, bands) + 1): + dst.write(image[b - 1], b) + memfile.seek(0) + materials[mat] = memfile.read() + + return materials + def load_mtl(mtl_file, obj_base_path, _info=print): mtl_file = os.path.join(obj_base_path, mtl_file) @@ -91,16 +143,8 @@ def load_mtl(mtl_file, obj_base_path, _info=print): raise IOError("Cannot open %s" % map_kd) _info("Loading %s" % map_kd_filename) - - with MemoryFile() as memfile: - with rasterio.open(map_kd, 'r') as src: - data = src.read() - with memfile.open(driver='JPEG', jpeg_quality=90, count=3, width=src.width, height=src.height, dtype=rasterio.dtypes.uint8) as dst: - for b in range(1, min(3, src.count) + 1): - # TODO: convert if uint16 or float - dst.write(data[b - 1], b) - memfile.seek(0) - mats[current_mtl] = memfile.read() + with rasterio.open(map_kd, 'r') as src: + mats[current_mtl] = src.read() return mats def paddedBuffer(buf, boundary): From ef41bed7d58966a22459f31b88fe1f31b0ae5eff Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Fri, 27 Jan 2023 15:23:13 -0500 Subject: [PATCH 8/9] Use KHR_materials_unlit extension, add draco CESIUM_RTC support --- SuperBuild/CMakeLists.txt | 4 ++-- opendm/gltf.py | 14 +++++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/SuperBuild/CMakeLists.txt b/SuperBuild/CMakeLists.txt index 4d9435b5..9793cddb 100644 --- a/SuperBuild/CMakeLists.txt +++ b/SuperBuild/CMakeLists.txt @@ -247,9 +247,9 @@ externalproject_add(lastools ) externalproject_add(draco - GIT_REPOSITORY https://github.com/google/draco + GIT_REPOSITORY https://github.com/OpenDroneMap/draco GIT_SHALLOW ON - GIT_TAG 1.5.5 + GIT_TAG 304 PREFIX ${SB_BINARY_DIR}/draco SOURCE_DIR ${SB_SOURCE_DIR}/draco CMAKE_ARGS -DDRACO_TRANSCODER_SUPPORTED=ON diff --git a/opendm/gltf.py b/opendm/gltf.py index e54969b2..df13bf48 100644 --- a/opendm/gltf.py +++ b/opendm/gltf.py @@ -9,6 +9,7 @@ from opendm import io warnings.filterwarnings("ignore", category=rasterio.errors.NotGeoreferencedWarning) + def load_obj(obj_path, _info=print): if not os.path.isfile(obj_path): raise IOError("Cannot open %s" % obj_path) @@ -253,8 +254,13 @@ def obj2glb(input_obj, output_glb, rtc=(None, None), draco_compression=True, _in images += [pygltflib.Image(bufferView=textureBufferView, mimeType="image/jpeg")] textures += [pygltflib.Texture(source=len(images) - 1, sampler=0)] - materials += [pygltflib.Material(pbrMetallicRoughness=pygltflib.PbrMetallicRoughness(baseColorTexture=pygltflib.TextureInfo(index=len(textures) - 1), metallicFactor=0, roughnessFactor=1), - alphaMode=pygltflib.MASK)] + + mat = pygltflib.Material(pbrMetallicRoughness=pygltflib.PbrMetallicRoughness(baseColorTexture=pygltflib.TextureInfo(index=len(textures) - 1), metallicFactor=0, roughnessFactor=1), + alphaMode=pygltflib.MASK) + mat.extensions = { + 'KHR_materials_unlit': {} + } + materials += [mat] gltf = pygltflib.GLTF2( scene=0, @@ -272,8 +278,10 @@ def obj2glb(input_obj, output_glb, rtc=(None, None), draco_compression=True, _in buffers=[pygltflib.Buffer(byteLength=len(binary))], ) + gltf.extensionsRequired = ['KHR_materials_unlit'] + if rtc != (None, None) and len(rtc) >= 2: - gltf.extensionsUsed = ['CESIUM_RTC'] + gltf.extensionsUsed = ['CESIUM_RTC', 'KHR_materials_unlit'] gltf.extensions = { 'CESIUM_RTC': { 'center': [float(rtc[0]), float(rtc[1]), 0.0] From 97c9a4f77352936b3726ff4b460f30dc680ff276 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Fri, 27 Jan 2023 18:33:15 -0500 Subject: [PATCH 9/9] Alpha mode opaque --- opendm/gltf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendm/gltf.py b/opendm/gltf.py index df13bf48..673dc86b 100644 --- a/opendm/gltf.py +++ b/opendm/gltf.py @@ -256,7 +256,7 @@ def obj2glb(input_obj, output_glb, rtc=(None, None), draco_compression=True, _in textures += [pygltflib.Texture(source=len(images) - 1, sampler=0)] mat = pygltflib.Material(pbrMetallicRoughness=pygltflib.PbrMetallicRoughness(baseColorTexture=pygltflib.TextureInfo(index=len(textures) - 1), metallicFactor=0, roughnessFactor=1), - alphaMode=pygltflib.MASK) + alphaMode=pygltflib.OPAQUE) mat.extensions = { 'KHR_materials_unlit': {} }