2

How can I convert an image like this (known as an Object ID pass or Clown Pass in 3d) to a layered file where every layer would be a color blob (using Photoshop only)?

enter image description here

I know how to do that manually, but I'm interested in an automatic algorithm.

Maybe I somehow can isolate colors and create BW-masks based on those to use them later in a script?

My current idea goes like this:

  1. get a color from a left-top corner (black in this case) of the original layer (OL)
  2. use Color Range to select this color
  3. create a new layer via cut, hide the new layer
  4. select the OL, create a selection using its transparency
  5. create a path based on this selection
  6. find a point inside this path and get a color from this point
  7. repeat from 2) with the new color

But this seems cumbersome and time consuming.

  • Are your images often way more complex than this? Because I can do this manually in no time. See this gif. – Wolff Nov 3 '19 at 12:21
  • 1
    You are probably better at Photoshop scripting than me, but here is an idea for automation: Convert to Indexed Color with Exact palette. Save the colors for later. Convert back to RGB (to enable layers). Select the colors one at a time (with Color Range or maybe create a single pixel of the color, select it and Select Similar?) and copy/paste to create the individual layers. Does this make sense? – Wolff Nov 3 '19 at 12:30
  • 1
    You should probably not be using photoshop here... Comp applications will do this automatically. And they can solve coverage so you get correct antialiasing too. – joojaa Nov 3 '19 at 12:41
  • 1
    @Wolff Yes, they're usually more complex: here's an example, and I do that often and with my OCD it drives me crazy :D. Indexed Color might be an option! I'll try to see if it's possible to get the indexed colors with a script and how Exact palette will behave with a pass that has AA between the color islands (so there will be halftones) – Sergey Kritskiy Nov 3 '19 at 12:43
  • 1
    Another idea (probably very slow and only without AA): Loop through each pixel. If the pixel is non-transparent pixel, select it, use Select > Grow to select the whole shape, cut the selection and paste in another layer. Continue to next pixel. But this requires areas to be continuous, they can't be separated by another color. I can see that occurs in your linked example. And it also seems that areas that are not supposed to be in the same layer can have similar colors. How should a script be able to understand the difference? – Wolff Nov 3 '19 at 13:00
2

If there are 256 or fewer regions, then this can be accomplished using an indexed colour duplicate of the image to define the matte regions.

Here's a script which attempts to automate this on the current document:

#target photoshop

var ChannelIDs = {
    RED: "Rd  ",
    GREEN: "Grn ",
    BLUE: "Bl  ",
    RGB: "RGB "
};

function isSelectionEmpty(doc) {
    try {
        return (doc.selection.bounds) ? false : true;
    } catch (err) {
        return true;
    }
}

function createChannelFromSelection(doc, channelName) {
    var chan = doc.channels.add();
    chan.name = channelName;
    chan.kind = ChannelType.SELECTEDAREA;
    doc.selection.store(chan, SelectionType.REPLACE);
}

function cutToLayer() { // "Layer via cut"
    executeAction(charIDToTypeID("CtTL"), undefined, DialogModes.NO);
}

function createSnapshot(snapshotName) {
    var makDescriptor = new ActionDescriptor(),
        snapshotAction = new ActionReference(),
        fromRef = new ActionReference();

    snapshotAction.putClass(charIDToTypeID("SnpS"));
    makDescriptor.putReference(charIDToTypeID("null"), snapshotAction);
    fromRef.putProperty(charIDToTypeID("HstS"), charIDToTypeID("CrnH"));
    makDescriptor.putReference(charIDToTypeID("From"), fromRef);

    if (snapshotName) { // Assign snapshot name
        makDescriptor.putString(charIDToTypeID("Nm  "), snapshotName );
        makDescriptor.putEnumerated(charIDToTypeID("Usng"), charIDToTypeID("HstS"), charIDToTypeID("FllD") );
    }

    executeAction(charIDToTypeID("Mk  "), makDescriptor, DialogModes.NO);
}

function restoreSnapshot(snapshotName) {
    if (!snapshotName) { throw new Error("Expected snapshot name"); }

    var selectDescriptor = new ActionDescriptor(),
        snapshotRef = new ActionReference();

    snapshotRef.putName(charIDToTypeID("SnpS"), snapshotName);
    selectDescriptor.putReference(charIDToTypeID("null"), snapshotRef);
    executeAction(charIDToTypeID("slct"), selectDescriptor, DialogModes.NO);
}

