Стереть часть растрового изображения другим растровым изображением

Позвольте мне предварить это реальным продуктом; Вы, возможно, помните, что в начальной школе у ​​них была бумага для заметок, которая в основном состояла из листа бумаги радужного цвета с черной пленкой сверху. Вы берете острый предмет и снимаете черную пленку, чтобы обнажить цветную бумагу.

Я пытаюсь сделать то же самое, используя изображения в окне изображения.

Моя идея состоит из следующих вещей:

  • Текстурированное изображение.
  • Черный прямоугольник размером с картинку.
  • Изображение круга.

Чего я пытаюсь добиться, так это открыть программу, нарисовать изображение в окне изображения с черным прямоугольником поверх него. При нажатии на поле изображения он использует круг, чтобы инвертировать альфу прямоугольника, где я нажимаю, используя круг в качестве ссылки.

  • Моя проблема. Я не могу найти способ стереть (установить прозрачность) часть черного прямоугольника, на который я нажимаю.

На всю жизнь я не знаю ни одного способа вырезать окно в изображении. Это похоже на обратную обрезку, когда я сохраняю внешние элементы, а не внутренние, открывая текстурированное изображение ниже.

Разве WinForms не может этого сделать? Я сумасшедший? Должен ли я просто сдаться?

Я должен упомянуть, что я предпочитаю не изменять альфа-канал на пиксель за пикселем. Это слишком сильно замедлило бы программу, чтобы ее можно было использовать в качестве псевдо-рисовальщика. Однако, если это единственный способ, не стесняйтесь показывать.

Вот изображение того, чего я пытаюсь достичь:

введите здесь описание изображения


person DJ Schrecker    schedule 03.09.2015    source источник
comment
Windows Forms, вероятно, не поможет вам реализовать это, хотя вы всегда можете создать собственный элемент управления. WPF может предоставить вам лучшие инструменты для работы, хотя я не уверен в этой конкретной задаче.   -  person Eric J.    schedule 03.09.2015


Ответы (1)


Это не очень сложно:

  • Установите цветное изображение как PictureBox BackgroundImage.
  • Установите черное изображение как его Image.
  • И рисуйте на изображении, используя обычные события мыши и прозрачный Pen..

введите здесь описание изображения

Нам нужен список точек для использования DrawCurve:

List<Point> currentLine = new List<Point>();

Нам нужно подготовить и очистить черный слой:

private void ClearSheet()
{
    if (pictureBox1.Image != null) pictureBox1.Image.Dispose();
    Bitmap bmp = new Bitmap(pictureBox1.ClientSize.Width, pictureBox1.ClientSize.Height);
    using (Graphics G = Graphics.FromImage(bmp)) G.Clear(Color.Black);
    pictureBox1.Image = bmp;
    currentLine.Clear();
}

private void cb_clear_Click(object sender, EventArgs e)
{
    ClearSheet();
}

Чтобы рисовать в Image, нам нужно использовать связанный объект Graphics..:

void drawIntoImage()
{
    using (Graphics G = Graphics.FromImage(pictureBox1.Image))
    {
        // we want the tranparency to copy over the black pixels
        G.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceCopy;
        G.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
        G.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;

        using (Pen somePen = new Pen(Color.Transparent, penWidth))
        {
            somePen.MiterLimit = penWidth / 2;
            somePen.EndCap = System.Drawing.Drawing2D.LineCap.Round;
            somePen.LineJoin = System.Drawing.Drawing2D.LineJoin.Round;
            somePen.StartCap = System.Drawing.Drawing2D.LineCap.Round;
            if (currentLine.Count > 1)
                G.DrawCurve(somePen, currentLine.ToArray());
        }

    }
    // enforce the display:
    pictureBox1.Image = pictureBox1.Image;
}

Обычные события мыши:

private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
    currentLine.Add(e.Location);
}

private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
    if (e.Button == System.Windows.Forms.MouseButtons.Left)
    {
        currentLine.Add(e.Location);
        drawIntoImage();
    }  
}

private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
{
    currentLine.Clear();
}

Это все, что нужно. Обязательно сохраните SizeMode = Normal PB, иначе пиксели не будут совпадать..!

Обратите внимание, что есть несколько проблем, когда вы хотите получить мягкие края, больше инструментов рисования, позволить простому щелчку рисовать точку или отмену или другие более мелкие детали для работы. Но основы совсем не сложные..

Кстати, изменение Alpha никаким не отличается от изменения цветовых каналов.

В качестве альтернативы вы можете поиграть с TextureBrush:

TextureBrush brush = new TextureBrush(pictureBox1.BackgroundImage);

using (Pen somePen = new Pen(brush) )
{
  // basically 
  // the same drawing code.. 
}

Но я обнаружил, что это довольно медленно.

Обновление:

