BardOfCodes/splitweave
SplitWeave: A DSL for 2D describing pattern Images.
SplitWeave
SplitWeave is a DSL for 2D pattern programs. SplitWeave can be used to represent patterns built by splitting the canvas into fragments, applying fragment-index dependant modulations, and weaving them back together. We created this library to support the Pattern Analogy project (CVPR 2025). We used this tool to create synthetic analogical quartets to learn analogical editing of 2D pattern images. Please refer to the paper for more information.
This repository builds on GeoLIPI.
Quick Notes:
-
The language was mostly designed for sampling synthetic patterns. Therefore the design is not very user friendly.
-
I plan to add a compiler to convert the pattern expressions into GLSL shader code. Then we can potentially evaluate expressions without GPUs :).
-
I used Claude Code to refactor the code base from the earlier version used for the paper submission. Please contact me (adityaganeshan@gmail.com) know if you need the original codebase.
What can you do with SplitWeave
1. Manually write and evaluate pattern programs.
Build expressions directly using splitweave.symbolic (sws) and evaluate with grid_eval or evaluate_pattern_expr.
import torch as th
from PIL import Image
import numpy as np
import splitweave.symbolic as sws
import geolipi.symbolic as gls
import geolipi.symbolic.primitives_2d as prim2d
from geolipi.torch_compute import Sketcher
from splitweave.torch_compute import evaluate_pattern
RES = 512
DEVICE = "cuda" if th.cuda.is_available() else "cpu"
# Create sketcher to evaluate the expression
sketcher = Sketcher(device=DEVICE, resolution=RES, n_dims=2)
# Load tile
tile_path = "assets/tiles/etawvihr_1.png"
pil_img = Image.open(tile_path).convert("RGBA").rotate(90, expand=True)
tile_np = np.array(pil_img).astype("float32") / 255.0
tile = th.from_numpy(tile_np).to(DEVICE)
# Specify Layout
layout = sws.RectRepeatShiftedX(sws.CartesianGrid(), (0.25, 0.25), (0.125,))
# Layout-level cell effects
layout = sws.LayoutCellScale(
layout, sws.YStripeSignal(2, True, True, False, False),
0.2, "single"
)
# Specify Tile
tile_expr = sws.TileRecolor(prim2d.TileUV2D(tile), 0.3)
tile_expr = sws.TileScale(tile_expr, 0.80)
tile_expr = sws.ApplyTile(layout, tile_expr)
# Specify some tile-level Cell Effects
cellfx_tile = sws.ApplyCellRecolor(tile_expr,
sws.XStripeSignal(2, False, False, False, False), "discrete",45, "single")
# Add some BG canvas
bg_color = th.tensor([0.95, 0.92, 0.85, 1.0], device=DEVICE)
bg = sws.ConstantBackground(bg_color)
root = gls.SourceOver(cellfx_tile, bg)
canvas, grid_ids = evaluate_pattern(root, sketcher, aa=2)
The canvas tensor has shape [H*W, 4] (RGBA). To visualise or save:
#Evaluate the expression
canvas = canvas.reshape(RES, RES, 4).cpu().numpy()
canvas = (canvas * 255).astype(np.uint8)
image = Image.fromarray(canvas)
image.save("output.png")
2. Generate Pattern Animations.
# Extend imports from previous code snippets.
def get_scaled_expr(tile, scale, translate):
layout = sws.CartesianGrid()
layout = sws.CartTranslate(layout,(translate, 0,))
layout = sws.RectRepeatShiftedX(layout,
(0.25, 0.25), (0.125,))
# Layout-level cell effects
layout = sws.LayoutCellScale(
layout, sws.XStripeSignal(2, False, False, False, False, False),
scale, "single"
)
# Specify Tile
tile_expr = sws.TileRecolor(prim2d.TileUV2D(tile), 0.3)
tile_expr = sws.TileScale(tile_expr, 0.80)
tile_expr = sws.ApplyTile(layout, tile_expr)
# Specify some tile-level Cell Effects
cellfx_tile = sws.ApplyCellRecolor(tile_expr,
sws.YStripeSignal(2, False, False, False, False, False), "discrete", 45, "single")
# Add some BG canvas
bg_color = th.tensor([0.95, 0.92, 0.85, 1.0], device=DEVICE)
bg = sws.ConstantBackground(bg_color)
root = gls.SourceOver(cellfx_tile, bg)
return root
# fps = 30
# N = 4 sec
images = []
for i in range(120):
scale_amount = np.sin(i/60 * 2 * np.pi) * 0.2
translate_amount = i/60 * 0.5
cur_expr = get_scaled_expr(tile, scale_amount, translate_amount)
canvas, grid_ids = evaluate_pattern(cur_expr, sketcher, aa=2)
canvas = canvas.reshape(RES, RES, 4).cpu().numpy()
canvas = (canvas * 255).astype(np.uint8)
image = Image.fromarray(canvas)
images.append(image)
Save gif:
from geolipi.utils import frames_to_animation
frames_to_animation(images,
"output.gif",
fps=30,
format="gif",
mp4_quality="high")
3. Generate random Motif Tiling & Region Splitting Patterns.
MTP: Motif Tiling Patterns
import numpy as np
import torch as th
from PIL import Image
from geolipi.torch_compute import Sketcher
from splitweave.pattern_gen.mtp_main import sample_mtp_pattern
from splitweave.cfg_to_sw.parser import mtp_config_to_expr, load_tile_tensors_from_config
from splitweave.utils.tiles import get_tile_content
from splitweave.torch_compute import evaluate_pattern
# Fixed seed for reproducible random patterns
np.random.seed(9)
# 9 88 97
TILE_DIR = "../assets/tiles/"
RES = 1024
MID_RES = 256
DEVICE = "cuda" if th.cuda.is_available() else "cpu"
# Create sketcher to evaluate the expression
sketcher = Sketcher(device=DEVICE, resolution=RES, n_dims=2)
filenames, filename_to_indices = get_tile_content(TILE_DIR, mode=None)
mtp_cfg = sample_mtp_pattern(filenames, filename_to_indices, n_tiles=2)
tile_tensors = load_tile_tensors_from_config(mtp_cfg.tile_cfg, TILE_DIR, device=DEVICE)
expr = mtp_config_to_expr(mtp_cfg, tile_tensors=tile_tensors, device=DEVICE)
canvas, grid_ids = evaluate_pattern(expr.tensor(), sketcher, aa=2)
canvas = canvas.reshape(RES, RES, 4).cpu().numpy()
canvas = canvas[MID_RES:-MID_RES, MID_RES:-MID_RES]
canvas = (canvas * 255).astype(np.uint8)
image = Image.fromarray(canvas)
Here are random samples from the MTP Generator.
RSP: Region Splitting Patterns
# Extend imports from previous
from splitweave.pattern_gen.rsp_main import sample_rsp_pattern
from splitweave.cfg_to_sw.parser import rsp_config_to_expr
from splitweave.utils.tiles import get_tile_content
# Fixed seed for reproducible random patterns
TILE_DIR = "../assets/tiles/"
RES = 1024
MID_RES = 256
DEVICE = "cuda" if th.cuda.is_available() else "cpu"
# Create sketcher to evaluate the expression
sketcher = Sketcher(device=DEVICE, resolution=RES, n_dims=2)
filenames, filename_to_indices = get_tile_content(TILE_DIR, mode=None)
rsp_cfg = sample_rsp_pattern(filenames, filename_to_indices)
tile_tensors = load_tile_tensors_from_config(rsp_cfg.tile_cfg, TILE_DIR, device=DEVICE)
expr = rsp_config_to_expr(rsp_cfg, tile_tensors=tile_tensors, device=DEVICE)
canvas, grid_ids = evaluate_pattern(expr.tensor(), sketcher, aa=2)
canvas = canvas.reshape(RES, RES, 4).cpu().numpy()
# canvas = canvas[MID_RES:-MID_RES, MID_RES:-MID_RES]
canvas = (canvas * 255).astype(np.uint8)
image = Image.fromarray(canvas)
Here are random samples from the RSP Generator.
### 4. Generate analogical quartets.# Analogy Sampler
import numpy as np
import torch as th
from PIL import Image
from geolipi.torch_compute import Sketcher
from splitweave.utils.tiles import get_tile_content
from splitweave.analogy_gen.mtp_layout_analogies import full_layout_change
from splitweave.cfg_to_sw.parser import mtp_config_to_expr, load_tile_tensors_from_config
from splitweave.torch_compute import evaluate_pattern
TILE_DIR = "../assets/tiles/"
RES = 1024
MID_RES = 256
DEVICE = "cuda" if th.cuda.is_available() else "cpu"
KEYS = ['patterns_a', 'patterns_b', 'patterns_a_star', 'patterns_b_star',]
# Create sketcher to evaluate the expression
sketcher = Sketcher(device=DEVICE, resolution=RES, n_dims=2)
filenames, filename_to_indices = get_tile_content(TILE_DIR, mode=None)
analogy_output = full_layout_change(filenames, filename_to_indices)
images = {}
for key in KEYS:
cur_cfg = analogy_output[key][0]
tile_tensors = load_tile_tensors_from_config(cur_cfg.tile_cfg, TILE_DIR, device=DEVICE)
expr = mtp_config_to_expr(cur_cfg, tile_tensors=tile_tensors, device=DEVICE)
canvas, grid_ids = evaluate_pattern(expr.tensor(), sketcher, aa=2)
canvas = canvas.reshape(RES, RES, 4).cpu().numpy()
canvas = canvas[MID_RES:-MID_RES, MID_RES:-MID_RES]
canvas = (canvas * 255).astype(np.uint8)
image = Image.fromarray(canvas)
images[key] = image
| RSP Analogy | MTP Analogy |
|---|---|
![]() |
![]() |
5. Generate MTP pattern tiles
Tile generation uses LayerDiffuse and requires installing it separately (not included in splitweave's pip dependencies). Use scripts/generate_tiles.py to generate tile images via LayerDiffuse + SDXL. Here are some examples:
Steps:
1. Create the word lists.
Clone corpora for word lists. Create a config as shown in splitweave/configs/world_list_configs/ (it already contains some useful ones). Then run:
# run python scripts/build_noun_lists.py --config <path-to-config> --base-path <path-to-corpora/data>
# for example:
python scripts/build_noun_lists.py --config configs/word_list_configs/all_noun_lists.yaml --base-path ../../patterns/corpora/data/
2. Create the tiles:
Clone LayerDiffuse_DiffusersCLI and install its requirements. Create a config as shown in splitweave/configs/tile_gen/ Then, run:
python scripts/generate_tiles.py --config configs/tile_gen/default.yaml
6. Use the ASMBLR interface for Analogically editing pattern Images.
TBD.
Generate Analogical Quartet Training data:
- Create tiles.
python scripts/generate_tiles.py --config configs/tile_gen/default.yaml
Adapt the script to generate over many different prompts. We generated 100_000 synthetic tiles for our experiments.
- Create analogical quartets.
# python scripts/generate_analogical_quartets.py --tile_dir <path-to-tiles> --out_dir <path-to-save-imgs>
python scripts/generate_analogical_quartets.py --tile_dir assets/tiles/ --out_dir assets/synth_analogy/ --n_pats 10 --verbose
Installation
From source
git clone https://github.com/BardOfCodes/splitweave.git
cd splitweave
pip install -r requirements.txt
pip install -e .
You need GeoLIPI installed (included in requirements.txt). For development (tests, linting):
pip install -e ".[dev]"
Testing
From the repo root (with the package installed):
pytest
Some tests require geolipi. The bridge tests in tests/test_patternator_bridge.py use splitweave's pattern samplers and cfg_to_sw parser.
License and disclaimer
MIT License. See LICENSE. This is research code — use at your own risk.







