SCRIPTS FIGURES recolor_imageΒΆ

# -*-Python-*-
# Created by eldond at 2017 Apr 19  09:51
# OMFIT-independent version at: http://stackoverflow.com/q/43528540/6605826

"""
This script reads in an image file (like .png), reads the image's color bar (you have to tell it where), interprets the
color map used in the image to convert colors to values, then recolors those values with a new color map and regenerates
the figure. Useful for fixing figures that were made with rainbow color maps.

Parameters

-----------

:param filename: Full path and filename of the image file.
:param colorbar_loc: Location of color bar, which will be used to analyze the image and convert colors into values.
    Pixels w/ 0,0 at top left corner: [[left, top], [right, bottom]]
:param working_loc: Location of the area to recolor. You don't have to recolor the whole image.
    Pixels w/ 0,0 at top left corner: [[left, top], [right, bottom]], set to [[0, 0], [-1, -1]] to do everything.
:param colorbar_orientation: Set to 'x', 'y', or 'auto' to specify whether color map is horizontal, vertical,
    or should be determined based on the dimensions of the colorbar_loc
:param colorbar_direction: Controls direction of ascending value
    +1: colorbar goes from top to bottom or left to right.
    -1: colorbar goes from bottom to top or right to left.
:param new_cmap: String describing the new color map to use in the recolored image.
:param normalize_before_compare: Divide r, g, and b each by (r+g+b) before comparing.
:param max_rgb: Do the values of r, g, and b range from 0 to 1 or from 0 to 255? Set to 1, 255, or 'auto'.
:param threshold: Sum of absolute differences in r, g, b values must be less than threshold to be valid
    (0 = perfect, 3 = impossibly bad). Higher numbers = less chance of missing pixels but more chance of recoloring
    plot axes, etc.
:param saturation_threshold: Minimum color saturation below which no replacement will take place
:param compare_hue: Use differences in HSV instead of RGB to determine with which index each pixel should be associated.
:param show_plot: T/F: Open a plot to explain what is going on. Also helpful for checking your aim on the colorbar
    coordinates and debugging.
:param debug: Save some intermediate quantities in INPUTS.

"""

import scipy.misc

defaultVars(
    filename=OMFITsrc + '/extras/graphics/LOM.png',
    colorbar_loc=[[80, 88], [100, 293]],
    working_loc=[[68, 10], [500, 410]],
    colorbar_orientation='auto',
    colorbar_direction=-1,
    new_cmap='viridis',
    normalize_before_compare=False,
    max_rgb='auto',
    threshold=0.4,
    saturation_threshold=0.25,
    compare_hue=True,
    show_plot=True,
    debug=True,
)

print('Recoloring image: {:} ...'.format(filename))

# Determine tag name and load original file into the tree
fn1 = filename.split(os.sep)[-1]
fn2 = fn1.split(os.extsep)[0]
ext = fn1.split(os.extsep)[-1]
path = os.sep.join(filename.split(os.sep)[0:-1])
a = root['INPUTS']['{:}_original'.format(fn1)] = imread(filename)

if max_rgb == 'auto':
    # Determine if values of R, G, and B range from 0 to 1 or from 0 to 255
    if a.max() > 1:
        max_rgb = 255.0
    else:
        max_rgb = 1.0
# Normalize a so RGB values go from 0 to 1 and are floats.
a /= max_rgb

# Extract the colorbar
x = array([colorbar_loc[0][0], colorbar_loc[1][0]])
y = array([colorbar_loc[0][1], colorbar_loc[1][1]])
cb = a[y[0] : y[1], x[0] : x[1]]

# Take just the working area, not the whole image
xw = array([working_loc[0][0], working_loc[1][0]])
yw = array([working_loc[0][1], working_loc[1][1]])
a1 = a[yw[0] : yw[1], xw[0] : xw[1]]

# Pick color bar orientation
if colorbar_orientation == 'auto':
    if diff(x) > diff(y):
        colorbar_orientation = 'x'
    else:
        colorbar_orientation = 'y'
    printd('Auto selected colorbar_orientation')
printd('Colorbar orientation is {:}'.format(colorbar_orientation))

# Analyze the colorbar
if colorbar_orientation == 'y':
    cb = mean(cb, axis=1)
else:
    cb = mean(cb, axis=0)
if colorbar_direction < 0:
    cb = cb[::-1]

if debug:
    root['INPUTS']['original_colorbar'] = cb
    root['INPUTS']['colorbar'] = cb

# Take just the working area, not the whole image
a1 = a[yw[0] : yw[1], xw[0] : xw[1]]

# Find and mask of special colors that should not be recolored
n1a = sum(a1[:, :, 0:3], axis=2)
replacement_mask = ones(shape(n1a), bool)
for col in [0, 3]:  # Black and white will come out as 0 and 3.
    mask_update = n1a != col
    if mask_update.max() == 0:
        print('Warning: masking to protect special colors prevented all changes to the image!')
    else:
        printd('Good: Special color mask {:} allowed at least some changes'.format(col))
    replacement_mask *= mask_update
    if replacement_mask.max() == 0:
        print('Warning: replacement mask will prevent all changes to the image! ' '(Reached this point during special color protection)')
    printd('Sum(replacement_mask) = {:}    (after considering special color {:})'.format(sum(atleast_1d(replacement_mask)), col))
# Also apply limits to total r+g+b
replacement_mask *= n1a > 0.75
replacement_mask *= n1a < 2.5
if replacement_mask.max() == 0:
    printw('Warning: replacement mask will prevent all changes to the image! ' '(Reached this point during total r+g+b+ limits)')
if saturation_threshold > 0:
    hsv1 = matplotlib.colors.rgb_to_hsv(a1[:, :, 0:3])
    sat = hsv1[:, :, 1]
    printd('Saturation ranges from {:} <= sat <= {:}'.format(sat.min(), sat.max()))
    sat_mask = sat > saturation_threshold
    if sat_mask.max() == 0:
        print('Warning: saturation mask will prevent all changes to the image!')
    else:
        printd('Good: Saturation mask will allow at least some changes')
    replacement_mask *= sat_mask
    if replacement_mask.max() == 0:
        print('Warning: replacement mask will prevent all changes to the image! ' '(Reached this point during saturation threshold)')

# Find where on the colorbar each pixel sits
if compare_hue:
    # Difference in hue
    hsv1 = matplotlib.colors.rgb_to_hsv(a1[:, :, 0:3])
    hsv_cb = matplotlib.colors.rgb_to_hsv(cb[:, 0:3])
    d2 = abs(hsv1[:, :, :, newaxis] - hsv_cb.T[newaxis, newaxis, :, :])
    # d2 = d2[:, :, 0, :]  # Take hue only
    d2 = sum(d2, axis=2)
    d = d2
    printd('  shape(d2) = {:}    (hue version)'.format(shape(d2)))
else:
    # Difference in RGB
    if normalize_before_compare:
        # Difference of normalized RGB arrays
        n1 = n1a[:, :, newaxis]
        n2 = sum(cb[:, 0:3], axis=1)[:, newaxis]
        w1 = n1 == 0
        w2 = n2 == 0
        n1[w1] = 1
        n2[w2] = 1
        d = (a1 / n1)[:, :, :, newaxis] - (cb / n2).T[newaxis, newaxis, :, :]
    else:
        # Difference of non-normalized RGB arrays
        d = a1[:, :, :, newaxis] - cb.T[newaxis, newaxis, :, :]
        valid1 = ones(shape(d), bool)
    d2 = sum(abs(d[:, :, 0:3, :]), axis=2)  # 0:3 excludes the alpha channel from this calculation

index = d2.argmin(axis=2)
md2 = d2.min(axis=2)
index_valid = md2 < threshold

if index_valid.max() == 0:
    print('Warning: minimum difference is greater than threshold: all changes rejected!')
else:
    printd('Good: Minimum difference filter is lower than threshold for at least one pixel.')
printd('Sum(index_valid) = {:}     (before *= replacement_mask)'.format(sum(atleast_1d(index_valid))))
printd('Sum(replacement_mask) = {:}    (final, before combining w/ index_valid)'.format(sum(atleast_1d(replacement_mask))))
index_valid *= replacement_mask
if index_valid.max() == 0:
    print('Warning: index_valid mask prevents all changes to the image after combination w/ replacement_mask.')
