<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Roy Hachnochi on Medium]]></title>
        <description><![CDATA[Stories by Roy Hachnochi on Medium]]></description>
        <link>https://medium.com/@roy.hachnochi?source=rss-63240837b082------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*pBMkelvdfdPDgicR.jpg</url>
            <title>Stories by Roy Hachnochi on Medium</title>
            <link>https://medium.com/@roy.hachnochi?source=rss-63240837b082------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 23 May 2026 15:33:10 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@roy.hachnochi/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Algorithmic String Art — Multicolor]]></title>
            <link>https://medium.com/@roy.hachnochi/algorithmic-string-art-multicolor-9f86084cf7b3?source=rss-63240837b082------2</link>
            <guid isPermaLink="false">https://medium.com/p/9f86084cf7b3</guid>
            <dc:creator><![CDATA[Roy Hachnochi]]></dc:creator>
            <pubDate>Tue, 02 Sep 2025 06:02:06 GMT</pubDate>
            <atom:updated>2025-09-02T06:02:06.158Z</atom:updated>
            <content:encoded><![CDATA[<h3>Algorithmic String Art — Multicolor</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/400/1*Scg0XuBCGipPspVQ13qT8w.gif" /></figure><p>In the <a href="https://medium.com/@roy.hachnochi/algorithmic-string-art-b-w-a61fda614f2a">previous part</a> we crafted a decent algorithm for monochrome String-Art images, but why stop here? Although this part diverged from what my original intention for this project was, I wanted to make the most out of it.</p><p>Generalizing the monochrome algorithm to multicolor images is definitely not straight-forward. Given the subtractive nature of real-life colors, the naive method would be to apply the monochrome algorithm per channel, and render using CMYK (cyan, blue, magenta, black — the opposites of red, green, blue, white) threads. This works in theory and even in simulation, but when rendering with opaque strings it just completely fails.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/499/1*EErRkp740L8OL7yEGfJMDg.jpeg" /><figcaption>Optimizing CMYK channels separately. Left: Color subtraction simulation. Right: “Real world” simulation with opaque strings.</figcaption></figure><blockquote><strong>A note about color theory</strong></blockquote><blockquote>I chose to model color blending in the straight-forward method — by simple addition in RGB space, but this is certainly not the most accurate color blending model there is. Our perception of colors (and color blending) is much more complicated, and modelling a correct color space is key for success of the multicolor algorithm. LAB for example, is considered a perceptual color space much more similar to the way the human eye works.</blockquote><p>In algorithms study, there are usually two main approaches to extend an algorithm to a broader/harder case:</p><ol><li><strong>Reduction</strong> — Taking the harder problem and breaking it into smaller, more manageable parts which we may already have the solution to.</li><li><strong>Generalization</strong> — Looking at the base case as a specific case of a more general problem, and extending the idea of the base case to handle the general case.</li></ol><p>With this in mind, I’ll break down how each approach leads us to different algorithms, both based on the monochrome algorithm which we already have.</p><h3>Reduction — Dithering + Monochrome Optimizer</h3><p>After trying a few methods, and conducting some research, I came across this excellent <a href="https://www.perfectlynormal.co.uk/blog-computational-thread-art">blogpost</a> by Callum Mcdougal, who made a very similar project (and from whom I borrowed most of my example images displayed here below). He proposed a very interesting idea, which I had been circling around myself in my trials.</p><p>The key here is an image processing algorithm called Dithering, specifically <a href="https://en.wikipedia.org/wiki/Floyd%E2%80%93Steinberg_dithering">Floyd-Steinberg dithering</a>. It is the process of approximating an image with a small color-palette, based on the idea that the human eye blends nearby colors. For example, a chess-board image with alternating black and white pixels will, from far enough away, just look gray. The algorithm works by scanning the image pixel by pixel, approximating the nearest color from the palette for each pixel, and diffusing the estimation error to the surrounding pixels.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/477/1*m8w1WNppHeB0k-Kg0yRjNg.jpeg" /><figcaption>Left: Original. Right: Dithered.</figcaption></figure><p>Building on this, we draw up the following method:</p><ol><li><strong>Initialization</strong> — Choose an appropriate color palette for the image (more on this later).</li><li><strong>Preprocessing</strong> — Apply dithering to produce a single binary image for each color in the palette.</li><li><strong>Optimization</strong> — Run the monochrome algorithm on each color-image separately.</li><li><strong>Postprocessing</strong> — Interweave the threads of each color-image to a single solution.</li></ol><p>For the postprocessing part, I came up with two methods:</p><ol><li><strong>Interweave</strong> — Based on Callum Mcdougal’s idea, we:<br>a. Order the colors from lightest to darkest.<br>b. Reverse the order of threads for each color. This is because we assume that in the optimization process, the more important lines were discovered earlier, so we would like them on top.<br>c. Cyclically add only a segment of each color’s threads (e.g., 25% of color #1, 25% of color #2, 25% of color #3, then the next 25% of each color until done).</li><li><strong>Combine by simulation</strong> — A different, optimization based, approach. Now that we have a fixed list of threads per color, we may solve a simpler optimization problem, which only chooses the next color to add in each step, progressing through the strings of each color in order. Since the search space is very small (only a single line to test per color) it runs pretty fast. Then just flip the order, placing the more important threads on top.</li></ol><p>Personally, I couldn’t decide which one worked better, but we may point out the difference between the approaches: <strong>Interweave</strong> is more user <em>controllable</em>, and <strong>Combine</strong> is more <em>automatic</em> but can sometimes turn out worst.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*g25QGcfRkZHM9nMy6zQMAw.jpeg" /><figcaption>Multicolor optimizer pipeline</figcaption></figure><p>The nice thing about this method, is that it’s agnostic to the monochrome optimization algorithm being used in the per-color part. Any String-Art algorithm that works on monochrome images may be plugged in here and be expanded to work on multicolor images.</p><h3>Generalization — Multicolor Binary Linear optimizer</h3><p>First, let’s recall our monochrome problem and optimization rules, equations <strong><em>(3)-(5)</em></strong> from the previous post:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*--VQEHf44WyPepcKHrHx_g.png" /></figure><p>The idea here is to rephrase our linear approximation of the optimization problem to the more general case of multicolor strings. Formally, we slightly change the definitions of the matrices and vectors, and add color representation:</p><ul><li><strong><em>b</em> (<em>hw </em>× 3) </strong>- The flattened RGB image.</li><li><strong><em>C (k </em>× <em>3) </em></strong>- The color dictionary. Each row represents the RGB of a different color in the dictionary.</li><li><strong><em>x</em> (<em>n</em> × <em>k</em>) </strong>- A binary vector representing which lines (from all possible nail-pairs) are added to the String-Art solution, and in which color of the color dictionary. We allow at most a single <strong><em>1</em></strong> per row.</li><li><strong><em>A</em> (<em>hw </em>×<em> n</em>) </strong>- The transformation matrix, doesn’t change from it’s original definition.</li></ul><p>Now we optimize for <strong><em>l</em></strong> — the line number (as before), but also for <strong><em>c</em></strong> — the color number. The new optimization problem becomes:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XrkfkpojgTJ9HNhuMiEKew.png" /></figure><p>And the updated equations (this time I won’t show the derivation):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*usZHgiA5Ev1pHxofE4gSTA.png" /></figure><p>With <strong><em>A⁎ₗ </em></strong>being the <strong><em>l</em></strong>-th column of <strong><em>A</em></strong>, and Cc<strong><em>⁎ </em></strong>being the <strong><em>c</em></strong>-th row of <strong><em>C</em></strong>. With these update equations, we apply the exact same greedy iteration algorithm as the original monochrome solution, with each step adding a string of a new color.</p><blockquote><strong>Implementing this in practice</strong></blockquote><blockquote>There are some software engineering technicalities here, such as how we enforce each color to start from its previous nail, how we efficiently implement the calculations of equation <strong>(4&#39;)</strong> or how we make sure that we don’t switch colors too often.</blockquote><p>Finally, as in the previous approach, we assume that the more important lines are found earlier, so we flip the order in postprocessing.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/250/1*pu76YuATepV32gzhZAJ4Ng.jpeg" /><figcaption>MCBL optimizer</figcaption></figure><h4>A slight improvement — CMY-log</h4><p>Diving a bit deeper into color theory, I found that color blending behaves differently than just simple additive RGB blending. A more accurate model assumes <strong>multiplicative</strong> blending in CMY space (the complement of RGB). But how do we simulate multiplicative blending when all our equations above are linear? The good old <strong><em>log </em></strong>trick will come in handy, as we find that we can just slightly change the inputs of the above optimization problem, and get the desired multiplicative behavior.</p><p>A single pixel color blending in this model is represented by:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xHGNKWekqgxsloS5H8sfdQ.png" /></figure><p>Therefore, we can change the inputs accordingly:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*31fXjhJVgPKTtPGGPrJFiw.png" /></figure><p>And from here we just solve the same optimization problem as before, with the updated inputs.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/501/1*C6uSY1BljZqAgYXLL98GPA.jpeg" /><figcaption>Left: MCBL optimization on regular additive space. Right: MCBL optimizer on CMY multiplicative space.</figcaption></figure><h3>Finding the Color Palette</h3><p>We have just one last problem left to solve. Both methods heavily rely on choosing the correct color palette to approximate the image. From my experience, the algorithm is very sensitive to the chosen colors, and choosing even a slightly off palette will significantly degrade the results for most images. It’s therefore very important to choose the best color palette in the preprocessing/initialization step.</p><p>Seeing as my aim was to have the algorithm be as much plug-and-play as possible, I gave a shot at a few different approaches, varying in levels of automaticity. I’ll lay out my trials process approximately as it went.</p><ol><li><strong>Manual color palette</strong> — The least automatic method, just receive a palette defined by the user. This has the best potential of being the most accurate, but finding the exact right palette may be hard and tiresome.</li><li><strong>Clustering-based palette</strong> — Run a clustering algorithm (such as KMeans or Median-Cut) on the image pixels to choose the <strong><em>k </em></strong>most dominant colors. This is the most automatic method, as it has no need for a predefined color dictionary, but unfortunately didn’t work for most images, since clustering algorithms tend to find average centers for clusters, and not the most common ones.</li><li><strong>Histogram-based palette</strong> — First define a fairly small (<strong><em>~25</em></strong> colors) but highly representative color dictionary. Now we calculate a discrete histogram on the image pixels with this dictionary. An important addition here is to smooth the histogram, so that different shades of the same color (or perceptually similar colors) will also be accounted for. This method unfortunately resulted in picking different shades of the same color over other, more important, colors, since colors with a very high representation (e.g., background) also increased their neighbor’s histogram value.</li><li><strong>Dithering simulation on patches</strong> — Seeing as our target is to use this palette for dithering, we could define the dithering error as our target function for the palette selection. The problem here is that we don’t want to dither the entire image for all possible color combinations, this would be very inefficient. Instead, we could randomly select a few small patches, dither them, calculate the dithering error vs. the original patches, and choose the palette which resulted in the overall lowest average error. This makes the search fairly quick although it’s brute-force.<br>4.1 <strong>RGBCMYKW</strong> — Use the 8 “corner” colors (red, green, blue, cyan, magenta, yellow, black, white) as our color dictionary, resulting in <strong><em>(8 choose k)</em></strong> color combinations.<br>4.2 <strong>Color dictionary</strong> — Use the representative color dictionary we defined for the histogram method. This has much more color combination possibilities (<strong><em>24 choose k</em></strong>), making it much slower in practice, but much more accurate.</li></ol><p>Finally, the method I arrived at was to take the “best of all worlds”:</p><ol><li>Define a representative color dictionary.</li><li>Resize the image to be max <strong><em>512 </em>× <em>512</em></strong>, for efficiency.</li><li>Calculate <strong><em>10 </em></strong>most common colors in the smoothed histogram (very quick), reducing the color dictionary size for the next step.</li><li>Simulate dithering error on small patches for all color combinations on the reduced color dictionary, and take the best one (much quicker than checking all combinations from the original dictionary).</li></ol><p>** In all steps, we fix black and white to the palette, since these provide the main details in the image.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WzmYj-HZxiqyrQ1lgNWLmw.jpeg" /><figcaption>Palette selection pipeline</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QPPCooxP7AEmm9K5qFeKOQ.jpeg" /><figcaption>Left to right: manual, clustering-based, RGBCMYKW patches dithering simulation, histogram-based, histogram + patches dithering simulation.</figcaption></figure><h3>Future Work</h3><p>I think this just about wraps up my work on this project for now, but here are a few directions I would love to explore in the future.</p><ul><li><strong>Better thread combination method</strong> — The two proposed methods (<em>interweave</em> and <em>combine</em>) work pretty fine, but I do believe that better results can be achieved here. The <em>important = last</em> approach might also be suboptimal.</li><li><strong>Deep Learning NN-based algorithm</strong> — Having a lot of background in the field of Deep Learning, as this is what I do in my day-job, I would really like to formalize the problem and solution as a neural network learning/optimization problem and give it a go sometime.</li><li><strong>Better Robustness</strong> — The aim of this project was for the algorithm to work automatically on <em>any</em> image. This was only partially achieved as, from my experience, the algorithm works better on single or double color palette images, and finds it harder to achieve good results on full color images. This leads me to…</li><li><strong>Get a deeper understanding on color blending</strong> — The way we simulate the multicolor String-Art makes assumptions on color blending which might miss how this actually works in real life. In my simulation, I assume natural RGB-space blending, but in real life occlusions are to be considered, and color blending is probably more subtle (or at least modelled in a different color space). Using a better, more accurate, understanding of the physical model could open the door to working with more diverse, in-the-wild images.</li><li><strong>Actually try to physically create one of the images</strong>, and see how it turns out in real life compared to the simulation results.</li></ul><p>I believe that this concludes the deep-dive into the process of developing my String-Art project. Although this has been a fairly long and thorough post, there are still some aspects of the project that I haven’t covered, such as how we turn this into a PDF with instructions for physically making the String-Art, how we convert a non-continuous list of lines to a continuous one, how we initialize the canvas, and some other minor improvements I made in the algorithm.</p><p>Once again, feel free to try the <a href="https://colab.research.google.com/github/roy-hachnochi/string-art/blob/main/algorithmic_string_art_playground.ipynb">Colab playground</a>, and take a look at the complete code in <a href="https://github.com/roy-hachnochi/string-art">GitHub</a>.</p><p>Special thanks to <a href="https://shira-baron.com/">Shira Bar On</a> for carefully reading initial drafts of this post and giving kind and thoughtful comments, and for being the first one aside of me to try out the algorithm on her own image.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MXNlGDBbGNfI6eY_oZr7-g.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qMdjFx3tH9xFmsP19WvOiA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zxF2fe7f2Ab4ind4l6wuSQ.png" /></figure><h3>References</h3><ul><li><a href="https://artof01.com/vrellis/works/knit.html"><strong>A New Way to Knit (2016)</strong></a><strong>, Petros Vrellis</strong> — The original String-Art project, which came up with this wonderful and creative idea, and inspired the work for everyone playing around with this problem.</li><li><a href="https://www.youtube.com/watch?v=WGccIFf6MF8&amp;t=17s"><strong>The Mathematics of StringArt</strong></a><strong>, Virtually Passed</strong> — A fun and well-described youtube video, which was the first interaction I got with this line of work and inspired me to try it myself.</li><li><a href="https://www.perfectlynormal.co.uk/blog-computational-thread-art"><strong>Computational String Art</strong></a><strong>, Callum Mcdougal</strong> — An excellent blogpost I found while working on the project, which gave me some very clever and innovative ideas for the multicolor part, as well as most of the example images used in this post.</li><li><strong>String Art: Towards Computational Fabrication of String Images, Birsak et. al.</strong> — The best (and almost only) academic article I found related to the String-Art problem.</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9f86084cf7b3" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Algorithmic String Art — B&W]]></title>
            <link>https://medium.com/@roy.hachnochi/algorithmic-string-art-b-w-a61fda614f2a?source=rss-63240837b082------2</link>
            <guid isPermaLink="false">https://medium.com/p/a61fda614f2a</guid>
            <dc:creator><![CDATA[Roy Hachnochi]]></dc:creator>
            <pubDate>Tue, 02 Sep 2025 06:00:28 GMT</pubDate>
            <atom:updated>2025-09-02T06:04:10.046Z</atom:updated>
            <content:encoded><![CDATA[<h3>Algorithmic String Art — B&amp;W</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/400/1*Scg0XuBCGipPspVQ13qT8w.gif" /></figure><p>String Art is a decorative craft where threads are stretched and woven across a set of fixed points on the perimeter of a canvas, gradually building up intricate patterns and images. What might appear at first as a purely artistic pursuit is deeply connected to mathematics: each line is chosen with precision, and together they approximate shapes, textures, and even photorealistic portraits. This interplay between art and algorithm was beautifully demonstrated in <a href="https://artof01.com/vrellis/works/knit.html">A New Way to Knit (2016)</a> by Petros Vrellis, whose pioneering work showed how a simple thread, looped thousands of times, can reconstruct an image with both elegance and surprising fidelity.</p><p>I took on myself a weekend project to explore this line of work, and try to create my own String Art algorithm. My vision was to make the algorithm as robust and automatic as possible, and although I can’t say this was fully achieved, I did learn a lot along the way, and there are still plenty more interesting ideas to implement towards this goal.</p><p>In this post I will try to deep dive into the full details of the project development process and thought process, including failed ideas, efficiency considerations, and carefully deriving the mathematical formulas required to solve this problem.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*r-fGmsmWeHSkvMofvZy9PA.jpeg" /><figcaption>Various close-up levels of one of Petros Vrellis’ original works — El Greco’s Christ, courtesy of his website.</figcaption></figure><h3>Setup and Simulation Model</h3><p>Our first course of action is to model the real-world String Art problem as a computer simulation. Doing this correctly is of vast importance, since the entire optimization process and rendering process are based on this.</p><h4>Single line simulation</h4><p>Each line may be represented by a pair of nails on the canvas perimeter, our main challenge is how we simulate a line given this nail-pair. My first attempt was to explicitly model a line equation and thread profile, for example:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2QuwHhnCiErWuPbgcJOUmw.png" /></figure><p>Where <strong><em>d(x,y;l)</em></strong> is the distance between pixel <strong><em>(x,y)</em></strong> and the line <strong><em>l</em></strong>, and <strong><em>t </em></strong>is the line thickness. This is very accurate and allows versatility in the line representation, but it’s quite slow, as we must perform this calculation for every pixel in the image, and for every nail-pair.</p><p>Therefore, I turned to implement the lines with a library function. Specifically I found skimage.draw.line_aa to be the fastest implementation that supports antialiasing lines, which is important since the real-world strings aren’t of constant intensity. This implementation also has the benefit of returning the image pixels which are included in the line, which will be important for efficient error calculation, since it will allow us to calculate the effect only on the relevant pixels.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/291/1*1QRgZz8OX5Res9DhtI5JBw.jpeg" /><figcaption>Left: Constant line. Right: Antialiasing line, better resembling real world threads.</figcaption></figure><h4>Canvas simulation</h4><p>At first glance, it might seem as though using the highest canvas resolution is the best choice, but in practice using a smaller image size (around <strong><em>600</em>×<em>600–900</em>×<em>900</em></strong>) is better. This has two main advantages:</p><ol><li><strong>Line simulation time</strong> — The less pixels there are to calculate, the less time it takes.</li><li><strong>Color blending</strong> — Using a low resolution means that a string takes less than a pixel (accurately, a string’s intensity through a pixel will be: <strong><em>I = string width [mm] </em>⋅ <em>canvas resolution [pixels] / canvas size [mm]</em></strong>). We haven’t gotten to the colored version of the work, but having <strong><em>I &lt;&lt; 1</em></strong> will come in handy when simulating color blending of different color strings passing through the same pixel, which closely approximates how the human eye perceives colors.</li></ol><p>However, for the final rendering we <em>will</em> use a bigger resolution, to simulate the real-world appearance of opaque overlapping strings, which don’t blend when looking up close. Using the same formula as above, we’ll set the canvas resolution so that the string intensity will turn out as <strong><em>1</em></strong> pixel.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/434/1*HjAmLv-mbCZucf7kbSZlLw.jpeg" /><figcaption>Left: High resolution canvas. Right: Using opaque strings on a low-resolution canvas doesn’t simulate the real world details.</figcaption></figure><h3>B&amp;W Algorithms</h3><p>Solving the black-and-white case is the core challenge. Multicolor optimization can either be reduced to, or built upon, a B&amp;W base. I examined several approaches for this algorithm.</p><blockquote><strong>A note on preprocessing:</strong><br>Since the strings are black and the canvas is white, the first thing we do<br>for each image is to negate it, making it&#39;s values <strong>1</strong> for black and <strong>0</strong> for<br>white. Now the optimization process will work by adding blacks to recreate<br>the image.</blockquote><h4>Naive greedy optimizer</h4><p>Although I called it “naive” due to it being the first method which comes to mind, it actually works surprisingly well! The greedy method works by minimizing the error for the current step only, and continuing from there. For our problem, this means that for each step we want to find the best line to add, i.e. the one which improves (minimizes) the error by the biggest factor. Putting this in mathematical terms, let <strong><em>x </em></strong>be the current String-Art result, <strong><em>y </em></strong>be the original image, and <strong><em>l </em></strong>be an image representing the line being checked, we want:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Du9GwSfdRpp2DViIjs86PA.png" /></figure><p>To make calculations a bit more efficient, we may subtract the previous error, which doesn’t effect the minimization because it doesn’t depend on <strong><em>l</em></strong>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3DLNdamlyYjivEb6xSryuw.png" /></figure><p>This allows us to efficiently calculate the error improvement only along the nonzero pixels of the line <strong><em>l</em></strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/250/1*cr6qNnAdnnztZM-Sozr8cA.jpeg" /><figcaption>Greedy optimizer</figcaption></figure><h4><strong>Linear Optimizer</strong></h4><p>I love rigorously formulating problems as optimization problems. I find it to be one of the most elegant ways to apply the full power of mathematics, and once we succeed in doing so, a vast range of optimization algorithms are suddenly added to our toolbox. So naturally, this is what I wanted to do for our String-Art problem. The go-to approach would be a linear model. It’s important to note that modelling the problem as a linear equation is an <strong>approximation</strong> (since in real world overlapping strings don’t add up, but just hide eachother), but it’s an approximation that works and helps as a relaxation of the problem to a solvable one.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*udBC7bJ3SwypV5hQnCGllA.png" /></figure><p>Where we have:</p><ul><li><strong><em>b</em> (<em>hw </em>× <em>1</em>) </strong>- The flattened image.</li><li><strong><em>x</em> (<em>n</em> × <em>1</em>) </strong>- A binary vector representing which lines (from all possible nail-pairs) are added to the String-Art solution.</li><li><strong><em>A</em> (<em>hw </em>×<em> n</em>) </strong>- The transformation matrix, which translates each line to it’s image representation, and sums them to the result image.</li></ul><p>We pre-calculate <strong><em>A</em></strong> by using the canvas simulation for each possible nail pair, flattening the result, and stacking them as columns in the matrix.</p><blockquote><strong>A note about efficiency</strong></blockquote><blockquote>The matrix <strong>A </strong>is a very big one, and therefore consumes a lot of memory, and calculations involving it are time-consuming. However, we may notice that most of it’s values are zeros (only the pixels related to the specific line in each column are nonzero), and choose to represent it as a <strong>sparse</strong> matrix (e.g., using scipy.sparse) , vastly improving both time and memory efficiency.</blockquote><p>Now that we have this representation of the problem as a linear equation, we may plug in various linear regressors. The problem here, is that the solution isn’t restricted to being binary (and not even restricted to being positive!). We may approximate the binary solution by setting some threshold, above which the values of <strong><em>x</em> </strong>will become <strong><em>1</em></strong>, and under which they will become <strong><em>0</em></strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/493/1*QPAk-sdrhefg52_DpVUjaQ.jpeg" /><figcaption>Left: Least Squares optimizer result. Right: After binarization.</figcaption></figure><p>Unsurprisingly, although the optimizer solution was near-perfect, the approximation is far from good, since the binarization process causes a divergence too big from the original non-binary solution.</p><p>So using Least-Squares optimization fails, but can this formulation help us develop something better, with just a little bit of hard work?</p><h4>Binary Linear optimizer</h4><p>Let’s try to combine the success of the greedy approach with the formulation of the linear model. What we want is a per-step update formula, derived from the minimization problem from equation <strong><em>(2)</em></strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*l4IQr7ictmpLU1SKSnzAbA.png" /></figure><p>Where we defined:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FeQOYFVF_acCBZ6nmj4lwQ.png" /></figure><p>Such that <strong><em>l</em>ₖ₊₁</strong><em> </em>is the next line number added to the solution, <strong><em>δₗ (n </em>× <em>1) </em></strong>is a one-hot vector with <strong><em>1</em></strong> in the index <strong><em>l</em></strong>, and <strong><em>A⁎ₗ (hw </em>× <em>1)</em> </strong>is the <strong><em>l</em></strong>-th column of <strong><em>A</em></strong>. And so, we arrive at a straight-forward update rule:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0w4OMQ2qNLMKUeDshkP_SQ.png" /></figure><p>Put in words, in each step we calculate equation <strong><em>(4)</em></strong> for all lines, find the best one, and update the residual error <strong><em>r</em>ₖ<em> </em></strong>after adding this line via equation <strong><em>(5)</em></strong>. If we implement these equations using sparse matrices and vectorized calculations, this process becomes <strong>extremely fast</strong>!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/250/1*ydf1SotGiSnWIKJECNaS6g.jpeg" /><figcaption>Binary Linear optimizer</figcaption></figure><p>The runtime improvement is also substantial. Compared on a <strong><em>300 </em>× <em>420</em></strong> image:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/875/1*7QR-SmKJEJrcZ9aqsC_PjQ.png" /></figure><h3>Algorithm Improvements</h3><p>There are a few natural improvements to add to this algorithm, making it work better and faster.</p><ol><li><strong>Continuous lines</strong> — All of the above algorithms could output any sequence of nail-pairs, in any order. To make the physical weaving process easier, we’d like for the algorithm to work with a single long thread, so we should demand that each line starts from the target nail of the previous line. This is done by limiting equation <strong><em>(4)</em> </strong>to check only such valid lines. One way to do it is to set <strong><em>e</em>ₖ₊₁<em>(l)=∞ </em></strong>for non-valid lines.</li><li><strong>Importance weights</strong> — In some images, where there is much detail in a specific region, we would like the algorithm to emphasize on these regions to capture those details. This may be done by adding a weight map <strong><em>W (hw </em>× <em>1) </em></strong>which gives higher weights to these regions. All it requires is a very simple manipulation (try developing the update formulas again yourself): <strong><em>A</em>ᵥᵥ<em>=A</em>⊙<em>W</em></strong>, where <strong>⊙ </strong>means element-wise multiplication of each column <strong><em>A </em></strong>of with <strong><em>W</em></strong>.</li><li><strong>Valid nails subset</strong> — We may make the algorithm even faster by limiting the number of nails we allow in each step. First, we don’t want a nail to connect to nails which are too close to it anyways, since the result will be perceptually insignificant. Second, we limit it even further by selecting a smaller <em>random</em> subset of nails in each step. This hardly affects the output, but significantly improves efficiency.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/518/1*ifz_1eZgtjTunuArLgYmDA.jpeg" /></figure><p>That wraps up the B&amp;W side of the project, laying the groundwork for everything. The real challenge, however, lies in extending these ideas to multicolor images. The next post will explore exactly how we can weave colored strings into the algorithm: <a href="https://medium.com/@roy.hachnochi/algorithmic-string-art-multicolor-9f86084cf7b3">Algorithmic String Art — Multicolor</a>.</p><p>Feel free to try the <a href="https://colab.research.google.com/github/roy-hachnochi/string-art/blob/main/algorithmic_string_art_playground.ipynb">Colab playground</a>, and take a look at the complete code in <a href="https://github.com/roy-hachnochi/string-art">GitHub</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LsLN5mwFaC3HPz0SUw6iTA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1QNe_2-IyHg6JKdzGWeopA.png" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a61fda614f2a" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>