Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support color map / palette manipulation #403

Closed
HorstBaerbel opened this issue Mar 25, 2019 · 17 comments

Comments

Projects
None yet
2 participants
@HorstBaerbel
Copy link

commented Mar 25, 2019

The "colorMap" and "colorMapSize" attributes of the Image class are not available for reading / writing (or I missed them in the docs) so you can't get / set the palette of an image.

@emcconville

This comment has been minimized.

Copy link
Owner

commented Mar 25, 2019

Not all palette methods have been mapped yet, and some minor IM-6/7 porting work is needed on the wand.color.Color to allow access to palette index.

What kind of behavior / methods are you expected? There are MagickWand methods to get/set a color at a specific index, or cycle a palette by an offset, but no wholesale palette manipulation methods. You can achieve most common types of palette manipulations through Color LookUp Tables.

@HorstBaerbel

This comment has been minimized.

Copy link
Author

commented Mar 25, 2019

I was trying to exchange the index of a specific color, so it is index 0. I was trying to read the palette, read the pixel/index data, shuffle it all and write it back again. I'm an ImageMagick beginner so I might have misses some methods. The C++-interface for Image has the attributes I mentioned. I'd appreciate if you could point me in the right direction if there's an easier way...

@emcconville

This comment has been minimized.

Copy link
Owner

commented Mar 26, 2019

Perhaps something like this would work...

from wand.api import library
from wand.color import Color
from wand.image import Image

with Image(filename='input.png') as img:
    with Color('GREEN') as color:
        r = library.MagickSetImageColormapColor(img.wand, 0, color.resource)
        if not r:
            img.raise_exceptions()

[...] I might have misses some methods. The C++-interface for Image has the attributes I mentioned.

Ah, your looking at Magick++ docs. This library connects to the C-API MagickWand library, and not the CXX-API Magick++ library. Both libraries are public wrappers for the MagickCore library, but in no way are they cost-free compatible.

@emcconville emcconville added this to the Wand 0.5.3 milestone Mar 26, 2019

@HorstBaerbel

This comment has been minimized.

Copy link
Author

commented Mar 26, 2019

Ok. So it's the C-API. Sorry for the missunderstanding. I think then you'd need "MagickGetImageColormapColor", "MagickSetImageColormapColor" and "MagickGetImageColors".
As I understand your snippet it replaces index 0 in the color map with a specific color. I want a specific color value to have / move to index 0. I'll look through the API and figure it out.

@emcconville

This comment has been minimized.

Copy link
Owner

commented Mar 26, 2019

Yep! Also the MagickCycleColormapImage method for shifting color placement. I'll start mapping API out for the next 0.5.3 release.

emcconville added a commit that referenced this issue Mar 26, 2019

@emcconville

This comment has been minimized.

Copy link
Owner

commented Mar 27, 2019

Added Image.color_map method. If you're able too, please checkout the master branch & see it will work for your needs. I'm not convinced it's the correct implementation as the function acts as a setter & getter. Feedback would be most welcome. Image.colors property + Image.cycle_color_map also provided.

@HorstBaerbel

This comment has been minimized.

Copy link
Author

commented Mar 28, 2019

