Message Boards Message Boards

Linear Features in Paintings

GROUPS:

Remington

We talk about paintings having line and direction. I made a short program to visualize linear features in an image. Here is what it does:

  • Resize the image.
  • Detect edges in the image.
  • Find lines among the edges.
  • Build a graphic showing the sum of the lines in various directions.
  • Show the longest line in the image.

Please see code below. I had to overcome a few obstacles to get this to work right. First, larger images (I tested up to 2500 by 2500 pixels) produced small artifacts during edge detection that muddied up the data, I think from the grain of the canvas, impasto highlights, cracks in the paint, etc. To solve this, I resize larger images to a maximum width of 480 pixels.

Paintings with indistinct edges can result in blank edge detection images. To solve this, I run the edge detection through a loop that tests to see whether there are enough white pixels (edges) to move on to the next step. If not, it lowers the edge detection threshold and tries again.

The ImageLines[] function returned lines along the borders of the images, so I filtered out lines that ran along the borders.

My first thought was to present the data in a polar coordinate plot. It worked, but the results didn't communicate what I had hoped. After some trial and error, I came up with a disk with 48 sectors each representing a bin of data. For instance, one bin would be the sum of the lines whose direction was between 0° and 7.5°, another bin would have the sum of lines between 7.5° and 15°, etc. (I used radians in the program.) The sectors have different radii based on the data. The graphic is symmetric because a line whose direction is 90°, say, also has a direction of 270°. On top of this multi-sectored disk, I show the direction of the longest continuous line that the program could find in the image.

Wide paintings produced exaggerated data in the horizontal, and tall paintings in the vertical, so I scaled each data bin according to its horizontal component.

In testing, I made a second graphic where I superimpose the longest line onto the image. This seemed to be a worthwhile bit of information, so I kept it. When I taught art, this would have been a useful starting point for a discussion about lines in the composition of a painting.

We sometimes use technology to analyze art. For example, we take x-rays of a painting, examine it with a raking light, and calculate its fractal dimension. All of these reveal something new about the piece.

The results are pretty cool. Here are some samples:

This Vermeer painting doesn't have prominent linear features or direction.

This Vermeer painting doesn't have prominent linear features or direction.

Escher

This Escher piece has strong lines in three distinct directions.

Remington

A good discussion starter: "Why does the computer think that this Remington painting has more directionality lower left to upper right?"

The failures are also interesting. The program didn't know what to make of a Jackson Pollock painting (who does?), I think because there are so few straight lines in his paintings. Roy Lichtenstein's technique of imitating printing dots causes the program to find way too many vertical lines for some reason.

I tried it on 35 different famous paintings and 35 photos of flowers. The results are pretty consistent with what I would expect.

This was a fun little project that pushed my Wolfram skills to their limits. If anyone in the community wants to use it or build on my code, feel free to do so. I wouldn't mind some feedback if you do.

Thanks for reading this longwinded post.

Mark Greenberg


CODE

img =;
If[ImageDimensions[img][[1]] > 480, img = ImageResize[img, 480]];
dims = ImageDimensions[img];

(* Section 2 forms an edge detection image. *)
thresh = .2;
whiteRat = 0;
While[whiteRat < .075,
  ed = EdgeDetect[img, 5, thresh, Method -> "ShenCastan"];
  whiteRat = Count[Flatten[ImageData[ed]], 1]/
   Count[Flatten[ImageData[ed]], 0];
  If[thresh > .005, thresh -= .005, Break[]]];

(* Section 3 forms a list of line segments not on the perimeter. *)

lines = Flatten[
   ImageLines[ed, 0, Method -> {"Segmented" -> True}, 
    MaxFeatures -> 240], 1];
limLt = dims[[1]]/20;
limRt = (19 dims[[1]])/20;
limTp = dims[[2]]/20;
limBt = (19 dims[[2]])/20;
lines = DeleteCases[
   lines, {{a_, b_}, {c_, 
      d_}} /; (a < limLt && c < limLt) || (a > limRt && 
       c > limRt) || (b < limTp && d < limTp) || (b > limBt && 
       d > limBt)];