function selectChannel(channelId) {
    if (!channelId) { throw new Error("Expected channel ID"); }

    var setDescriptor = new ActionDescriptor(),
        selectRef = new ActionReference(),
        channelRef = new ActionReference(),
        idChnl = charIDToTypeID("Chnl");

    selectRef.putProperty(idChnl, charIDToTypeID("fsel"));
    setDescriptor.putReference(charIDToTypeID("null"), selectRef);
    channelRef.putEnumerated(idChnl, idChnl, charIDToTypeID(channelId));
    setDescriptor.putReference(charIDToTypeID("T   "), channelRef);
    executeAction(charIDToTypeID("setd"), setDescriptor, DialogModes.NO);
};

function loadSelection(docName, channelName) {
    var setDescriptor = new ActionDescriptor(),
        selectRef = new ActionReference(),
        docRef = new ActionReference(),
        idChnl = charIDToTypeID("Chnl");

    selectRef.putProperty(idChnl, charIDToTypeID("fsel"));
    setDescriptor.putReference(charIDToTypeID("null"), selectRef);
    docRef.putName(idChnl, channelName);
    docRef.putName(charIDToTypeID("Dcmn"), docName);
    setDescriptor.putReference(charIDToTypeID("T   "), docRef);
    executeAction(charIDToTypeID("setd"), setDescriptor, DialogModes.NO);
}

function convertToIndexedColour(doc) {
    var opts = new IndexedConversionOptions();

    opts.dither = Dither.NONE;
    opts.forced = ForcedColors.NONE;
    opts.matte = MatteType.NONE;
    opts.palette = Palette.EXACT;
    opts.transparency = false;

    doc.changeMode(ChangeMode.INDEXEDCOLOR, opts);
}

function selectColourTableEntry(i) {
    var actionSet = charIDToTypeID("setd");
    var setDescriptor = new ActionDescriptor();
    var ref = new ActionReference();
    ref.putProperty(charIDToTypeID("Clr "), charIDToTypeID("ClrT"));
    setDescriptor.putReference(charIDToTypeID("null"), ref);

    var colourTable = new ActionList();

    if (i > 0) { // Fill low part of colour table
        for (var n = 0; n < i; ++n) {
            addColourTableEntry(colourTable, 0, 0, 0);
        }
    }

    // Select single entry
    addColourTableEntry(colourTable, 255, 255, 255);

    if (i < 255) { // Fill high part of colour table
        for (var n = i + 1; n < 256; ++n) {
            addColourTableEntry(colourTable, 0, 0, 0);
        }
    }

    setDescriptor.putList(charIDToTypeID("T   "), colourTable);
    executeAction(actionSet, setDescriptor, DialogModes.NO);
}

function addColourTableEntry(colourTable, r, g, b) {
    var entry = new ActionDescriptor();
    entry.putDouble(charIDToTypeID(ChannelIDs.RED), r);
    entry.putDouble(charIDToTypeID(ChannelIDs.GREEN), g);
    entry.putDouble(charIDToTypeID(ChannelIDs.BLUE), b);
    colourTable.putObject(charIDToTypeID("RGBC"), entry);
}

function main() {
    var doc = activeDocument;
    var lyr = doc.activeLayer;

    // Create indexed duplicate
    var dupDoc = doc.duplicate();
    activeDocument = dupDoc;
    convertToIndexedColour(dupDoc);

    var snapshotName = "indexed";
    var channelName = "matte";
    var i;

    // Snapshot the current history state
    createSnapshot(snapshotName);

    for (i = 0; i < 256; ++i) {
        selectColourTableEntry(i);

        // For some reason selecting channels doesn't work in indexed mode?
        dupDoc.changeMode(ChangeMode.RGB);

        // Create new matte from red channel
        selectChannel(ChannelIDs.RED);

        // If there is no selection then we're done
        if (isSelectionEmpty(dupDoc)) { break; }

        createChannelFromSelection(dupDoc, channelName);

        // Use matte to cut new layer in original document
        activeDocument = doc;
        loadSelection(dupDoc.name, channelName);
        cutToLayer();
        doc.activeLayer.name = "Region " + (i + 1);

        // Reset ready for next layer
        doc.activeLayer = lyr;
        activeDocument = dupDoc;
        restoreSnapshot(snapshotName);
    }

    // Clean up
    activeDocument = dupDoc;
    dupDoc.close(SaveOptions.DONOTSAVECHANGES);

    activeDocument = doc;
    doc.activeLayer.remove();

    alert("Found " + i + " region" + (i === 1 ? "" : "s"));
}