Thanks a lot. Looks nice. Not sure how get/set stuff is usually done in the Python world (I'm a C++ guy), but I think it's pretty convenient that way. I have absolutely no idea how to test the master though. I installed via pip...

@HorstBaerbel

This comment has been minimized.

Copy link
Author

commented Apr 2, 2019

Ok. I've figured out how to install the git master version. I tried .colors and getting colors through .color_map and it seems to work fine for me.

@emcconville

This comment has been minimized.

Copy link
Owner

commented Apr 2, 2019

Great! I'll close this issue, and the changes will be apart of the 0.5.3 release.

@emcconville emcconville closed this Apr 2, 2019

@HorstBaerbel

This comment has been minimized.

Copy link
Author

commented Apr 2, 2019

Well, uhm not quite. I have this script:

#!/usr/bin/env python

# Reorder a color in the images color map to a different index in the color map
# Call with reorderpalette.py <in> color newindex <out>
# color is a hex RGB color, e.g. ABCDEF
# index is a number [0,255]
# Wand docs: http://docs.wand-py.org/en/latest/wand/image.html

import sys, getopt
from wand.image import Image, Color

def getColorMap(img):
    assert isinstance(img, Image)
    colorMap = []
    for index in range(img.colors):
        colorMap.append(img.color_map(index))
    return colorMap

def setColorMap(img, colorMap):
    assert isinstance(img, Image)
    assert img.colors >= len(colorMap)
    for index in range(len(colorMap)):
        img.color_map(index, colorMap[index])

def main():
    with Image(filename=sys.argv[1]) as img:
        # check arguments
        print("Reading " + sys.argv[1] + " " + str(img.size))
        if img.type != 'palette':
            print("Image is not a paletted image")
            return -1
        newIndex = int(sys.argv[3])
        newIndex = max(0, min(255, newIndex))
        print("Reordering color #" + sys.argv[2] + " to index " + sys.argv[3])
        # find index of color in color map
        color = Color("#" + sys.argv[2])
        colorMap = getColorMap(img)
        oldIndex = colorMap.index(color)
        if oldIndex != newIndex:
            # swap colors in color map
            colorMap[oldIndex] = colorMap[newIndex]
            colorMap[newIndex] = color
            setColorMap(img, colorMap)
            # get pixels and swap all indices
            pixels = img.export_pixels(channel_map='I')
            for i in range(len(pixels)):
                if pixels[i] == oldIndex:
                    pixels[i] = newIndex
                elif pixels[i] == newIndex:
                    pixels[i] = oldIndex
            img.import_pixels(channel_map='I', data=pixels)
            # save image
            print("Saving to " + sys.argv[4])
            img.save(filename=sys.argv[4])
        else:
            print("Color #" + sys.argv[2] + " already at index " + sys.argv[3] + ". Doing nothing.")
    return 0

if __name__ == "__main__":
    sys.exit(main())

When I run it it saves a grayscale image without a palette. What am I doing wrong?

@HorstBaerbel

This comment has been minimized.

Copy link
Author

commented Apr 3, 2019

If I call import_pixels first and then set the palette instead, I get an assertion:

File "./reordercolor.py", line 23, in setColorMap
img.color_map(index, colorMap[index])
File "/usr/local/lib/python2.7/dist-packages/wand/image.py", line 2451, in color_map
raise ValueError('index is out of palette range')
ValueError: index is out of palette range

@emcconville

This comment has been minimized.

Copy link
Owner

commented Apr 3, 2019

I'll take a look at the script shortly. I can say that importing/exporting pixels will throw out the palette, and that I channel is for intensity, not index. Do you have a test image handy? If not, no worries, I can pull something from the tests/assets/ directory.

@emcconville

This comment has been minimized.

Copy link
Owner

commented Apr 3, 2019

Okay. I see what's going on. The issue in your setColorMap method. Palettes are a Set of unique colors, and the internal C holds them as a linked list. By iterating over the modified colorMap, the palette get's trashed (reordered) if attempting to set a value that's already in the list. Don't know if that makes sense.

Here's the solution. Flush all the palette values to a temporary state before applying the updated ColorMap.

(note: I dropped the pixel import/export, forced all import to 'palette', and added a flush method)

#!/usr/bin/env python

# Reorder a color in the images color map to a different index in the color map
# Call with reorderpalette.py <in> color newindex <out>
# color is a hex RGB color, e.g. ABCDEF
# index is a number [0,255]
# Wand docs: http://docs.wand-py.org/en/latest/wand/image.html

import sys, getopt
from wand.image import Image, Color

def getColorMap(img):
    assert isinstance(img, Image)
    colorMap = []
    for index in range(img.colors):
        colorMap.append(img.color_map(index))
    return colorMap

def flushColorMap(img):
    assert isinstance(img, Image)
    for index in range(img.colors):
        img.color_map(index, Color('#{0:06}'.format(index)))

def setColorMap(img, colorMap):
    assert isinstance(img, Image)
    assert img.colors >= len(colorMap)
    for index in range(len(colorMap)):
        img.color_map(index, colorMap[index])

def main():
    with Image(filename=sys.argv[1]) as img:
        # check arguments
        print("Reading " + sys.argv[1] + " " + str(img.size))
        img.type = 'palette'
        newIndex = int(sys.argv[3])
        newIndex = max(0, min(255, newIndex))
        print("Reordering color #" + sys.argv[2] + " to index " + sys.argv[3])
        # find index of color in color map
        color = Color("#" + sys.argv[2])
        colorMap = getColorMap(img)
        oldIndex = colorMap.index(color)
        if oldIndex != newIndex:
            # swap colors in color map
            colorMap[oldIndex] = colorMap[newIndex]
            colorMap[newIndex] = color
            flushColorMap(img)
            setColorMap(img, colorMap)
            # save image
            print("Saving to " + sys.argv[4])
            img.save(filename=sys.argv[4])
        else:
            print("Color #" + sys.argv[2] + " already at index " + sys.argv[3] + ". Doing nothing.")
    return 0

if __name__ == "__main__":
    sys.exit(main())

For the temporary state, I'm just using low blue values with '#{0:06}'.format(index). This will not work if you actually have close-to-black in the palette. Another option is to use high green values (think green-screen). For example..

>>> for index in range(10):
...  print('#00{0:02X}00'.format(0xFF - index))
... 
#00FF00
#00FE00
#00FD00
#00FC00
#00FB00
#00FA00
#00F900
#00F800
#00F700
#00F600
@emcconville

This comment has been minimized.

Copy link
Owner

commented Apr 3, 2019

And to clarify on the out-of-index error, and what "trashed" palette means.

Imagine that you have the palette...

Palette: red, yellow, orange, green

In an attempt to move orange to the start of the list. We start iterating over orange, red, ... &etc

Palette: orange, yellow, green

Setting the first element removes red, and reduces palette to three colors. Continuing the iterations work as expected for the next two iterations.

Palette: orange, red, green
Palette: orange, red, yellow

Now when we attempt to add the green we have an error, as the first iteration merged both red, and orange pixels. Hope that helps

@HorstBaerbel

This comment has been minimized.

Copy link
Author

commented Apr 3, 2019

As a beginner Python is a real mess to me, honestly. I have no idea what data types operations spit out and the docs are mute on that. Anyways.

I've attached an example image. This has a large area with color #ff00ff. This is fine, but the color index is 37 (check in Gimp->Colors->?Display?->Sort palette). I want the index of that color to be 0.
test
When calling the modified script with

./reordercolor.py test.png ff00ff 0 out.png

#ff00ff is moved to index 1.

The background to all of this is I'm converting a lot of images for Game Boy Advance. It has 16/256 color palettes, but color index 0 is always the transparent index. I want color #ff00ff to be at index 0 so it is transparent when being displayed.

About the API:

  • Could you please give an example on how to manipulate image pixels correctly? How do I get the index "channel"/data?
  • Could you please give an example on how to manipulate image color palettes correctly?
  • Could you add them to the docs? A lot of the functions could use small, 3-4 lines examples, e.g. export/import_pixels.
  • Why the linked list? Are the Color-objects directly coupled to the C-Objects?
  • Is the need to flush intended? Is it the same in MagickWand API?

The check for paletted image was intentional btw. I just want to manipulate paletted images and leave others alone.

By iterating over the modified colorMap, the palette get's trashed (reordered) if attempting to set a value that's already in the list. Don't know if that makes sense

Why can't we just set the value of the element that is in the linked list? We don't need to alter the list itself. Or decouple the Python objects from the C-Objects (I'm just wildly guessing here).

@HorstBaerbel

This comment has been minimized.

Copy link
Author

commented Apr 5, 2019

This is a C++-program which does what I want. I took me 1-2 hours to cobble it together from the Magick++ docs and make it work:

// Reorder a color in the images color map to a different index in the color map
// Call with reorderpalette.py <in> color newindex <out>
// color is a hex RGB color, e.g. ABCDEF
// index is a number [0,255]
// You'll need libmagick-dev installed

#include <iostream>
#include <string>
#include <Magick++.h>

using namespace Magick;

void printUsage()
{
    std::cout << "Usage: reordercolor INFILE COLOR INDEX OUTFILE" << std::endl;
    std::cout << "COLOR: RGB color in hex format, e.g. \"abc012\"." << std::endl;
    std::cout << "INDEX: New index in color map [0,255]." << std::endl;
}

int main(int argc, char *argv[])
{
    // check arguments
    if (argc != 5) {
        printUsage();
        return -1;
    }
    // try converting argument 2 to a color
    std::string colorArg = argv[2];
    colorArg = std::string("#") + colorArg;
    Color color;
    try {
        color = Color(colorArg);
    }
    catch (const Exception& ex) {
        std::cerr << colorArg << " is not a valid color. Format must be e.g. \"#abc123\". Aborting. " << std::endl;
        return -2;
    }
    // try to convert argument 3 to an integer
    size_t newIndex = 0;
    std::string indexArg = argv[3];
    try {
        std::size_t pos;
        int x = std::stoi(indexArg, &pos);
        if (pos < indexArg.size()) {
            std::cerr << "Trailing characters after number: " << indexArg << std::endl;
            return -3;
        }
        if (x < 0) {
            std::cerr << "No negative indices allowed: " << indexArg << std::endl;
            return -3;
        }
        newIndex = static_cast<size_t>(x);
    }
    catch (const std::invalid_argument& ex) {
        std::cerr << "Invalid number: " << indexArg << std::endl;
        return -3;
    }
    catch (const std::out_of_range& ex) {
        std::cerr << "Number out of range: " << indexArg << std::endl;
        return -3;
    }
    // fire up ImageMagick
    InitializeMagick(*argv);
    // open image
    std::cout << "Reading " << argv[1] << std::endl;
    Image img;
    try {
        img.read(argv[1]);
    }
    catch (const Exception& ex) {
        std::cerr << "Failed to read " << argv[1] << ": " << ex.what() << std::endl;
        return -4;
    }
    // check if paletted image
    if (img.classType() != PseudoClass || img.type() != PaletteType) {
        std::cerr << "Only paletted images are supported! Aborting." << std::endl;
        return -5;
    }
    // read palette
    std::vector<Color> colorMap;
    for (size_t i = 0; i < img.colorMapSize(); ++i) {
        colorMap.push_back(img.colorMap(i));
    }
    // try to find color in palette
    auto oldColorIt = std::find(colorMap.cbegin(), colorMap.cend(), color);
    if (oldColorIt == colorMap.cend()) {
        std::cerr << "Color " << argv[2] << " not found in image color map. Aborting." << std::endl;
        return -6; 
    }
    const auto oldIndex = std::distance(colorMap.cbegin(), oldColorIt);
    // check if index needs to move
    if (oldIndex == newIndex) {
        std::cout << "Color " << argv[2] << " already at index " << oldIndex << ". Quitting." << std::endl;
        return 0;
    }
    // swap color in color map
    img.modifyImage();
    img.colorMap(oldIndex, colorMap[newIndex]);
    img.colorMap(newIndex, colorMap[oldIndex]);
    // swap color indicies in pixels
    const auto nrOfIndices = img.columns() * img.rows();
    auto pixels = img.getPixels(0, 0, img.columns(), img.rows()); // we nned to call this first
    auto indices = img.getIndexes();
    for (size_t i = 0; i < nrOfIndices; ++i) {
        if (indices[i] == oldIndex) {
            indices[i] = newIndex;
        }
        else if (indices[i] == newIndex) {
            indices[i] = oldIndex;
        }
    }
    // sync and write modified image
    img.syncPixels();
    try {
        img.write(argv[4]);
    }
    catch(const Exception& ex) {
        std::cerr << "Failed to write " << argv[4] << ": " << ex.what() << std::endl;
        return -7;
    }
    return 0;
}

I'm giving up on writing those image conversion scripts using Python, so feel free to leave the API as-is and ignore my comments above.

@emcconville

This comment has been minimized.

Copy link
Owner

commented Apr 5, 2019

Looks good!

I've done a bit more research on handling this in Python, but it's looking like this library would not be the correct tool for your needs. I am happy your continuing with C++. It might be even less work to access png.h directly (assuming all assets are in that format).

When you're calling Magick::Image.colorMap, it's directly setting the palette value on MagickCore::Image.colors linked list. However when using the MagickWand library (what this python project connects to), it indirectly sets the value by passing it along some helper clamp method(s). I believe this is way duplicate palette values are being removed (trashing palette order), and why I recommended to use intermediate temp values. This also explains why C++ allows you to add colors to the palette, even if no pixels are referencing it, but C would ignore that set operation request.

(╯°□°)╯︵ ┻━┻

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.