designcoding
Search
About Table of Contents Keywords Support designcoding!

String Art in Rhino Python

June 19, 2025 | 3,739 views
Decorative Arts | Rhino Python | Tools
image sampling | polyline | string | string art

String art is a captivating technique that combines geometry and aesthetics to recreate images using lines between pins. Here is a good reference to start studying it. In this post, I explored a custom Rhino Python script designed for Rhinoceros 7 and 8. It transforms a grayscale image into a dense web of threads. The algorithm analyzes the darkest areas of the image and connects pins in a circular layout to form compositions. The script begins by converting a user-selected image into grayscale and masking it within a circular region. The user defines the number of pins and lines. From there, the algorithm evaluates possible connections. Then, it selects the one with the lowest average brightness at each step. Each line subtly lightens the image along its path, progressively refining the output. The code displays the final result in the Rhino viewport as a polyline, with numbered pins for reference.

I developed this code in Rhino 8 (Python 3), but it also works well in Rhino 7 (IronPython, a bit slower, I guess). To run the code, simply open the Python editor in Rhino, paste the script, and run it. Then, you will be prompted to select an image file and enter parameters like contrast, number of lines, and number of pins. After processing, the string art pattern will appear in the Rhino viewport with numbered pins for reference. Note that you may wait for a long time if you enter large numbers. Also, the physical output will depend on the thickness and the transparency of the string you choose. This is why the String Art algorithm does not guarantee a perfect outcome at every trial. Below is the complete Rhino Python code. You can download the code at the bottom of this page, also. The script gives the numbered order of the pins in the toolbar. If you are patient enough, you can try building it physically. Honestly, I didn’t test that. The result is also dependent on the thickness of the wire you use.

import rhinoscriptsyntax as rs
import math
import Rhino
import scriptcontext as sc
import random
from System.Drawing import Bitmap
def grayscale(color):
    return (color.R + color.G + color.B) / 3
def get_image_pixels(image_path):
    img = Bitmap(image_path)
    width, height = img.Width, img.Height
    pixels = []
    for y in range(height):
        row = []
        for x in range(width):
            color = img.GetPixel(x, height - 1 - y)
            gray = grayscale(color)
            row.append(gray)
        pixels.append(row)
    return pixels, width, height
def mask_circle(pixels, width, height):
    cx, cy = width / 2, height / 2
    r = min(width, height) / 2
    masked = []
    for y in range(height):
        row = []
        for x in range(width):
            d = math.hypot(x - cx, y - cy)
            row.append(pixels[y][x] if d <= r else 255)
        masked.append(row)
    return masked
pin_points = []
def create_pin_points(center, radius, count):
    global pin_points
    pin_points = []
    for i in range(count):
        angle = 2 * math.pi * i / count
        x = center[0] + radius * math.cos(angle)
        y = center[1] + radius * math.sin(angle)
        z = center[2]
        pt = Rhino.Geometry.Point3d(x, y, z)
        pin_points.append((pt, i+1))
        if count <= 200 or i % 10 == 0:
            rs.AddTextDot(str(i+1), pt)
def average_grayscale_on_line(p1, p2, pixels, width, height):
    dx, dy = p2.X - p1.X, p2.Y - p1.Y
    length = math.hypot(dx, dy)
    samples = int(length)
    if samples == 0:
        return 255
    inv = 1.0 / samples
    total, count = 0, 0
    for i in range(samples):
        t = i * inv
        x = (1 - t) * p1.X + t * p2.X
        y = (1 - t) * p1.Y + t * p2.Y
        px, py = int(x + 0.5), int(y + 0.5)
        if 0 <= px < width and 0 <= py < height:
            total += pixels[py][px]
            count += 1
    return total / count if count > 0 else 255
def lighten_pixel(pixels, x, y, strength=0.02):
    if 0 <= x < len(pixels[0]) and 0 <= y < len(pixels):
        g = pixels[y][x]
        pixels[y][x] = min(255, g + (255 - g) * strength)
def draw_string_art(pixels, pin_points, width, height, total_lines=500, min_dist_ratio=None):
    sequence = []
    radius = min(width, height) / 2
    if min_dist_ratio is None:
        min_dist_ratio = max(0.1, 10.0 / len(pin_points))
    min_dist = radius * min_dist_ratio
    used_lines = set()
    current_pt, _ = random.choice(pin_points)
    poly_pts = [current_pt]
    lines_drawn = 0
    while lines_drawn < total_lines:
        min_gray = float('inf')
        next_pt = None
        for pt, _ in pin_points:
            if pt == current_pt:
                continue
            dist = current_pt.DistanceTo(pt)
            if dist < min_dist:
                continue
            key = tuple(sorted([(current_pt.X, current_pt.Y), (pt.X, pt.Y)]))
            if key in used_lines:
                continue
            avg = average_grayscale_on_line(current_pt, pt, pixels, width, height)
            if avg < min_gray:
                min_gray = avg
                next_pt = pt
        if next_pt:
            key = tuple(sorted([(current_pt.X, current_pt.Y), (next_pt.X, next_pt.Y)]))
            used_lines.add(key)
            l = current_pt.DistanceTo(next_pt)
            samples = int(l)
            for i in range(samples):
                t = i / samples
                x = int((1 - t) * current_pt.X + t * next_pt.X + 0.5)
                y = int((1 - t) * current_pt.Y + t * next_pt.Y + 0.5)
                lighten_pixel(pixels, x, y, strength=contrast)
            poly_pts.append(next_pt)
            current_pt = next_pt
            for pt, idx in pin_points:
                if pt == next_pt:
                    sequence.append(idx)
                    break
            lines_drawn += 1
        else:
            current_pt, _ = random.choice(pin_points)
            poly_pts.append(current_pt)
    polyline = Rhino.Geometry.Polyline(poly_pts)
    sc.doc.Objects.AddPolyline(polyline)
    sc.doc.Views.Redraw()
    print("Done. Here is the ordered list of pin IDs:")
    print(sequence)