main();

If there are more than 256 objects, then some will be merged when converting to indexed colour.

Tested in Photoshop CC

| improve this answer | |
  • Hi Mike, thank you, this is amazing: I didn't know hot to approach the creation of color masks from an indexed palette. There's an issue: I think exact palette always creates black and white colors as 1 and 2 entry and if these colors aren't in the image, the script will stop right away. I added a condition to the line 161 so that if there's no selection, but i is 0 or 1 the script wouldn't stop. Other than that it works great on low color images! If there're more than 256 colors (like this one) the results are weird sometimes – Sergey Kritskiy Nov 6 '19 at 13:00
  • I used a different approach, I'll upload my version later, but I was using color range to limit the regions. The color was used from the polygon, created by selection – Sergey Kritskiy Nov 6 '19 at 13:06
  • The script has been amended to not force black and white in the colour table. If there are more than 256 colours in the image then Photoshop seems to disregard these settings and uses the defaults of a Local (selective) palette including dithering. This causes the strange stippling effect you reported. – Mike D Sutton Nov 6 '19 at 14:52
  • If you know you're working with more than 256 colours and don't mind the object merging, then change the IndexedConversionOptions to: opts.colors = 256; opts.dither = Dither.NONE; opts.forced = ForcedColors.NONE; opts.matte = MatteType.NONE; opts.palette = Palette.LOCALPERCEPTUAL; opts.transparency = false; – Mike D Sutton Nov 6 '19 at 14:55
  • Weird, I used it on an image I provided as an example (which has 6 colors with black) and it still created white color with an exact table – Sergey Kritskiy Nov 6 '19 at 14:55
1

You can create Custom Action, to speed up the process. Start recording and make selection with "Selection by Color Range". Then make Layer Via Copy and return to previous layer and stop recording. Make sure, that you toggle dialog box for Selection. Also you can add shortcut. I've made a sample of such action. Sample action

| improve this answer | |
  • Hi Kirill. As I understand this will require manually selecting color blobs using the Color Range dialog, my goal is to make this an automated process. – Sergey Kritskiy Nov 4 '19 at 10:38
1

Thanks everyone for their contribution, in the end I went with my original idea and from what I tested it works fastest and most reliable with antialiased images (aka that have >256 colors).

  1. I first put a color sampler (CS) in the left top corner of the image just to have something to work with;
  2. using Color Range I load a selection with a low fuziness, do a quickmask in case the selection is half-transparent;
  3. I cut the layer using the selection and make it invisible;
  4. I select the Original Layer (OL), load a selection based on its transparency, contract and expand it by a set value (to make sure there're no 3-4 px islands);
  5. Then I cinvert a selection to path and use this algorythm to find a point within the polygon of the path;
  6. I move my CP to that position and sample a new color — this gives me a new color for color range;
  7. I repeat the whole thing till there's nothing selectable left on the OL;

Here's some pseudocode:

var colorSample = colorSamplers.add([0,0]);
var shouldContinue = true;

while (shouldContinue)
{
  cutLayer();
}

clean() // deletes color sampler and a path

function cutLayer() {
    colorRange(colorSample.color); // loads a selection based on color from colorSample
    doQuickMask(); // gets rid of large half-transparent areas)
    cut(); // cuts current layer to a new one based on selection from color range
    makeInvisible(); //... and makes it invisible
    selectOriginalLayer(); // selects the original layer
    loadTransparency(); // loads layer non-transparency as selection
    shouldContinue = checkSelection(); // returns false if there's no selection;
    if (!shouldContinue) return; // kills the function
    modifySelection(); // contract and expand selection to get rid of small islands)
    selectionToPath(); // creates a path from selection so that I could get its coordinates
    colorPoint = findPointInsidePolygon(); // returns a point inside the polygon of the selection;
    colorSample.move(colorPoint);
}

and the result:

enter image description here

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.