import io import numpy as np from PIL import Image as PIL_Image # gltflibのImageと被るので別名にする。 from PIL import ImageDraw as PIL_ImageDraw # 上記と同じ命名規則にする。 from scipy.spatial import Delaunay import cv2 import os import struct import matplotlib.pyplot as plt import triangle import uuid from gltflib import ( GLTF, GLTFModel, Asset, Scene, Node, Mesh, Primitive, Attributes, Buffer, BufferView, Image, Texture, TextureInfo, Material, Sampler, Accessor, AccessorType, BufferTarget, ComponentType, GLBResource, FileResource, PBRMetallicRoughness) # Common configuration information # Parts that are independent of the binary section's offset and size can be predefined. # Asset asset=Asset() # Image images=[ Image(mimeType='image/jpeg', bufferView=4), ] # Sampler samplers = [Sampler(magFilter=9728, minFilter=9984)] # magFilter:最近傍フィルタリング、minFilter:ミップマップ+最近傍フィルタリング # Texture textures = [ Texture(name='Main',sampler=0,source=0), ] # Material materials = [ Material( pbrMetallicRoughness=PBRMetallicRoughness( baseColorTexture=TextureInfo(index=0), metallicFactor=0, roughnessFactor=1 ), name='Material0', alphaMode='OPAQUE', doubleSided=True ), ] # Mesh meshes = [ Mesh(name='Main', primitives=[Primitive(attributes=Attributes(POSITION=0, NORMAL=1,TEXCOORD_0=2), indices=3, material=0, mode=4)]), ] # Scene scene = 0 scenes = [Scene(name='Scene', nodes=[0])] def create_manhole_model(original_img_bytearray): # Image img = PIL_Image.open(original_img_bytearray).convert('RGB') # Add edge color space. # (edge color is decided at 'Decide edge color' section) temp_img = PIL_Image.new('RGB', (img.size[0] + 3, img.size[1] + 3), 'black') temp_img.paste(img, (3, 3)) img = temp_img.copy() # Get coordinates of manhole ret, mask = cv2.threshold(cv2.cvtColor(np.array(img), cv2.COLOR_RGB2GRAY), 0, 255, cv2.THRESH_OTSU+cv2.THRESH_BINARY_INV) edges = cv2.Canny(mask , 50, 150, apertureSize=3) circle = cv2.HoughCircles(edges, cv2.HOUGH_GRADIENT, 1, int(max(img.size)*2), param1=50, param2=30, minRadius=0, maxRadius=0)[0][0] coordinate_list = [] num_polygons = 360 for i in range(num_polygons): coordinate_list.append(( int(circle[0] + np.cos((i / num_polygons) * (2 * np.pi)) * circle[2]), int(circle[1] + np.sin((i / num_polygons) * (2 * np.pi)) * circle[2]) )) coordinate_list = [coordinate_list] # Decide edge color coordinate_colors = list(map(lambda x: img.getpixel(x), coordinate_list[0])) edge_color = tuple(np.mean(np.array(coordinate_colors), axis=0).astype(np.uint8)) draw = PIL_ImageDraw.Draw(img) draw.point(tuple((i, j) for i in range(3) for j in range(3)), fill=edge_color) # image binary data img_ext = "JPEG" img_bytearray = io.BytesIO() img.save(img_bytearray, format=img_ext, quality=95) img_bytearray = img_bytearray.getvalue() img_bytelen = len(img_bytearray) # calculate scale of 3d model scale_factor = np.power(img.size[0] * img.size[1], 0.5) scale = (img.size[0] / scale_factor, img.size[1] / scale_factor, 0.4) # Thickness of manhole thickness = 0.2 # manhole # Vertex data(POSITION) front_vertex_list = [] back_vertex_list = [] for coordinates in coordinate_list: front_vertices = [] back_vertices = [] # Be aware that the Y-axis direction is inverted between images and GLB files for coordinate in coordinates: front_vertices.append((coordinate[0] * 2 / img.size[0] - 1.0, -(coordinate[1] * 2 / img.size[1] - 1.0), thickness)) back_vertices.append((coordinate[0] * 2 / img.size[0] - 1.0, -(coordinate[1] * 2 / img.size[1] - 1.0), -thickness)) front_vertex_list.append(front_vertices) back_vertex_list.append(back_vertices) vertices = front_vertex_list[0] + back_vertex_list[0] vertex_bytearray = bytearray() # Set vertices with 'Front+Back' and 'Edge' for i in range(2): # first for 'Front+Back', second for 'Edge' for vertex in vertices: for value in vertex: vertex_bytearray.extend(struct.pack('f', value)) vertex_bytelen = len(vertex_bytearray) mins = [min([vertex[i] for vertex in vertices]) for i in range(3)] maxs = [max([vertex[i] for vertex in vertices]) for i in range(3)] # Normal data(NORMAL) normals = [( 0.0, 0.0, 1.0)] * len(front_vertex_list[0]) + [( 0.0, 0.0, -1.0)] * len(back_vertex_list[0]) normal_bytearray = bytearray() # Consider not only 'Front+Back' but also 'Edge' for i in range(2): # first for 'Front+Back', second for 'Edge' for normal in normals: for value in normal: normal_bytearray.extend(struct.pack('f', value)) normal_bytelen = len(normal_bytearray) # Texture coordinates(TEXCOORD_0) texcoord_0s = [ ((vertex[0] + 1.0) / 2.0, 1.0 - ((vertex[1] + 1.0) / 2.0) ) for vertex in vertices ] texcoord_0_bytearray = bytearray() # 'Front+Back' for texcoord_0 in texcoord_0s: for value in texcoord_0: texcoord_0_bytearray.extend(struct.pack('f', value)) # 'Edge' for texcoord_0 in texcoord_0s: for value in texcoord_0: texcoord_0_bytearray.extend(struct.pack('f', 0.0)) texcoord_0_bytelen = len(texcoord_0_bytearray) # Vertex indices polygon = { 'vertices': np.array(front_vertex_list[0])[:, :2], 'segments': np.array([( i, (i + 1) % (len(front_vertex_list[0])) ) for i in range(len(front_vertex_list[0]))]) } triangulate_result = triangle.triangulate(polygon, 'p') vertex_indices = [] # Front vertex_indices.extend(list(np.array(triangulate_result['triangles']).flatten())) # Back temp_list = list((np.array(triangulate_result['triangles'])+len(front_vertex_list[0])).flatten()) # Swap vertex indices to get Back vertex indices from Front vertex indices def swap_elements(lst): if len(lst) < 2: return lst for i in range(len(lst) - 1): if i % 3 == 1: lst[i], lst[i + 1] = lst[i + 1], lst[i] return lst temp_list = swap_elements(temp_list) vertex_indices.extend(temp_list) # Back # Edge vertex_indices.extend(list(np.array([[len(vertices) + i, len(vertices) + (i + 1) % len(front_vertex_list[0]), len(vertices) + len(front_vertex_list[0]) + i] for i in range(len(front_vertex_list[0]))]).flatten())) vertex_indices.extend(list(np.array([[len(vertices) + len(front_vertex_list[0]) + (i + 1) % len(front_vertex_list[0]), len(vertices) + len(front_vertex_list[0]) + i, len(vertices) + (i + 1) % len(front_vertex_list[0])] for i in range(len(front_vertex_list[0]))]).flatten())) vertex_index_bytearray = bytearray() for value in vertex_indices: vertex_index_bytearray.extend(struct.pack('H', value)) vertex_index_bytelen = len(vertex_index_bytearray) # Concatenate binar data bytearray_list = [ vertex_bytearray, normal_bytearray, texcoord_0_bytearray, vertex_index_bytearray, img_bytearray, ] bytelen_list = [ vertex_bytelen, normal_bytelen, texcoord_0_bytelen, vertex_index_bytelen, img_bytelen, ] bytelen_cumsum_list = list(np.cumsum(bytelen_list)) bytelen_cumsum_list = list(map(lambda x: int(x), bytelen_cumsum_list)) all_bytearray = bytearray() for temp_bytearray in bytearray_list: all_bytearray.extend(temp_bytearray) offset_list = [0] + bytelen_cumsum_list # First offset is 0 offset_list.pop() # Delete the last element # Resource resources = [GLBResource(data=all_bytearray)] # Buffer buffers = [Buffer(byteLength=len(all_bytearray))] # BufferView bufferViews = [ BufferView(buffer=0, byteOffset=offset_list[0], byteLength=bytelen_list[0], target=BufferTarget.ARRAY_BUFFER.value), BufferView(buffer=0, byteOffset=offset_list[1], byteLength=bytelen_list[1], target=BufferTarget.ARRAY_BUFFER.value), BufferView(buffer=0, byteOffset=offset_list[2], byteLength=bytelen_list[2], target=BufferTarget.ARRAY_BUFFER.value), BufferView(buffer=0, byteOffset=offset_list[3], byteLength=bytelen_list[3], target=BufferTarget.ELEMENT_ARRAY_BUFFER.value), BufferView(buffer=0, byteOffset=offset_list[4], byteLength=bytelen_list[4], target=None), ] # Accessor accessors = [ Accessor(bufferView=0, componentType=ComponentType.FLOAT.value, count=len(vertices*2), type=AccessorType.VEC3.value, max=maxs, min=mins), Accessor(bufferView=1, componentType=ComponentType.FLOAT.value, count=len(normals*2), type=AccessorType.VEC3.value, max=None, min=None), Accessor(bufferView=2, componentType=ComponentType.FLOAT.value, count=len(texcoord_0s*2), type=AccessorType.VEC2.value, max=None, min=None), Accessor(bufferView=3, componentType=ComponentType.UNSIGNED_SHORT.value, count=len(vertex_indices), type=AccessorType.SCALAR.value, max=None, min=None) ] # Node nodes = [ Node(mesh=0,rotation=None, scale=scale), ] model = GLTFModel( asset=asset, buffers=buffers, bufferViews=bufferViews, accessors=accessors, images=images, samplers=samplers, textures=textures, materials=materials, meshes=meshes, nodes=nodes, scene=scene, scenes=scenes, ) gltf = GLTF(model=model, resources=resources) tmp_filename = uuid.uuid4().hex model_path = f'../tmp/{tmp_filename}.glb' gltf.export(model_path) return model_path