diff --git a/README.RU.md b/README.RU.md index 422523b..851d8e7 100644 --- a/README.RU.md +++ b/README.RU.md @@ -2,23 +2,35 @@ # Конвертер из растровых картинок в 3D-сетку треугольников -Программа на Python для трассировки карты высот в графическом формате ([PNG](http://www.libpng.org/pub/png/)) в трёхмерную векторную сетку треугольников (triangle meash) в формате [POVRay](https://www.povray.org/). Координаты x, y пикселя соответствуют координатам x, y узлов сетки, яркость пикселя соответствует высоте (z) узла сетки. В случае исходных графических файлов с низким разрешением полученная при трассировке 3D-сетка обеспечивает лучшее визуальное качество рендеринга, нежели исходные графические файлы при их использовании в качестве heightfield напрямую. +Программа на Python для трассировки карты высот в графическом формате ([PNG](http://www.libpng.org/pub/png/)) в трёхмерную векторную сетку треугольников (triangle meash) в форматах [POVRay](https://www.povray.org/) POV, Wavefront OBJ, а также STL для 3D-принтеров. Координаты x, y пикселя соответствуют координатам x, y узлов сетки, яркость пикселя соответствует высоте (z) узла сетки. В случае исходных графических файлов с низким разрешением полученная при трассировке 3D-сетка обеспечивает лучшее визуальное качество рендеринга, нежели исходные графические файлы при их использовании в качестве heightfield напрямую. ![Example of img2mesh output rendering](https://dnyarri.github.io/imgmesh/640/img2mesh.png) -В данной директории находится наиболее свежая версия программы; несколько более старых закопаны в *"old_versions"* для археологов будущего. +Содержимое: + +- **img2mesh** - удобный GUI, импортирующий все функции из программ ниже. + +- **img2pov** - конвертер PNG в POVRay scene. Полученная сцена содержит 3D-сетку, объект box (CSG intersection), создающий боковые стенки и дно, свет и камеру. Текстуры заявлены в declare отдельно для удобства редактирования. + +- **img2obj** - конвертер PNG в Wavefront OBJ. Экспортированный файл содержит только 3D-сетку. -**Update:** В рамках крупного обновления, помимо полной переделки экспорта в POVRay, также добавлены экспорт в Wavefront OBJ и STL для 3D-принтеров. +- **img2stl** - конвертер PNG в STL. Экспортированный файл содержит 3D-сетку и боковые и нижнюю поверхности в виде сетки, поскольку они необходимы 3D-принтеру. + +Следует заметить, что img2pov, img2obj и img2stl могут как работать самостоятельно по отдельности, так и быть удобно импортированы во внешнюю программу (как это сделано в img2mesh). + +![Preview of img2mesh output files in one folder](https://dnyarri.github.io/imgmesh/printscreen.png) + +В данной директории находится наиболее свежая версия программы; несколько более старых закопаны в *"old_versions"* для археологов будущего. -*Зависимости от внешних библиотек:* [PyPNG](https://gitlab.com/drj11/pypng), Tkinter. Первая лежит рядом с программой в репозитории, и, слава Создателю, способна работать в таком виде без установки; вторая входит во все типовые дистрибутивы Python. +*Зависимости от внешних библиотек:* [PyPNG](https://gitlab.com/drj11/pypng), Tkinter. Первая лежит рядом с программой в репозитории, и, слава cоздателю, способна работать в таком виде без установки; вторая входит во все типовые дистрибутивы Python. -*Инструкция по эксплуатации:* программы оборудованы минималистическим GUI, в результате всё, что вы должны сделать после запуска программы, это с помощью окна "Open..." выбрать и открыть файл PNG, с помощью окна "Save..." выбрать POV-файл для сохранения, подождать, пока программа отработает и закроется, затем открыть полученный POV-файл в POVRay и нажать кнопочку "Render". Экспортированная сцена содержит необходимый минимум глобальных переменных и объектов (камера, свет) для ознакомительного рендеринга без редактирования. Текстуры объекта и параметры камеры и т.п. записаны в максимально общем виде с комментариями, и должны быть просты для понимания и редактирования. +*Инструкция по эксплуатации:* программы оборудованы минималистическим GUI, в результате всё, что вы должны сделать после запуска программы, это с помощью окна "Open..." выбрать и открыть файл PNG, с помощью окна "Save..." выбрать файл для сохранения, подождать, пока программа отработает и закроется, затем открыть полученный файл в подходящей программе. Прочие программы: -[Dnyarri website](https://dnyarri.github.io/) +[Вебсайт Жабы Огромной Умственной Силы](https://dnyarri.github.io/) -Во избежание санкций проект зеркалится: +Во избежание перебоев проект зеркалится: [github Dnyarri](https://github.com/Dnyarri/img2mesh) diff --git a/README.md b/README.md index 4ce5fe4..77e3a5d 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,29 @@ # Bitmap to POVRay 3D triangle mesh converter -Python program for conversion of bitmap heightfield (in [PNG format](http://www.libpng.org/pub/png/)) to 3D triangle mesh in [POVRay](https://www.povray.org/) format. Resulting triangle mesh provides better rendering in case of low-res source files as compared to using source bitmaps as a heightfield directly. +Python program for conversion of bitmap heightfield (in [PNG format](http://www.libpng.org/pub/png/)) to 3D triangle mesh in [POVRay](https://www.povray.org/) POV, Wavefront OBJ and stereolithography (3D printer) STL format. Resulting triangle mesh provides better rendering in case of low-res source files as compared to using source bitmaps as a heightfield directly. ![Example of img2mesh output rendering](https://dnyarri.github.io/imgmesh/640/img2mesh.png) -Current dir contain most recent version of img2mesh program. Some previous versions are saved in *"old_versions"* for future alien archeologist to dig. +Project content: + +- **img2mesh** - suitable GUI frontend linked to all programs below. + +- **img2pov** - PNG to POVRay scene converter. Exported scene contains 3D mesh, bounding box (CSG intersection) to make it solid object with interior, camera and light. Textures are declared separately and easy to edit. + +- **img2obj** - PNG to Wavefront OBJ converter. Exported file contains 3D mesh only. -**Update:** As a part of major update, beside complete rewriting of POVRay export, Wavefront OBJ and stereolithography STL (for 3D-printing) exports were also added. +- **img2stl** - PNG to STL converter. Exported file contain 3D mesh with side and bottom meshes necessary for 3D printer software. + +Note that img2pov, img2obj and img2stl may be both run as standalone programs and be imported into some other software (currently in main img2mesh). + +![Preview of img2mesh output files in one folder](https://dnyarri.github.io/imgmesh/printscreen.png) + +Current dir contain most recent version of img2mesh program. Some previous versions are saved in *"old_versions"* for future alien archeologist to dig. *Dependencies:* [PyPNG](https://gitlab.com/drj11/pypng), Tkinter. The former is placed in this repo and, thank the Maker, will work right after downloading; the latter included in all typical Python installation. -*Usage:* program equipped with minimal GUI, so all you have to do after starting the program is use standard "Open..." GUI to open image file, then use standard "Save..." GUI to set POVRay scene file to be created, then wait while program does the job, then open resulting POV file with POVRay and render the scene. Scene contains enough basic stuff (globals, light, camera) to be rendered successfully right after exporting without any editing. +*Usage:* programs are equipped with minimal GUI, so all you have to do after starting the program is use standard "Open..." dialog to open image file, then use standard "Save..." to name exported file to be created, then wait while program does the job, then open resulting file with suitable software and render the scene. More software at: diff --git a/bsk.ico b/bsk.ico deleted file mode 100644 index 88b96f1..0000000 Binary files a/bsk.ico and /dev/null differ diff --git a/g.ico b/g.ico deleted file mode 100644 index b50adb2..0000000 Binary files a/g.ico and /dev/null differ diff --git a/geo.ico b/geo.ico deleted file mode 100644 index 0eca9f2..0000000 Binary files a/geo.ico and /dev/null differ diff --git a/img2mesh-OBJ.py b/img2mesh-OBJ.py deleted file mode 100644 index cd39849..0000000 --- a/img2mesh-OBJ.py +++ /dev/null @@ -1,226 +0,0 @@ -#!/usr/bin/env python - -''' -IMG2MESH OBJ - Program for conversion of image heightfield to triangle mesh in OBJ format ------------------------------------------------------------------------------------------ - -Created by: Ilya Razmanov (mailto:ilyarazmanov@gmail.com) - aka Ilyich the Toad (mailto:amphisoft@gmail.com) -History: -1.0.0.0 Initial production release - - Main site: - https://dnyarri.github.io - - Project mirrored at: - https://github.com/Dnyarri/img2mesh - https://gitflic.ru/project/dnyarri/img2mesh - -''' - -__author__ = "Ilya Razmanov" -__copyright__ = "(c) 2024 Ilya Razmanov" -__credits__ = "Ilya Razmanov" -__license__ = "unlicense" -__version__ = "1.0.0.0" -__maintainer__ = "Ilya Razmanov" -__email__ = "ilyarazmanov@gmail.com" -__status__ = "Production" - -from tkinter import Tk -from tkinter import Label -from tkinter import filedialog - -from pathlib import Path - -from png import Reader # I/O with PyPNG from: https://gitlab.com/drj11/pypng - -# -------------------------------------------------------------- -# Creating dialog - -iconpath = Path(__file__).resolve().parent / 'g.ico' -iconname = str(iconpath) -useicon = iconpath.exists() # Check if icon file really exist. If False, it will not be used later. - -sortir = Tk() -sortir.title('PNG to OBJ conversion') -if useicon: - sortir.iconbitmap(iconname) # Replacement for simple sortir.iconbitmap('name.ico') - ugly but stable. -sortir.geometry('+200+100') -zanyato = Label(sortir, text='Allons-y!', font=("arial", 14), padx=16, pady=10, justify='center') -zanyato.pack() -sortir.withdraw() - -# Main dialog created and hidden -# -------------------------------------------------------------- - -# -------------------------------------------------------------- -# Open source image -sourcefilename = filedialog.askopenfilename( - title='Open source PNG file', filetypes=[('PNG', '.png')], defaultextension=('PNG', '.png') -) -if sourcefilename == '': - quit() - -source = Reader(filename=sourcefilename) # starting PyPNG - -X, Y, pixels, info = source.asDirect() # Opening image, iDAT comes to "pixels" as bytearray, to be tuple'd later - -Z = info['planes'] # Maximum CHANNEL NUMBER -imagedata = tuple((pixels)) # Attempt to fix all bytearrays into something solid - -if info['bitdepth'] == 8: - maxcolors = 255 # Maximal value for 8-bit channel -if info['bitdepth'] == 16: - maxcolors = 65535 # Maximal value for 16-bit channel - -# Open export file -resultfile = filedialog.asksaveasfile( - mode='w', - title='Save Wavefront OBJ file', - filetypes=[ - ('Wavefront OBJ file', '*.obj'), - ('All Files', '*.*'), - ], - defaultextension=('Wavefront OBJ file', '.obj'), -) -if resultfile == '': - quit() -# Both files opened -# -------------------------------------------------------------- - -# -------------------------------------------------------------- -# Functions block: -# -# src a-la FM style src(x,y,z) -# Image should be opened as "imagedata" by main program before -# Note that X, Y, Z are not determined in function, you have to determine it in main program - - -def src(x, y, z): - ''' - Analog of src from FilterMeister, force repeat edge instead of out of range - ''' - cx = x - cy = y - cx = max(0, cx) - cx = min((X - 1), cx) - cy = max(0, cy) - cy = min((Y - 1), cy) - - position = (cx * Z) + z # Here is the main magic of turning two x, z into one array position - channelvalue = int(((imagedata[cy])[position])) - - return channelvalue - - -# end of src function - - -def srcY(x, y): - ''' - Converting to greyscale, returns Yntensity, force repeat edge instead of out of range - ''' - cx = x - cy = y - cx = max(0, cx) - cx = min((X - 1), cx) - cy = max(0, cy) - cy = min((Y - 1), cy) - - if info['planes'] < 3: # supposedly L and LA - Yntensity = src(x, y, 0) - else: # supposedly RGB and RGBA - Yntensity = int(0.2989 * src(x, y, 0) + 0.587 * src(x, y, 1) + 0.114 * src(x, y, 2)) - - return Yntensity - - -# end of srcY function -# -# end of Functions block -# -------------------------------------------------------------- - -# Global positioning and scaling to tweak. - -xOffset = -0.5*float(X-1) # To be added BEFORE rescaling to center object. -yOffset = -0.5*float(Y-1) # To be added BEFORE rescaling to center object -zOffset = 0.0 - -xRescale = 1.0 / float(max(X, Y)) # To fit object into 1,1,1 cube -yRescale = xRescale -zRescale = 1.0 / float(maxcolors) - -# WRITING OBJ FILE, finally - -resultfile.write('o pryanik_nepechatnyj\n') # opening object - -# Now going to cycle through image and build mesh - -for y in range(0, Y, 1): - - message = 'Processing row ' + str(y) + ' of ' + str(Y) + '...' - sortir.deiconify() - zanyato.config(text=message) - sortir.update() - sortir.update_idletasks() - - for x in range(0, X, 1): - - # Since I was unable to find clear declaration of coordinate system, I'll plug a coordinate switch here - - # Reading switch: - xRead = x - yRead = (Y - 1 - y) # 'yRead = Y - y' coordinate mirror to mimic Photoshop coordinate system; +/- 1 steps below are inverted correspondingly vs. original img2mesh - - # Remains of Writing switch. No longer used since v. 0.1.0.2 but var names remained so dummy plug must be here. - xWrite = x - yWrite = y - - v9 = srcY(xRead, yRead) # Current pixel to process and write. Then going to neighbours - v1 = 0.25 * (v9 + srcY((xRead - 1), yRead) + srcY((xRead - 1), (yRead + 1)) + srcY(xRead, (yRead + 1))) - v3 = 0.25 * (v9 + srcY(xRead, (yRead + 1)) + srcY((xRead + 1), (yRead + 1)) + srcY((xRead + 1), yRead)) - v5 = 0.25 * (v9 + srcY((xRead + 1), yRead) + srcY((xRead + 1), (yRead - 1)) + srcY(xRead, (yRead - 1))) - v7 = 0.25 * (v9 + srcY(xRead, (yRead - 1)) + srcY((xRead - 1), (yRead - 1)) + srcY((xRead - 1), yRead)) - - # finally going to pyramid building - - # top part begins - resultfile.writelines( - [ - f'v {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v1):e}\n', - f'v {(xRescale*(xWrite+xOffset)):e} {(yRescale*(yWrite+yOffset)):e} {(zOffset+zRescale*v9):e}\n', - f'v {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v3):e}\n', - 'f -3 -2 -1\n', # triangle 2 - - f'v {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v3):e}\n', - f'v {(xRescale*(xWrite+xOffset)):e} {(yRescale*(yWrite+yOffset)):e} {(zOffset+zRescale*v9):e}\n', - f'v {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v5):e}\n', - 'f -3 -2 -1\n', # triangle 4 - - f'v {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v5):e}\n', - f'v {(xRescale*(xWrite+xOffset)):e} {(yRescale*(yWrite+yOffset)):e} {(zOffset+zRescale*v9):e}\n', - f'v {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v7):e}\n', - f'f -3 -2 -1\n', # triangle 6 - - f'v {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v7):e}\n', - f'v {(xRescale*(xWrite+xOffset)):e} {(yRescale*(yWrite+yOffset)):e} {(zOffset+zRescale*v9):e}\n', - f'v {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v1):e}\n', - f'f -3 -2 -1\n', # triangle 8 - ] - ) - # top part ends - -resultfile.write('# end pryanik_nepechatnyj') # closing object - -# Close output -resultfile.close() - -# -------------------------------------------------------------- -# Destroying dialog - -sortir.destroy() -sortir.mainloop() - -# Dialog destroyed and closed -# -------------------------------------------------------------- diff --git a/img2mesh-POV.py b/img2mesh-POV.py deleted file mode 100644 index dfeba79..0000000 --- a/img2mesh-POV.py +++ /dev/null @@ -1,308 +0,0 @@ -#!/usr/bin/env python - -''' -IMG2MESH-POV - Program for conversion of image heightfield to triangle mesh in POVRay format ---------------------------------------------------------------------------------------------- - -Created by: Ilya Razmanov (mailto:ilyarazmanov@gmail.com) - aka Ilyich the Toad (mailto:amphisoft@gmail.com) -History: -001 Abandoned img2mesh v.1 and turned to img2mesh v.2 with completely different mesh structure. -005 Replaced Pillow I/O with PyPNG from: https://gitlab.com/drj11/pypng - Support for 16 bit/channel PNGs added. Added mesh encapsulation with cubic box - to provide solid walls and bottom. Restructured output for easy reading. - Extended POVRay camera description. -007 Output cleanup and generalization. GUI improved to show progress during long processing. - Reducing unnecessary import. -2.7.1.0 Significant code cleanup with .writelines. Versioning more clear. -2.8.0.0 Total rewrite to remove all transforms from POVRay. - - Main site: - https://dnyarri.github.io - - Project mirrored at: - https://github.com/Dnyarri/img2mesh - https://gitflic.ru/project/dnyarri/img2mesh - -''' - -__author__ = "Ilya Razmanov" -__copyright__ = "(c) 2023-2024 Ilya Razmanov" -__credits__ = "Ilya Razmanov" -__license__ = "unlicense" -__version__ = "2.8.0.0" -__maintainer__ = "Ilya Razmanov" -__email__ = "ilyarazmanov@gmail.com" -__status__ = "Production" - -from tkinter import Tk -from tkinter import Label -from tkinter import filedialog - -from time import time -from time import ctime - -from pathlib import Path - -from png import Reader # I/O with PyPNG from: https://gitlab.com/drj11/pypng - -# -------------------------------------------------------------- -# Creating dialog - -iconpath = Path(__file__).resolve().parent / 'bsk.ico' -iconname = str(iconpath) -useicon = iconpath.exists() # Check if icon file really exist. If False, it will not be used later. - -sortir = Tk() -sortir.title('PNG to POV conversion') -if useicon: - sortir.iconbitmap(iconname) # Replacement for simple sortir.iconbitmap('name.ico') - ugly but stable. -sortir.geometry('+200+100') -zanyato = Label(sortir, text = 'Starting...', font=("arial", 14), padx=16, pady=10, justify='center') -zanyato.pack() -sortir.withdraw() - -# Main dialog created and hidden -# -------------------------------------------------------------- - -# Open source image -sourcefilename = filedialog.askopenfilename(title='Open source PNG file', filetypes=[('PNG','.png')], defaultextension = ('PNG','.png')) -if (sourcefilename == ''): - quit() - -source = Reader(filename = sourcefilename) # starting PyPNG - -X,Y,pixels,info = source.asDirect() # Opening image, iDAT comes to "pixels" as bytearray, to be tuple'd later - -Z = (info['planes']) # Maximum CHANNEL NUMBER -imagedata = tuple((pixels)) # Attempt to fix all bytearrays - -if (info['bitdepth'] == 8): - maxcolors = 255 # Maximal value for 8-bit channel -if (info['bitdepth'] == 16): - maxcolors = 65535 # Maximal value for 16-bit channel - -# Open export file -resultfile = filedialog.asksaveasfile(mode='w', title='Save resulting POV file', filetypes = - [ - ('POV-Ray scene file', '*.pov'), - ('All Files', '*.*'), - ], - defaultextension = ('POV-Ray scene file','.pov')) -if (resultfile == ''): - quit() -# Both files opened - -# src a-la FM style src(x,y,z) -# Image should be opened as "imagedata" by main program before -# Note that X, Y, Z are not determined in function, you have to determine it in main program - -def src(x, y, z): - ''' - Analog src from FM, force repeat edge instead of out of range - ''' - cx = x; cy = y - cx = max(0,cx); cx = min((X-1),cx) - cy = max(0,cy); cy = min((Y-1),cy) - - position = (cx*Z) + z # Here is the main magic of turning two x, z into one array position - channelvalue = int(((imagedata[cy])[position])) - - return channelvalue -# end of src function - -def srcY(x, y): - ''' - Converting to greyscale, returns Yntensity, force repeat edge instead of out of range - ''' - cx = x; cy = y - cx = max(0,cx); cx = min((X-1),cx) - cy = max(0,cy); cy = min((Y-1),cy) - - if (info['planes'] < 3): # supposedly L and LA - Yntensity = src(x, y, 0) - else: # supposedly RGB and RGBA - Yntensity = int(0.2989*src(x, y, 0) + 0.587*src(x, y, 1) + 0.114*src(x, y, 2)) - - return Yntensity -# end of srcY function - -# WRITING POV FILE - -# ------------ -# POV header -# ------------ - -resultfile.writelines( - [ - '/*\n', - 'Persistence of Vision Ray Tracer Scene Description File\n', - 'Version: 3.7\n', - 'Description: A triangle mesh scene file converted from image heightfield\n', - 'Author: Automatically generated by img2mesh program\n', - ' https://github.com/Dnyarri/img2mesh\n', - ' https://gitflic.ru/project/dnyarri/img2mesh\n', - 'developed by Ilya Razmanov aka Ilyich the Toad\n', - ' https://dnyarri.github.io/\n', - ' mailto:ilyarazmanov@gmail.com\n', - '*/\n\n', - ] -) - -resultfile.write(f'// Converted from: {sourcefilename} ') -seconds = time() -localtime = ctime(seconds) -resultfile.write(f'at: {localtime}\n// Source info: {info}\n\n') - -# Statements - -resultfile.writelines( - [ - '\n', - '#version 3.7;\n\n', - 'global_settings{\n', - ' max_trace_level 3 // Set low to speed up rendering. May need to be increased for metals and glasses\n', - ' adc_bailout 0.01 // Set high to speed up rendering. May need to be decreased to 1/256 for better quality\n', - ' ambient_light <0.5,0.5,0.5>\n', - ' assumed_gamma 1.0\n}\n\n', - - '#include "colors.inc"\n', - '#include "finish.inc"\n', - '#include "metals.inc"\n', - '#include "golds.inc"\n\n', - - '#declare thethingtexturebottom =\n', - ' texture {\n', - ' pigment {\n', - ' gradient z\n', - ' colour_map {\n', - ' [0, Red]\n', - ' [0.5 Blue]\n', - ' [1, White]\n', - ' }\n', - ' }\n', - ' }\n', - - '#declare thethingtexturetop =\n', - ' texture {\n', - ' pigment {\n', - ' gradient z\n', - ' colour_map {\n', - ' [0.00, rgbt <0,0,0,1>]\n', - ' [0.48, rgbt <0,0,0,1>]\n', - ' [0.50, rgbt <0,0,0,0>]\n', - ' [0.52, rgbt <0,0,0,1>]\n', - ' [1.00, rgbt <0,0,0,1>]\n', - ' }\n', - ' }\n', - ' scale 0.2\n', - ' }\n', - ] -) - -# Mesh - -# Global positioning and scaling to tweak. - -xOffset = -0.5*float(X-1) # To be added BEFORE rescaling to center object. -yOffset = -0.5*float(Y-1) # To be added BEFORE rescaling to center object -zOffset = 0.0 - -xRescale = 1.0 / float(max(X, Y)) # To fit object into 1,1,1 cube -yRescale = xRescale -zRescale = 1.0 / float(maxcolors) - - -resultfile.write('\n\n#declare thething = mesh {\n') # Opening mesh object "thething" - -# Now going to cycle through image and build mesh - -for y in range(0, Y, 1): - - message = ('Processing row ' + str(y) +' of ' + str(Y) + '...') - sortir.deiconify() - zanyato.config(text = message) - sortir.update() - sortir.update_idletasks() - - resultfile.write(f'\n\n // Row {y}\n') - - for x in range(0, X, 1): - - # Reading switch: - xRead = (X - 1 - x) - yRead = (Y - 1 - y) - - # Last remains of writing switch. No longer used but var names remained active so dummy plug must be here. - xWrite = x - yWrite = y - - v9 = srcY(xRead, yRead) # Current pixel to process and write. Then going to neighbours - v1 = 0.25 * (v9 + srcY((xRead + 1), yRead) + srcY((xRead + 1), (yRead + 1)) + srcY(xRead, (yRead + 1))) - v3 = 0.25 * (v9 + srcY(xRead, (yRead + 1)) + srcY((xRead - 1), (yRead + 1)) + srcY((xRead - 1), yRead)) - v5 = 0.25 * (v9 + srcY((xRead - 1), yRead) + srcY((xRead - 1), (yRead - 1)) + srcY(xRead, (yRead - 1))) - v7 = 0.25 * (v9 + srcY(xRead, (yRead - 1)) + srcY((xRead + 1), (yRead - 1)) + srcY((xRead + 1), yRead)) - - # finally going to pyramid building - - resultfile.write(f'\n triangle{{<{xRescale*(xWrite-0.5+xOffset)}, {yRescale*(yWrite-0.5+yOffset)}, {zRescale*v1}> <{xRescale*(xWrite+xOffset)}, {yRescale*(yWrite+yOffset)}, {zRescale*v9}> <{xRescale*(xWrite+0.5+xOffset)}, {yRescale*(yWrite-0.5+yOffset)}, {zRescale*v3}>}}') # Triangle 2 1-9-3 - - resultfile.write(f'\n triangle{{<{xRescale*(xWrite+0.5+xOffset)}, {yRescale*(yWrite-0.5+yOffset)}, {zRescale*v3}> <{xRescale*(xWrite+xOffset)}, {yRescale*(yWrite+yOffset)}, {zRescale*v9}> <{xRescale*(xWrite+0.5+xOffset)}, {yRescale*(yWrite+0.5+yOffset)}, {zRescale*v5}>}}') # Triangle 4 3-9-5 - - resultfile.write(f'\n triangle{{<{xRescale*(xWrite+0.5+xOffset)}, {yRescale*(yWrite+0.5+yOffset)}, {zRescale*v5}> <{xRescale*(xWrite+xOffset)}, {yRescale*(yWrite+yOffset)}, {zRescale*v9}> <{xRescale*(xWrite-0.5+xOffset)}, {yRescale*(yWrite+0.5+yOffset)}, {zRescale*v7}>}}') # Triangle 6 5-9-7 - - resultfile.write(f'\n triangle{{<{xRescale*(xWrite-0.5+xOffset)}, {yRescale*(yWrite+0.5+yOffset)}, {zRescale*v7}> <{xRescale*(xWrite+xOffset)}, {yRescale*(yWrite+yOffset)}, {zRescale*v9}> <{xRescale*(xWrite-0.5+xOffset)}, {yRescale*(yWrite-0.5+yOffset)}, {zRescale*v1}>}}') # Triangle 8 7-9-1 - - # completed pyramid. Ave me! - -resultfile.write('\n\n inside_vector <0, 0, 1>\n\n') - -# Sample texture of textures -resultfile.writelines( - [ - ' texture {thethingtexturebottom}\n', - ' texture {thethingtexturetop}\n', - '}\n// Closed thething\n\n', - '#declare boxedthing = object{\n', - ' intersection {\n', - ' box {<-0.5, -0.5, 0>, <0.5, 0.5, 1.0>\n', - ' pigment {rgb <0.5, 0.5, 5>}\n', - ' }\n', - ' thething\n', - ' }\n', - '}', - '// Constructed CGS "boxedthing" of mesh plus bounding box thus adding side walls and bottom\n\n', - 'object {boxedthing}\n\n', - ] -) # Closing solids - -# Camera -proportions = max(X,Y)/X -resultfile.writelines( - [ - '#declare camera_height = 3.0;\n\n', - 'camera{\n', - '// orthographic\n', - ' location <0.0, 0.0, camera_height>\n', - ' right x*image_width/image_height\n' - ' up y\n' - ' direction <0, 0, 1>\n', - f' angle 2.0*(degrees(atan2({0.5 * proportions}, camera_height-1.0))) // Supposed to fit object\n', - ' look_at <0.0, 0.0, 0.0>\n}\n\n', - ] -) - -# Light -resultfile.write('light_source {0*x\n color rgb <1.0, 1.0, 1.0>\n translate <20, 20, 20>\n}\n') -resultfile.write('\n/*\n\nhappy rendering\n\n 0~0\n (---)\n(.>|<.)\n-------\n\n*/') -# Close output -resultfile.close() - -# -------------------------------------------------------------- -# Destroying dialog - -sortir.destroy() -sortir.mainloop() - -# Dialog destroyed and closed -# -------------------------------------------------------------- diff --git a/img2mesh-STL.py b/img2mesh-STL.py deleted file mode 100644 index 2486fa7..0000000 --- a/img2mesh-STL.py +++ /dev/null @@ -1,358 +0,0 @@ -#!/usr/bin/env python - -''' -IMG2MESH STL - Program for conversion of image heightfield to triangle mesh in STL format ------------------------------------------------------------------------------------------ - -Created by: Ilya Razmanov (mailto:ilyarazmanov@gmail.com) - aka Ilyich the Toad (mailto:amphisoft@gmail.com) -History: -1.0.0.0 Initial production release - - Main site: - https://dnyarri.github.io - - Project mirrored at: - https://github.com/Dnyarri/img2mesh - https://gitflic.ru/project/dnyarri/img2mesh - -''' - -__author__ = "Ilya Razmanov" -__copyright__ = "(c) 2024 Ilya Razmanov" -__credits__ = "Ilya Razmanov" -__license__ = "unlicense" -__version__ = "1.0.0.0" -__maintainer__ = "Ilya Razmanov" -__email__ = "ilyarazmanov@gmail.com" -__status__ = "Production" - -from tkinter import Tk -from tkinter import Label -from tkinter import filedialog - -from pathlib import Path - -from png import Reader # I/O with PyPNG from: https://gitlab.com/drj11/pypng - -# -------------------------------------------------------------- -# Creating dialog - -iconpath = Path(__file__).resolve().parent / 'geo.ico' -iconname = str(iconpath) -useicon = iconpath.exists() # Check if icon file really exist. If False, it will not be used later. - -sortir = Tk() -sortir.title('PNG to STL conversion') -if useicon: - sortir.iconbitmap(iconname) # Replacement for simple sortir.iconbitmap('name.ico') - ugly but stable. -sortir.geometry('+200+100') -zanyato = Label(sortir, text='Allons-y!', font=("arial", 14), padx=16, pady=10, justify='center') -zanyato.pack() -sortir.withdraw() - -# Main dialog created and hidden -# -------------------------------------------------------------- - -# -------------------------------------------------------------- -# Open source image -sourcefilename = filedialog.askopenfilename( - title='Open source PNG file', filetypes=[('PNG', '.png')], defaultextension=('PNG', '.png') -) -if sourcefilename == '': - quit() - -source = Reader(filename=sourcefilename) # starting PyPNG - -X, Y, pixels, info = source.asDirect() # Opening image, iDAT comes to "pixels" as bytearray, to be tuple'd later - -Z = info['planes'] # Maximum CHANNEL NUMBER -imagedata = tuple((pixels)) # Attempt to fix all bytearrays into something solid - -if info['bitdepth'] == 8: - maxcolors = 255 # Maximal value for 8-bit channel -if info['bitdepth'] == 16: - maxcolors = 65535 # Maximal value for 16-bit channel - -# Open export file -resultfile = filedialog.asksaveasfile( - mode='w', - title='Save StereoLithography file', - filetypes=[ - ('StereoLithography object file', '*.stl'), - ('All Files', '*.*'), - ], - defaultextension=('StereoLithography object file', '.stl'), -) -if resultfile == '': - quit() -# Both files opened -# -------------------------------------------------------------- - -# -------------------------------------------------------------- -# Functions block: -# -# src a-la FM style src(x,y,z) -# Image should be opened as "imagedata" by main program before -# Note that X, Y, Z are not determined in function, you have to determine it in main program - - -def src(x, y, z): - ''' - Analog of src from FilterMeister, force repeat edge instead of out of range - ''' - cx = x - cy = y - cx = max(0, cx) - cx = min((X - 1), cx) - cy = max(0, cy) - cy = min((Y - 1), cy) - - position = (cx * Z) + z # Here is the main magic of turning two x, z into one array position - channelvalue = int(((imagedata[cy])[position])) - - return channelvalue - - -# end of src function - - -def srcY(x, y): - ''' - Converting to greyscale, returns Yntensity, force repeat edge instead of out of range - ''' - cx = x - cy = y - cx = max(0, cx) - cx = min((X - 1), cx) - cy = max(0, cy) - cy = min((Y - 1), cy) - - if info['planes'] < 3: # supposedly L and LA - Yntensity = src(x, y, 0) - else: # supposedly RGB and RGBA - Yntensity = int(0.2989 * src(x, y, 0) + 0.587 * src(x, y, 1) + 0.114 * src(x, y, 2)) - - return Yntensity - - -# end of srcY function -# -# end of Functions block -# -------------------------------------------------------------- - -# Global positioning and scaling to tweak. Offset supposed to make everyone feeling positive, rescale supposed to scale anything to [0..1.0] regardless of what the units are - -xOffset = 1.0 # To be added BEFORE rescaling to compensate 0.5 X expansion -yOffset = 1.0 # To be added BEFORE rescaling to compensate 0.5 Y expansion -zOffset = 0.0 # To be added AFTER rescaling just in case there should be something to fix - -xRescale = 1.0 / float(max(X, Y)) # To fit object into 1,1,1 cube -yRescale = xRescale -zRescale = 1.0 / float(maxcolors) - -# WRITING STL FILE, finally - -resultfile.write('solid pryanik_nepechatnyj\n') # opening object - -# Now going to cycle through image and build mesh - -for y in range(0, Y, 1): - - message = 'Processing row ' + str(y) + ' of ' + str(Y) + '...' - sortir.deiconify() - zanyato.config(text=message) - sortir.update() - sortir.update_idletasks() - - for x in range(0, X, 1): - - # Since I was unable to find clear declaration of coordinate system, I'll plug a coordinate switch here - - # Reading switch: - xRead = x - yRead = (Y - 1 - y) # 'yRead = Y - y' coordinate mirror to mimic Photoshop coordinate system; +/- 1 steps below are inverted correspondingly vs. original img2mesh - - # Remains of Writing switch. No longer used since v. 0.1.0.2 but var names remained so dummy plug must be here. - xWrite = x - yWrite = y - - v9 = srcY(xRead, yRead) # Current pixel to process and write. Then going to neighbours - v1 = 0.25 * (v9 + srcY((xRead - 1), yRead) + srcY((xRead - 1), (yRead + 1)) + srcY(xRead, (yRead + 1))) - v3 = 0.25 * (v9 + srcY(xRead, (yRead + 1)) + srcY((xRead + 1), (yRead + 1)) + srcY((xRead + 1), yRead)) - v5 = 0.25 * (v9 + srcY((xRead + 1), yRead) + srcY((xRead + 1), (yRead - 1)) + srcY(xRead, (yRead - 1))) - v7 = 0.25 * (v9 + srcY(xRead, (yRead - 1)) + srcY((xRead - 1), (yRead - 1)) + srcY((xRead - 1), yRead)) - - # finally going to pyramid building - - # top part begins - resultfile.writelines( - [ - ' facet normal 0 0 1\n', # triangle 2 normal up - ' outer loop\n', # 1 - 9 - 3 - f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v1):e}\n', - f' vertex {(xRescale*(xWrite+xOffset)):e} {(yRescale*(yWrite+yOffset)):e} {(zOffset+zRescale*v9):e}\n', - f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v3):e}\n', - ' endloop\n', - ' endfacet\n', - ' facet normal 0 0 1\n', # triangle 4 normal up - ' outer loop\n', # 3 - 9 - 5 - f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v3):e}\n', - f' vertex {(xRescale*(xWrite+xOffset)):e} {(yRescale*(yWrite+yOffset)):e} {(zOffset+zRescale*v9):e}\n', - f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v5):e}\n', - ' endloop\n', - ' endfacet\n', - ' facet normal 0 0 1\n', # triangle 6 normal up - ' outer loop\n', # 5 - 9 - 7 - f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v5):e}\n', - f' vertex {(xRescale*(xWrite+xOffset)):e} {(yRescale*(yWrite+yOffset)):e} {(zOffset+zRescale*v9):e}\n', - f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v7):e}\n', - ' endloop\n', - ' endfacet\n', - ' facet normal 0 0 1\n', # triangle 8 normal up - ' outer loop\n', # 7 - 9 - 1 - f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v7):e}\n', - f' vertex {(xRescale*(xWrite+xOffset)):e} {(yRescale*(yWrite+yOffset)):e} {(zOffset+zRescale*v9):e}\n', - f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v1):e}\n', - ' endloop\n', - ' endfacet\n', - ] - ) - # top part ends - - # left side begins - if x == 0: - resultfile.writelines( - [ - ' facet normal -1 0 0\n', # triangle 8- normal left - ' outer loop\n', # 1 - down1 - 7 - f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v1):e}\n', - f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+0.0):e}\n', - f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v7):e}\n', - ' endloop\n', - ' endfacet\n', - ' facet normal -1 0 0\n', # triangle 8- normal left - ' outer loop\n', # down1 - down7 - 7 - f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+0.0):e}\n', - f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+0.0):e}\n', - f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v7):e}\n', - ' endloop\n', - ' endfacet\n', - ] - ) - # left side ends - - # right side begins - if x == (X - 1): - resultfile.writelines( - [ - ' facet normal 1 0 0\n', # triangle 4+ normal left - ' outer loop\n', # 5 - down5 - 3 - f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v5):e}\n', - f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+0.0):e}\n', - f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v3):e}\n', - ' endloop\n', - ' endfacet\n', - ' facet normal 1 0 0\n', # triangle 4+ normal left - ' outer loop\n', # 3 - down5 - down3 - f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v3):e}\n', - f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+0.0):e}\n', - f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+0.0):e}\n', - ' endloop\n', - ' endfacet\n', - ] - ) - # right side ends - - # far side begins - if y == 0: - resultfile.writelines( - [ - ' facet normal 0 -1 0\n', # triangle 2- normal far - ' outer loop\n', # 3 - down - 1 - f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v3):e}\n', - f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+0.0):e}\n', - f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v1):e}\n', - ' endloop\n', - ' endfacet\n', - ' facet normal 0 -1 0\n', # triangle 2- normal far - ' outer loop\n', # down - down - 1 - f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+0.0):e}\n', - f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+0.0):e}\n', - f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v1):e}\n', - ' endloop\n', - ' endfacet\n', - ] - ) - # far side ends - - # close side begins - if y == (Y - 1): - resultfile.writelines( - [ - ' facet normal 0 1 0\n', # triangle 6+ normal close - ' outer loop\n', # 7 - down - 5 - f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v7):e}\n', - f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+0.0):e}\n', - f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v5):e}\n', - ' endloop\n', - ' endfacet\n', - ' facet normal 0 1 0\n', # triangle 6+ normal close - ' outer loop\n', # down - down - 5 - f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+0.0):e}\n', - f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+0.0):e}\n', - f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v5):e}\n', - ' endloop\n', - ' endfacet\n', - ] - ) - # close side ends - - # bottom part begins - resultfile.writelines( - [ - ' facet normal 0 0 -1\n', # triangle 2 normal up - ' outer loop\n', # 1 - 9 - 3 - f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+0.0):e}\n', - f' vertex {(xRescale*(xWrite+xOffset)):e} {(yRescale*(yWrite+yOffset)):e} {(zOffset+0.0):e}\n', - f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+0.0):e}\n', - ' endloop\n', - ' endfacet\n', - ' facet normal 0 0 -1\n', # triangle 4 normal up - ' outer loop\n', # 3 - 9 - 5 - f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+0.0):e}\n', - f' vertex {(xRescale*(xWrite+xOffset)):e} {(yRescale*(yWrite+yOffset)):e} {(zOffset+0.0):e}\n', - f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+0.0):e}\n', - ' endloop\n', - ' endfacet\n', - ' facet normal 0 0 -1\n', # triangle 6 normal up - ' outer loop\n', # 5 - 9 - 7 - f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+0.0):e}\n', - f' vertex {(xRescale*(xWrite+xOffset)):e} {(yRescale*(yWrite+yOffset)):e} {(zOffset+0.0):e}\n', - f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+0.0):e}\n', - ' endloop\n', - ' endfacet\n', - ' facet normal 0 0 -1\n', # triangle 8 normal up - ' outer loop\n', # 7 - 9 - 1 - f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+0.0):e}\n', - f' vertex {(xRescale*(xWrite+xOffset)):e} {(yRescale*(yWrite+yOffset)):e} {(zOffset+0.0):e}\n', - f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+0.0):e}\n', - ' endloop\n', - ' endfacet\n', - ] - ) - # bottom part ends - -resultfile.write('endsolid pryanik_nepechatnyj') # closing object - -# Close output -resultfile.close() - -# -------------------------------------------------------------- -# Destroying dialog - -sortir.destroy() -sortir.mainloop() - -# Dialog destroyed and closed -# -------------------------------------------------------------- diff --git a/img2mesh.py b/img2mesh.py new file mode 100644 index 0000000..212f46a --- /dev/null +++ b/img2mesh.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python +''' +IMG2MESH - Program for conversion of image heightfield to triangle mesh in different formats +--------------------------------------------------------------------------------------------- +Common GUI shell for img2pov, img2obj and img2stl modules. + +Created by: Ilya Razmanov (mailto:ilyarazmanov@gmail.com) + aka Ilyich the Toad (mailto:amphisoft@gmail.com) +History: + +1.0.0.0 Initial production release. + + Main site: + https://dnyarri.github.io + + Project mirrored at: + https://github.com/Dnyarri/img2mesh + https://gitflic.ru/project/dnyarri/img2mesh + +''' + +__author__ = "Ilya Razmanov" +__copyright__ = "(c) 2024 Ilya Razmanov" +__credits__ = "Ilya Razmanov" +__license__ = "unlicense" +__version__ = "1.0.0.0" +__maintainer__ = "Ilya Razmanov" +__email__ = "ilyarazmanov@gmail.com" +__status__ = "Production" + +from tkinter import Tk +from tkinter import Label, Button, TOP, BOTTOM, X + +from pathlib import Path + +from img2pov import img2pov +from img2obj import img2obj +from img2stl import img2stl + +# ACHTUNG! User break definition below. Take care. +DyeDye = False # Variable for breaking program anywhere upon conditions + + +def DisMiss(): # Kill dialog and continue + global DyeDye + DyeDye = False + stopper.destroy() + + +def DyeDyeMyDarling(): # Kill dialog and kill program + global DyeDye + DyeDye = True + stopper.destroy() + quit() + + +# -------------------------------------------------------------- +# Creating startup dialog (stopper) + +iconpath = Path(__file__).resolve().parent / 'vaba.ico' +iconname = str(iconpath) +useicon = iconpath.exists() # Check if icon file really exist. If False, it will not be used later. + +stopper = Tk() +stopper.title('IMG2MESH') +if useicon: + stopper.iconbitmap(iconname) +stopper.geometry('+200+100') +stopper.minsize(300, 300) +stopper.maxsize(400, 400) + +preved01 = Label(stopper, text = 'img2mesh', font=("arial", 36), padx=16, pady=10, justify='center') +preved01.pack(side=TOP, fill=X) + +preved02 = Label(stopper, text = 'PNG height fields to 3D mesh converter', font=("arial", 12), padx=16, pady=10, justify='center') +preved02.pack(side=TOP, fill=X) + +butt01 = Button(stopper, text='PNG to POV...', font=('arial', 16), cursor='hand2', justify='center', command=img2pov +) +butt01.pack(side=TOP, padx=4, pady=2, fill=X) + +butt02 = Button(stopper, text='PNG to OBJ...', font=('arial', 16), cursor='hand2', justify='center', command=img2obj +) +butt02.pack(side=TOP, padx=4, pady=2, fill=X) + +butt03 = Button(stopper, text='PNG to STL...', font=('arial', 16), cursor='hand2', justify='center', command=img2stl +) +butt03.pack(side=TOP, padx=4, pady=2, fill=X) + +butt04 = Button( + stopper, text='Exit', font=('arial', 16), cursor='hand2', justify='center', command=DyeDyeMyDarling +) +butt04.pack(side=BOTTOM, padx=4, pady=2, fill=X) + +stopper.mainloop() + +# Startup dialog created, used and killed +# -------------------------------------------------------------- + +if DyeDye: + quit() # Kill program if "Quit" was pressed in stopper diff --git a/img2obj.py b/img2obj.py new file mode 100644 index 0000000..343e39e --- /dev/null +++ b/img2obj.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python + +''' +IMG2OBJ - Program for conversion of image heightfield to triangle mesh in OBJ format +----------------------------------------------------------------------------------------- + +Created by: Ilya Razmanov (mailto:ilyarazmanov@gmail.com) + aka Ilyich the Toad (mailto:amphisoft@gmail.com) +History: +1.0.0.0 Initial production release. +1.0.1.0 Program converted into self-calling function to have a possibility to import it. + + Main site: + https://dnyarri.github.io + + Project mirrored at: + https://github.com/Dnyarri/img2mesh + https://gitflic.ru/project/dnyarri/img2mesh + +''' + +__author__ = "Ilya Razmanov" +__copyright__ = "(c) 2024 Ilya Razmanov" +__credits__ = "Ilya Razmanov" +__license__ = "unlicense" +__version__ = "1.0.1.0" +__maintainer__ = "Ilya Razmanov" +__email__ = "ilyarazmanov@gmail.com" +__status__ = "Production" + +from tkinter import Tk +from tkinter import Label +from tkinter import filedialog + +from pathlib import Path + +from png import Reader # I/O with PyPNG from: https://gitlab.com/drj11/pypng + +# ACHTUNG! Starting a whole-program procedure! + +def img2obj(): + ''' + Procedure for opening PNG heightfield and creating Wavefront .obj 3D mesh file from it. + + ''' + + # -------------------------------------------------------------- + # Creating dialog + + iconpath = Path(__file__).resolve().parent / 'vaba.ico' + iconname = str(iconpath) + useicon = iconpath.exists() # Check if icon file really exist. If False, it will not be used later. + + sortir = Tk() + sortir.title('PNG to OBJ conversion') + if useicon: + sortir.iconbitmap(iconname) # Replacement for simple sortir.iconbitmap('name.ico') - ugly but stable. + sortir.geometry('+200+100') + zanyato = Label(sortir, text='Allons-y!', font=("arial", 14), padx=16, pady=10, justify='center') + zanyato.pack() + sortir.withdraw() + + # Main dialog created and hidden + # -------------------------------------------------------------- + + + # Open source image + sourcefilename = filedialog.askopenfilename( + title='Open source PNG file', filetypes=[('PNG', '.png')], defaultextension=('PNG', '.png') + ) + # Source file name taken + + if (sourcefilename == '') or (sourcefilename == None): + return None + # break if user press 'Cancel' + + source = Reader(filename=sourcefilename) + # opening file with PyPNG + + X, Y, pixels, info = source.asDirect() + # Opening image, iDAT comes to "pixels" as bytearray, to be tuple'd later + + Z = info['planes'] # Maximum CHANNEL NUMBER + imagedata = tuple((pixels)) # Attempt to fix all bytearrays + + if info['bitdepth'] == 8: + maxcolors = 255 # Maximal value for 8-bit channel + if info['bitdepth'] == 16: + maxcolors = 65535 # Maximal value for 16-bit channel + + # source file opened, initial data received + + # opening result file, first get name + resultfilename = filedialog.asksaveasfilename( + title='Save Wavefront OBJ file', + filetypes=[ + ('Wavefront OBJ file', '*.obj'), + ('All Files', '*.*'), + ], + defaultextension=('Wavefront OBJ file', '.obj'), + ) + + if (resultfilename == '') or (sourcefilename == None): + return None + # break if user press 'Cancel' + # return doesn't seem to work well with .asksaveasfile + + resultfile = open(resultfilename, 'w') + # result file opened + + # Both files opened + + # -------------------------------------------------------------- + # Functions block: + # + # src a-la FM style src(x,y,z) + # Image should be opened as "imagedata" by main program before + # Note that X, Y, Z are not determined in function, you have to determine it in main program + + def src(x, y, z): + ''' + Analog of src from FilterMeister, force repeat edge instead of out of range + ''' + cx = x + cy = y + cx = max(0, cx) + cx = min((X - 1), cx) + cy = max(0, cy) + cy = min((Y - 1), cy) + + position = (cx * Z) + z # Here is the main magic of turning two x, z into one array position + channelvalue = int(((imagedata[cy])[position])) + + return channelvalue + + + # end of src function + + + def srcY(x, y): + ''' + Converting to greyscale, returns Yntensity, force repeat edge instead of out of range + ''' + cx = x + cy = y + cx = max(0, cx) + cx = min((X - 1), cx) + cy = max(0, cy) + cy = min((Y - 1), cy) + + if info['planes'] < 3: # supposedly L and LA + Yntensity = src(x, y, 0) + else: # supposedly RGB and RGBA + Yntensity = int(0.2989 * src(x, y, 0) + 0.587 * src(x, y, 1) + 0.114 * src(x, y, 2)) + + return Yntensity + + # end of srcY function + + # end of Functions block + # -------------------------------------------------------------- + + # Global positioning and scaling to tweak. + + xOffset = -0.5*float(X-1) # To be added BEFORE rescaling to center object. + yOffset = -0.5*float(Y-1) # To be added BEFORE rescaling to center object + zOffset = 0.0 + + xRescale = 1.0 / float(max(X, Y)) # To fit object into 1,1,1 cube + yRescale = xRescale + zRescale = 1.0 / float(maxcolors) + + # WRITING OBJ FILE, finally + + resultfile.write('o pryanik_nepechatnyj\n') # opening object + + # Now going to cycle through image and build mesh + + for y in range(0, Y, 1): + + message = 'Processing row ' + str(y) + ' of ' + str(Y) + '...' + sortir.deiconify() + zanyato.config(text=message) + sortir.update() + sortir.update_idletasks() + + for x in range(0, X, 1): + + # Since I was unable to find clear declaration of coordinate system, I'll plug a coordinate switch here + + # Reading switch: + xRead = x + yRead = (Y - 1 - y) # 'yRead = Y - y' coordinate mirror to mimic Photoshop coordinate system; +/- 1 steps below are inverted correspondingly vs. original img2mesh + + # Remains of Writing switch. No longer used since v. 0.1.0.2 but var names remained so dummy plug must be here. + xWrite = x + yWrite = y + + v9 = srcY(xRead, yRead) # Current pixel to process and write. Then going to neighbours + v1 = 0.25 * (v9 + srcY((xRead - 1), yRead) + srcY((xRead - 1), (yRead + 1)) + srcY(xRead, (yRead + 1))) + v3 = 0.25 * (v9 + srcY(xRead, (yRead + 1)) + srcY((xRead + 1), (yRead + 1)) + srcY((xRead + 1), yRead)) + v5 = 0.25 * (v9 + srcY((xRead + 1), yRead) + srcY((xRead + 1), (yRead - 1)) + srcY(xRead, (yRead - 1))) + v7 = 0.25 * (v9 + srcY(xRead, (yRead - 1)) + srcY((xRead - 1), (yRead - 1)) + srcY((xRead - 1), yRead)) + + # finally going to pyramid building + + # top part begins + resultfile.writelines( + [ + f'v {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v1):e}\n', + f'v {(xRescale*(xWrite+xOffset)):e} {(yRescale*(yWrite+yOffset)):e} {(zOffset+zRescale*v9):e}\n', + f'v {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v3):e}\n', + 'f -3 -2 -1\n', # triangle 2 + + f'v {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v3):e}\n', + f'v {(xRescale*(xWrite+xOffset)):e} {(yRescale*(yWrite+yOffset)):e} {(zOffset+zRescale*v9):e}\n', + f'v {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v5):e}\n', + 'f -3 -2 -1\n', # triangle 4 + + f'v {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v5):e}\n', + f'v {(xRescale*(xWrite+xOffset)):e} {(yRescale*(yWrite+yOffset)):e} {(zOffset+zRescale*v9):e}\n', + f'v {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v7):e}\n', + f'f -3 -2 -1\n', # triangle 6 + + f'v {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v7):e}\n', + f'v {(xRescale*(xWrite+xOffset)):e} {(yRescale*(yWrite+yOffset)):e} {(zOffset+zRescale*v9):e}\n', + f'v {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v1):e}\n', + f'f -3 -2 -1\n', # triangle 8 + ] + ) + # top part ends + + resultfile.write('# end pryanik_nepechatnyj') # closing object + + # Close output + resultfile.close() + + # -------------------------------------------------------------- + # Destroying dialog + + sortir.destroy() + sortir.mainloop() + + # Dialog destroyed and closed + # -------------------------------------------------------------- + +# Procedure ended, the program begins +if __name__ == "__main__": + img2obj() \ No newline at end of file diff --git a/img2pov.py b/img2pov.py new file mode 100644 index 0000000..05b248d --- /dev/null +++ b/img2pov.py @@ -0,0 +1,359 @@ +#!/usr/bin/env python + +''' +IMG2POV - Program for conversion of image heightfield to triangle mesh in POVRay format +--------------------------------------------------------------------------------------------- + +Created by: Ilya Razmanov (mailto:ilyarazmanov@gmail.com) + aka Ilyich the Toad (mailto:amphisoft@gmail.com) +History: + +001 Abandoned img2mesh v.1 and turned to img2mesh v.2 with completely different mesh structure. +005 Replaced Pillow I/O with PyPNG from: https://gitlab.com/drj11/pypng + Support for 16 bit/channel PNGs added. Added mesh encapsulation box. + Extended POVRay camera description. + Restructured output for easy reading. +007 Output cleanup and generalization. GUI improved to show progress during long processing. + Reducing unnecessary import. +2.7.1.0 Significant code cleanup with .writelines. Versioning more clear. +2.8.0.0 Total rewrite to remove all transforms from POVRay. +2.8.1.0 Program converted into self-calling function to have a possibility to import it. + + Main site: + https://dnyarri.github.io + + Project mirrored at: + https://github.com/Dnyarri/img2mesh + https://gitflic.ru/project/dnyarri/img2mesh + +''' + +__author__ = "Ilya Razmanov" +__copyright__ = "(c) 2023-2024 Ilya Razmanov" +__credits__ = "Ilya Razmanov" +__license__ = "unlicense" +__version__ = "2.8.1.0" +__maintainer__ = "Ilya Razmanov" +__email__ = "ilyarazmanov@gmail.com" +__status__ = "Production" + +from tkinter import Tk +from tkinter import Label +from tkinter import filedialog + +from time import time +from time import ctime + +from pathlib import Path + +from png import Reader # I/O with PyPNG from: https://gitlab.com/drj11/pypng + +# ACHTUNG! Starting a whole-program procedure! + + +def img2pov(): + ''' + Procedure for opening PNG heightfield and creating POVRay .pov 3D mesh file from it. + + ''' + + # -------------------------------------------------------------- + # Creating dialog + + iconpath = Path(__file__).resolve().parent / 'vaba.ico' + iconname = str(iconpath) + useicon = iconpath.exists() # Check if icon file really exist. If False, it will not be used later. + + sortir = Tk() + sortir.title('PNG to POV conversion') + if useicon: + sortir.iconbitmap(iconname) # Replacement for simple sortir.iconbitmap('name.ico') - ugly but stable. + sortir.geometry('+200+100') + zanyato = Label(sortir, text='Starting...', font=("arial", 14), padx=16, pady=10, justify='center') + zanyato.pack() + sortir.withdraw() + + # Main dialog created and hidden + # -------------------------------------------------------------- + + # Open source image + sourcefilename = filedialog.askopenfilename( + title='Open source PNG file', filetypes=[('PNG', '.png')], defaultextension=('PNG', '.png') + ) + # Source file name taken + + if (sourcefilename == '') or (sourcefilename == None): + return None + # break if user press 'Cancel' + + source = Reader(filename=sourcefilename) + # opening file with PyPNG + + X, Y, pixels, info = source.asDirect() + # Opening image, iDAT comes to "pixels" as bytearray, to be tuple'd later + + Z = info['planes'] # Maximum CHANNEL NUMBER + imagedata = tuple((pixels)) # Attempt to fix all bytearrays + + if info['bitdepth'] == 8: + maxcolors = 255 # Maximal value for 8-bit channel + if info['bitdepth'] == 16: + maxcolors = 65535 # Maximal value for 16-bit channel + + # source file opened, initial data received + + # opening result file, first get name + resultfilename = filedialog.asksaveasfilename( + title='Save POVRay scene file', + filetypes=[ + ('POV-Ray scene file', '*.pov'), + ('All Files', '*.*'), + ], + defaultextension=('POV-Ray scene file', '.pov'), + ) + + if (resultfilename == '') or (sourcefilename == None): + return None + # break if user press 'Cancel' + # return doesn't seem to work well with .asksaveasfile + + resultfile = open(resultfilename, 'w') + # result file opened + + # Both files opened + + # -------------------------------------------------------------- + # Functions block: + # + # src a-la FM style src(x,y,z) + # Image should be opened as "imagedata" by main program before + # Note that X, Y, Z are not determined in function, you have to determine it in main program + + def src(x, y, z): + ''' + Analog src from FM, force repeat edge instead of out of range + ''' + cx = x + cy = y + cx = max(0, cx) + cx = min((X - 1), cx) + cy = max(0, cy) + cy = min((Y - 1), cy) + + position = (cx * Z) + z # Here is the main magic of turning two x, z into one array position + channelvalue = int(((imagedata[cy])[position])) + + return channelvalue + + # end of src function + + def srcY(x, y): + ''' + Converting to greyscale, returns Yntensity, force repeat edge instead of out of range + ''' + cx = x + cy = y + cx = max(0, cx) + cx = min((X - 1), cx) + cy = max(0, cy) + cy = min((Y - 1), cy) + + if info['planes'] < 3: # supposedly L and LA + Yntensity = src(x, y, 0) + else: # supposedly RGB and RGBA + Yntensity = int(0.2989 * src(x, y, 0) + 0.587 * src(x, y, 1) + 0.114 * src(x, y, 2)) + + return Yntensity + + # end of srcY function + + # WRITING POV FILE + + # ------------ + # POV header + # --- + + resultfile.writelines( + [ + '/*\n', + 'Persistence of Vision Ray Tracer Scene Description File\n', + 'Version: 3.7\n', + 'Description: A triangle mesh scene file converted from image heightfield\n', + 'Author: Automatically generated by img2mesh program\n', + ' https://github.com/Dnyarri/img2mesh\n', + ' https://gitflic.ru/project/dnyarri/img2mesh\n', + 'developed by Ilya Razmanov aka Ilyich the Toad\n', + ' https://dnyarri.github.io/\n', + ' mailto:ilyarazmanov@gmail.com\n', + '*/\n\n', + ] + ) + + resultfile.write(f'// Converted from: {sourcefilename} ') + seconds = time() + localtime = ctime(seconds) + resultfile.write(f'at: {localtime}\n// Source info: {info}\n\n') + + # Statements + + resultfile.writelines( + [ + '\n', + '#version 3.7;\n\n', + 'global_settings{\n', + ' max_trace_level 3 // Set low to speed up rendering. May need to be increased for metals and glasses\n', + ' adc_bailout 0.01 // Set high to speed up rendering. May need to be decreased to 1/256 for better quality\n', + ' ambient_light <0.5,0.5,0.5>\n', + ' assumed_gamma 1.0\n}\n\n', + '#include "colors.inc"\n', + '#include "finish.inc"\n', + '#include "metals.inc"\n', + '#include "golds.inc"\n\n', + '#declare thethingtexturebottom =\n', + ' texture {\n', + ' pigment {\n', + ' gradient z\n', + ' colour_map {\n', + ' [0.0, Red]\n', + ' [0.5, Blue]\n', + ' [1.0, White]\n', + ' }\n', + ' }\n', + ' finish {phong 1.0}\n', + ' }\n', + '\n', + '#declare thethingtexturetop =\n', + ' texture {\n', + ' pigment {\n', + ' gradient z\n', + ' colour_map {\n', + ' [0.00, rgbt <0,0,0,1>]\n', + ' [0.48, rgbt <0,0,0,1>]\n', + ' [0.50, rgbt <0,0,0,0>]\n', + ' [0.52, rgbt <0,0,0,1>]\n', + ' [1.00, rgbt <0,0,0,1>]\n', + ' }\n', + ' }\n', + ' scale 0.2\n', + ' }\n', + ] + ) + + # Mesh + + # Global positioning and scaling to tweak. + + xOffset = -0.5 * float(X - 1) # To be added BEFORE rescaling to center object. + yOffset = -0.5 * float(Y - 1) # To be added BEFORE rescaling to center object + zOffset = 0.0 + + xRescale = 1.0 / float(max(X, Y)) # To fit object into 1,1,1 cube + yRescale = xRescale + zRescale = 1.0 / float(maxcolors) + + resultfile.write('\n\n#declare thething = mesh {\n') # Opening mesh object "thething" + + # Now going to cycle through image and build mesh + + for y in range(0, Y, 1): + + message = 'Processing row ' + str(y) + ' of ' + str(Y) + '...' + sortir.deiconify() + zanyato.config(text=message) + sortir.update() + sortir.update_idletasks() + + resultfile.write(f'\n\n // Row {y}\n') + + for x in range(0, X, 1): + + # Reading switch: + xRead = X - 1 - x + yRead = Y - 1 - y + + # Last remains of writing switch. No longer used but var names remained active so dummy plug must be here. + xWrite = x + yWrite = y + + v9 = srcY(xRead, yRead) # Current pixel to process and write. Then going to neighbours + v1 = 0.25 * (v9 + srcY((xRead + 1), yRead) + srcY((xRead + 1), (yRead + 1)) + srcY(xRead, (yRead + 1))) + v3 = 0.25 * (v9 + srcY(xRead, (yRead + 1)) + srcY((xRead - 1), (yRead + 1)) + srcY((xRead - 1), yRead)) + v5 = 0.25 * (v9 + srcY((xRead - 1), yRead) + srcY((xRead - 1), (yRead - 1)) + srcY(xRead, (yRead - 1))) + v7 = 0.25 * (v9 + srcY(xRead, (yRead - 1)) + srcY((xRead + 1), (yRead - 1)) + srcY((xRead + 1), yRead)) + + # finally going to pyramid building + + resultfile.write( + f'\n triangle{{<{xRescale*(xWrite-0.5+xOffset)}, {yRescale*(yWrite-0.5+yOffset)}, {zRescale*v1}> <{xRescale*(xWrite+xOffset)}, {yRescale*(yWrite+yOffset)}, {zRescale*v9}> <{xRescale*(xWrite+0.5+xOffset)}, {yRescale*(yWrite-0.5+yOffset)}, {zRescale*v3}>}}' + ) # Triangle 2 1-9-3 + + resultfile.write( + f'\n triangle{{<{xRescale*(xWrite+0.5+xOffset)}, {yRescale*(yWrite-0.5+yOffset)}, {zRescale*v3}> <{xRescale*(xWrite+xOffset)}, {yRescale*(yWrite+yOffset)}, {zRescale*v9}> <{xRescale*(xWrite+0.5+xOffset)}, {yRescale*(yWrite+0.5+yOffset)}, {zRescale*v5}>}}' + ) # Triangle 4 3-9-5 + + resultfile.write( + f'\n triangle{{<{xRescale*(xWrite+0.5+xOffset)}, {yRescale*(yWrite+0.5+yOffset)}, {zRescale*v5}> <{xRescale*(xWrite+xOffset)}, {yRescale*(yWrite+yOffset)}, {zRescale*v9}> <{xRescale*(xWrite-0.5+xOffset)}, {yRescale*(yWrite+0.5+yOffset)}, {zRescale*v7}>}}' + ) # Triangle 6 5-9-7 + + resultfile.write( + f'\n triangle{{<{xRescale*(xWrite-0.5+xOffset)}, {yRescale*(yWrite+0.5+yOffset)}, {zRescale*v7}> <{xRescale*(xWrite+xOffset)}, {yRescale*(yWrite+yOffset)}, {zRescale*v9}> <{xRescale*(xWrite-0.5+xOffset)}, {yRescale*(yWrite-0.5+yOffset)}, {zRescale*v1}>}}' + ) # Triangle 8 7-9-1 + + # completed pyramid. Ave me! + + resultfile.write('\n\n inside_vector <0, 0, 1>\n\n') + + # Sample texture of textures + resultfile.writelines( + [ + ' texture{thethingtexturebottom}\n', + ' texture{thethingtexturetop}\n', + '}\n// Closed thething\n\n', + '#declare boxedthing = object{\n', + ' intersection {\n', + ' box {<-0.5, -0.5, 0>, <0.5, 0.5, 1.0>\n', + ' pigment {rgb <0.5, 0.5, 5>}\n', + ' }\n', + ' thething\n', + ' }\n', + '}', + '// Constructed CGS "boxedthing" of mesh plus bounding box thus adding side walls and bottom\n\n', + 'object{boxedthing}\n\n', + ] + ) # Closing solids + + # Camera + proportions = max(X, Y) / X + resultfile.writelines( + [ + '#declare camera_height = 3.0;\n\n', + 'camera{\n', + '// orthographic\n', + ' location <0.0, 0.0, camera_height>\n', + ' right x*image_width/image_height\n' ' up y\n' ' direction <0, 0, 1>\n', + f' angle 2.0*(degrees(atan2({0.5 * proportions}, camera_height-1.0))) // Supposed to fit object\n', + ' look_at <0.0, 0.0, 0.0>\n}\n\n', + ] + ) + + # Light + resultfile.write('light_source{0*x\n color rgb <1.0, 1.0, 1.0>\n translate <20, 20, 20>\n}\n') + resultfile.write('\n/*\n\nhappy rendering\n\n 0~0\n (---)\n(.>|<.)\n-------\n\n*/') + # Close output + resultfile.close() + + # -------------------------------------------------------------- + # Destroying dialog + + sortir.destroy() + sortir.mainloop() + + # Dialog destroyed and closed + # -------------------------------------------------------------- + + return None + + +# Procedure ended, the program begins +if __name__ == "__main__": + img2pov() diff --git a/img2stl.py b/img2stl.py new file mode 100644 index 0000000..71baa04 --- /dev/null +++ b/img2stl.py @@ -0,0 +1,382 @@ +#!/usr/bin/env python + +''' +IMG2STL - Program for conversion of image heightfield to triangle mesh in STL format +----------------------------------------------------------------------------------------- + +Created by: Ilya Razmanov (mailto:ilyarazmanov@gmail.com) + aka Ilyich the Toad (mailto:amphisoft@gmail.com) +History: +1.0.0.0 Initial production release. +1.0.1.0 Program converted into self-calling function to have a possibility to import it. + + Main site: + https://dnyarri.github.io + + Project mirrored at: + https://github.com/Dnyarri/img2mesh + https://gitflic.ru/project/dnyarri/img2mesh + +''' + +__author__ = "Ilya Razmanov" +__copyright__ = "(c) 2024 Ilya Razmanov" +__credits__ = "Ilya Razmanov" +__license__ = "unlicense" +__version__ = "1.0.1.0" +__maintainer__ = "Ilya Razmanov" +__email__ = "ilyarazmanov@gmail.com" +__status__ = "Production" + +from tkinter import Tk +from tkinter import Label +from tkinter import filedialog + +from pathlib import Path + +from png import Reader # I/O with PyPNG from: https://gitlab.com/drj11/pypng + +# ACHTUNG! Starting a whole-program procedure! + +def img2stl(): + ''' + Procedure for opening PNG heightfield and creating stereolithography .stl 3D mesh file from it. + + ''' + + # -------------------------------------------------------------- + # Creating dialog + + iconpath = Path(__file__).resolve().parent / 'vaba.ico' + iconname = str(iconpath) + useicon = iconpath.exists() # Check if icon file really exist. If False, it will not be used later. + + sortir = Tk() + sortir.title('PNG to STL conversion') + if useicon: + sortir.iconbitmap(iconname) # Replacement for simple sortir.iconbitmap('name.ico') - ugly but stable. + sortir.geometry('+200+100') + zanyato = Label(sortir, text='Allons-y!', font=("arial", 14), padx=16, pady=10, justify='center') + zanyato.pack() + sortir.withdraw() + + # Main dialog created and hidden + # -------------------------------------------------------------- + + # Open source image + sourcefilename = filedialog.askopenfilename( + title='Open source PNG file', filetypes=[('PNG', '.png')], defaultextension=('PNG', '.png') + ) + # Source file name taken + + if (sourcefilename == '') or (sourcefilename == None): + return None + # break if user press 'Cancel' + + source = Reader(filename=sourcefilename) + # opening file with PyPNG + + X, Y, pixels, info = source.asDirect() + # Opening image, iDAT comes to "pixels" as bytearray, to be tuple'd later + + Z = info['planes'] # Maximum CHANNEL NUMBER + imagedata = tuple((pixels)) # Attempt to fix all bytearrays + + if info['bitdepth'] == 8: + maxcolors = 255 # Maximal value for 8-bit channel + if info['bitdepth'] == 16: + maxcolors = 65535 # Maximal value for 16-bit channel + + # source file opened, initial data received + + # opening result file, first get name + resultfilename = filedialog.asksaveasfilename( + title='Save stereolithography STL file', + filetypes=[ + ('3D object file', '*.stl'), + ('All Files', '*.*'), + ], + defaultextension=('3D object file', '.stl'), + ) + + if (resultfilename == '') or (sourcefilename == None): + return None + # break if user press 'Cancel' + # return doesn't seem to work well with .asksaveasfile + + resultfile = open(resultfilename, 'w') + # result file opened + + # Both files opened + + # -------------------------------------------------------------- + # Functions block: + # + # src a-la FM style src(x,y,z) + # Image should be opened as "imagedata" by main program before + # Note that X, Y, Z are not determined in function, you have to determine it in main program + + + def src(x, y, z): + ''' + Analog of src from FilterMeister, force repeat edge instead of out of range + ''' + cx = x + cy = y + cx = max(0, cx) + cx = min((X - 1), cx) + cy = max(0, cy) + cy = min((Y - 1), cy) + + position = (cx * Z) + z # Here is the main magic of turning two x, z into one array position + channelvalue = int(((imagedata[cy])[position])) + + return channelvalue + + + # end of src function + + + def srcY(x, y): + ''' + Converting to greyscale, returns Yntensity, force repeat edge instead of out of range + ''' + cx = x + cy = y + cx = max(0, cx) + cx = min((X - 1), cx) + cy = max(0, cy) + cy = min((Y - 1), cy) + + if info['planes'] < 3: # supposedly L and LA + Yntensity = src(x, y, 0) + else: # supposedly RGB and RGBA + Yntensity = int(0.2989 * src(x, y, 0) + 0.587 * src(x, y, 1) + 0.114 * src(x, y, 2)) + + return Yntensity + + + # end of srcY function + # + # end of Functions block + # -------------------------------------------------------------- + + # Global positioning and scaling to tweak. Offset supposed to make everyone feeling positive, rescale supposed to scale anything to [0..1.0] regardless of what the units are + + xOffset = 1.0 # To be added BEFORE rescaling to compensate 0.5 X expansion + yOffset = 1.0 # To be added BEFORE rescaling to compensate 0.5 Y expansion + zOffset = 0.0 # To be added AFTER rescaling just in case there should be something to fix + + xRescale = 1.0 / float(max(X, Y)) # To fit object into 1,1,1 cube + yRescale = xRescale + zRescale = 1.0 / float(maxcolors) + + # WRITING STL FILE, finally + + resultfile.write('solid pryanik_nepechatnyj\n') # opening object + + # Now going to cycle through image and build mesh + + for y in range(0, Y, 1): + + message = 'Processing row ' + str(y) + ' of ' + str(Y) + '...' + sortir.deiconify() + zanyato.config(text=message) + sortir.update() + sortir.update_idletasks() + + for x in range(0, X, 1): + + # Since I was unable to find clear declaration of coordinate system, I'll plug a coordinate switch here + + # Reading switch: + xRead = x + yRead = (Y - 1 - y) # 'yRead = Y - y' coordinate mirror to mimic Photoshop coordinate system; +/- 1 steps below are inverted correspondingly vs. original img2mesh + + # Remains of Writing switch. No longer used since v. 0.1.0.2 but var names remained so dummy plug must be here. + xWrite = x + yWrite = y + + v9 = srcY(xRead, yRead) # Current pixel to process and write. Then going to neighbours + v1 = 0.25 * (v9 + srcY((xRead - 1), yRead) + srcY((xRead - 1), (yRead + 1)) + srcY(xRead, (yRead + 1))) + v3 = 0.25 * (v9 + srcY(xRead, (yRead + 1)) + srcY((xRead + 1), (yRead + 1)) + srcY((xRead + 1), yRead)) + v5 = 0.25 * (v9 + srcY((xRead + 1), yRead) + srcY((xRead + 1), (yRead - 1)) + srcY(xRead, (yRead - 1))) + v7 = 0.25 * (v9 + srcY(xRead, (yRead - 1)) + srcY((xRead - 1), (yRead - 1)) + srcY((xRead - 1), yRead)) + + # finally going to pyramid building + + # top part begins + resultfile.writelines( + [ + ' facet normal 0 0 1\n', # triangle 2 normal up + ' outer loop\n', # 1 - 9 - 3 + f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v1):e}\n', + f' vertex {(xRescale*(xWrite+xOffset)):e} {(yRescale*(yWrite+yOffset)):e} {(zOffset+zRescale*v9):e}\n', + f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v3):e}\n', + ' endloop\n', + ' endfacet\n', + ' facet normal 0 0 1\n', # triangle 4 normal up + ' outer loop\n', # 3 - 9 - 5 + f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v3):e}\n', + f' vertex {(xRescale*(xWrite+xOffset)):e} {(yRescale*(yWrite+yOffset)):e} {(zOffset+zRescale*v9):e}\n', + f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v5):e}\n', + ' endloop\n', + ' endfacet\n', + ' facet normal 0 0 1\n', # triangle 6 normal up + ' outer loop\n', # 5 - 9 - 7 + f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v5):e}\n', + f' vertex {(xRescale*(xWrite+xOffset)):e} {(yRescale*(yWrite+yOffset)):e} {(zOffset+zRescale*v9):e}\n', + f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v7):e}\n', + ' endloop\n', + ' endfacet\n', + ' facet normal 0 0 1\n', # triangle 8 normal up + ' outer loop\n', # 7 - 9 - 1 + f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v7):e}\n', + f' vertex {(xRescale*(xWrite+xOffset)):e} {(yRescale*(yWrite+yOffset)):e} {(zOffset+zRescale*v9):e}\n', + f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v1):e}\n', + ' endloop\n', + ' endfacet\n', + ] + ) + # top part ends + + # left side begins + if x == 0: + resultfile.writelines( + [ + ' facet normal -1 0 0\n', # triangle 8- normal left + ' outer loop\n', # 1 - down1 - 7 + f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v1):e}\n', + f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+0.0):e}\n', + f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v7):e}\n', + ' endloop\n', + ' endfacet\n', + ' facet normal -1 0 0\n', # triangle 8- normal left + ' outer loop\n', # down1 - down7 - 7 + f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+0.0):e}\n', + f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+0.0):e}\n', + f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v7):e}\n', + ' endloop\n', + ' endfacet\n', + ] + ) + # left side ends + + # right side begins + if x == (X - 1): + resultfile.writelines( + [ + ' facet normal 1 0 0\n', # triangle 4+ normal left + ' outer loop\n', # 5 - down5 - 3 + f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v5):e}\n', + f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+0.0):e}\n', + f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v3):e}\n', + ' endloop\n', + ' endfacet\n', + ' facet normal 1 0 0\n', # triangle 4+ normal left + ' outer loop\n', # 3 - down5 - down3 + f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v3):e}\n', + f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+0.0):e}\n', + f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+0.0):e}\n', + ' endloop\n', + ' endfacet\n', + ] + ) + # right side ends + + # far side begins + if y == 0: + resultfile.writelines( + [ + ' facet normal 0 -1 0\n', # triangle 2- normal far + ' outer loop\n', # 3 - down - 1 + f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v3):e}\n', + f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+0.0):e}\n', + f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v1):e}\n', + ' endloop\n', + ' endfacet\n', + ' facet normal 0 -1 0\n', # triangle 2- normal far + ' outer loop\n', # down - down - 1 + f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+0.0):e}\n', + f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+0.0):e}\n', + f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+zRescale*v1):e}\n', + ' endloop\n', + ' endfacet\n', + ] + ) + # far side ends + + # close side begins + if y == (Y - 1): + resultfile.writelines( + [ + ' facet normal 0 1 0\n', # triangle 6+ normal close + ' outer loop\n', # 7 - down - 5 + f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v7):e}\n', + f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+0.0):e}\n', + f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v5):e}\n', + ' endloop\n', + ' endfacet\n', + ' facet normal 0 1 0\n', # triangle 6+ normal close + ' outer loop\n', # down - down - 5 + f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+0.0):e}\n', + f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+0.0):e}\n', + f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+zRescale*v5):e}\n', + ' endloop\n', + ' endfacet\n', + ] + ) + # close side ends + + # bottom part begins + resultfile.writelines( + [ + ' facet normal 0 0 -1\n', # triangle 2 normal up + ' outer loop\n', # 1 - 9 - 3 + f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+0.0):e}\n', + f' vertex {(xRescale*(xWrite+xOffset)):e} {(yRescale*(yWrite+yOffset)):e} {(zOffset+0.0):e}\n', + f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+0.0):e}\n', + ' endloop\n', + ' endfacet\n', + ' facet normal 0 0 -1\n', # triangle 4 normal up + ' outer loop\n', # 3 - 9 - 5 + f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+0.0):e}\n', + f' vertex {(xRescale*(xWrite+xOffset)):e} {(yRescale*(yWrite+yOffset)):e} {(zOffset+0.0):e}\n', + f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+0.0):e}\n', + ' endloop\n', + ' endfacet\n', + ' facet normal 0 0 -1\n', # triangle 6 normal up + ' outer loop\n', # 5 - 9 - 7 + f' vertex {(xRescale*(xWrite+0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+0.0):e}\n', + f' vertex {(xRescale*(xWrite+xOffset)):e} {(yRescale*(yWrite+yOffset)):e} {(zOffset+0.0):e}\n', + f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+0.0):e}\n', + ' endloop\n', + ' endfacet\n', + ' facet normal 0 0 -1\n', # triangle 8 normal up + ' outer loop\n', # 7 - 9 - 1 + f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite+0.5+yOffset)):e} {(zOffset+0.0):e}\n', + f' vertex {(xRescale*(xWrite+xOffset)):e} {(yRescale*(yWrite+yOffset)):e} {(zOffset+0.0):e}\n', + f' vertex {(xRescale*(xWrite-0.5+xOffset)):e} {(yRescale*(yWrite-0.5+yOffset)):e} {(zOffset+0.0):e}\n', + ' endloop\n', + ' endfacet\n', + ] + ) + # bottom part ends + + resultfile.write('endsolid pryanik_nepechatnyj') # closing object + + # Close output + resultfile.close() + + # -------------------------------------------------------------- + # Destroying dialog + + sortir.destroy() + sortir.mainloop() + + # Dialog destroyed and closed + # -------------------------------------------------------------- + +# Procedure ended, the program begins +if __name__ == "__main__": + img2stl() \ No newline at end of file diff --git a/vaba.ico b/vaba.ico new file mode 100644 index 0000000..9cf974b Binary files /dev/null and b/vaba.ico differ