Skip to content

Instantly share code, notes, and snippets.

@luisdamed
Last active May 7, 2024 20:43
Show Gist options
  • Save luisdamed/dbd8ad05961af19b893bbe7857e9d71f to your computer and use it in GitHub Desktop.
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...
# 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