Convert OBJ file to PNG#

To produce images of wind turbines for visual impact assessment, the program loads .obj files and converts them into .png files. This conversion involves utilizing the view angle and wind direction to appropriately rotate the object file in the desired orientation.

Hide code cell source
import os
from pathlib import Path
import sys
sys.path.append(str(Path(os.getcwd()).parent.parent))
from src.utils import print_code, load_and_normalize_obj, plot_trisurface, crop_image, adjust_image, plot_trisurface
from IPython.core.display import HTML

Initially, the program loads vertices and faces from the .obj file and normalizes them based on the total height of the wind turbine.

HTML(print_code(load_and_normalize_obj))
def load_and_normalize_obj(file_path, total_height = 1):
    V, F = [], []
    with open(file_path) as f:
        for line in f.readlines():
            if line.startswith('#'):
                continue
            values = line.split()
            if not values:
                continue
            if values[0] == 'v':
                V.append([float(x) for x in values[1:4]])
            elif values[0] == 'f' :
                F.append([int(x.split("/", 1)[0]) for x in values[1:4]])
    V, F = np.array(V), np.array(F)-1
    V = (V - np.min(V[:,2])) / (np.max(V[:,2]) - np.min(V[:,2]))*total_height
    return V, F

The vertices are represented by points in 3D space, while the faces are defined by the indices of the vertices that form the face.

V, F = load_and_normalize_obj("../../assets/turbine.obj", total_height = 1)
print("V: ",V,"\n\nF: ", F)
V:  [[-0.38323411 -0.04002556  0.67077201]
 [-0.38322772 -0.03992025  0.67064993]
 [-0.38322314 -0.04018633  0.67075629]
 ...
 [ 0.19505491 -0.03958282  0.99919489]
 [ 0.19505738 -0.03963892  0.99921006]
 [ 0.1950604  -0.03974164  0.99916952]] 

F:  [[475 444 471]
 [477 492 471]
 [475 471 492]
 ...
 [388 583 348]
 [532 583 388]
 [466 532 388]]

Following that, the vertices and faces are loaded into matplotlib, and a 3D plot is created using plt.plot_trisurf. The background of the 3D plot is set to transparent and the image is thereafter saved as a .png file.

HTML(print_code(plot_trisurface))
def plot_trisurface(file_path, elevation = 0, azimuth = 0, view_height = 0, total_height = 1, show_plot = True):
    
    V, F = load_and_normalize_obj(file_path, total_height = total_height)

    fig = plt.figure(figsize=(6,6))
    ax = fig.add_subplot(111, projection="3d")

    plt.axis('off')
    plt.grid(visible=None)
    ax.view_init(elev=elevation, azim=azimuth)

    tri = ax.plot_trisurf(V[:, 0], V[:,1], F, V[:, 2], linewidth=0.1, antialiased=True, closed=True)
    tri.set(facecolor = "white", edgecolor = "gray")

    limits = np.array([getattr(ax, f"get_{axis}lim")() for axis in "xyz"])
    lower_bound = np.minimum(0, 2*view_height - total_height)
    upper_bound = np.maximum(total_height, 2*view_height)
    limits[2,:] = [lower_bound, upper_bound]
    ax.set(zlim=limits[2,:], aspect="equal")

    plt.savefig(Path.joinpath(ROOT_DIR, "temp/obj2png.png"), dpi=600, transparent=True)
    if show_plot:
        plt.show()
    else:
        plt.close() # prevent plot from showing
plot_trisurface("../../assets/turbine.obj", azimuth=-90)    
../../_images/9a7006ffe2f340cfc8d19db490fb10ae03a3cc69fa9a2def07ba7959bfd8d5b5.png

The image needs to be cropped to remove the axis from the plot. This is done be removing the 5 border pixels of the image.

HTML(print_code(crop_image))
def crop_image(file_path, display_image = True):
    pil_image = Image.open(file_path)
    pil_image = pil_image.crop((5, 5, pil_image.size[0]-5, pil_image.size[1]-5))
    np_array = np.array(pil_image)
    blank_px = [255, 255, 255, 0]
    mask = np_array != blank_px
    coords = np.argwhere(mask)
    x0, y0, z0 = coords.min(axis=0)
    x1, y1, z1 = coords.max(axis=0) + 1
    cropped_box = np_array[x0:x1, y0:y1, 0:4]
    pil_image = Image.fromarray(cropped_box, 'RGBA')
    pil_image.save(file_path)

    if display_image:
        display(pil_image)

    return pil_image
pil_image = crop_image("../../temp/obj2png.png")
../../_images/8d1a7db9a805c59212aa1ceced57ef2af5c86ebc34ba03c8c67a7944a0f1dbee.png

Lastly, the image needs to be adjusted according to the weather conditions of the Google Street View image.

HTML(print_code(adjust_image))
def adjust_image(image_path, brightness = 1, contrast = 1, display_image = True):
    # Load the image using PIL.
    if isinstance(image_path, (pathlib.PosixPath, str)):
        image = Image.open(image_path)
    elif isinstance(image_path, PIL.Image.Image):
        image = image_path
    else:
        print(f"DataTypeError: {type(image_path)} is an unsupported DataType.")

    # Apply the brightness and contrast adjustments to the image.
    enhancer = ImageEnhance.Brightness(image)
    image = enhancer.enhance(brightness)

    enhancer = ImageEnhance.Contrast(image)
    image = enhancer.enhance(contrast)

    # Save or display the adjusted image.
    # image.save("../../temp/obj2png.png")
    image.save(Path.joinpath(ROOT_DIR, "temp/obj2png.png"))
    if display_image:
        display(image)
    return image
image = adjust_image("../../temp/obj2png.png", brightness = 0.5, contrast = 0.5)
../../_images/40bc33a0f73067169063b02cfb803df4f3577b395883f252a4f8d91f526ff73c.png

Currently, the program applies a fixed 50% reduction in contrast and brightness, as weather data hasn’t been integrated. This configuration is subject to adjustment in future iterations.