Last active
May 7, 2024 20:43
-
-
Save luisdamed/dbd8ad05961af19b893bbe7857e9d71f to your computer and use it in GitHub Desktop.
A simple script that I use to generate arc-deformed images that wrap perfectly around a truncated straight cone shape. Example: vinyl wrapping for a camera lens hood, print image for a latte mug, you get the idea...
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Compute the key dimensions of the flattened surface of a truncated cone | |
# Use the cone height H, large diameter L and small diamater D to calculate the | |
# ring sector that wraps around it, given by the parameters: | |
# P (inner radius), Q (outer radius) and alpha (sector angle) | |
# check out https://www.templatemaker.nl/en/cone/ | |
# Need to install https://imagemagick.org/ CLI | |
# Consider using wand (https://github.com/emcconville/wand) for a | |
# more "pythonic" interface | |
#%% Import libraries | |
import os | |
import numpy as np | |
import subprocess | |
from tkinter import * | |
from tkinter.filedialog import * | |
from PIL import Image | |
from fractions import Fraction | |
#%% Get user inputs | |
def getUserInputs(): | |
while True: | |
if 'height' not in locals(): | |
height = input("Type the height of the cone in mm: ") | |
try: | |
height = float(height) | |
except: | |
print('Please use numeric digits.') | |
continue | |
if height < 1: | |
print('Please enter a positive number.') | |
continue | |
if 'largeDiam' not in locals(): | |
largeDiam = input("Type the cone's large diameter in mm: ") | |
try: | |
largeDiam = float(largeDiam) | |
except: | |
print('Please use numeric digits.') | |
continue | |
if largeDiam < 1: | |
print('Please enter a positive number.') | |
continue | |
smallDiam = input("Type the cone's small diameter in mm: ") | |
try: | |
smallDiam = float(smallDiam) | |
except: | |
print('Please use numeric digits.') | |
continue | |
if smallDiam < 1: | |
print('Please enter a positive number.') | |
continue | |
if smallDiam > largeDiam: | |
print('The small diameter should be smaller than the large one.') | |
continue | |
break | |
return (float(height), float(largeDiam), float(smallDiam)) | |
#%% Compute flat surface dimensions | |
def computeFlatDimensions(size): | |
height = size[0] | |
base = size[1] | |
top = size[2] | |
radius = np.sqrt(np.square(0.5*base - 0.5*top) + np.square(height)) | |
innerRadius = (top*radius)/(base-top) | |
innerLenght = np.pi*top | |
outerRadius = radius + innerRadius | |
alpha = innerLenght/innerRadius * 360/ (2*np.pi ) | |
print('Flattened cone dimensions: ') | |
print(f'Central Radius = {radius:.10f} mm') | |
print(f'Inner Length = {innerLenght:.10f} mm') | |
print(f'Inner Radius = {innerRadius:.10f} mm') | |
print(f'Outer Radius: {outerRadius:.10f} mm') | |
print(f'Span Angle = {alpha:.10f}°\n') | |
return {"centralRadius":radius, | |
"innerLenght": innerLenght, | |
"innerRadius": innerRadius, | |
"outerRadius": outerRadius, | |
"alphaDeg": alpha} | |
#%% ImageMagick commands | |
def generateTilesImage(img): | |
out_image = os.path.splitext(img)[0] + '_tiles.jpg' | |
# Identify image size to match viewport | |
identify_command = f'magick identify "{img}"' | |
identified_image = subprocess.run(identify_command, stdout=subprocess.PIPE) | |
print('Target image:' + identified_image.stdout.decode("utf-8") + '\n') | |
imgWidth = identified_image.stdout.decode("utf-8").split('.')[1].split(' ')[2].split('x')[0] | |
imgHeight = identified_image.stdout.decode("utf-8").split('.')[1].split(' ')[2].split('x')[1] | |
imgAspectRatio = float(imgWidth)/float(imgHeight) | |
imgAspectRatioFraction = Fraction(imgAspectRatio).limit_denominator() | |
print((f'Source image size: {imgWidth}x{imgHeight} px. ' | |
f'Aspect ratio {imgAspectRatioFraction.numerator}:' | |
f'{imgAspectRatioFraction.denominator}')) | |
tile_command = (f'magick "{img}" -set option:distort:viewport {imgWidth}x{imgHeight}+0+0 ' | |
'-virtual-pixel tile -filter point -distort SRT 0 "{out_image}"') | |
tile_result = subprocess.run(tile_command, stdout=subprocess.PIPE) | |
if tile_result.returncode == 0: | |
print(f'Generated tiled image: {out_image}\n') | |
return [True, out_image, imgAspectRatio] | |
def generateArcImage(img, aspectRatio, params): | |
arcRatio = params['innerLenght']/params['centralRadius'] | |
distortMinQty = round(arcRatio) / round(aspectRatio) | |
distortAngle = params["alphaDeg"]/distortMinQty | |
print((f'We can fit {round(distortMinQty)} images ' | |
'with minimal distortion in the flat cone shape')) | |
print(f'The actual distortion angle use will be {distortAngle} degrees') | |
out_image = os.path.splitext(img)[0] + '_arc_qrtr.jpg' | |
arc_command = (f'magick "{img}" -virtual-pixel Black ' | |
f'-distort Arc "{distortAngle} 0 {params["outerRadius"]*10} ' | |
f'{params["innerRadius"]*10}" "{out_image}"') | |
arc_result = subprocess.run(arc_command, stdout=subprocess.PIPE) | |
if arc_result.returncode == 0: | |
print(f'Generated arc image: {out_image}\n') | |
return [True, out_image] | |
#%% Program | |
if __name__ == "__main__": | |
#%% Get user inputs | |
results = getUserInputs() | |
print(f'''height: {results[0]} mm, | |
large diameter: {results[1]}, | |
small diameter: {results[2]}\n''') | |
dimesions = computeFlatDimensions(results) | |
#%% Select target image | |
print('Select the desired image form GUI prompt\n') | |
imgPath = askopenfilename(title='Select the desired image') | |
# Open image | |
im = Image.open(imgPath) | |
# Show image | |
im.show() | |
#%% Tile it | |
tilingResult = generateTilesImage(imgPath) | |
if tilingResult[0]: | |
arcResult = generateArcImage(tilingResult[1], tilingResult[2], dimesions) | |
if arcResult[0]: | |
# Open image | |
imArc = Image.open(arcResult[1]) | |
# Show image | |
imArc.show() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment