Finding the Interface of two regions of a segmented image - python

I have a segmented (by watershed) image of two regions that share one boundary. How do I easily find the position of the pixels on the interface? I tried using hints from this answer but could not get it working. Here is my example code:
import matplotlib.pyplot as plt
from scipy import ndimage as ndi
from skimage.segmentation import watershed
from skimage.feature import peak_local_max
from skimage import future
from skimage.measure import label, regionprops, regionprops_table
# Generate an initial image with two overlapping circles
x, y = np.indices((80, 80))
x1, y1, x2, y2 = 28, 28, 44, 52
r1, r2 = 16, 20
mask_circle1 = (x - x1)**2 + (y - y1)**2 < r1**2
mask_circle2 = (x - x2)**2 + (y - y2)**2 < r2**2
image = np.logical_or(mask_circle1, mask_circle2)
# Now we want to separate the two objects in image
# Generate the markers as local maxima of the distance to the background
distance = ndi.distance_transform_edt(image)
coords = peak_local_max(distance, footprint=np.ones((3, 3)), labels=image)
mask = np.zeros(distance.shape, dtype=bool)
mask[tuple(coords.T)] = True
markers, _ = ndi.label(mask)
labels = watershed(-distance, markers, mask=image)
fig, axes = plt.subplots(ncols=3, figsize=(9, 3), sharex=True, sharey=True)
ax = axes.ravel()
ax[0].imshow(image, cmap=plt.cm.gray)
ax[0].set_title('Overlapping objects')
ax[1].imshow(-distance, cmap=plt.cm.gray)
ax[1].set_title('Distances')
ax[2].imshow(labels, cmap=plt.cm.nipy_spectral)
ax[2].set_title('Separated objects')
for a in ax:
a.set_axis_off()
fig.tight_layout()
plt.show()
#---------------- find the interface pixels (either of the two interfaces) of these two objects -----------
rag = future.graph.RAG(labels)
rag.remove_node(0)
for region in regionprops(labels):
nlist=list(rag.neighbors(region.label))
print(nlist)
The nlist seems to be just a list containing one element 1: [1]. I was expecting position of pixels.
I do not have much experience in using the graph and RAG. It seems that rag creates a graph/network of the regions and has the information of which region is next to which one but I cannot extract that information in the form of the interface pixels. Thanks for any help.

Currently the RAG object doesn't keep track of all the regions and boundaries, though we hope to support that in the future. What you found is just the list of adjacent regions.
For now, if you only have two regions, it's not too expensive to do this manually:
from skimage.morphology import dilation
label1 = labels == 1
label2 = labels == 2
boundary = dilation(label1) & dilation(label2)

Related

Is there a way to improve the line quality when exporting streamplots from matplotlib?

I am drawing streamplots using matplotlib, and exporting them to a vector format. However, I find the streamlines are exported as a series of separate lines - not joined objects. This has the effect of reducing the quality of the image, and making for an unwieldy file for further manipulation. An example; the following images are of a pdf generated by exportfig and viewed in Acrobat Reader:
This is the entire plot
and this is a zoom of the center.
Interestingly, the length of these short line segments is affected by 'density' - increasing the density decreases the length of the lines. I get the same behavior whether exporting to svg, pdf or eps.
Is there a way to get a streamplot to export streamlines as a single object, preferably as a curved line?
MWE
import matplotlib.pyplot as plt
import numpy as np
square_size = 101
x = np.linspace(-1,1,square_size)
y = np.linspace(-1,1,square_size)
u, v = np.meshgrid(-x,y)
fig, axis = plt.subplots(1, figsize = (4,3))
axis.streamplot(x,y,u,v)
fig.savefig('YourDirHere\\test.pdf')
In the end, it seemed like the best solution was to extract the lines from the streamplot object, and plot them using axis.plot. The lines are stored as individual segments with no clue as to which line they belong, so it is necessary to stitch them together into continuous lines.
Code follows:
import matplotlib.pyplot as plt
import numpy as np
def extract_streamlines(sl):
# empty list for extracted lines, flag
new_lines = []
for line in sl:
#ignore zero length lines
if np.array_equiv(line[0],line[1]):
continue
ap_flag = 1
for new_line in new_lines:
#append the line segment to either start or end of exiting lines, if either the star or end of the segment is close.
if np.allclose(line[0],new_line[-1]):
new_line.append(list(line[1]))
ap_flag = 0
break
elif np.allclose(line[1],new_line[-1]):
new_line.append(list(line[0]))
ap_flag = 0
break
elif np.allclose(line[0],new_line[0]):
new_line.insert(0,list(line[1]))
ap_flag = 0
break
elif np.allclose(line[1],new_line[0]):
new_line.insert(0,list(line[0]))
ap_flag = 0
break
# otherwise start a new line
if ap_flag:
new_lines.append(line.tolist())
return [np.array(line) for line in new_lines]
square_size = 101
x = np.linspace(-1,1,square_size)
y = np.linspace(-1,1,square_size)
u, v = np.meshgrid(-x,y)
fig_stream, axis_stream = plt.subplots(1, figsize = (4,3))
stream = axis_stream.streamplot(x,y,u,v)
np_new_lines = extract_streamlines(stream.lines.get_segments())
fig, axis = plt.subplots(1, figsize = (4,4))
for line in np_new_lines:
axis.plot(line[:,0], line[:,1])
fig.savefig('YourDirHere\\test.pdf')
A quick solution to this issue is to change the default cap styles of those tiny segments drawn by the streamplot function. In order to do this, follow the below steps.
Extract all the segments from the stream plot.
Bundle these segments through LineCollection function.
Set the collection's cap style to round.
Set the collection's zorder value smaller than the stream plot's default 2. If it is higher than the default value, the arrows of the stream plot will be overdrawn by the lines of the new collection.
Add the collection to the figure.
The solution of the example code is presented below.
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.collections import LineCollection # Import LineCollection function.
square_size = 101
x = np.linspace(-1,1,square_size)
y = np.linspace(-1,1,square_size)
u, v = np.meshgrid(-x,y)
fig, axis = plt.subplots(1, figsize = (4,3))
strm = axis.streamplot(x,y,u,v)
# Extract all the segments from streamplot.
strm_seg = strm.lines.get_segments()
# Bundle segments with round capstyle. The `zorder` value should be less than 2 to not
# overlap streamplot's arrows.
lc = LineCollection(strm_seg, zorder=1.9, capstyle='round')
# Add the bundled segment to the subplot.
axis.add_collection(lc)
fig.savefig('streamline.pdf')
Additionally, if you want to have streamlines their line widths changing throughout the graph, you have to extract them and append this information to LineCollection.
strm_lw = strm.lines.get_linewidths()
lc = LineCollection(strm_seg, zorder=1.9, capstyle='round', linewidths=strm_lw)
Sadly, the implementation of a color map is not as straight as the above solution. Therefore, using a color map with above approach will not be very pleasing. You can still automate the coloring process, as shown below.
strm_col = strm.lines.get_color()
lc = LineCollection(strm_seg, zorder=1.9, capstyle='round', color=strm_col)
Lastly, I opened a pull request to change the default capstyle option in the matplotlib repository, it can be seen here. You can apply this commit using below code too. If you prefer to do so, you do not need any tricks explained above.
diff --git a/lib/matplotlib/streamplot.py b/lib/matplotlib/streamplot.py
index 95ce56a512..0229ae107c 100644
--- a/lib/matplotlib/streamplot.py
+++ b/lib/matplotlib/streamplot.py
## -222,7 +222,7 ## def streamplot(axes, x, y, u, v, density=1, linewidth=None, color=None,
arrows.append(p)
lc = mcollections.LineCollection(
- streamlines, transform=transform, **line_kw)
+ streamlines, transform=transform, **line_kw, capstyle='round')
lc.sticky_edges.x[:] = [grid.x_origin, grid.x_origin + grid.width]
lc.sticky_edges.y[:] = [grid.y_origin, grid.y_origin + grid.height]
if use_multicolor_lines:

Cutting a patch around a segment of a segmented image

I have an segmented image into superpixels as follows:
from skimage.data import astronaut
img = astronaut()
segments_slic = slic(img, n_segments=250, compactness=10, sigma=1,
start_label=1)
fig = plt.figure(figsize = (16,8));
plt.imshow(mark_boundaries(img, segments_slic))
And got the following image:
I wish to cut a patch around each superpixel. Consider, for example, the patch around the shining part of the helmet colored red:
If I want to take a close (manual) look at the segments using plt.imshow(segments_slic[425:459,346:371]), I get this patch around the segment:
The pixels with the specific superpixel labe streach on row 425:459 and on columns 346:371.
Currently, I am doing this:
patches = list()
for superpixel in np.unique(segments_slic ):
x_min = np.min(np.where(segments == 15)[0]);
x_max = np.max(np.where(segments == 15)[0]);
y_min = np.min(np.where(segments == 15)[1]);
y_max = np.max(np.where(segments == 15)[1]);
patches.append(I[x_min:x_max,y_min:y_max,:]);
Not sure if it is correct, though it seems to be fine. What is the best way to generate such a patch for each superpixel? Moreover, is it possible to set the pixels in the patch, which do not belong to the superpixel, to black?
You can use regionprops and access the patch coordinates via region.bbox as in
from skimage.data import astronaut
import matplotlib.pyplot as plt
from skimage.segmentation import slic
from skimage.segmentation import mark_boundaries
from skimage.measure import regionprops
import matplotlib.patches as mpatches
img = astronaut()
segments_slic = slic(img, n_segments=250, compactness=10, sigma=1, start_label=1)
fig, ax = plt.subplots(figsize=(16, 8))
ax.imshow(img)
for region in regionprops(segments_slic):
# draw rectangle around segmented coins
minr, minc, maxr, maxc = region.bbox
rect = mpatches.Rectangle((minc, minr), maxc - minc, maxr - minr,
fill=False, edgecolor='red', linewidth=2)
ax.add_patch(rect)
# access patch via img[minr:maxr, minc:maxc]
plt.imshow(mark_boundaries(img, segments_slic))
plt.show()
This results in
Example adapted from here.
EDIT: Furthermore, with region.image you get a mask of your region to set the others to black.

Plot the timeframe of each unique sound loop in a song, with rows sorted by sound similarity using python Librosa

