Add Your Own Image to AIO Pump – Lithophane Generator Included
Obliczymy ostateczną cenę na podstawie wybranego pliku. Sprawdzisz ją i zaakceptujesz przed płatnością.
Zapisane na tym urządzeniu.
Make your AIO cooler personal – generate and export a custom lithophane cap with just a few clicks.
- Ocena
- 0 / 5
- Polubienia
- 20
- Pobrania
- 54
- Wydruki
- 0
- Wyświetlenia
- 437
- Oceny
- 0
Aktualizacja u źródła:
Informacje o modelu
- Źródło
- Printables
- Kategoria
- Computers
- Licencja
- Creative Commons — Attribution — Noncommercial
Turn your Corsair TITAN AIO cooler into a glowing showcase of you. Whether it’s your pet, your logo, your favorite anime, or a meme that makes you smile — this mod lets you display any image you want, beautifully backlit right on your pump cap.
With the Hydrolith Generator, you can instantly convert any photo into a curved lithophane STL that fits perfectly into the included adapter. Just load your image, export the file, and print. No complex tools. No guesswork. Just plug, play, and flex your style.
Need inspiration? A few pre-made lithophanes are included to get you started — just slice, print, and drop them in.
Generating Lithophanes
Option 1 – Easy: .exe File
Use the included hydrolith.exe to generate the lithophane STL from your image in a few clicks.
No installation needed — just run and export.
Option 2 – Manual: Python + Thonny
If you'd rather not run an .exe from Printables, I get it!
You can run the open-source generator yourself using Thonny and the provided hydrolith.py file.
Required Libraries:
PIL
numpy
numpy-stlHow it works
The Hydrolith Generator creates a curved-square lithophane shaped exactly to fit the TITAN AIO pump cap. You can customize thickness, border height, resolution, and choose whether the image is inverted (negative). The result is a smooth, backlit lithophane with rounded edges — held securely in place by the adapter and lit by the cooler’s LEDs.
If the generator helped you create a great lithophane, I’d love to see your make!
Feel free to share photos of your result :).
Printing
Adapter | |
![]() | |
| Material | PLA |
| Nozzle | 0.40mm |
| Layer Height | 0.20mm |
| Infill | 25% |
| Supports | Yes |
| Brim | No |
| Print Orientation | Upside Down |
Lithophane | |
![]() | |
| Material | PLA |
| Nozzle | 0.40mm |
| Layer Height | 0.10mm |
| Infill | 100% |
| Supports | No |
| Brim | No |
| Print Orientation | Face up |
Code
# Summer 2025 – Hydrolith Generator
# https://www.printables.com/@BGDglider_2502284#****************************** Libraries *******************************#
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from PIL import Image, ImageOps, ImageDraw, ImageTk
import numpy as np
from stl import mesh
import os
#********************** Fix the Dimensions *******************************#
WIDTH_MM = 70.886
HEIGHT_MM = 70.886
CORNER_RADIUS_MM = 21.943
BORDER_WIDTH_MM = 2.0
#********************** Round the Corner *********************************#
def rounded_mask(w: int, h: int, r: int, upscale=16) -> np.ndarray:
up_w, up_h, up_r = w * upscale, h * upscale, r * upscale
mask_up = Image.new("L", (up_w, up_h), 0)
draw = ImageDraw.Draw(mask_up)
draw.rounded_rectangle([0, 0, up_w, up_h], radius=up_r, fill=255)
mask = mask_up.resize((w, h), resample=Image.LANCZOS)
return np.array(mask) > 128
#********************** Lithophane generation ***************************#
def create_lithophane(image_path, output_path, min_thick, max_thick, resolution, border_height, invert=False, progress_callback=None):
px_per_mm = 1 / resolution
size_px = (int(WIDTH_MM * px_per_mm), int(HEIGHT_MM * px_per_mm))
radius_px = int(CORNER_RADIUS_MM * px_per_mm)
border_px = int(BORDER_WIDTH_MM * px_per_mm)
img_raw = Image.open(image_path).convert("L")
img = ImageOps.fit(img_raw, size_px, method=Image.LANCZOS, centering=(0.5, 0.5))
img = img.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
grayscale = np.asarray(img) / 255.0
if invert:
grayscale = 1.0 - grayscale
height_map = min_thick + (1.0 - grayscale) * (max_thick - min_thick)
h, w = height_map.shape
mask = rounded_mask(w, h, radius_px)
outer_mask = mask.copy()
inner_mask = rounded_mask(w - 2 * border_px, h - 2 * border_px, max(0, radius_px - border_px))
inner_mask_padded = np.zeros_like(outer_mask, dtype=bool)
inner_h, inner_w = inner_mask.shape
start_y = (h - inner_h) // 2
start_x = (w - inner_w) // 2
inner_mask_padded[start_y:start_y + inner_h, start_x:start_x + inner_w] = inner_mask
rim_mask = outer_mask & ~inner_mask_padded
vertices = []
faces = []
vertex_index = {}
def get_index(x, y, ztype):
key = (x, y, ztype)
if key not in vertex_index:
if ztype == 'top':
z = height_map[y, x]
elif ztype == 'rim':
z = border_height
else:
z = 0.0
vertices.append([x * resolution, y * resolution, z])
vertex_index[key] = len(vertices) - 1
return vertex_index[key]
for y in range(h - 1):
for x in range(w - 1):
region = [
(mask[y, x], rim_mask[y, x]),
(mask[y, x+1], rim_mask[y, x+1]),
(mask[y+1, x], rim_mask[y+1, x]),
(mask[y+1, x+1], rim_mask[y+1, x+1]),
]
if not all([m for m, _ in region]):
continue
def ztype(ix, iy):
return 'rim' if rim_mask[iy, ix] else 'top'
i0 = get_index(x, y, ztype(x, y))
i1 = get_index(x + 1, y, ztype(x + 1, y))
i2 = get_index(x, y + 1, ztype(x, y + 1))
i3 = get_index(x + 1, y + 1, ztype(x + 1, y + 1))
b0 = get_index(x, y, 'bottom')
b1 = get_index(x + 1, y, 'bottom')
b2 = get_index(x, y + 1, 'bottom')
b3 = get_index(x + 1, y + 1, 'bottom')
faces += [[i0, i1, i2], [i1, i3, i2]]
faces += [[b2, b1, b0], [b2, b3, b1]]
faces += [
[i0, b0, i1], [i1, b0, b1],
[i1, b1, i3], [i3, b1, b3],
[i3, b3, i2], [i2, b3, b2],
[i2, b2, i0], [i0, b2, b0]
]
if progress_callback and y % 10 == 0:
progress_callback(y / (h - 1))
vertices_np = np.array(vertices)
faces_np = np.array(faces)
litho = mesh.Mesh(np.zeros(faces_np.shape[0], dtype=mesh.Mesh.dtype))
for i, f in enumerate(faces_np):
for j in range(3):
litho.vectors[i][j] = vertices_np[f[j], :]
litho.save(output_path)
#****************************** GUI **************************************#
class LithopaneApp:
def init(self, root):
self.root = root
self.root.title("Hydrolith")
self.image_path = ""
tk.Button(root, text="Select Image", command=self.load_image).grid(row=0, column=0, columnspan=2, pady=10)
self.entries = {}
labels = [
("Min. Thickness (mm)", "0.8"),
("Max. Thickness (mm)", "3.2"),
("Resolution (mm/px)", "0.3"),
("Border Height (mm)", "2.0")
]
for i, (label, default) in enumerate(labels, start=1):
tk.Label(root, text=label).grid(row=i, column=0, sticky="e")
entry = tk.Entry(root)
entry.insert(0, default)
entry.grid(row=i, column=1)
self.entries[label] = entry
self.invert_var = tk.BooleanVar()
tk.Checkbutton(root, text="Invert colors (negative)", variable=self.invert_var).grid(row=5, column=0, columnspan=2, pady=(5, 0))
self.status = tk.Label(root, text="", fg="blue")
self.status.grid(row=6, column=0, pady=10, sticky="e")
self.progress = ttk.Progressbar(root, orient="horizontal", length=200, mode="determinate")
self.progress.grid(row=6, column=1, sticky="w", padx=5)
tk.Button(root, text="Export STL", command=self.export_stl).grid(row=7, column=0, columnspan=2, pady=10)
tk.Button(root, text="Show Preview", command=self.show_preview).grid(row=8, column=0, columnspan=2, pady=5)
def load_image(self):
file_path = filedialog.askopenfilename(filetypes=[("Image files", "*.jpg *.png *.bmp")])
if file_path:
self.image_path = file_path
self.status.config(text=f"Loaded: {os.path.basename(file_path)}")
def update_progress(self, fraction):
self.progress["value"] = int(fraction * 100)
self.root.update_idletasks()
def export_stl(self):
if not self.image_path:
messagebox.showwarning("Warning", "Please select an image first.")
return
try:
min_thick = float(self.entries["Min. Thickness (mm)"].get())
max_thick = float(self.entries["Max. Thickness (mm)"].get())
resolution = float(self.entries["Resolution (mm/px)"].get())
border_height = float(self.entries["Border Height (mm)"].get())
if resolution <= 0:
messagebox.showerror("Error", "Resolution must be > 0.")
return
if min_thick >= max_thick:
messagebox.showerror("Error", "Min thickness must be less than max thickness.")
return
if border_height < 2.0:
messagebox.showwarning("Warning", "Border height too low. Set to 2.0 mm.")
border_height = 2.0
except ValueError:
messagebox.showerror("Error", "Please enter valid numbers.")
return
output_path = filedialog.asksaveasfilename(defaultextension=".stl", filetypes=[("STL file", "*.stl")])
if not output_path:
return
self.status.config(text="Generating STL...")
self.progress["value"] = 0
self.root.update()
try:
create_lithophane(
image_path=self.image_path,
output_path=output_path,
min_thick=min_thick,
max_thick=max_thick,
resolution=resolution,
border_height=border_height,
invert=self.invert_var.get(),
progress_callback=self.update_progress
)
self.progress["value"] = 100
self.status.config(text="STL export complete.")
except Exception as e:
messagebox.showerror("Error", str(e))
self.status.config(text="Export failed.")
def show_preview(self):
if not self.image_path:
messagebox.showwarning("Warning", "Please select an image first.")
return
try:
min_thick = float(self.entries["Min. Thickness (mm)"].get())
max_thick = float(self.entries["Max. Thickness (mm)"].get())
resolution = float(self.entries["Resolution (mm/px)"].get())
except ValueError:
messagebox.showerror("Error", "Please enter valid numbers.")
return
px_per_mm = 1 / resolution
size_px = (int(WIDTH_MM * px_per_mm), int(HEIGHT_MM * px_per_mm))
radius_px = int(CORNER_RADIUS_MM * px_per_mm)
img_raw = Image.open(self.image_path).convert("L")
img = ImageOps.fit(img_raw, size_px, method=Image.LANCZOS, centering=(0.5, 0.5))
grayscale = np.asarray(img) / 255.0
if self.invert_var.get():
grayscale = 1.0 - grayscale
height_map = min_thick + (1.0 - grayscale) * (max_thick - min_thick)
mask = rounded_mask(*height_map.shape[::-1], radius_px)
height_map = height_map * mask
preview_img = Image.fromarray(np.uint8((1.0 - (height_map - min_thick) / (max_thick - min_thick)) * 255))
preview_img = preview_img.resize((300, 300), Image.LANCZOS)
preview_window = tk.Toplevel(self.root)
preview_window.title("Preview – Height Map")
tk_img = ImageTk.PhotoImage(preview_img)
tk.Label(preview_window, image=tk_img).pack()
preview_window.image = tk_img
Run app
if name == "main":
root = tk.Tk()
app = LithopaneApp(root)
root.mainloop()

