Single line continuous Art

awesome, very very inspirational!
looks like you’re building the first layers of a neural network, I’m very curious to see any result you’d be willing to share in the future!
by the way, they told me voronoi is like lsd: not completely wrong, just don’t get addicted :wink:

@laurent_delrieu the Extreme Algorithms link you provided contains what will become my next 2 months of life! thank you so much!

all these are very valuable learning opportunities for who -like me- comes from a “not very computational” background!

No … I’m building the first layer of dollars (if that rather absurd project gets the green light - it shouldn’t due to elementary common sense). And fear not: I hate Voronoi so much that I barely can imagine doing any Voronoi thingy in the next 20 years.

BTW: All these are easy … the big thing is the knitting C# option (you know what I mean: connect pins in a circle with a single thread etc etc). Let’s say that we have 2000 circular “pins” in this plaza. For each location we test, say 1500 candidate lines and sort them according their pixel/pts/whatever match score [doing this randomly makes no difference at all since we MUST pick the best match line]. So for 2000 iterations we have around 4M Method calls (where Method is the thing that evaluates the line and returns the score). If the Method takes 0.1ms then the Elapsed could be around the 4K ms mark (out of question). So the challenge is a Method that cuts the mustard in ~0.01 ms (400 ms) or less. Implementing a thread safe // lines evaluation (per pin) and using a proper 32 core I9 K … well … one could hope for 50 to 100 ms (cross fingers).

BTW: BEFORE spending 2 months on this (or that) read this 100 times:

Moral: life sucks

1 Like

to stay on the safe side I have just bought the whole book :slight_smile:
pretty sure -after having methabolized the first TWENTY chapters- the threading part will be much more understandeable to me :+1:

You mean the Dark Side I guess. But remember: power corrupts while absolute power corrupts absolutely > meaning that the more things you can do the less things you should do (viewed this way the whole parametric thingy in AEC [with regard topology/geometry matters] … er … hmm … you guess my message I do hope).

In the mean time I have a challenge for you: the notorious Image knitting thingy in a couple of milliseconds. Impossible you may think (people in Internet talk about big times blah blah).

So … I could provide an indicative approach on the candidate line score issue that (on a pathetic I5) takes ~0.5 milliseconds. Obviously no // is possible for that part (but a thread safe // is critical for evaluating every candidate).

Your goal would be to do some tweeks here and there (every if counts) in order to achieve an elapsed time “around” the 0.01 millisecond threshold (on some I9 K) and then … implement the Method in the main knitting algo (but that’s the easy part). This means that solving any problem is nothing … but solving it fast is everything.

For instance (irrelevant with the actual challenge) you can achive ~5-10 faster times just by creating your nodes (knitting pins) the proper way:

On the captured “tutorial” there’s - on purpose - 6 topics that require some “equivalent” with the above division proper approach … so the 0.01 ms mark is entirely realistic.

Notify if you feel brave enough.

1 Like

ohhh nice, doing my best to follow, super interesting!!

pins are generated through a loop => from the center of the bp[…] array (called BoxCent) at vx distance (half diagonal of the bp[…] bounding rectangle + offset value) you put the first Node[0] then vector vx is rotated by angle “a” around vector vz (= Z) in such a way vx is ready for next loop

This is the interesting thing (become the latest of my Interview cases/tests: find and fix 6 bottlenecks): (129.1 KB)

On first sight it does nothing special. But if properly implemented in some knitting algo and with the right thread safe // approach it can cut the mustard in milliseconds instead of milliyears:

I hear you: so what? who cares about image knitting other some freak artists? Well … indeed but on the other hand cases like these (i.e. a simple thing that executes a zillion times) can sharpen your “kill the elapsed time” skills … and when a real-life complex AEC case occurs … blah, blah.

And always remember:

Hallo guys,
thanks a lot for taking part and adding more to the topic. I really learnt a lot. some of the concepts i need to still dwell deep and explore.

