0

I am currently using Rectangle in an attempt to fill an area under a curve with a single colour per rectangle. However the Rectanges are > 1 pixel wide. I want to draw lines 1 pixel wide so that they dont overlap. Currently the vertical rectangles under the curve overlap horizontally by 1 or two pixels.

def rect(x,y,w,h,c):
    ax = plt.gca()
    polygon = plt.Rectangle((x,y),w,h,color=c, antialiased=False)
    ax.add_patch(polygon)

def mask_fill(X,Y, fa, cmap='Set1'):
    plt.plot(X,Y,lw=0)  
    plt.xlim([X[0], X[-1]])
    plt.ylim([0, MAX])
    dx = X[1]-X[0]
    for n, (x,y, f) in enumerate(zip(X,Y, fa)):
        color = cmap(f)
        rect(x,0,dx,y,color)

If I use the code below to draw lines, the overlap is reduced but there is still an overlap

def vlines(x_pos, y1, y2, c):
    plt.vlines(x_pos, ymin=y1, ymax=y2, color=c)

def draw_lines(X, Y, trend_len, cmap='Blues_r']):
    plt.plot(X, Y, lw=0)  
    plt.xlim([X[0], X[-1]])
    plt.ylim([0, MAX])
    dx = X[1] - X[0]
    ydeltas = y_trend(Y, trend_len)
    for n, (x, y, yd) in enumerate(zip(X, Y, ydeltas)):
        color = cmap(y / MAX)
        vlines(x, y1=0, y2=y, c=color)

Printing the first 3 iterations of values of parameters into vlines we can see that x_pos is incrementing by 1 - yet the red line clearly overlaps the first blue line as per image below (NB first (LHS) blue line is 1 pixel wide):

x_pos: 0, y1: 0, y2: 143.51, c: (0.7816378316032295,     0.8622683583237216, 0.9389773164167627, 1.0)
x_pos: 1, y1: 0, y2: 112.79092811646952, c: (0.9872049211841599, 0.5313341022683583, 0.405843906189927, 1.0)
x_pos: 2, y1: 0, y2: 123.53185623293905, c: (0.9882352941176471, 0.6059669357939254, 0.4853671664744329, 1.0)

Sample data:

47.8668447889, 1
78.5668447889, 1
65.9768447889, 1
139.658525932, 2
123.749454049, 2
116.660382165, 3
127.771310282, 3
114.792238398, 3

The first column above corresponds the the y value of the series (x values just number of values, counting from 0) The second column corresponds to the class.

I am generating two images:

One with unique values per class (0-6), each a different colour (7 unique colours), with colour filled up the to the y value this will be used as a mask over data image below.

Second image (example shown) uses different colour maps for different class values (eg 0=Blues_r, 1=Reds_r etc) and the intensity of the colour is given by the value of y.

The code for calculating the colours is fine, but I just cant get matplotlib to plot vertical lines a sigle pixel wide.