else:
    printd('Good: Mask will allow at least one pixel to change.')
printd('Sum(index_valid) = {:}'.format(sum(atleast_1d(index_valid))))
printd('Index ranges from {:} to {:}'.format(index.min(), index.max()))

value = index / (len(cb) - 1.0)

if debug:
    root['INPUTS']['diff'] = d
    root['INPUTS']['diff2'] = d2
    root['INPUTS']['index'] = index
    root['INPUTS']['min_diff2'] = md2
    root['INPUTS']['index_valid'] = index_valid

# Make a new image with replaced colors
b = matplotlib.cm.ScalarMappable(cmap=new_cmap).to_rgba(value)  # Remap everything
printd('shape(b) = {:}, min(b) = {:}, max(b) = {:}'.format(shape(b), b.min(), b.max()))
c = copy.copy(a1)  # Copy original
c[index_valid] = b[index_valid]  # Transfer only pixels where color was close to colormap

# Transfer working area to full image
c2 = copy.copy(a)  # Copy original full image
c2[yw[0] : yw[1], xw[0] : xw[1], :] = c  # Replace working area
c2[:, :, 3] = a[:, :, 3]  # Preserve original alpha channel

# Save the image
new_filename = '{:}{:}{:}_recolored{:}{:}'.format(path, os.sep, fn2, os.extsep, ext)
try:
    from imageio import imwrite

    imwrite(new_filename, c2)
except ImportError:
    pass

print('Done recoloring. Result saved to {:} .'.format(new_filename))

if show_plot:
    # Setup figure for showing things to the user
    f, axs = plt.subplots(2, 3)
    axo = axs[0, 0]  # Axes for original figure
    axoc = axs[0, 1]  # Axes for original color bar
    axf = axs[0, 2]  # Axes for final figure
    axm = axs[1, 1]  # Axes for mask
    axre = axs[1, 2]  # Axes for recolored section only (it might not be the whole figure)
    axraw = axs[1, 0]  # Axes for raw recoloring result before masking

    for ax in axs.flatten():
        ax.set_xlabel('x pixel')
        ax.set_ylabel('y pixel')

    axo.set_title('Original image w/ colorbar ID overlay')
    axoc.set_title('Color progression from original colorbar')
    axm.set_title('Mask')
    axre.set_title('Recolored section')
    axraw.set_title('Raw recolor result (no masking)')
    axf.set_title('Final image')

    axoc.set_xlabel('Index')
    axoc.set_ylabel('Value')

    # Show original figure
    axo.imshow(a)

    # Show the user where they placed the color bar and working location
    xx = x[array([0, 0, 1, 1, 0])]
    yy = y[array([0, 1, 1, 0, 0])]
    axo.plot(xx, yy, '+-', label='colorbar')

    xxw = xw[array([0, 0, 1, 1, 0])]
    yyw = yw[array([0, 1, 1, 0, 0])]
    axo.plot(xxw, yyw, '+-', label='target')

    tots = sum(cb[:, 0:3], axis=1)

    if normalize_before_compare:
        # Normalized version
        axoc.plot(cb[:, 0] / tots, 'r', label='r/(r+g+b)', lw=2)
        axoc.plot(cb[:, 1] / tots, 'g', label='g/(r+g+b)', lw=2)
        axoc.plot(cb[:, 2] / tots, 'b', label='b/(r+g+b)', lw=2)
        axoc.set_ylabel('Normalized value')
    else:
        axoc.plot(cb[:, 0], 'r', label='r', lw=2)
        axoc.plot(cb[:, 1], 'g', label='g', lw=2)
        axoc.plot(cb[:, 2], 'b', label='b', lw=2)
        axoc.plot(cb[:, 3], color='gray', linestyle='--', label='$\\alpha$')
        axoc.plot(tots, 'k', label='r+g+b')

    # Display the new colors with no mask, the mask, and the recolored section
    axraw.imshow(b)
    axm.imshow(index_valid)
    axre.imshow(c)

    # Display the final result
    axf.imshow(c2)

    # Finishing touches on plots
    axo.legend(loc=0).draggable()
    axoc.legend(loc=0).draggable()