That said, I am looking to add a new feature to the continuous line art script we developed together. Is it possible to add varying linewidth to the line( i.e the line gets thicker at areas where the image is darker and the line weight is thinner in brighter areas?

Is this possible ?

wow! yeah
ehm… will study this in deep and get back with my findings :slight_smile:

I’m not aware of the possibility to change the thickness of a single continuous curve along the curve itself, but for sure the curve can be split in smaller parts, and each of them can get a different thickness (11.8 KB)

Human plugin is awesome and lets you do this:

1 Like

You guys rock!

For that you’ll need a TSP approach. Here’s why:

Assume that using some filter (shown brightness) and some band (lower or upper VS a threshold value) you get your points where each one is assosiated with a value (due to the filter). If you sort the values you can have a preview of the prox or TSP polyline in terms of circles with (obviously) variable radii. Say like these:

BTW: as an option you can add random points inside the circles (pts N: (int) Math.Round(R/Rmin,1) and attempt to have some sort of … er … hmm … who knows what (and why).

But if you solve the case using IP (i.e. iterative proximity with an added condition: the pt value [from the Image sampler]) … well … the thickness has no meaning since due to the nature of the algo … “big” connecting/crossing lines are more than expected (that could ruin the result for obvious reasons). So the solution is a special TSP (or a special Assignment solution or a variation of Knitting or a Knitting “like” solution without the pin to pin restriction [ that’s rather the best for the occasion] ).

Ah super thanks inno a lot this is what i am looking for.

Seems really interesting i will try it out… thanks a lot peter. :wink:

Get first a fast TSP (Hungarian, Genetic or classic) and do your tests … or rather way better use a fast (NOT O(N!)) Hamiltonian Loop algo like this (shown an abstract implementation on a Mesh treated as Graph):


I’ve made a little component that can help you achieve what you’re looking for:

SingleLineArt.gha (11 KB) (8.0 KB)
Maryam.jpg (43.0 KB)

Sourse Code:

namespace SingleLineArt
    public class SingleLineArtGhc : GH_Component
        private GH_MemoryBitmap _bmpMemory;
        private Transform _scale;
        private Transform _orient;
        private Rectangle3d _rec;
        private Point3dList _pts;
        public SingleLineArtGhc()
          : base("SingleLineArt", "SingleLineArt", "SingleLineArt", "Extra", "SingleLineArt") { }
        protected override void RegisterInputParams(GH_InputParamManager pManager)
            pManager.AddBooleanParameter("Reset", "Reset", "Reset", GH_ParamAccess.item);
            var paramFilePath = new Param_FilePath
                FileFilter = "All image files|*.bmp;*.jpg;*.jpeg;*.png;*.tif;*.tiff",
                ExpireOnFileEvent = true
            pManager.AddParameter(paramFilePath, "File", "File", "Location of image file", GH_ParamAccess.item);
            pManager.AddRectangleParameter("Rectangle", "Rectangle", "Rectangle", GH_ParamAccess.item);
            pManager.AddCurveParameter("Curve", "Curve", "Starting Curve", GH_ParamAccess.item);
            pManager.AddIntegerParameter("Start", "Start", "Number of starting circles", GH_ParamAccess.item);
            pManager.AddIntegerParameter("End", "End", "Maximum number of circles", GH_ParamAccess.item);
            pManager.AddNumberParameter("Min", "Min", "Smallest Circle", GH_ParamAccess.item);
            pManager.AddNumberParameter("Max", "Max", "Largest Circle", GH_ParamAccess.item);
            pManager[0].Optional = true;
        protected override void RegisterOutputParams(GH_OutputParamManager pManager)
            pManager.AddPointParameter("Points", "Points", "Points", GH_ParamAccess.list);
            pManager.AddNumberParameter("Radiuses", "Radiuses", "Radiuses", GH_ParamAccess.list);
        protected override void SolveInstance(IGH_DataAccess DA)
            var reset = false;
            DA.GetData(0, ref reset);
            if (reset || _pts is null)
                string path = null;
                if (!DA.GetData(1, ref path) || !File.Exists(path))
                    AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Image file does not exist");

                DA.GetData(2, ref _rec);
                var bmp = new Bitmap(path);
                var imgRec = new Rectangle3d(Plane.WorldXY, bmp.Width, bmp.Height);

                _bmpMemory = new GH_MemoryBitmap(bmp, WrapMode.Clamp);

                var plane1 = _rec.Plane;
                var plane2 = imgRec.Plane;
                plane1.Origin = _rec.Center;
                plane2.Origin = imgRec.Center;
                _scale = Transform.Scale(plane1, imgRec.X.Length / _rec.X.Length, -imgRec.Y.Length / _rec.Y.Length, 1.0);
                _orient = Transform.PlaneToPlane(plane1, plane2);
                Curve curve = null;
                DA.GetData(3, ref curve);
                var start = 0;
                DA.GetData(4, ref start);
                curve.DivideByCount(start, true, out var pts);
                _pts = new Point3dList(pts);
            var rads = new double[_pts.Count];
            var min = double.NaN;
            var max = double.NaN;
            DA.GetData(6, ref min);
            DA.GetData(7, ref max);
            Parallel.For(0, _pts.Count, i =>
                var pt = _pts[i];
                var red = Convert.ToDouble(_bmpMemory.R(Convert.ToInt32(pt.X), Convert.ToInt32(pt.Y))) / 255;
                rads[i] = red * (max - min) + min;
            var counts = new int[_pts.Count];
            var vecs = new Vector3d[_pts.Count];
            var rTree = RTree.CreateFromPointArray(_pts);
            var r = rads.Max() * 2;
            var indices = new List<int>[_pts.Count];
            Parallel.For(0, _pts.Count, i =>
                indices[i] = new List<int>();
                rTree.Search(new Sphere(_pts[i], r), (sender, args) =>
                    if (args.Id > i) indices[i].Add(args.Id);
            Parallel.For(0, _pts.Count, (i) =>
                foreach (var j in indices[i])
                    var vector = _pts[i] - _pts[j];
                    var d2 = vector.SquareLength;
                    if (d2 > Math.Pow(rads[i] + rads[j], 2)) continue;
                    vector *= (rads[i] + rads[j] - Math.Sqrt(d2)) * 0.5;
                    vecs[i] += vector;
                    vecs[j] -= vector;
            Parallel.For(0, _pts.Count, i =>
                if (counts[i] == 0) return;
                vecs[i] /= counts[i];
                if (Math.Min(_pts[i].X, _rec.Width - _pts[i].X) < rads[i]) vecs[i].X = 0;
                if (Math.Min(_pts[i].Y, _rec.Height - _pts[i].Y) < rads[i]) vecs[i].Y = 0;
                _pts[i] += vecs[i];
            DA.SetDataList(0, _pts);
            DA.SetDataList(1, rads);
            var end = 0;
            DA.GetData(5, ref end);
            if (_pts.Count > end) return;
            var newPts = new List<Point3d>();
            var newIndices = new List<int>();
            for (var i = 0; i < _pts.Count - 1; i++)
                var vec = _pts[i] - _pts[i + 1];
                if (vec.SquareLength < Math.Pow((rads[i] + rads[i + 1]), 2)) continue;
                newIndices.Add(i + 1 + newIndices.Count);
                newPts.Add((_pts[i] + _pts[i + 1]) * 0.5);
            for (var i = 0; i < newIndices.Count; i++)
                _pts.Insert(newIndices[i], newPts[i]);
        protected override Bitmap Icon => null;
        public override Guid ComponentGuid => new Guid("83b6bf0b-0312-4184-935c-b48d952f65bd");
} (33.0 KB)


Hello mister wizard Mahdiyar,
Is it possible at all that you can tweak the component to fit a not updated version of rhino6?
Cant run the component because of it being built on sdk 7.3… (?)
Would love to use the component for a picture of a special person…

Thanks so much!

@Mahdiyar thanks for sharing the code. I begin to study it and it is very interesting. I will surely try to make it in 3d applied on a mesh. Did you try in 3D?


I manage to make it on 3d with mesh. Each vertex of the mesh handle a radius. So it could be possible to put an image through the Import Image. The borders are handled through extra points that doesn’t move. Still work to do.

One layer

2 layers


Fun (continuous) thread all :slight_smile:

Kangaroo’s ImageCircles component takes a coloured mesh as input, which can be something 3 dimensional, so can be used for this: (116.8 KB)

This one starts with all the points existing from the beginning, and gradually increases the radius multiplier. More efficient is to start with only a few points and divide curves when they get above a certain length, but that currently needs to be scripted like shown in this example.
I can make another example showing how to combine the image based sizing with this iterative line splitting if it would be of interest.

In a different direction, I also played a little recently with another way of making shaded images from a continuous line, but by adjusting tension in a grid-

I’ll post the script for this one too soon.


Daniel, you have always tons of new really cool stuff. It is funny to see that there are more viewers/likers on Tweeter than on this forum.

I continued to play with Differential growth using multiple closed curves. It could be used to make single skinned or double skinned things.