(* Section 4 transforms each line segment into a {angle, length} \
pair. *)
angLen = {If[#[[2, 1]] == #[[1, 1]], \[Pi]/2, 
      ArcTan[(#[[2, 2]] - #[[1, 2]])/(#[[2, 1]] - #[[1, 1]])]], 
     Sqrt[(#[[2, 2]] - #[[1, 2]])^2 + (#[[2, 1]] - #[[1, 1]])^2]} & /@
    lines;

(* Section 5 finds the longest line. *)

longLine = First[Sort[angLen, #1[[2]] > #2[[2]] &]];
longLineEnds = First[lines[[First[Position[angLen, longLine]]]]];

(* Section 6 creates 24 bins for angles -90\[Degree] to 90\[Degree]. *)

binLen = {Round[#[[1]], \[Pi]/24], #[[2]]} & /@ angLen;
bins = DeleteDuplicates[#[[1]] & /@ binLen];

(* Section 7 records the total segment lengths in each bin. *)

tots = {};
For[i = 1, i <= Length[bins], i++, 
  tots = Append[tots, 
    Total[#[[2]] & /@ Cases[binLen, {bins[[i]], _Real}]]]];
data = {#[[1]], (Abs[Sin[#[[1]]]]*#[[2]]) + (Abs[Cos[#[[1]]]]*
        dims[[2]]/dims[[1]]*#[[2]])} & /@ 
   Partition[Riffle[bins, tots], 2];

(* Section 8 forms the graphic objects. *)

sectors = 
  Disk[{0, 0}, #[[2]], {#[[1]], #[[1]] + \[Pi]/24}] & /@ data;
mirrors = 
  Disk[{0, 0}, #[[
      2]], {#[[1]] + \[Pi], #[[1]] + \[Pi] + \[Pi]/24}] & /@ data;
arrowLen = .5*Max[#[[2]] & /@ sectors];

(* Section 9 assembles the sector and image graphics. *)

pic1 = Graphics[{
    Purple, Rotate[Join[mirrors, sectors], -(\[Pi]/48)],
    Yellow, Thick, Arrowheads[{-.03, .03}], 
    Rotate[Arrow[{{-arrowLen, 0}, {arrowLen, 0}}], longLine[[1]]]
    }, ImageSize -> {{480}, {540}}];
pic2 = Show[img, 
   Graphics[{Purple, Thick, Arrowheads[{-.03, .03}], 
     Translate[Arrow[longLineEnds], {-2, -2}], Yellow, 
     Translate[Arrow[longLineEnds], {2, 2}]}], 
   ImageSize -> {{480}, {540}}];

{pic1, pic2}
POSTED BY: Mark Greenberg
Answer
1 month ago

enter image description here - you have earned "Featured Contributor" badge, congratulations !

This is a great post and it has been selected for the curated Staff Picks group. Your profile is now distinguished by a "Featured Contributor" badge and displayed on the "Featured Contributor" board. We also slightly reformatted the post for potential promotion, but the content was left intact.

POSTED BY: Moderation Team
Answer
1 month ago

Very nice post! Thanks for sharing!

Another way of doing this is by using the GradientOrientationFilter:

{binspec,count}=HistogramList[Flatten@ImageData@GradientOrientationFilter[img,40,Method->{"DerivativeKernel"->"Gaussian"}],100];
data={MovingAverage[N@binspec,2]~Join~(Pi+MovingAverage[N@binspec,2]),count~Join~count};
ListPolarPlot[data//Transpose,PlotRange->All, AspectRatio -> Automatic,PolarAxes -> {True, False}]

with img being the painting by Remington, and 40 being a length scale, gives:

enter image description here

Another way would be to use DerivativeFilter, but I guess that will give a similar result.

POSTED BY: Sander Huisman
Answer
1 month ago

Excellent post and excellent response! - I was playing around with Radon, but in vain ...

POSTED BY: Henrik Schachner
Answer
1 month ago

Radon should also work I think, but might prefer lines that cross the entire image, rather than a local feature; not sure though... I was also very satisfied to see the work of two Dutch artists, one of which is Frisian :-)

POSTED BY: Sander Huisman
Answer
1 month ago

Group Abstract Group Abstract