Source code for excolor.wallpaper

#!/usr/bin/env python
# -*- coding: utf8 -*-

"""
This module contains functions to create wallpaper-like images.
"""

import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
import matplotlib.colors as mc
from pythonperlin import perlin
from matplotlib.colors import LinearSegmentedColormap, Colormap
from matplotlib.axes import Axes
from typing import Union, Tuple, List, Optional, Any
from .patch import Patch
from .cmaptools import get_bgcolor
from .palette import generate_stepwise_palette
from .colortools import _is_cmap, get_colors, lighten, darken, to_rgb, to_hex
from .gradient import _get_gradient_colors, fill_gradient
from .imagetools import *
import random
import io
import cv2

import warnings
warnings.filterwarnings("ignore")




def _sigmoid(x: Union[float, np.ndarray], midpoint: float = 0.5, slope: float = 25) -> Union[float, np.ndarray]:
    """
    Sigmoid function for smooth transitions

    Parameters
    ----------
    x : float or np.ndarray
        Input value (0 to 1)
    midpoint : float, default 0.5
        Center of the transition (0 to 1)
    slope : float, default 25
        Steepness of the transition

    Returns
    -------
    float
        Sigmoid value (0 to 1)
    """
    return 1 / (1 + np.exp(-slope * (x - midpoint)))


def _get_circle_dots(r: float = 1, n: int = 360) -> Tuple[np.ndarray, np.ndarray]:
    """
    Generates points at equal arc length intervals on a circle.

    This function creates a set of points distributed evenly along the circumference
    of a circle. The points are generated using complex number representation and
    converted to Cartesian coordinates.

    Parameters
    ----------
    r : float, default=1
        Radius of the circle
    n : int, default=360
        Number of points to generate

    Returns
    -------
    Tuple[numpy.ndarray, numpy.ndarray]
        A tuple containing:
        - numpy.ndarray: X coordinates of points on the circle
        - numpy.ndarray: Y coordinates of points on the circle

    Examples
    --------
    >>> # Generate points on a unit circle
    >>> x, y = _get_circle_dots()
    >>> # Generate points on a circle with radius 2
    >>> x, y = _get_circle_dots(r=2)
    >>> # Generate fewer points
    >>> x, y = _get_circle_dots(n=100)
    """
    phi = np.linspace(0, 2 * np.pi, n + 1)[:-1]
    z = r * np.exp(1j * phi)
    x, y = z.real, z.imag
    return x, y
    

def _get_ellipse_dots(a: float = 5, b: float = 3, n: int = 360) -> Tuple[np.ndarray, np.ndarray]:
    """
    Generates points at approximately equal arc length intervals on an ellipse.

    This function creates a set of points distributed along the circumference
    of an ellipse. The points are generated using a two-step process:
    1. First creates a high-density set of points
    2. Then selects points that are approximately equidistant along the arc

    Parameters
    ----------
    a : float, default=5
        Semi-major axis length
    b : float, default=3
        Semi-minor axis length
    n : int, default=360
        Number of points to generate

    Returns
    -------
    Tuple[numpy.ndarray, numpy.ndarray]
        A tuple containing:
        - numpy.ndarray: X coordinates of points on the ellipse
        - numpy.ndarray: Y coordinates of points on the ellipse

    Examples
    --------
    >>> # Generate points on a default ellipse (a=5, b=3)
    >>> x, y = _get_ellipse_dots()
    >>> # Generate points on a circle (a=b=1)
    >>> x, y = _get_ellipse_dots(a=1, b=1)
    >>> # Generate fewer points
    >>> x, y = _get_ellipse_dots(n=100)
    """
    # Initialize half of points with increased density
    x = np.linspace(a, -a, 20 * n)
    y = np.sqrt(a**2 - x**2) * b / a
    # Add lower half of points
    x = np.concatenate([x[:-1], x[::-1]])
    y = np.concatenate([y[:-1], -y[::-1]])
    # Calc distance between high-density points
    l = np.sqrt(np.diff(x)**2 + np.diff(y)**2)
    # Calc distance between requested points
    dl = np.sum(l) / n
    # Find indices of equidistant points along the ellipse arc
    l = np.cumsum(l) % dl
    d = np.diff(l)
    mask = (d[1:-1] < d[:-2]) & (d[1:-1] < d[2:])
    idx = np.arange(len(mask)) + 2
    idx = np.concatenate([(0,), idx[mask]])
    x = x[idx]
    y = y[idx]
    return x, y