Использование файла png в качестве пользовательской подсказки немного сложнее; основная причина в том, что рисунок перевернут: мы не хотим рисовать пиксели, мы хотим их очистить. GDI+ не поддерживает такие режимы композиции, поэтому нам нужно сделать это в коде.

Чтобы быть быстрым, мы используем два трюка: LockBits будет настолько быстрым, насколько это возможно, и ограничение области нашим пользовательским кончиком кисти предотвратит трату времени.

Предположим, у вас есть файл, который нужно использовать, и загрузите его в растровое изображение:

string stampFile = @"yourStampFile.png";
Bitmap stamp = null;

private void Form1_Load(object sender, EventArgs e)
{
    stamp = (Bitmap) Bitmap.FromFile(stampFile);
}

Теперь нам нужна новая функция, чтобы нарисовать его в нашем Image; вместо DrawCurve нам нужно использовать DrawImage:

void stampIntoImage(Point pt)
{
    Point point =  new Point(pt.X - stamp.Width / 2, pt.Y - stamp.Height / 2);
    using (Bitmap stamped = new Bitmap(stamp.Width, stamp.Height) )
    {
        using (Graphics G = Graphics.FromImage(stamped))
        {
            stamp.SetResolution(stamped.HorizontalResolution, stamped.VerticalResolution);
            G.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceOver;
            G.DrawImage(pictureBox1.Image, 0, 0, 
                        new Rectangle(point, stamped.Size), GraphicsUnit.Pixel);
            writeAlpha(stamped, stamp);
        }
        using (Graphics G = Graphics.FromImage(pictureBox1.Image))
        {
            G.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceCopy;
            G.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
            G.CompositingQuality = 
               System.Drawing.Drawing2D.CompositingQuality.HighQuality;
            G.DrawImage(stamped, point);
        }
    }
    pictureBox1.Image = pictureBox1.Image;
}

Несколько замечаний: я обнаружил, что не должен делать явное SetResolution, так как файл штампа, который я отфотошопил, был 72dpi, а растровые изображения по умолчанию в моей программе были 120dpi. Обратите внимание на эти различия!

Я начинаю отрисовку растрового изображения, копируя правую часть текущего изображения.

Затем я вызываю быструю процедуру, которая применяет к ней альфу штампа:

void writeAlpha(Bitmap target, Bitmap source)
{
   // this method assumes the bitmaps both are 32bpp and have the same size
    int Bpp = 4;  
    var bmpData0 = target.LockBits(
                    new Rectangle(0, 0, target.Width, target.Height),
                    ImageLockMode.ReadWrite, target.PixelFormat);
    var bmpData1 = source.LockBits(
                    new Rectangle(0, 0, source.Width, source.Height),
                    ImageLockMode.ReadOnly, source.PixelFormat);

    int len = bmpData0.Height * bmpData0.Stride;
    byte[] data0 = new byte[len];
    byte[] data1 = new byte[len];
    Marshal.Copy(bmpData0.Scan0, data0, 0, len);
    Marshal.Copy(bmpData1.Scan0, data1, 0, len);

    for (int i = 0; i < len; i += Bpp)
    {
        int tgtA = data0[i+3];        // opacity
        int srcA = 255 - data1[i+3];  // transparency
        if (srcA > 0) data0[i + 3] = (byte)(tgtA < srcA ? 0 : tgtA - srcA);
    }
    Marshal.Copy(data0, 0, bmpData0.Scan0, len);
    target.UnlockBits(bmpData0);
    source.UnlockBits(bmpData1);
}

Я использую простое правило: уменьшите непрозрачность цели на прозрачность источника и убедитесь, что мы не получаем отрицательное значение. Вы можете поиграть с этим.

Теперь все, что нам нужно, это адаптировать MouseMove; для своих тестов я добавил два RadioButtons для переключения между оригинальной круглой ручкой и пользовательским наконечником штампа:

private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
    if (e.Button == System.Windows.Forms.MouseButtons.Left)
    {
        if (rb_pen.Checked)
        {
            currentLine.Add(e.Location);
            drawIntoImage();
        }
        else if (rb_stamp.Checked) { stampIntoImage(e.Location); };
    }
}

Я не использовал рыбу, но вы можете видеть мягкие края:

введите здесь описание изображения

Обновление 2. Вот MouseDown, позволяющий выполнять простые клики:

private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
   if (rb_pen.Checked) currentLine.Add(e.Location);
   else if (rb_stamp.Checked)
   {
       { stampIntoImage(e.Location); };
   }
}
person TaW    schedule 03.09.2015
comment
Это почти все, что я искал, за одним исключением. Есть ли способ использовать изображение PNG в качестве кончика кисти? Например, используйте кисть в форме рыбы для вырезания вместо круглого пера. Я бы согласился на то, чтобы это был просто вырез для каждого щелчка мыши. - person DJ Schrecker; 03.09.2015
comment
Это потрясающе. Большое спасибо за помощь. Я мог бы обнять тебя прямо сейчас. - person DJ Schrecker; 03.09.2015