按颜色查找像素坐标(颜色选择器控件)

Find pixel coordinates by color (color picker control)

我目前正在尝试修改拾色器控件。一切似乎都按预期进行。但是我希望有可能在初始化时设置“selectedColor”。因此流程如下:

  1. 选择需要的颜色
  2. 将其保存在首选项中
  3. 关闭应用程序
  4. 再次打开应用程序
  5. 颜色选择器在之前选择的颜色上初始化

目前拾取器只考虑指针 X 和 Y 的坐标。这意味着如果我为拾色器控件提供先前选择的颜色,它将无法将指针放在正确的位置,因为它正在等待X、Y 坐标而不是颜色。我有一个解决方法,将所有需要的参数保存到字符串中(颜色十六进制代码,以及 X 和 Y 坐标)。它正在工作,但是这增加了组合字符串然后在 ViewModels 中解析它们的额外复杂性。

我已经熟悉了读取像素、搜索所需颜色并获取其坐标的可能性。这里有一些问题:

  1. 用于读取像素的循环迭代正在冻结 UI,尤其是对于较大的颜色选择器(大图像)
  2. 并不总是提供正确的坐标
  3. 在初始化期间,黑白颜色 #00000000 和 #FFFFFFFF 出现问题。所以我将它们添加到 if 方法中。看起来在颜色选择器实际生成之前图像是黑白的?这在实际情况下当然不是一个好的解决方案,因为选择的颜色可以是白色或黑色:

这里是Color Picker控件的OnPaintSurface方法(可以在最下面的方法this.GetPixelCoordinates(bitmap)中看到;被注释掉了):

protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
{
  SKImageInfo skImageInfo = e.Info;
  SKSurface skSurface = e.Surface;
  this.SKCanvas = skSurface.Canvas;

  int skCanvasWidth = skImageInfo.Width;
  int skCanvasHeight = skImageInfo.Height;

  this.SKCanvas.Clear();

  // Draw gradient rainbow Color spectrum
  using (SKPaint paint = new SKPaint())
  {
    paint.IsAntialias = true;

    System.Collections.Generic.List<SKColor> colors = new System.Collections.Generic.List<SKColor>();
    this.ColorList.ForEach((color) => { colors.Add(Color.FromHex(color).ToSKColor()); });

    // create the gradient shader between Colors
    using (SKShader shader = SKShader.CreateLinearGradient(
        new SKPoint(0, 0),
        this.ColorListDirection == ColorListDirection.Horizontal ?
            new SKPoint(skCanvasWidth, 0) : new SKPoint(0, skCanvasHeight),
        colors.ToArray(),
        null,
        SKShaderTileMode.Clamp))
    {
      paint.Shader = shader;
      this.SKCanvas.DrawPaint(paint);
    }
  }

  // Draw darker gradient spectrum
  using (SKPaint paint = new SKPaint())
  {
    paint.IsAntialias = true;

    // Initiate the darkened primary color list
    SKColor[] colors = GetGradientOrder();

    // create the gradient shader 
    using (SKShader shader = SKShader.CreateLinearGradient(
        new SKPoint(0, 0),
        this.ColorListDirection == ColorListDirection.Horizontal ?
            new SKPoint(0, skCanvasHeight) : new SKPoint(skCanvasWidth, 0),
        colors,
        null,
        SKShaderTileMode.Clamp))
    {
      paint.Shader = shader;
      this.SKCanvas.DrawPaint(paint);
    }
  }

  // Picking the Pixel Color values on the Touch Point

  // Represent the color of the current Touch point
  SKColor touchPointColor;

  // Efficient and fast
  // https://forums.xamarin.com/discussion/92899/read-a-pixel-info-from-a-canvas
  // create the 1x1 bitmap (auto allocates the pixel buffer)
  using (SKBitmap bitmap = new SKBitmap(skImageInfo))
  {
    // get the pixel buffer for the bitmap
    IntPtr dstpixels = bitmap.GetPixels();

    // read the surface into the bitmap
    skSurface.ReadPixels(skImageInfo,
        dstpixels,
        skImageInfo.RowBytes,
        (int)this.SelectedPoint.X,
        (int)this.SelectedPoint.Y);

    // access the color
    touchPointColor = bitmap.GetPixel(0, 0);

    //this.GetPixelCoordinates(bitmap);

    //bitmap.SetPixel(50, 50, this.PickedColor.ToSKColor());
  }

这是 GetPixelCoordinates 方法:

private void GetPixelCoordinates(SKBitmap bitmap)
{
  if (bitmap == null)
  {
    return;
  }

  for (int x = 0; x < bitmap.Width; x++)
  {
    for (int y = 0; y < bitmap.Height; y++)
    {
      SKColor pixelColor = bitmap.GetPixel(x, y);

      if (this.PickedColor.ToSKColor() == pixelColor 
        && this.PickedColor.ToSKColor() != Color.FromHex("#00000000").ToSKColor() 
        && this.PickedColor.ToSKColor() != Color.FromHex("#FFFFFFFF").ToSKColor())
      {
        //this.SelectedPoint = new Point(x, y);
        Debug.WriteLine(String.Format("Color: {0} | Coordinate: {1} {2}", pixelColor, x, y));
      }
    }
  }
}

这里是 PickedColor 属性:

public static readonly BindableProperty PickedColorProperty
  = BindableProperty.Create(
    propertyName: nameof(PickedColor),
    returnType: typeof(Color),
    declaringType: typeof(ColorPicker),
    defaultValue: Color.Green,
    defaultBindingMode: BindingMode.TwoWay,
    propertyChanged: OnColorChanged);

private static void OnColorChanged(BindableObject bindable, object oldValue, object newValue)
{
  ColorPicker control = (ColorPicker)bindable;
  control.PickedColor = (Color)newValue;
}

/// <summary>
/// Set the Color Spectrum Gradient Style
/// </summary>
public GradientColorStyle GradientColorStyle
{
  get { return (GradientColorStyle)GetValue(GradientColorStyleProperty); }
  set { SetValue(GradientColorStyleProperty, value); }
}

public static readonly BindableProperty ColorListProperty
  = BindableProperty.Create(
        propertyName: nameof(ColorList),
        returnType: typeof(string[]),
        declaringType: typeof(ColorPicker),
        defaultValue: new string[]
        {
          new Color(255, 0, 0).ToHex(), // Red
                new Color(255, 255, 0).ToHex(), // Yellow
                new Color(0, 255, 0).ToHex(), // Green (Lime)
                new Color(0, 255, 255).ToHex(), // Aqua
                new Color(0, 0, 255).ToHex(), // Blue
                new Color(255, 0, 255).ToHex(), // Fuchsia
                new Color(255, 0, 0).ToHex(), // Red
        },
        defaultBindingMode: BindingMode.OneTime, null);

我的问题是:生成带有参数的字符串是唯一的方法吗(彩色十六进制代码,以及 X 和 Y 坐标)?是否有可能以某种有效的方式通过提供的颜色将指针放置在控件初始化上,而无需不断循环迭代和冻结 UI?

调色板:

Whosebug 专为每个 post 提出和回答一个编码问题而设计。
(要问第二个问题,请创建一个新的 post。在 post 中包括理解该问题所需的基本细节。Link 回到这个问题,所以你没有重复提供额外 background/context 的信息。)

您的主要问题可以表述为:

Given: A color palette [see picture] generated by [see OnPaintSurface code, starting at // Draw gradient rainbow Color spectrum. How calculate (x,y) coordinates that correspond to a given color?


首先,一个观察。该 2D 调色板给出了 3 个颜色轴中的 2 个。您需要一个单独的“饱和度”滑块,以允许选择任何颜色。

您显示的调色板是“HSV”颜色模型的近似值。
wiki HSL-HSV models 中,单击右侧的图表。您的调色板看起来像标有 S(HSV) = 1 的矩形。

色调 + 饱和度 + 明度。
您的 ColorList 应具有最大值的完全饱和颜色。
沿着屏幕向下,调色板将值降低到接近零。


这是回答的开始。

需要的是与绘制的内容相对应的数学公式。
让我们看看那个矩形图像是如何生成的。

重命名颜色列表以便于使用。 存储为字段,以便以后使用。使用生成 SkColors 的原始 Colors,以便于操作。

    private List<Color> saturatedColors;
    private List<Color> darkenedColors;
    private int nColors => saturatedColors.Count;
    private int maxX, maxY;   // From your UI rectangle.

顶行 (y=0) saturatedColors,在 x 上均匀 spaced。

底行 (y=maxY) darkenedColors,在 x 上均匀 spaced。

像素颜色从顶行到底行线性插值。

目标是找到最接近给定颜色“Color goalColor”的像素。

考虑每个高而细的矩形,其角是两个 topColors 和相应的两个 bottomColors。目标是找到哪个矩形包含 goalColor,然后找到该矩形内最接近 goalColor 的像素。

最棘手的部分是“比较”颜色,以确定一种颜色何时“介于”两种颜色之间。这在 RGB 中很难做到;将颜色转换为 HSV 以匹配您正在使用的调色板。参见 Greg's answer - ColorToHSV

如果你做一个 HSV 就更容易了 class:

using System;
using System.Collections.Generic;
using System.Linq;
// OR could use System.Drawing.Color.
using Color = Xamarin.Forms.Color;
...
    public class HSV
    {
        #region --- static ---
        public static HSV FromColor(Color color)
        {
            ColorToHSV(color, out double hue, out double saturation, out double value);
            return new HSV(hue, saturation, value);
        }

        public static List<HSV> FromColors(IEnumerable<Color> colors)
        {
            return colors.Select(color => FromColor(color)).ToList();
        }

        const double Epsilon = 0.000001;

        // returns Tuple<int colorIndex, double wgtB>.
        public static Tuple<int, double> FindHueInColors(IList<HSV> colors, double goalHue)
        {
            int colorIndex;
            double wgtB = 0;
            // "- 1": because each iteration needs colors[colorIndex+1].
            for (colorIndex = 0; colorIndex < colors.Count - 1; colorIndex++)
            {
                wgtB = colors[colorIndex].WgtFromHue(colors[colorIndex + 1], goalHue);
                // Epsilon compensates for possible round-off error in WgtFromHue.
                // To ensure the color is considered within one of the ranges.
                if (wgtB >= 0 - Epsilon && wgtB < 1)
                    break;
            }

            return new Tuple<int, double>(colorIndex, wgtB);
        }

        // From 
        public static void ColorToHSV(Color color, out double hue, out double saturation, out double value)
        {
            int max = Math.Max(color.R, Math.Max(color.G, color.B));
            int min = Math.Min(color.R, Math.Min(color.G, color.B));

            hue = color.GetHue();
            saturation = (max == 0) ? 0 : 1d - (1d * min / max);
            value = max / 255d;
        }
        // From 
        public static Color ColorFromHSV(double hue, double saturation, double value)
        {
            int hi = Convert.ToInt32(Math.Floor(hue / 60)) % 6;
            double f = hue / 60 - Math.Floor(hue / 60);

            value = value * 255;
            int v = Convert.ToInt32(value);
            int p = Convert.ToInt32(value * (1 - saturation));
            int q = Convert.ToInt32(value * (1 - f * saturation));
            int t = Convert.ToInt32(value * (1 - (1 - f) * saturation));

            if (hi == 0)
                return Color.FromArgb(255, v, t, p);
            else if (hi == 1)
                return Color.FromArgb(255, q, v, p);
            else if (hi == 2)
                return Color.FromArgb(255, p, v, t);
            else if (hi == 3)
                return Color.FromArgb(255, p, q, v);
            else if (hi == 4)
                return Color.FromArgb(255, t, p, v);
            else
                return Color.FromArgb(255, v, p, q);
        }
        #endregion


        public double H { get; set; }
        public double S { get; set; }
        public double V { get; set; }

        // c'tors
        public HSV()
        {
        }
        public HSV(double h, double s, double v)
        {
            H = h;
            S = s;
            V = v;
        }

        public Color ToColor()
        {
            return ColorFromHSV(H, S, V);
        }

        public HSV Lerp(HSV b, double wgtB)
        {
            return new HSV(
                MathExt.Lerp(H, b.H, wgtB),
                MathExt.Lerp(S, b.S, wgtB),
                MathExt.Lerp(V, b.V, wgtB));
        }

        // Returns "wgtB", such that goalHue = Lerp(H, b.H, wgtB).
        // If a and b have same S and V, then this is a measure of
        // how far to move along segment (a, b), to reach goalHue.
        public double WgtFromHue(HSV b, double goalHue)
        {
            return MathExt.Lerp(H, b.H, goalHue);
        }
        // Returns "wgtB", such that goalValue = Lerp(V, b.V, wgtB).
        public double WgtFromValue(HSV b, double goalValue)
        {
            return MathExt.Lerp(V, b.V, goalValue);
        }
    }

    public static class MathExt
    {
        public static double Lerp(double a, double b, double wgtB)
        {
            return a + (wgtB * (b - a));
        }

        // Converse of Lerp:
        // returns "wgtB", such that
        //   result == lerp(a, b, wgtB)
        public static double WgtFromResult(double a, double b, double result)
        {
            double denominator = b - a;

            if (Math.Abs(denominator) < 0.00000001)
            {
                if (Math.Abs(result - a) < 0.00000001)
                    // Any value is "valid"; return the average.
                    return 0.5;

                // Unsolvable - no weight can return this result.
                return double.NaN;
            }

            double wgtB = (result - a) / denominator;
            return wgtB;
        }
    }

用法:

    public static class Tests {
        public static void TestFindHueInColors(List<Color> saturatedColors, Color goalColor)
        {
            List<HSV> hsvColors = HSV.FromColors(saturatedColors);
            HSV goalHSV = HSV.FromColor(goalColor);
            var hueAt = HSV.FindHueInColors(hsvColors, goalHSV.H);
            int colorIndex = hueAt.Item1;
            double wgtB = hueAt.Item2;
            // ...
        }
    }

这就是方法的本质。从 colorIndexnColorswgtBmaxX,可以计算出 x。我建议编写几个测试用例,以弄清楚如何做到这一点。

计算y就简单多了。应该可以使用 goalHSV.VmaxY.

如您所见,这对代码而言并非易事。

最重要的几点:

  • 转换为 HSV 颜色 space。
  • 调色板由又高又细的矩形组成。每个矩形的顶部两个角都有两种饱和颜色 max-value:(H1, 1.0, 1.0) 和 (H2, 1.0, 1.0)。底部两个角的色调和饱和度相同,但值较小。也许是 (H1, 1.0, 0.01) 和 (H2, 1.0, 0.01)。将实际变暗的值转换为 HSV,以查看确切的值。
  • 找出哪个 H 的 goalHSV 介于两者之间。
  • 了解“线性插值”(“Lerp”)。在该矩形中,顶部边缘是两种饱和颜色之间的 Lerp,侧边缘是从亮色到相应暗色的 Lerp。

如果上面的数学题太复杂了,那就用其中一个矩形画一个盒子。也就是说,使顶部列表中只有两种颜色的渐变。尝试在该矩形内定位颜色像素。

重要提示:可能没有一个像素与您开始使用的颜色完全相同。找出哪个像素最接近该颜色。
如果您不确定自己是否拥有“最佳”像素,请读取附近的几个像素,确定哪个像素“最接近”。即var error = (r2-r1)*(r2-r1) + (g2-g1)*(g2-g1) + (b2-b1)*(b2-b1);.

哪个最小