def _distort_radius(x: np.ndarray, y: np.ndarray, p: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
    """
    Applies radial distortion to a set of 2D points.

    This function takes a set of (x, y) coordinates centered at the origin
    and applies a radial distortion by adding noise to the radius while
    preserving the angle of each point.

    Parameters
    ----------
    x : numpy.ndarray
        X coordinates of points (centered at origin)
    y : numpy.ndarray
        Y coordinates of points (centered at origin)
    p : numpy.ndarray
        Noise values to add to the radius of each point

    Returns
    -------
    Tuple[numpy.ndarray, numpy.ndarray]
        A tuple containing:
        - numpy.ndarray: Distorted X coordinates
        - numpy.ndarray: Distorted Y coordinates

    Examples
    --------
    >>> # Create points on a circle
    >>> x, y = get_circle_dots(r=1, n=100)
    >>> # Add random noise
    >>> rng = np.random.default_rng(0)
    >>> noise = rng.normal(0, 0.1, 100)
    >>> x_dist, y_dist = _distort_radius(x, y, noise)
    >>> # Add systematic distortion
    >>> distortion = np.sin(np.linspace(0, 2*np.pi, 100)) * 0.2
    >>> x_dist, y_dist = _distort_radius(x, y, distortion)
    """
    z = x + 1j * y
    phi = np.angle(z)
    r = np.abs(z) + p
    z = r * np.exp(1j * phi)
    x, y = z.real, z.imag
    return x, y


[docs] def sigmoid_wallpaper( colors: Union[List[str], str, Colormap], background: Union[str, Image.Image, None] = None, size: Tuple[int, int] = (1280, 720), n: int = 5, sigmoid_shift: Union[float, None] = None, sigmoid_span: Union[float, None] = None, midpoint: float = 0.5, slope: float = 25, shadow: bool = True, fname: Optional[str] = None, ) -> Image.Image: """ Creates a wallpaper with sigmoid patches. This function generates a wallpaper with sigmoid patches, using the provided colors and background. Parameters ---------- colors : list of str, str, matplotlib.colors.Colormap Input colors or colormap. Can be: - A list of color strings - A color name (str) - A colormap name (str) - A Colormap instance background : str, or PIL.Image.Image, optional Background color or image. Can be: - A color name or hex string - A PIL Image object size : tuple of int, default=(1280, 720) Size of the output image in pixels (width, height) n : int, default=5 Number of color patches. Used if list of colors is not provided explicitly. sigmoid_shift : float, default=0.0 Vertical shift of the sigmoid function (0 - 0.5) sigmoid_span : float, default=1.0 Vertical span of the sigmoid function (0.6 - 1) midpoint : float, default=0.5 Midpoint of the sigmoid function in fraction of the image width (0.2 - 0.8) slope : float, default=25 Slope of the sigmoid function shadow : bool, default=True Whether to add a shadow to the wallpaper fname : str, optional If provided, saves the image to the specified file path Returns ------- PIL.Image.Image The generated wallpaper as a PIL Image object. If `fname` is provided, the image is also saved to the specified file path. Examples -------- >>> colors = ['#FF0000', '#00FF00', '#0000FF'] >>> wallpaper = sigmoid_wallpaper(colors, size=(800, 600)) >>> plt.imshow(wallpaper) >>> plt.show() """ # Parse background image if isinstance(background, Image.Image): # resize background image if background.size != size: background = background.resize(size) # Flip vertically background = background.transpose(Image.FLIP_TOP_BOTTOM) # Parse colors and fill background if _is_cmap(colors): if background is None: background = Image.new("RGB", size, get_bgcolor(colors)) colors = generate_stepwise_palette(colors, n) else: colors = _get_gradient_colors(colors, n) n = len(colors) if background is None: background = Image.new("RGB", size, 'black') else: background = Image.new("RGB", size, background) # Create sigmoid patches midpoint = np.clip(midpoint, 0.2, 0.8) midpoints = np.array([midpoint - 0.1 * i for i in range(n)]) if midpoints.min() < 0.1: midpoints = np.linspace(0.1, midpoint, n)[::-1] midpoints = np.round(midpoints, 2) if sigmoid_shift is None: sigmoid_shifts = [0 for _ in range(n)] else: sigmoid_shift = np.clip(sigmoid_shift, 0.0, 0.5) sigmoid_shifts = np.array([sigmoid_shift + 0.05 * i for i in range(n)]) if sigmoid_shifts.max() > 0.4: sigmoid_shifts = np.linspace(sigmoid_shift, 0.4, n) sigmoid_shifts = np.round(sigmoid_shifts, 2) if sigmoid_span is None: sigmoid_scales = [1 for _ in range(n)] elif sigmoid_shift is None: sigmoid_span = np.clip(sigmoid_span, 0.5, 1.0) sigmoid_spans = np.array([sigmoid_span + 0.05 * i for i in range(n)]) if sigmoid_spans.max() > 1: sigmoid_spans = np.linspace(sigmoid_span, 1, n + 1)[:-1] sigmoid_spans = np.round(sigmoid_spans, 2) sigmoid_scales = sigmoid_spans else: sigmoid_scales = [sigmoid_span - sigmoid_shifts.max()] * n sigmoid_scales = np.round(sigmoid_scales, 2) # Creaate matplotlib figure inches, dpi = pixels_to_size_and_dpi(size, exact=True) fig = plt.figure(figsize=inches, dpi=dpi, facecolor='w') plt.xlim(0,size[0]) plt.ylim(0,size[1]) # Show background image wallpaper = background plt.imshow(wallpaper) remove_margins() # Draw sigmoid patches nx = 100 epsilon = 0.02 x_max = size[0] * (1 + epsilon) y_max = size[1] * (1 + epsilon) x_min = -size[0] * epsilon y_min = -size[1] * epsilon for i in range(n): # Generate sigmoid y values x = np.linspace(0, 1, nx + 1) y = _sigmoid(x, midpoints[i], slope) y = y * sigmoid_scales[i] + sigmoid_shifts[i] y = y * (1 + 2 * epsilon) - epsilon # Scale to image size x = (x * size[0]).astype(int) y = (y * size[1]).astype(int) # Add corner point coordinates to the patch x = np.concatenate([x, [x_max, x_min, x_min]]) y = np.concatenate([y, [y_max, y_max, y_min]]) coords = [(x[i], y[i]) for i in range(nx + 3)] # Create patch patch = Patch(coords) patch.fill_solid(colors[i]) if shadow: sigma = 10 * size[1] / 100 kernel = 20 * size[1] / 100 kernel = (kernel, kernel) patch.cast_shadow(wallpaper, kernel=kernel, sigma=sigma) # Draw patch patch.draw(fig, size) # Convert figure to image wallpaper = fig2img(fig) plt.close(fig) # Save to file if provided if fname is not None: wallpaper.save(fname) return wallpaper
[docs] def perlin_wallpaper( colors: Union[List[str], str, Colormap], background: Union[str, Image.Image, None] = None, size: Tuple[int, int] = (1280, 720), n: int = 5, shadow: bool = True, center: Tuple[float, float] = (0, 0), seed: int = 0, fname: Optional[str] = None, ) -> Image.Image: """ Creates a wallpaper with Perlin noise patches. This function generates a wallpaper with Perlin noise patches, using the provided colors and background. Parameters ---------- colors : list of str, str, matplotlib.colors.Colormap Input colors or colormap. Can be: - A list of color strings - A color name (str) - A colormap name (str) - A Colormap instance background : str, or PIL.Image.Image, optional Background color or image. Can be: - A color name or hex string - A PIL Image object size : tuple of int, default=(1280, 720) Size of the output image in pixels (width, height) n : int, default=5 Number of color patches. Used if list of colors is not provided explicitly. shadow : bool, default=True Whether to add a shadow to the wallpaper center : tuple of float, default=(0, 0) Coordinates of the circle center from the right bottom corner in pixels seed : int, default=0 Random seed for the Perlin noise generation fname : str, optional If provided, saves the image to the specified file path Returns ------- PIL.Image.Image The generated wallpaper as a PIL Image object. If `fname` is provided, the image is also saved to the specified file path. Examples -------- >>> colors = ['#FF0000', '#00FF00', '#0000FF'] >>> wallpaper = perlin_wallpaper(colors, size=(800, 600)) >>> plt.imshow(wallpaper) >>> plt.show() """ # Parse background image if isinstance(background, Image.Image): # resize background image if background.size != size: background = background.resize(size) # Flip vertically background = background.transpose(Image.FLIP_TOP_BOTTOM) elif isinstance(background, str): background = Image.new("RGB", size, background) # Parse colors and fill background if _is_cmap(colors): if background is None: background = Image.new("RGB", size, get_bgcolor(colors)) colors = generate_stepwise_palette(colors, n) else: colors = _get_gradient_colors(colors) n = len(colors) if background is None: background = Image.new("RGB", size, 'black') # Calculate radius of perlin patches n_major = len(colors) x_max = size[0] + center[0] y_max = size[1] - center[1] r_max = np.sqrt(x_max**2 + y_max**2) r_min = 0.6 * r_max dr_major = (r_max - r_min) / n_major # Generate perlin noise dens = 20 n = np.ceil(n_major * 6 / dens).astype(int) + 1 p = perlin((n,36), dens=dens, seed=seed)[dens//2:] # Creaate matplotlib figure inches, dpi = pixels_to_size_and_dpi(size, exact=True) fig = plt.figure(figsize=inches, dpi=dpi, facecolor='w') plt.xlim(0,size[0]) plt.ylim(0,size[1]) # Show background image wallpaper = background plt.imshow(wallpaper) remove_margins() # Draw sigmoid patches epsilon = 0.02 x_max = size[0] * (1 + epsilon) y_max = size[1] * (1 + epsilon) x_min = -size[0] * epsilon y_min = -size[1] * epsilon for i in range(n_major): k = 8 * i r = r_min + dr_major * i x, y = _get_circle_dots(r, n=720) scale = 15 * size[1] / 100 x, y = _distort_radius(x, y, scale * p[k]) x += size[0] + center[0] y += center[1] # Change angle direction to clockwise (so that the coords are oriented like in sigmoid_wallpaper) x = x[::-1] y = y[::-1] mask = (x >= x_min) & (x <= x_max) & (y >= y_min) & (y <= y_max) if any(mask): # Add corner point coordinates to the patch x = np.concatenate([x[mask], [x_max, x_min, x_min]]) y = np.concatenate([y[mask], [y_max, y_max, y_min]]) coords =[(x[i], y[i]) for i in range(len(x))] # Create patch patch = Patch(coords) patch.fill_solid(colors[i]) if shadow: sigma = 10 * size[1] / 100 kernel = 20 * size[1] / 100 kernel = (kernel, kernel) patch.cast_shadow(wallpaper, kernel=kernel, sigma=sigma) # Draw patch patch.draw(fig, size) # Convert figure to image wallpaper = fig2img(fig) plt.close(fig) # Save to file if provided if fname is not None: wallpaper.save(fname) return wallpaper
[docs] def perlin_lines( colors: List[str], background: Union[str, Image.Image, None] = None, size: Tuple[int, int] = (1280, 720), n: int = 5, m: int = 5, center: Tuple[float, float] = (0, 0), seed: int = 0, fname: Optional[str] = None, ) -> Image.Image: """ Creates a wallpaper with Perlin noise lines. This function generates a wallpaper with Perlin noise lines, using the provided colors and background. Parameters ---------- colors : list of str, str, matplotlib.colors.Colormap Input colors or colormap. Can be: - A list of color strings - A color name (str) - A colormap name (str) - A Colormap instance background : str, or PIL.Image.Image, optional Background color or image. Can be: - A color name or hex string - A PIL Image object size : tuple of int, default=(1280, 720) Size of the output image in pixels (width, height) n : int, default=4 Number of color line blocks. m : int, default=5 Number of lines in each block. Used if list of colors is not provided explicitly. center : tuple of float, default=(0, 0) Coordinates of the circle center from the right bottom corner in pixels seed : int, default=0 Random seed for the Perlin noise generation fname : str, optional If provided, saves the image to the specified file path Returns ------- PIL.Image.Image The generated wallpaper as a PIL Image object. If `fname` is provided, the image is also saved to the specified file path. Examples -------- >>> colors = ['#FF0000', '#00FF00', '#0000FF'] >>> wallpaper = perlin_lines(colors, size=(800, 600)) >>> plt.imshow(wallpaper) >>> plt.show() """ # Parse background image if isinstance(background, Image.Image): # resize background image if background.size != size: background = background.resize(size) # Flip vertically background = background.transpose(Image.FLIP_TOP_BOTTOM) # Parse colors and fill background if _is_cmap(colors): if background is None: background = Image.new("RGB", size, get_bgcolor(colors)) colors = generate_stepwise_palette(colors, m) else: colors = _get_gradient_colors(colors) m = len(colors) if background is None: background = Image.new("RGB", size, 'black') # Calculate radius of perlin patches n_major = n n_minor = len(colors) x_max = size[0] + center[0] y_max = size[1] - center[1] r_max = np.sqrt(x_max**2 + y_max**2) r_min = 0.6 * r_max dr_major = (r_max - r_min) / n_major dr_minor = dr_major / (n_minor + 1) # Generate perlin noise dens = 20 n = np.ceil(n_major * 6 / dens).astype(int) + 1 p = perlin((n,36), dens=dens, seed=seed)[dens//2:] # Draw color line blocks wallpaper = background inches, dpi = pixels_to_size_and_dpi(size, exact=True) fig = plt.figure(figsize=inches, dpi=dpi, facecolor='#00000000') plt.xlim(0,size[0]) plt.ylim(0,size[1]) # Show background image wallpaper = background plt.imshow(wallpaper) remove_margins() # Draw color line blocks scale = 15 * size[1] / 100 for i in range(n_major): for j in range(n_minor): k = n_minor * i + j r = r_min + dr_major * i + dr_minor * j x, y = _get_circle_dots(r, n=720) x, y = _distort_radius(x, y, scale * p[k]) x += size[0] + center[0] y += center[1] lw = n_minor + 2 - 1 * (j % n_minor) plt.plot(x, y, lw=lw, color = colors[j]) remove_margins() # Convert figure to image wallpaper = fig2img(fig) plt.close(fig) # Save to file if provided if fname is not None: wallpaper.save(fname) return wallpaper
[docs] def gradient_wallpaper( colors: Union[List[str], str, Colormap, None] = None, size: Tuple[int, int] = (1280, 720), angle: float = 0, img: Optional[Image.Image] = None, fname: Optional[str] = None, ) -> Image.Image: """ Creates a wallpaper with a gradient background. This function generates a wallpaper with a gradient fill, using either provided colors or colors sampled from sectors of a reference image. Notes ----- When using a reference image: 1. The central rectangle (half width and height) is excluded 2. 16 sectors are defined around the center 3. Each sector's average color is calculated from the non-excluded area 4. These colors are used as sources for the gradient Parameters ---------- colors : list of str, str, matplotlib.colors.Colormap, or None, default=None Input colors or colormap. Can be: - A list of color strings - A color name (str) - A colormap name (str) - A Colormap instance size : tuple of int, default=(1280, 720) Size of the output image in pixels (width, height) angle : float, default=0 Angle of the gradient in degrees. 0 means first color source is at the right edge. The angle increases in math style (counter-clockwise). img : PIL.Image.Image or None, default=None Reference image. If provided, 16 colors will be sampled from image sectors. fname : str, optional If provided, saves the image to the specified file path Returns ------- PIL.Image.Image The generated wallpaper as a PIL Image object. If `fname` is provided, the image is also saved to the specified file path. Examples -------- >>> colors = ['#FF0000', '#00FF00', '#0000FF'] >>> wallpaper = gradient_wallpaper(colors, size=(800, 600)) >>> plt.imshow(wallpaper) >>> plt.show() """ if colors is None and img is None: raise ValueError("Either colors or img must be provided") if colors is not None and img is not None: raise ValueError("colors must not be provided when using img") if img is not None: # Convert image to numpy array arr = img2arr(img) h, w = arr.shape[:2] center = np.array([w/2, h/2]) # Create mask for the central rectangle to exclude mask = np.ones((h, w), dtype=bool) y, x = np.ogrid[:h, :w] mask[int(h/8):int(7*h/8), int(w/8):int(7*w/8)] = False # Calculate angles for 8 sources n_sources = 8 source_angles = np.linspace(0, 2*np.pi, n_sources, endpoint=False) + np.pi / 2 # Calculate sector angles (midpoints between sources) sector_angles = (source_angles + np.roll(source_angles, -1)) / 2 # Initialize list for source colors colors = [] # For each source for i in range(n_sources): # Calculate angles for this sector angle1 = sector_angles[i-1] # Previous sector boundary angle2 = sector_angles[i] # Next sector boundary # Create sector mask y_rel = y - center[1] x_rel = x - center[0] angles = np.arctan2(y_rel, x_rel) % (2*np.pi) # Handle sector that crosses 0/2π boundary if angle1 > angle2: sector_mask = (angles >= angle1) | (angles < angle2) else: sector_mask = (angles >= angle1) & (angles < angle2) # Combine with central rectangle mask sector_mask = sector_mask & mask # Calculate average color for this sector if np.any(sector_mask): if len(arr.shape) == 2: # Grayscale rgb = [np.mean(arr[sector_mask])] * 3 else: # RGB rgb = [np.mean(arr[..., i][sector_mask]) for i in range(3)] rgb = tuple(np.clip(c / 255, 0, 1) for c in rgb) else: rgb = (0, 0, 0) # Default if no pixels in sector colors.append(rgb) # Convert RGB tuples to hex colors colors = [mc.to_hex(c) for c in colors] # Create gradient wallpaper wallpaper = fill_gradient(colors, size=size, angle=angle, show=False, fname=fname) return wallpaper
[docs] def triangle_wallpaper( colors: Union[str, List[str], None] = None, size: Tuple[int, int] = (1280, 720), img: Optional[Image.Image] = None, density: Optional[int] = 10, padding: float = 0.1, distortion: float = 0.15, seed: int = 0, fname: Optional[str] = None, ) -> Image.Image: """ Creates a wallpaper with a grid of distorted triangles. This function generates a wallpaper consisting of a grid of triangles that are randomly distorted and colored. The grid dimensions are calculated to maintain the aspect ratio of the requested image size. Parameters ---------- colors : str or list of str or None, default=None List of colors for the triangles. If None, colors are sampled from the reference image. size : tuple of int, default=(1280, 720) Size of the output image in pixels (width, height) img : PIL.Image.Image or None, default=None Reference image. If provided, colors will be sampled from the image. density : int, optional Number of grid cells in the shorter dimension. padding : float, default=0.1 Extra padding around the image to ensure coverage after distortion (0-1) distortion : float, default=0.15 Amount of random distortion to apply to grid points (0-1) seed : int, default=0 Random seed for the distortion and Perlin noise fname : str, optional If provided, saves the image to the specified file path Returns ------- PIL.Image.Image The generated wallpaper as a PIL Image object. If `fname` is provided, the image is also saved to the specified file path. """ if colors is None and img is None: raise ValueError("Either colors or img must be provided") if colors is not None and img is not None: raise ValueError("colors must not be provided when using img") # Set seed for reproducibility rng = np.random.default_rng(seed) # Convert single color to list of lighter and darker shades if colors is not None: colors = _get_gradient_colors(colors) # Calculate grid size aspect_ratio = size[0] / size[1] grid_size = (int(density * aspect_ratio), density) # Calculate perlin grid shape and density perlin_density = 3 perlin_shape = ( int(grid_size[0] // perlin_density + 1), int(grid_size[1] // perlin_density + 1) ) # Calculate grid with padding width_pad = int(size[0] * padding) height_pad = int(size[1] * padding) total_width = size[0] + 2 * width_pad total_height = size[1] + 2 * height_pad # Create grid points x = np.linspace(-width_pad, size[0] + width_pad, grid_size[0]) y = np.linspace(-height_pad, size[1] + height_pad, grid_size[1]) xs, ys = np.meshgrid(x, y) # Apply Perlin noise distortion to generate zs p = perlin(perlin_shape, dens=perlin_density, seed=seed) zs = p.T[:grid_size[1], :grid_size[0]] * (xs[0,1] - xs[0,0]) # Apply random distortion to xs and ys max_distortion_x = total_width * distortion / grid_size[0] max_distortion_y = total_height * distortion / grid_size[1] xs += rng.uniform(-max_distortion_x, max_distortion_x, xs.shape) ys += rng.uniform(-max_distortion_y, max_distortion_y, ys.shape) # Calculate light direction vector (normalized) light_angle_rad = np.radians(0) light_elevation_rad = np.radians(30) light_dir = np.array([ np.cos(light_elevation_rad) * np.cos(light_angle_rad), np.cos(light_elevation_rad) * np.sin(light_angle_rad), np.sin(light_elevation_rad) ]) light_dir = light_dir / np.linalg.norm(light_dir) light_dir = np.array([1, 0, 0]) # Create list to store triangle vertices and normals triangles = [] normals = [] # Generate triangles from grid points for i in range(grid_size[1] - 1): for j in range(grid_size[0] - 1): # First triangle v1 = np.array([xs[i, j], ys[i, j], zs[i, j]]) v2 = np.array([xs[i, j+1], ys[i, j+1], zs[i, j+1]]) v3 = np.array([xs[i+1, j], ys[i+1, j], zs[i+1, j]]) # Calculate normal vector for first triangle edge1 = v2 - v1 edge2 = v3 - v1 normal = np.cross(edge1, edge2) normal = normal / np.linalg.norm(normal) triangle1 = [ (v1[0], v1[1]), (v2[0], v2[1]), (v3[0], v3[1]) ] triangles.append(triangle1) normals.append(normal) # Second triangle v1 = np.array([xs[i+1, j], ys[i+1, j], zs[i+1, j]]) v2 = np.array([xs[i, j+1], ys[i, j+1], zs[i, j+1]]) v3 = np.array([xs[i+1, j+1], ys[i+1, j+1], zs[i+1, j+1]]) # Calculate normal vector for second triangle edge1 = v2 - v1 edge2 = v3 - v1 normal = np.cross(edge1, edge2) normal = normal / np.linalg.norm(normal) triangle2 = [ (v1[0], v1[1]), (v2[0], v2[1]), (v3[0], v3[1]) ] triangles.append(triangle2) normals.append(normal) # Create matplotlib figure inches, dpi = pixels_to_size_and_dpi(size, exact=True) fig = plt.figure(figsize=inches, dpi=dpi, facecolor='#00000000') plt.xlim(0, size[0]) plt.ylim(0, size[1]) remove_margins() # Draw triangle patches with lighting effect for triangle, normal in zip(triangles, normals): coords = np.array(triangle) patch = Patch(coords) # Assign random angle and color angle = rng.uniform(0, 360) if colors is None: img_ = resize_image(img, size) base_color = patch.get_centroid_color(img_) else: base_color = random.choice(colors) # Apply lighting effect intensity = np.dot(normal, light_dir) intensity = np.round(np.clip(intensity, -0.2, 0.2), 2) if intensity > 0: lit_color = lighten(base_color, intensity) else: lit_color = darken(base_color, -intensity) # Create gradient colors with lighting gradient_colors = [ lighten(lit_color, 0.1), lit_color, darken(lit_color, 0.1) ] # Fill triangle with gradient patch.fill_gradient(gradient_colors, angle=angle) patch.draw(fig, size) # Convert figure to image wallpaper = fig2img(fig) plt.close(fig) # Create blurred background to fill gaps # Convert to numpy array for processing arr = np.array(wallpaper) # Create alpha mask from the original alpha channel alpha_mask = arr[..., 3] / 255.0 # Create blurred version blur_sigma = 10 kernel_size = int(2 * blur_sigma + 1) | 1 # Ensure odd kernel size blurred = cv2.GaussianBlur(arr, (kernel_size, kernel_size), blur_sigma) # Handle color channels separately to preserve transparency result = np.zeros_like(arr) # Blend RGB channels for i in range(3): # Original color where alpha is high, blurred color where alpha is low result[..., i] = (arr[..., i] * alpha_mask + blurred[..., i] * (1 - alpha_mask)).astype(np.uint8) # Keep original alpha channel but fill gaps result[..., 3] = np.maximum(arr[..., 3], blurred[..., 3]).astype(np.uint8) # Convert back to PIL Image wallpaper = Image.fromarray(result) # Save to file if provided if fname is not None: wallpaper.save(fname) return wallpaper
import types __all__ = [name for name, thing in globals().items() if not (name.startswith("_") or isinstance(thing, types.ModuleType))] del types