image_path = rs.OpenFileName("Choose an image file")
contrast = rs.GetReal("Contrast level (0.00-1.00)", 0.25)
line_count = rs.GetInteger("Number of lines", 700)
pin_count = rs.GetInteger("Number of pins", 200)
max_lines = min(line_count, pin_count * 6)
if image_path:
    print("Calculating...")
    pixels, img_w, img_h = get_image_pixels(image_path)
    masked = mask_circle(pixels, img_w, img_h)
    create_pin_points(center=(img_w/2, img_h/2, 0), radius=img_w/2, count=pin_count)
    draw_string_art(masked, pin_points, img_w, img_h, total_lines=max_lines)
string art
String Art Rhino Python codeDownload
Sample image used in the test fileDownload
Result of the test in Rhinoceros 8Download
  • Search

  • Categories

    • Education
      • Basic Design
      • Design Geometry
      • Design Mathematics
      • Digital Fabrication
      • Parametric Modeling
      • Tutorials
    • Philosophy
      • Analytical Tradition
      • Phenomenology
    • Practice
      • 3D Models
      • Projects
      • Publications
      • Workshops
    • Research
      • 3D Printing
      • Building Facade
      • Calculus
      • Climate Analysis
      • Compass Constructions
      • Computational Geometry
      • Curves
      • Decorative Arts
      • Digital Fabrication
      • Evolutionary Solvers
      • Folding Structures
      • Fractals
      • Graph Theory
      • Interlocking Structures
      • Islamic Patterns
      • Linear Algebra
      • Minimal Surfaces
      • Muqarnas
      • Non-Euclidean Geometry
      • Paneling
      • Parametric Curves
      • Parametric Objects
      • Parametric Surfaces
      • Pattern Deformations
      • Patterns
      • Pavilions
      • Polyhedra
      • Rammed Earth Structures
      • Robotic Fabrication
      • Shape Grammars
      • Simulation
      • Space Syntax
      • Surface Constructions
      • Tessellations
      • Tools
      • Vector Fields
      • Virtual Reality
    • Tools and Languages
      • 3DS Max
      • 3DS Max Script
      • Grasshopper
      • Photoshop
      • Physical Prototyping
      • Revit
      • Rhino
      • Rhino Macro
      • Rhino Python
      • Rhino Script
      • Unity
  • Monthly Archive

    • August 2026 (1)
    • October 2025 (1)
    • June 2025 (2)
    • May 2025 (2)
    • April 2025 (5)
    • December 2024 (40)
    • August 2024 (5)
    • July 2024 (6)
    • April 2024 (4)
    • March 2024 (10)
    • February 2024 (10)
    • January 2024 (8)
    • December 2023 (10)
    • August 2023 (3)
    • July 2023 (3)
    • June 2023 (7)
    • May 2023 (8)
    • April 2023 (7)
    • March 2023 (2)
    • February 2023 (2)
    • January 2023 (3)
    • December 2022 (6)
    • November 2022 (7)
    • January 2022 (1)
    • December 2021 (1)
    • October 2021 (3)
    • September 2021 (4)
    • August 2021 (4)
    • May 2019 (2)
    • April 2019 (1)
    • March 2019 (5)
    • January 2019 (2)
    • December 2018 (1)
    • November 2018 (4)
    • October 2018 (9)
    • July 2018 (1)
    • June 2018 (3)
    • May 2018 (1)
    • April 2018 (4)
    • February 2018 (2)
    • January 2018 (7)
    • August 2017 (9)
    • July 2017 (6)
    • October 2016 (1)
    • May 2015 (5)
    • April 2015 (8)
    • March 2015 (12)
    • February 2015 (4)
    • January 2015 (11)
    • November 2014 (1)
    • August 2014 (1)
    • June 2014 (2)
    • May 2014 (12)
    • April 2014 (5)
    • March 2014 (3)
    • February 2014 (6)
    • January 2014 (4)
    • December 2013 (5)
    • November 2013 (11)
    • October 2013 (2)
    • September 2013 (9)
    • August 2013 (4)
    • July 2013 (2)
    • June 2013 (14)
    • May 2013 (4)
    • April 2013 (10)
    • March 2013 (11)
    • February 2013 (11)
    • January 2013 (10)
    • December 2012 (10)
    • November 2012 (6)
    • October 2012 (13)
    • September 2012 (2)
    • August 2012 (5)
    • July 2012 (14)
    • June 2012 (6)
    • May 2012 (17)
    • April 2012 (15)
    • March 2012 (9)
    • February 2012 (16)
    • January 2012 (18)
    • December 2011 (20)
    • November 2011 (2)
  • Support designcoding!

    Enjoying designcoding? Support its future with a small donation on Patreon. Thank you!

    Patreon


    copyright 2026 designcoding.net | about | table of contents | keywords | privacy policy | end user license agreement