如何根据一个特定的图像调整大量图像的颜色?

How to adjust the colors of a large number of images based on one spesific?

我有大量照片想要使用相同的 'level' - 相同的颜色/亮度/对比度等。为此,我有一个带有黑白颜色检查器的初始/指南(基本上是带颜色的方块),我将其添加到所有其他照片中。

这是初始/指南https://imgur.com/a/Jlozy1e and these are some of the photos https://imgur.com/JUsKMt2 , https://imgur.com/PvqsleR , https://imgur.com/tcMROU9

正如我所见,所有照片中带有小方块颜色(颜色控制方块)的区域必须具有相同的颜色(十六进制值),以便它们处于同一水平 - 这样我可以获得有意义的数据从下面的地带。

有没有办法在 photoshop 或其他工具中使用自动/批处理方式?

编辑:请注意,可能有比控制方块中的区域更暗/更亮的区域,我想保留它们(相应地得到 lighter/darker 但不完全用阈值颜色替换它们)

我不知道这是否可以通过任何高级工具实现,但这是我对 Photoshop 的看法。这个想法很简单——使用渐变贴图将目标颜色重新映射到源值(因此这不适用于 32 位 tiff):

  1. 来自活动文档(源文档)的样本源颜色;
  2. 求一个带其他文件打开的路径,开始一个一个打开;
  3. 采样目标颜色并获取它们在渐变贴图中的位置
  4. 使用源颜色和目标位置创建渐变贴图

这是我得到的结果:左行是原始文档,上面有一块源方块供参考,右行是应用了渐变图的结果文档,以及来自源文档的相同切片顶部(几乎看不见):

这是我制作的脚本。

请注意,我使用的是您的 png 文件,因此如果您的文件大小不同,您可能需要调整颜色采样器的坐标。

var sampler, sampledColors, sourceCoords, targetCoords;

// defining coordinates to sample 6 colors from the active source-document
sourceCoords = [
    [55, 318],
    [190, 318],
    [310, 318],
    [420, 318],
    [560, 318],
    [690, 318],
];

// defining coordinates to sample target colors from target documents
targetCoords = [
    [78, 120],
    [206, 120],
    [328, 120],
    [453, 120],
    [577, 120],
    [709, 120],
]

// a library
var Utils = Utils ||
{

    // will add photoshop Color Sampler to document
    addSample: function(coord)
    {
        return app.activeDocument.colorSamplers.add(coord);
    },

    // reads color from a Color Sampler
    readSample: function(sample)
    {
        return sample.color;
    },

    // gets a collection of Color Samplers
    getSamplers: function()
    {
        return app.activeDocument.colorSamplers;
    },

    // deletes a Color Sampler
    deleteSample: function(sample)
    {
        sample.remove();
    },

    // RGB > YUV color translation
    rgb2yuv: function(rgb)
    {
        var r = rgb[0] / 255,
            g = rgb[1] / 255,
            b = rgb[2] / 255;

        var y = (r * 0.299) + (g * 0.587) + (b * 0.114);
        var u = (r * -0.14713) + (g * -0.28886) + (b * 0.436);
        var v = (r * 0.615) + (g * -0.51499) + (b * -0.10001);

        return [y, u, v];
    },

    // Linear transformation
    linear: function(X, A, B, C, D, _cut)
    {
        var _cut = _cut !== undefined ? _cut : false;
        var Y = (X - A) / (B - A) * (D - C) + C
        if (_cut)
        {
            if (Y > D) Y = D;
            if (Y < C) Y = C;
        }
        return Y;
    },

    // changes active document color space to RGB
    docToRgb: function()
    {
        var desc16 = new ActionDescriptor();
        desc16.putClass(charIDToTypeID('T   '), charIDToTypeID('RGBM'));
        desc16.putBoolean(charIDToTypeID('Fltt'), false);
        desc16.putBoolean(charIDToTypeID('Rstr'), false);
        executeAction(charIDToTypeID('CnvM'), desc16, DialogModes.NO);
    },

    /**
     * @description Creates a rectangle selection in a specific coordinates with a predefined delta: -7 / +7 to 'coord' values
     * @param  {array}  - [0] is X, [1] is Y coordinates
     *
     * @return nothing
     */
    rectangleSelection: function(coord)
    {
        var delta = 7;
        var descRectangleSelection = new ActionDescriptor();
        var rectSelectionRef = new ActionReference();
        rectSelectionRef.putProperty(charIDToTypeID('Chnl'), charIDToTypeID('fsel'));
        descRectangleSelection.putReference(charIDToTypeID('null'), rectSelectionRef);
        var descCoords = new ActionDescriptor();
        descCoords.putUnitDouble(charIDToTypeID('Top '), charIDToTypeID('#Pxl'), coord[1] - delta);
        descCoords.putUnitDouble(charIDToTypeID('Left'), charIDToTypeID('#Pxl'), coord[0] - delta);
        descCoords.putUnitDouble(charIDToTypeID('Btom'), charIDToTypeID('#Pxl'), coord[1] + delta);
        descCoords.putUnitDouble(charIDToTypeID('Rght'), charIDToTypeID('#Pxl'), coord[0] + delta);
        descRectangleSelection.putObject(charIDToTypeID('T   '), charIDToTypeID('Rctn'), descCoords);
        executeAction(charIDToTypeID('setd'), descRectangleSelection, DialogModes.NO);
    },

    /**
     * @description saves an active document as a TIF file
     * @param  {object} data - .name (without extension) for a name and data.path for a path
     *
     * @return nothing
     */
    saveTIF: function(data)
    {
        if (!new Folder(data.path).exists) new Folder(data.path).create();
        var desc = new ActionDescriptor();
        var descOptions = new ActionDescriptor();
        descOptions.putEnumerated(charIDToTypeID('BytO'), charIDToTypeID('Pltf'), charIDToTypeID('Mcnt'));
        descOptions.putEnumerated(stringIDToTypeID('layerCompression'), charIDToTypeID('Encd'), stringIDToTypeID('RLE'));
        desc.putObject(charIDToTypeID('As  '), charIDToTypeID('TIFF'), descOptions);
        desc.putPath(charIDToTypeID('In  '), new File(data.path + "/" + data.name + ".tif"));
        executeAction(charIDToTypeID('save'), desc, DialogModes.NO);
    },
};

// this will get colors from the source document
var getSamplersData = function(coordinates)
{
    var colors = [];
    var color, sampler;

    // makes sure the doc is in rgb
    Utils.docToRgb();

    // for all coordinates..
    for (var i = 0; i < coordinates.length; i++)
    {
        // create a rectangular selection of 14x14 pixels in the coordinate
        Utils.rectangleSelection(coordinates[i]);

        // average blur it to make sure color sampler samples an average color from noisy square because there's no option for color sample size for Color Samplers
        activeDocument.activeLayer.applyAverage();
        activeDocument.selection.deselect();

        // ads a color sample
        sampler = Utils.addSample(coordinates[i]);

        // reads a color sample
        color = Utils.readSample(sampler);

        // color is added to [colors]
        colors.push(color);
        Utils.deleteSample(sampler);
    }
    return colors;
};

// creates gradient maps for new documents
var setSamplerData = function()
{
    var workFolder;

    var controller = function(originalColors)
    {
        var docs, doc, docSampler, sampledColors, gradientColors;

        try
        {
            docs = getDocs(); // asks for a folder to work with
        }
        catch (e)
        {
            return false;
        }

        // for all found documents...
        for (var i = 0; i < docs.length; i++)
        {
            try
            {
                // opening it and makes sure it's in rgb mode
                doc = openDocument(docs[i]);
            }
            catch (e)
            {
                return false;
            }

            // getting current colors in the color boxes
            sampledColors = getSamplersData(targetCoords);

            // create an array of color for a gradient map using current colors positions and original colors
            gradientColors = createGradientDataFromColors(originalColors, sampledColors);

            // creates a gradient map
            createGradient(gradientColors);

            // saves a file
            Utils.saveTIF(
            {
                path: workFolder + "/export",
                name: activeDocument.name
            });
        }
    };

    /////////////////////////////////////////////////////////////////////////////////////
    // this will as for a folder and will return found docs
    var getDocs = function()
    {
        var docs;

        workFolder = Folder.selectDialog();
        if (workFolder == null) throw 'cancelled';

        docs = workFolder.getFiles('*');

        for (var i = docs.length - 1; i >= 0; i--)
        {

            if (docs[i] instanceof Folder) docs.splice(i, 1);
        }

        if (docs.length == 0) throw 'no files in the folder';

        return docs;
    }; // end of getDocs()

    // opens a doc and makes sure it's in rgb color mode
    var openDocument = function(path)
    {
        var doc;
        try
        {
            doc = app.open(new File(path));
            Utils.docToRgb();
            return doc;
        }
        catch (e)
        {
            alert("can't open " + path + "\nAborting");
            throw e;
        }
    };

    // this will create a gradient map 
    var createGradientDataFromColors = function(original, sampled)
    {
        var colors = [];
        var rgbOriginal, rgbSampled, positionSampled;

        for (var i = 0; i < original.length; i++)
        {
            rgbOriginal = getRGB(original[i]); // get an array of [r,g,b] from SolidColor object
            rgbSampled = getRGB(sampled[i]); // get an array of [r,g,b] from SolidColor object
            positionSampled = Math.round(Utils.rgb2yuv(rgbSampled)[0] * 10000) / 100; // getting positions from the current document colors

            colors.push(
            {
                color: rgbOriginal,
                pos: positionSampled
            });
        }

        return colors;
    }; // end of createGradientDataFromColors()

    // this will convert an rgb from Solid Color to an array of [r, g and b]
    var getRGB = function(color)
    {
        return [color.rgb.red, color.rgb.green, color.rgb.blue];
    }; // end of getRGB()

    // creates a gradient map
    // colors are from the original doc, positions are from the target docs
    var createGradient = function(data)
    {
        var descGradMap = new ActionDescriptor();
        var referenceMap = new ActionReference();
        referenceMap.putClass(charIDToTypeID('AdjL'));
        descGradMap.putReference(charIDToTypeID('null'), referenceMap);
        var desc5 = new ActionDescriptor();
        var desc6 = new ActionDescriptor();
        var desc7 = new ActionDescriptor();

        desc7.putEnumerated(charIDToTypeID('GrdF'), charIDToTypeID('GrdF'), charIDToTypeID('CstS'));
        desc7.putDouble(charIDToTypeID('Intr'), 4096.000000);

        var list1 = new ActionList();
        var el;

        for (var i = 0; i < data.length; i++)
        {
            el = data[i];

            var descTemp = new ActionDescriptor();
            var descColor = new ActionDescriptor();
            descColor.putDouble(charIDToTypeID('Rd  '), el.color[0]);
            descColor.putDouble(charIDToTypeID('Grn '), el.color[1]);
            descColor.putDouble(charIDToTypeID('Bl  '), el.color[2]);
            descTemp.putObject(charIDToTypeID('Clr '), charIDToTypeID('RGBC'), descColor);
            descTemp.putEnumerated(charIDToTypeID('Type'), charIDToTypeID('Clry'), charIDToTypeID('UsrS'));
            descTemp.putInteger(charIDToTypeID('Lctn'), Utils.linear(el.pos, 0, 100, 0, 4096));
            descTemp.putInteger(charIDToTypeID('Mdpn'), 50);
            list1.putObject(charIDToTypeID('Clrt'), descTemp);
        }

        desc7.putList(charIDToTypeID('Clrs'), list1);

        var list2 = new ActionList();
        var desc12 = new ActionDescriptor();
        desc12.putUnitDouble(charIDToTypeID('Opct'), charIDToTypeID('#Prc'), 100.000000);
        desc12.putInteger(charIDToTypeID('Lctn'), 0);
        desc12.putInteger(charIDToTypeID('Mdpn'), 50);
        list2.putObject(charIDToTypeID('TrnS'), desc12);
        var desc13 = new ActionDescriptor();
        desc13.putUnitDouble(charIDToTypeID('Opct'), charIDToTypeID('#Prc'), 100.000000);
        desc13.putInteger(charIDToTypeID('Lctn'), 4096);
        desc13.putInteger(charIDToTypeID('Mdpn'), 50);
        list2.putObject(charIDToTypeID('TrnS'), desc13);
        desc7.putList(charIDToTypeID('Trns'), list2);

        desc6.putObject(charIDToTypeID('Grad'), charIDToTypeID('Grdn'), desc7);
        desc5.putObject(charIDToTypeID('Type'), charIDToTypeID('GdMp'), desc6);

        descGradMap.putObject(charIDToTypeID('Usng'), charIDToTypeID('AdjL'), desc5);
        executeAction(charIDToTypeID('Mk  '), descGradMap, DialogModes.NO);
    };

    return controller;
};