enter image description here

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
proximacentauri
  • 1,749
  • 5
  • 25
  • 53
  • Why do you need the lines to be one pixel wide? Are you trying to completely fill the area under the curve? – wwii Dec 21 '19 at 03:50
  • Yes complete fill with single pixel width lines each with a specific colour. I added a test of using vlines, still get overlap – proximacentauri Dec 21 '19 at 03:54
  • [linewidth](https://matplotlib.org/api/_as_gen/matplotlib.lines.Line2D.html#matplotlib.lines.Line2D.set_linewidth) is a float, did you try something less than one? It seems like you might need to calculate the value based on dpi. – wwii Dec 21 '19 at 04:03
  • Are you looking for [`fill`](https://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.fill)? – Mad Physicist Dec 21 '19 at 04:13
  • Re: linewidth I tried a linewidth = 1/DPI but still plots most lines 2 pixels wide rather than 1 pixel wide (also turned antialised=False) – proximacentauri Dec 21 '19 at 04:17
  • Combine this question: https://stackoverflow.com/q/24976471/2988730 with this one: https://stackoverflow.com/q/29321835/2988730 – Mad Physicist Dec 21 '19 at 04:18
  • Create the colored image using a few data points and interpolation, then set the clip region to your envelope function. – Mad Physicist Dec 21 '19 at 04:18
  • Re: fill, no, that fills with one colour as far as I can tell, I want to potentially use a different colour for each x value – proximacentauri Dec 21 '19 at 04:19
  • Could you provide sample data? – Mad Physicist Dec 21 '19 at 04:19
  • Thanks. So no blending or gradients, just fills? Are the regions for a given category always contiguous? I.e., if you find the split points between regions, will that actually give you anything? – Mad Physicist Dec 21 '19 at 04:37
  • Overall aim is to classify classes of the series, which I am doing with a 2D CNN. Category values are contigous and each category is minimum n values. I am thinking the way to do this is just double the width of the images generated in matplotlib then shrink them in pillow/opencv – proximacentauri Dec 21 '19 at 04:52
  • Could you add @MadPhysicist to your comments so I get pinged in the future? Since wii left the first comment, he's the only one getting auto-pinged. – Mad Physicist Dec 21 '19 at 05:00
  • @proximacentauri. Would you be OK with a separate fill for each region, as long as they were contiguous? – Mad Physicist Dec 21 '19 at 05:02
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/204641/discussion-between-proximacentauri-and-mad-physicist). – proximacentauri Dec 21 '19 at 05:17

1 Answers1

1

Since your goal is not to create an interactive figure, and you are trying to manipulate columns of pixels, you can use numpy instead of matplotlib to generate the result.

Here is a function that will take in y and category arrays, and create an image that's as wide as y is long, with the specified height. Color scaling is done similarly to your solution, where y is divided by the max.

from matplotlib import pyplot as plt
import numpy as np

def draw_lines(y, category, filename, cmap='Set1', max=None, height=None):
    y = np.asanyarray(y).ravel()
    category = np.asanyarray(category).ravel()
    assert y.size == category.size
    if max is None:
        max = y.max()
    if height is None:
        height = int(np.ceil(max))
    if isinstance(cmap, str):
        cmap = plt.get_cmap(cmap)

    colors = cmap(category)
    colors[:, 3] = y / max
    colors = (255 * colors).astype(np.uint8)
    output = np.repeat(colors[None, ...], height, axis=0)

    heights = np.round(height * (y / max))
    mask = np.arange(height)[:, None] >= heights
    mask = np.broadcast_to(mask[::-1, :, None], output.shape)
    output[mask] = 0

    plt.imsave(filename, output)
    return output

The first part just sets up the input values. The second part gets the color values. Calling a colormap with an array of n values returns an (n, 4) array of colors in the range [0, 1.0]. colors[:, 3] = y / max sets the alpha channel proportional to the height. The colors are then smeared vertically to the desired height. The last part creates a mask to set the top part of each column to zero, according to the method proposed here.

This version uses transparency to turn off the colors, and to trim the shape. You can do the same thing with a white background, if you are willing to scale the colors instead of adjusting the transparency:

def draw_lines_b(y, category, filename, cmap='Set1', max=None, height=None):
    y = np.asanyarray(y).ravel()
    category = np.asanyarray(category).ravel()
    assert y.size == category.size
    if max is None:
        max = y.max()
    if height is None:
        height = int(np.ceil(max))
    if isinstance(cmap, str):
        cmap = plt.get_cmap(cmap)

    colors = cmap(category)
    colors[..., :3] *= (y / max)[..., None]
    colors = (255 * colors).astype(np.uint8)
    output = np.repeat(colors[None, ...], height, axis=0)

    heights = np.round(height * (y / max))
    mask = np.arange(height)[:, None] >= heights
    mask = np.broadcast_to(mask[::-1, :, None], output.shape)
    output[mask] = 255

    plt.imsave(filename, output)
    return output

In both cases, as you can imagine, matplotlib is not strictly necessary. You can define your own list of colors, and use a more appropriate library, such as PIL, to save the images.

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264