1 / 9
Add Your Own Image to AIO Pump – Lithophane Generator Included — zdjęcie 1
1 / 9

Add Your Own Image to AIO Pump – Lithophane Generator Included

Zamów wydruk tego modelu

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-stl

 

How 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
Nozzle0.40mm
Layer Height0.20mm
Infill25%
SupportsYes
BrimNo
Print OrientationUpside Down

Lithophane

Material  PLA
Nozzle0.40mm
Layer Height0.10mm
Infill100%
SupportsNo
BrimNo
Print OrientationFace 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()

Tagi