Background
Here's a video of a song clip from an electronic song. At the beginning of the video, the song plays at full speed. When you slow down the song you can hear all the unique sounds that the song uses. Some of these sounds repeat.
Mp3, Wav and MIDI of the audio in the video
Problem Description
What I am trying to do is create a visual like below, where a horizontal track/row is created for each unique sound, with a colored block on that track that corresponds to each timeframe in the song that sound is playing. The tracks/rows should be sorted by how similar the sounds are to each, with more similar sounds being closer together. If sounds are so identical a human couldn't tell them apart, they should be considered the same sound.
I'll accept an imperfect solution if it can generally do what I'm asking
Watch the video linked above for a video description of what I am saying. It includes a visual grid that I created manually which almost matches the grid I am trying to produce.
If for example, each of the 5 waves below represents the soundwave that a sound makes, each of these sounds would be considered similar, and would be placed close to each other vertically on the grid.
Attempts
I've been looking at an example for Laplacian segmentation in librosa. The graph labelled structure components looks like it might be what I need. From reading the paper, it looks like they are trying to break the song into segments like chorus, verse, bridge... but I am essentially trying to break the song into 1 or 2 beat fragments.
Here is the code for the Laplacian Segmentation (there's a Jupyter Notebook as well if you would prefer that).
# -*- coding: utf-8 -*-
"""
======================
Laplacian segmentation
======================
This notebook implements the laplacian segmentation method of
`McFee and Ellis, 2014 <http://bmcfee.github.io/papers/ismir2014_spectral.pdf>`_,
with a couple of minor stability improvements.
Throughout the example, we will refer to equations in the paper by number, so it will be
helpful to read along.
"""
# Code source: Brian McFee
# License: ISC
###################################
# Imports
# - numpy for basic functionality
# - scipy for graph Laplacian
# - matplotlib for visualization
# - sklearn.cluster for K-Means
#
import numpy as np
import scipy
import matplotlib.pyplot as plt
import sklearn.cluster
import librosa
import librosa.display
import matplotlib.patches as patches
#############################
# First, we'll load in a song
def laplacianSegmentation(fileName):
y, sr = librosa.load(librosa.ex('fishin'))
##############################################
# Next, we'll compute and plot a log-power CQT
BINS_PER_OCTAVE = 12 * 3
N_OCTAVES = 7
C = librosa.amplitude_to_db(np.abs(librosa.cqt(y=y, sr=sr,
bins_per_octave=BINS_PER_OCTAVE,
n_bins=N_OCTAVES * BINS_PER_OCTAVE)),
ref=np.max)
fig, ax = plt.subplots()
librosa.display.specshow(C, y_axis='cqt_hz', sr=sr,
bins_per_octave=BINS_PER_OCTAVE,
x_axis='time', ax=ax)
##########################################################
# To reduce dimensionality, we'll beat-synchronous the CQT
tempo, beats = librosa.beat.beat_track(y=y, sr=sr, trim=False)
Csync = librosa.util.sync(C, beats, aggregate=np.median)
# For plotting purposes, we'll need the timing of the beats
# we fix_frames to include non-beat frames 0 and C.shape[1] (final frame)
beat_times = librosa.frames_to_time(librosa.util.fix_frames(beats,
x_min=0,
x_max=C.shape[1]),
sr=sr)
fig, ax = plt.subplots()
librosa.display.specshow(Csync, bins_per_octave=12*3,
y_axis='cqt_hz', x_axis='time',
x_coords=beat_times, ax=ax)
#####################################################################
# Let's build a weighted recurrence matrix using beat-synchronous CQT
# (Equation 1)
# width=3 prevents links within the same bar
# mode='affinity' here implements S_rep (after Eq. 8)
R = librosa.segment.recurrence_matrix(Csync, width=3, mode='affinity',
sym=True)
# Enhance diagonals with a median filter (Equation 2)
df = librosa.segment.timelag_filter(scipy.ndimage.median_filter)
Rf = df(R, size=(1, 7))
###################################################################
# Now let's build the sequence matrix (S_loc) using mfcc-similarity
#
# :math:`R_\text{path}[i, i\pm 1] = \exp(-\|C_i - C_{i\pm 1}\|^2 / \sigma^2)`
#
# Here, we take :math:`\sigma` to be the median distance between successive beats.
#
mfcc = librosa.feature.mfcc(y=y, sr=sr)
Msync = librosa.util.sync(mfcc, beats)
path_distance = np.sum(np.diff(Msync, axis=1)**2, axis=0)
sigma = np.median(path_distance)
path_sim = np.exp(-path_distance / sigma)
R_path = np.diag(path_sim, k=1) + np.diag(path_sim, k=-1)
##########################################################
# And compute the balanced combination (Equations 6, 7, 9)
deg_path = np.sum(R_path, axis=1)
deg_rec = np.sum(Rf, axis=1)
mu = deg_path.dot(deg_path + deg_rec) / np.sum((deg_path + deg_rec)**2)
A = mu * Rf + (1 - mu) * R_path
###########################################################
# Plot the resulting graphs (Figure 1, left and center)
fig, ax = plt.subplots(ncols=3, sharex=True, sharey=True, figsize=(10, 4))
librosa.display.specshow(Rf, cmap='inferno_r', y_axis='time', x_axis='s',
y_coords=beat_times, x_coords=beat_times, ax=ax[0])
ax[0].set(title='Recurrence similarity')
ax[0].label_outer()
librosa.display.specshow(R_path, cmap='inferno_r', y_axis='time', x_axis='s',
y_coords=beat_times, x_coords=beat_times, ax=ax[1])
ax[1].set(title='Path similarity')
ax[1].label_outer()
librosa.display.specshow(A, cmap='inferno_r', y_axis='time', x_axis='s',
y_coords=beat_times, x_coords=beat_times, ax=ax[2])
ax[2].set(title='Combined graph')
ax[2].label_outer()
#####################################################
# Now let's compute the normalized Laplacian (Eq. 10)
L = scipy.sparse.csgraph.laplacian(A, normed=True)
# and its spectral decomposition
evals, evecs = scipy.linalg.eigh(L)
# We can clean this up further with a median filter.
# This can help smooth over small discontinuities
evecs = scipy.ndimage.median_filter(evecs, size=(9, 1))
# cumulative normalization is needed for symmetric normalize laplacian eigenvectors
Cnorm = np.cumsum(evecs**2, axis=1)**0.5
# If we want k clusters, use the first k normalized eigenvectors.
# Fun exercise: see how the segmentation changes as you vary k
k = 5
X = evecs[:, :k] / Cnorm[:, k-1:k]
# Plot the resulting representation (Figure 1, center and right)
fig, ax = plt.subplots(ncols=2, sharey=True, figsize=(10, 5))
librosa.display.specshow(Rf, cmap='inferno_r', y_axis='time', x_axis='time',
y_coords=beat_times, x_coords=beat_times, ax=ax[1])
ax[1].set(title='Recurrence similarity')
ax[1].label_outer()
librosa.display.specshow(X,
y_axis='time',
y_coords=beat_times, ax=ax[0])
ax[0].set(title='Structure components')
#############################################################
# Let's use these k components to cluster beats into segments
# (Algorithm 1)
KM = sklearn.cluster.KMeans(n_clusters=k)
seg_ids = KM.fit_predict(X)
# and plot the results
fig, ax = plt.subplots(ncols=3, sharey=True, figsize=(10, 4))
colors = plt.get_cmap('Paired', k)
librosa.display.specshow(Rf, cmap='inferno_r', y_axis='time',
y_coords=beat_times, ax=ax[1])
ax[1].set(title='Recurrence matrix')
ax[1].label_outer()
librosa.display.specshow(X,
y_axis='time',
y_coords=beat_times, ax=ax[0])
ax[0].set(title='Structure components')
img = librosa.display.specshow(np.atleast_2d(seg_ids).T, cmap=colors,
y_axis='time', y_coords=beat_times, ax=ax[2])
ax[2].set(title='Estimated segments')
ax[2].label_outer()
fig.colorbar(img, ax=[ax[2]], ticks=range(k))
###############################################################
# Locate segment boundaries from the label sequence
bound_beats = 1 + np.flatnonzero(seg_ids[:-1] != seg_ids[1:])
# Count beat 0 as a boundary
bound_beats = librosa.util.fix_frames(bound_beats, x_min=0)
# Compute the segment label for each boundary
bound_segs = list(seg_ids[bound_beats])
# Convert beat indices to frames
bound_frames = beats[bound_beats]
# Make sure we cover to the end of the track
bound_frames = librosa.util.fix_frames(bound_frames,
x_min=None,
x_max=C.shape[1]-1)
###################################################
# And plot the final segmentation over original CQT
# sphinx_gallery_thumbnail_number = 5
bound_times = librosa.frames_to_time(bound_frames)
freqs = librosa.cqt_frequencies(n_bins=C.shape[0],
fmin=librosa.note_to_hz('C1'),
bins_per_octave=BINS_PER_OCTAVE)
fig, ax = plt.subplots()
librosa.display.specshow(C, y_axis='cqt_hz', sr=sr,
bins_per_octave=BINS_PER_OCTAVE,
x_axis='time', ax=ax)
for interval, label in zip(zip(bound_times, bound_times[1:]), bound_segs):
ax.add_patch(patches.Rectangle((interval[0], freqs[0]),
interval[1] - interval[0],
freqs[-1],
facecolor=colors(label),
alpha=0.50))
One major thing that I believe would have to change would be the number of clusters, they have 5 in the example, but I don't know what I would want it to be because I don't know how many sounds there are. I set it to 400 producing the following result, which didn't really feel like something I could work with. Ideally I would want all the blocks to be solid colors: not colors in between the max red and blue values.
(I turned it sideways to look more like my examples above and more like the output I'm trying to produce)
Additional Info
There may also be a drum track in the background and sometimes multiple sounds are played at the same time. If these multiple sound groups get interpreted as one unique sound that's ok, but I'd obviously prefer if they could be distinguished as separate sounds.
If it makes it easier you can remove a drum loop using
y, sr = librosa.load(librosa.ex('exampleSong.mp3'))
y_harmonic, y_percussive = librosa.effects.hpss(y)
Update
I was able to separate the sounds by transients. Currently this kind of works, but it separates into too many sounds, from I could tell, it seemed like it was mostly just separating some sounds into two though. I can also create a midi file from the software I'm using, and using that to determine the transient times, but I would like to solve this problem without the midi file if I could. The midi file was pretty accurate, and split the sound file into 33 sections, whereas that transient code split the sound file into 40 sections. Here's a visualization of the midi
So that parts that still need to be solved would be
Better transient separation
Sorting the sounds
Below is a script that uses Non-negative Matrix Factorization (NMF) on mel-spectrograms to decompose the input audio. I took the first seconds with complete audio of your uploaded audio WAV, and ran the code to get the following output.
Both the code and the audio clip can be found in the Github repository.
This approach seems to do pretty reasonably on short audio clips when the BPM is known (seems to be around 130 with given example) and the input audio is roughly aligned to the beat. No guarantee it will work as well on a whole song, or other songs.
There are many ways it could be improved:
Using a more compact and perceptual vector than mel-spectrogram as NMF. Possibly a transformation learned from music. Either an embedding an autoencoder.
De-duplicate NMF components into "primary" components.
Adding constraints to the NMF, such as temporal. Lots of research papers out there
Automatically detecting BPM and doing alignment
Better perceptual sorting. Might want to have groups, such as chords, single tones, percussive
import os.path
import sys
import librosa
import pandas
import numpy
import sklearn.decomposition
import skimage.color
from matplotlib import pyplot as plt
import librosa.display
import seaborn
def decompose_audio(y, sr, bpm, per_beat=8,
n_components=16, n_mels=128, fmin=100, fmax=6000):
"""
Decompose audio using NMF spectrogram decomposition,
using a fixed number of frames per beat (#per_beat) for a given #bpm
NOTE: assumes audio to be aligned to the beat
"""
interval = (60/bpm)/per_beat
T = sklearn.decomposition.NMF(n_components)
S = numpy.abs(librosa.feature.melspectrogram(y, hop_length=int(sr*interval), n_mels=128, fmin=100, fmax=6000))
comps, acts = librosa.decompose.decompose(S, transformer=T, sort=False)
# compute feature to sort components by
ind = numpy.apply_along_axis(numpy.argmax, 0, comps)
#ind = librosa.feature.spectral_rolloff(S=comps)[0]
#ind = librosa.feature.spectral_centroid(S=comps)[0]
# apply sorting
order_idx = numpy.argsort(ind)
ordered_comps = comps[:,order_idx]
ordered_acts = acts[order_idx,:]
# plot components
librosa.display.specshow(librosa.amplitude_to_db(ordered_comps,
ref=numpy.max),y_axis='mel', sr=sr)
return S, ordered_comps, ordered_acts
def plot_colorized_activations(acts, ax, hop_length=None, sr=None, value_mod=1.0):
hsv = numpy.stack([
numpy.ones(shape=acts.shape),
numpy.ones(shape=acts.shape),
acts,
], axis=-1)
# Set hue based on a palette
colors = seaborn.color_palette("husl", hsv.shape[0])
for row_no in range(hsv.shape[0]):
c = colors[row_no]
c = skimage.color.rgb2hsv(numpy.stack([c]))[0]
hsv[row_no, :, 0] = c[0]
hsv[row_no, :, 1] = c[1]
hsv[row_no, :, 2] *= value_mod
colored = skimage.color.hsv2rgb(hsv)
# use same kind of order as librosa.specshow
flipped = colored[::-1, :, :]
ax.imshow(flipped)
ax.set(aspect='auto')
ax.tick_params(axis='x',
which='both',
bottom=False,
top=False,
labelbottom=False)
ax.tick_params(axis='both',
which='both',
bottom=False,
left=False,
top=False,
labelbottom=False)
def plot_activations(S, acts):
fig, ax = plt.subplots(nrows=4, ncols=1, figsize=(25, 15), sharex=False)
# spectrogram
db = librosa.amplitude_to_db(S, ref=numpy.max)
librosa.display.specshow(db, ax=ax[0], y_axis='mel')
# original activations
librosa.display.specshow(acts, x_axis='time', ax=ax[1])
# colorize
plot_colorized_activations(acts, ax=ax[2], value_mod=3.0)
# thresholded
q = numpy.quantile(acts, 0.90, axis=0, keepdims=True) + 1e-9
norm = acts / q
threshold = numpy.quantile(norm, 0.93)
plot_colorized_activations((norm > threshold).astype(float), ax=ax[3], value_mod=1.0)
return fig
def main():
audio_file = 'silence-end.wav'
audio_bpm = 130
sr = 22050
audio, sr = librosa.load(audio_file, sr=sr)
S, comps, acts = decompose_audio(y=audio, sr=sr, bpm=audio_bpm)
fig = plot_activations(S, acts)
fig.savefig('plot.png', transparent=False)
main()

Creating a hexagonal grid (u-matrix) in Python using a Regularpolycollection

I am trying to create a hexagonal grid to use with a u-matrix in Python (3.4) using a RegularPolyCollection (see code below) and have run into two problems:
The hexagonal grid is not tight. When I plot it there are empty spaces between the hexagons. I can fix this by resizing the window, but since this is not reproducible and I want all of my plots to have the same size, this is not satisfactory. But even if it were, I run into the second problem.
Either the top or right hexagons don't fit in the figure and are cropped.
I have tried a lot of things (changing figure size, subplot_adjust(), different areas, different values of d, etc.) and I am starting to get crazy! It feels like the solution should be simple, but I simply cannot find it!
import SOM
import matplotlib.pyplot as plt
from matplotlib.collections import RegularPolyCollection
import numpy as np
import matplotlib.cm as cm
from mpl_toolkits.axes_grid1 import make_axes_locatable
m = 3 # The height
n = 3 # The width
# Some maths regarding hexagon geometry
d = 10
s = d/(2*np.cos(np.pi/3))
h = s*(1+2*np.sin(np.pi/3))
r = d/2
area = 3*np.sqrt(3)*s**2/2
# The center coordinates of the hexagons are calculated.
x1 = np.array([d*x for x in range(2*n-1)])
x2 = x1 + r
x3 = x2 + r
y = np.array([h*x for x in range(2*m-1)])
c = []
for i in range(2*m-1):
if i%4 == 0:
c += [[x,y[i]] for x in x1]
if (i-1)%2 == 0:
c += [[x,y[i]] for x in x2]
if (i-2)%4 == 0:
c += [[x,y[i]] for x in x3]
c = np.array(c)
# The color of the hexagons
d_matrix = np.zeros(3*3)
# Creating the figure
fig = plt.figure(figsize=(5, 5), dpi=100)
ax = fig.add_subplot(111)
# The collection
coll = RegularPolyCollection(
numsides=6, # a hexagon
rotation=0,
sizes=(area,),
edgecolors = (0, 0, 0, 1),
array= d_matrix,
cmap = cm.gray_r,
offsets = c,
transOffset = ax.transData,
)
ax.add_collection(coll, autolim=True)
ax.axis('off')
ax.autoscale_view()
plt.show()
See this topic
Also you need to add scale on axis like
ax.axis([xmin, xmax, ymin, ymax])
The hexalattice module of python (pip install hexalattice) gives solution to both you concerns:
Grid tightness: You have full control over the hexagon border gap via the 'plotting_gap' argument.
The grid plotting takes into account the grid final size, and adds sufficient margins to avoid the crop.
Here is a code example that demonstrates the control of the gap, and correctly fits the grid into the plotting window:
from hexalattice.hexalattice import *
create_hex_grid(nx=5, ny=5, do_plot=True) # Create 5x5 grid with no gaps
create_hex_grid(nx=5, ny=5, do_plot=True, plotting_gap=0.2)
See this answer for additional usage examples, more images and links
Disclosure: the hexalattice module was written by me

Matplotlib RegularPolyCollection with static (data like) sizes?

Is it possible to create a RegularPolyCollection with static sizes?
I'd like to give the size in data units, not in screen units. Just like the offsetts.
The target is to have an image of a camera with 1440 hexagonal Pixels with a diameter of 9.5 mm.
It is possible to achieve this with looping over 1440 Polygons but i was not successfull creating it with a PolyCollection which has big advantages, for creating colormaps etc.
Here is the code i use to plot the 1440 hexagons with static size:
for c, x, y in zip(pixel_color, pixel_x, pixel_y):
ax.add_artist(
RegularPolygon(
xy=(x, y),
numVertices=6,
radius=4.75,
orientation=0.,
facecolor=c,
edgecolor=edgecolor,
linewidth=1.5,
)
)
And this code produces the same but with wrong and not static (in terms of data) sizes:
a = 1/np.sqrt(3) * 9.5
collection = RegularPolyCollection(
numsides=6,
rotation=0.,
sizes=np.ones(1440)*np.pi*a**2, # tarea of the surrounding circle
facecolors=pixel_colors,
edgecolors="g",
linewidth=np.ones(1440)*1.5,
offsets=np.transpose([pixel_x, pixel_y]),
transOffset=self.transData,
)
self.add_collection(collection)
How can I achieve the static sizes of the hexagons with the advantages of having a collection?
I recently had the same problem. The solution is to simply use PatchCollection instead of RegularPolyCollection. The disadvantage is, however, that you have instantiate every single patch manually. Below you'll find a code example that plots 10,000 regular hexagons on a regular grid.
# imports
import matplotlib.pyplot as plt
from matplotlib.patches import RegularPolygon
from matplotlib.collections import PatchCollection
import numpy as np
# set up figure
fig, ax = plt.subplots(1)
# positions
pixel_x, pixel_y = np.indices((100, 100))
pixel_color = np.random.random_sample(30000).reshape(10000, 3)
dx = 4 # horizontal stride
dy = 5 # vertical stride
# set static radius
poly_radius = 2.5
# list to hold patches
patch_list = []
# creat the patches
for c, x, y in zip(pixel_color, pixel_x.flat, pixel_y.flat):
patch_list.append(
RegularPolygon(
xy=(x*dy, y*dy),
numVertices=6,
radius=poly_radius,
orientation=0.,
facecolor=c,
edgecolor='k'
)
)
pc = PatchCollection(patch_list, match_original=True)
ax.add_collection(pc)
ax.axis([-3, 480, -3, 480])
plt.show()
On my machine this code takes about 2.8 seconds to render everything.
If you'd like to use RegularPolyCollection, I've figured out how to set the sizes correctly. The main limitation is that the sizes depend on the axes transform, and so both the axes limits and the figure size need to be locked in before you calculate the sizes.
In the version below, the figure - and axis - also has to be square.
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
sin60 = np.sin(np.pi/3)
fig, ax = plt.subplots()
fig.set_size_inches(8, 8)
ax.set_aspect(1)
ax.set_xlim(-1.5*sin60, +1.5*sin60)
ax.set_ylim(-1.5*sin60, +1.5*sin60)
ax.set_frame_on(False)
ax.set_xticks([])
ax.set_yticks([])
coords = [[-1/2, +sin60/2], [+1/2, +sin60/2], [0, -sin60/2]]
radius = .5/sin60
data_to_pixels = ax.transData.get_matrix()[0, 0]
pixels_to_points = 1/fig.get_dpi()*72.
size = np.pi*(data_to_pixels*pixels_to_points*radius)**2
hexes = mpl.collections.RegularPolyCollection(
numsides=6,
sizes=3*(size,),
offsets=coords,
edgecolors=3*('k',),
linewidths=1,
transOffset=ax.transData)
ax.add_collection(hexes)

Categories