sampledColors = getSamplersData(sourceCoords);

sampler = setSamplerData();
sampler(sampledColors);

我会用 ImageMagick 自动执行此操作,它安装在大多数 Linux 发行版上,可用于 macOS 和 Windows.

首先,我会 运行 一个脚本来从您的校准图像中获取黑白点。这从校准条的黑色和白色端裁剪出 50x50 正方形,并计算它们在 50x50 正方形上的平均值。看起来像这样:

#!/bin/bash

# Check parameters
if [ $# -ne 1 ] ; then
   echo "Usage: calibrate CALIBRATIONIMAGE" >&2 
   exit 1
fi
# Pick up parameter
image=
check="check-$image"

# User-adjustable x and y corrdinates of top-left corner of black and white rectangles
blkx0=660
blky0=300
whtx0=40
whty0=300

# Calculate bottom-right corners of rectangles, given top-left
((blkx1=blkx0+50))
((blky1=blky0+50))
((whtx1=whtx0+50))
((whty1=whty0+50))

# Output a check showing where we got black and white points from
convert "$image" -fill none \
    -stroke red  -draw "rectangle $blkx0,$blky0 $blkx1,$blky1" \
    -stroke blue -draw "rectangle $whtx0,$whty0 $whtx1,$whty1" \
    "$check"

# Output black and white points (as rounded percentages)
blkpt=$(convert "$image" -crop 50x50+$blkx0+$blky0 -format "%[fx:round(mean*100)]" info:)
whtpt=$(convert "$image" -crop 50x50+$whtx0+$whty0 -format "%[fx:round(mean*100)]" info:)

echo "[$image]: Black point: $blkpt, white point: $whtpt. Check image: [$check]"

你会 运行:

./calibrate calibration.png

并获得以下输出:

./calibrate calibration.png
[calibration.png]: Black point: 5, white point: 91. Check image: [check-calibration.png]

所以现在我们知道红色方块的平均亮度是 5,蓝色方块的平均亮度是 91,我们也可以检查方块是从哪里提取的。

现在我们需要将其应用于其他图像。让我们先做一个。 apply 的代码是:

#!/bin/bash

# Check parameters
if [ $# -ne 3 ] ; then
   echo "Usage: apply blackpoint whitepoint image" >&2
   exit 1
fi

# Pick up parameters
newblkpt=
newwhtpt=
image=
newname="corrected-$image"

# User-adjustable x and y coordinates of top-left corner of black and white rectangles
blkx0=670
blky0=100
whtx0=50
whty0=100

# Calculate bottom-right corners of rectangles, given top-left
((blkx1=blkx0+50))
((blky1=blky0+50))
((whtx1=whtx0+50))
((whty1=whty0+50))

# Output a check showing where we got black and white points from
convert "$image" -fill none \
    -stroke red  -draw "rectangle $blkx0,$blky0 $blkx1,$blky1" \
    -stroke blue -draw "rectangle $whtx0,$whty0 $whtx1,$whty1" \
    check-$image.png

# Get current black and white points
blkpt=$(convert "$image" -crop 50x50+$blkx0+$blky0 -format "%[fx:round(mean*100)]" info:)
whtpt=$(convert "$image" -crop 50x50+$whtx0+$whty0 -format "%[fx:round(mean*100)]" info:)

# The following line actually does the entire calibration!
convert "$image" -level ${blkpt},${whtpt}% +level ${newblkpt},${newwhtpt}% "$newname"
echo "[$image]: Black point: $blkpt, white point: $whtpt => [$newname]: Black point: $newblkpt, white point: $newwhtpt"

因此,如果我们 运行 将我们刚刚学到的 5、91 的校准应用到 im1.png,我们得到:

./apply 5 91 im1.png 
[im1.png]: Black point: 4, white point: 71 => [corrected-im1.png]: Black point: 5, white point: 91 

这给了我们这个校正后的图像(白色明显凸起):

这张检查图像显示了我们校准的区域:

那么我们只需要一个循环来处理目录中的所有图像:

for f in *.png ; do
    ./apply 5 91 "$f"
done

这给了我们这些结果:

关键词: ImageMagick, command line, command line, image, image processing, calibrate, calibration, calibration strip, test strip.

请注意,如果您使用 ImageMagick v7 或更新版本,请将两个脚本中的命令 convert 替换为 magick

如果您想使用 Photoshop 执行此操作,您需要通过打开直方图 window 来获取校准图像中黑色校准方块的平均值,然后在黑色方块上绘制一个选取框并注意平均值 (11.89):

然后同样注意白色校准方块的平均值 231:

然后您需要在未校准图像中获得相同的两个值。黑色值为10:

而白色值为180:

现在添加一个色阶调整图层(见绿色区域)并输入上面的值(蓝色区域):

所以,我想您可以创建一个快捷方式,添加一个 色阶调整层 ,其中包含编程中校准图像中的两个值,然后将其批量应用于所有图像。然后您需要为每个特定图像手动添加其他两个值。