How to automate isolating line art from the background with code. Is there a way?

时间秒杀一切 提交于 2020-02-02 12:59:06

问题


This is what I like to do with code. Nothing is manually done in the process with Photoshop, so I think there is a way? but can't quite figure it out.


This is what I did in Python:

from PIL import Image
im_rgb = Image.open('lena.jpg')
im_a = Image.open('frame.png').convert('L').resize(im_rgb.size)
im_rgba = im_rgb.copy()
im_rgba.putalpha(im_a)
im_rgba.save('final.png')

but I'm looking for a solution in Java/Kotlin on Android Studio while I could live with a sample in Dart or C++ as well.


回答1:


from PIL import Image
im_rgb = Image.open('lena.jpg')
im_a = Image.open('frame.png').convert('L').resize(im_rgb.size)
im_rgba = im_rgb.copy()
im_rgba.putalpha(im_a)
im_rgba.save('final.png')

`

I figured it out by myself on Python. but it is not really as complete as I wanted it to be initially. I would still like to know how to do it with Java/Kotlin on Android Studio or with C++ or Dart.




回答2:


What OP described, I know from GIMP where it is called Color to Alpha.

While I used that command from time to time I tried to imagine how this could be implemented.

Multiple approaches come into my mind:

  • a very simple: compare every pixel with a pivot color and set alpha to 0 in case of match
  • a threshold-based: determine the Euclidean distance of pixel to pivot color in RGB space (as 3D space) and set alpha according to distance when under a given threshold
  • threshold-based in HSV space: the similar approach like above but applied to HSV space (for better color matching).

Out of curiosity, I wrote a sample application to try this out.

First a C++ code for color to alpha transformation:

imageColorToAlpha.h:

#ifndef IMAGE_COLOR_TO_ALPHA_H
#define IMAGE_COLOR_TO_ALPHA_H

// standard C++ header:
#include <cstdint>
#include <functional>

// convenience types
typedef std::uint32_t Pixel;
typedef std::uint8_t Comp;

// convenience constants
const int ShiftR = 16;
const int ShiftG = 8;
const int ShiftB = 0;
const int ShiftA = 24;

const Pixel MaskR = 0xff << ShiftR;
const Pixel MaskG = 0xff << ShiftG;
const Pixel MaskB = 0xff << ShiftB;
const Pixel MaskA = 0xff << ShiftA;

const Pixel MaskRGB = MaskR | MaskG | MaskB;

// convenience functions
inline Comp getR(Pixel pixel) { return Comp(pixel >> ShiftR); }
inline Comp getG(Pixel pixel) { return Comp(pixel >> ShiftG); }
inline Comp getB(Pixel pixel) { return Comp(pixel >> ShiftB); }
inline Comp getA(Pixel pixel) { return Comp(pixel >> ShiftA); }

inline void setR(Pixel &pixel, Comp r)
{
  pixel &= ~MaskR;
  pixel |= r << ShiftR;
}
inline void setG(Pixel &pixel, Comp g)
{
  pixel &= ~MaskG;
  pixel |= g << ShiftG;
}
inline void setB(Pixel &pixel, Comp b)
{
  pixel &= ~MaskB;
  pixel |= b << ShiftB;
}
inline void setA(Pixel &pixel, Comp r)
{
  pixel &= ~MaskA;
  pixel |= r << ShiftA;
}
inline void set(Pixel &pixel, Comp r, Comp g, Comp b)
{
  pixel &= ~MaskRGB;
  pixel |= r << ShiftR | g << ShiftG | b << ShiftB;
}
inline void set(Pixel &pixel, Comp r, Comp g, Comp b, Comp a)
{
  pixel = r << ShiftR | g << ShiftG | b << ShiftB | a << ShiftA;
}

extern void transformImage(
  size_t w, size_t h, // width and height
  size_t bytesPerRow, // bytes per row (to handle row alignment)
  const Pixel *imgSrc, // source image
  Pixel *imgDst, // destination image
  std::function<Pixel(Pixel)> transform);

// color to alpha (very simple)
extern Pixel colorToAlpha(Pixel pixel, Pixel color);

// color to alpha (with threshold)
extern Pixel colorToAlpha(
  Pixel pixel, Pixel color, unsigned threshold);

// convenience functions for image

inline void colorToAlphaSimple(
  size_t w, size_t h, // width and height
  size_t bytesPerRow, // bytes per row (to handle row alignment)
  const Pixel *imgSrc, // source image
  Pixel *imgDst, // destination image
  Pixel color) // pivot color
{
  transformImage(w, h, bytesPerRow, imgSrc, imgDst,
    [color](Pixel pixel) { return colorToAlpha(pixel, color); });
}

inline void colorToAlphaThreshold(
  size_t w, size_t h, // width and height
  size_t bytesPerRow, // bytes per row (to handle row alignment)
  const Pixel *imgSrc, // source image
  Pixel *imgDst, // destination image
  Pixel color, // pivot color
  unsigned threshold) // threshold
{
  transformImage(w, h, bytesPerRow, imgSrc, imgDst,
    [color, threshold](Pixel pixel) {
      return colorToAlpha(pixel, color, threshold);
    });
}

inline void fillRGB(
  size_t w, size_t h, // width and height
  size_t bytesPerRow, // bytes per row (to handle row alignment)
  Pixel *img, // image to modify
  Pixel color) // fill color (alpha ignored)
{
  color &= MaskRGB;
  transformImage(w, h, bytesPerRow, img, img,
    [color](Pixel pixel) {
      pixel &= ~MaskRGB; pixel |= color; return pixel;
    });
}

#endif // IMAGE_COLOR_TO_ALPHA_H

and the corresponding imageColorToAlpha.cc:

// standard C++ header:
#include <cmath>

// header of this module:
#include "imageColorToAlpha.h"

void transformImage(
  size_t w, size_t h, // width and height
  size_t bytesPerRow, // bytes per row (to handle row alignment)
  const Pixel *imgSrc, // source image
  Pixel *imgDst, // destination image
  std::function<Pixel(Pixel)> transform)
{
  for (size_t y = 0; y < h; ++y) {
    const Pixel *pixelSrc = (const Pixel*)((const Comp*)imgSrc + y * bytesPerRow);
    Pixel *pixelDst = (Pixel*)((Comp*)imgDst + y * bytesPerRow);
    for (size_t x = 0; x < w; ++x) pixelDst[x] = transform(pixelSrc[x]);
  }
}

Pixel colorToAlpha(Pixel pixel, Pixel color)
{
  // eliminate current alpha values from pixel and color
  pixel &= MaskRGB; color &= MaskRGB;
  // compare pixel with color
  const int match = pixel == color;
  // set alpha according to match of pixel and color
  setA(pixel, ~(match * 0xff));
  // done
  return pixel;
}

Pixel colorToAlpha(Pixel pixel, Pixel color, unsigned threshold)
{
  // delta values per component
  const int dR = (int)getR(pixel) - (int)getR(color);
  const int dG = (int)getG(pixel) - (int)getG(color);
  const int dB = (int)getB(pixel) - (int)getB(color);
  // square Euclidean distance
  const unsigned dSqr = dR * dR + dG * dG + dB * dB;
  // compute alpha
  Comp a = 0xff;
  if (dSqr < threshold * threshold) { // distance below threshold?
    // compute alpha weighted by distance
    const double d = sqrt((double)dSqr);
    const double f = d / threshold;
    a = (Comp)(f * 0xff);
  }
  // done
  setA(pixel, a);
  return pixel;
}

This image manipulation code is based on the C++ std library only. This is intended to make the code as exemplary and re-usable as possible.

However, the code for decoding image file formats is often neither short nor simple. Hence, I wrote a wrapper application in Qt to show this in action. Qt provides image support as well as the frame work for a desktop application and seemed to me as most appropriate for this task (beside of the fact that I've some experience with it).

The Qt wrapper application testQImageColorToAlpha.cc:

// Qt header:
#include <QtWidgets>

// own header:
#include "imageColorToAlpha.h"
#include "qColorButton.h"

// convenience functions
QPixmap fromImage(const QImage &qImg)
{
  QPixmap qPixmap;
  qPixmap.convertFromImage(qImg);
  return qPixmap;
}

QPixmap fromAlphaImage(
  const QImage &qImg,
  QColor qColor1 = Qt::darkGray,
  QColor qColor2 = Qt::gray,
  int whCell = 32)
{
  QPixmap qPixmap(qImg.width(), qImg.height());
  { QPainter qPainter(&qPixmap);
    // draw chessboard
    qPixmap.fill(qColor1);
    for (int y = 0; y < qImg.height(); y += 2 * whCell) {
      for (int x = 0; x < qImg.width(); x += 2 * whCell) {
        qPainter.fillRect(x, y, whCell, whCell, qColor2);
        qPainter.fillRect(x + whCell, y + whCell, whCell, whCell, qColor2);
      }
    }
    // overlay with image
    qPainter.drawImage(0, 0, qImg);
  } // close Qt painter
  // done
  return qPixmap;
}

enum {
  SingleValue,
  RGBRange
};

QImage colorToAlphaSimple(
  const QImage &qImgSrc, QColor qColor,
  bool fill, QColor qColorFill)
{
  // ensure expected format for input image
  QImage qImg = qImgSrc.convertToFormat(QImage::Format_ARGB32);
  const int w = qImg.width(), h = qImg.height(), bpr = qImg.bytesPerLine();
  // allocate storage for output image
  QImage qImgDst(w, h, QImage::Format_ARGB32);
  colorToAlphaSimple(w, h, bpr,
    (const Pixel*)qImg.constBits(), (Pixel*)qImgDst.bits(), qColor.rgba());
  // override RGB if required
  if (fill) fillRGB(w, h, bpr, (Pixel*)qImgDst.bits(), qColorFill.rgba());
  // done
  return qImgDst;
}

QImage colorToAlphaThreshold(
  const QImage &qImgSrc, QColor qColor, unsigned threshold,
  bool fill, QColor qColorFill)
{
  // ensure expected format for input image
  QImage qImg = qImgSrc.convertToFormat(QImage::Format_ARGB32);
  const int w = qImg.width(), h = qImg.height(), bpr = qImg.bytesPerLine();
  // allocate storage for output image
  QImage qImgDst(w, h, QImage::Format_ARGB32);
  colorToAlphaThreshold(w, h, bpr,
    (const Pixel*)qImg.constBits(), (Pixel*)qImgDst.bits(), qColor.rgba(), threshold);
  // override RGB if required
  if (fill) fillRGB(w, h, bpr, (Pixel*)qImgDst.bits(), qColorFill.rgba());
  // done
  return qImgDst;
}

// main application
int main(int argc, char **argv)
{
  qDebug() << "Qt Version:" << QT_VERSION_STR;
  QApplication app(argc, argv);
  // setup data
  QImage qImgIn("cat.drawn.png");
  QImage qImgOut(qImgIn);
  // setup GUI
  // main window
  QWidget qWin;
  qWin.setWindowTitle(QString::fromUtf8("Color to Alpha"));
  QGridLayout qGrid;
  // input image
  QHBoxLayout qHBoxLblIn;
  QLabel qLblIn(QString::fromUtf8("Input Image:"));
  qHBoxLblIn.addWidget(&qLblIn);
  qHBoxLblIn.addStretch(1);
  QPushButton qBtnLoad(QString::fromUtf8("Open..."));
  qHBoxLblIn.addWidget(&qBtnLoad);
  qGrid.addLayout(&qHBoxLblIn, 0, 0);
  QLabel qLblImgIn;
  qLblImgIn.setPixmap(fromImage(qImgIn));
  qGrid.addWidget(&qLblImgIn, 1, 0);
  // config. color to alpha
  QGroupBox qBoxCfg(QString::fromUtf8("Configuration:"));
  QFormLayout qFormCfg;
  QComboBox qMenuColorToAlpha;
  qMenuColorToAlpha.addItem(QString::fromUtf8("Single Value"));
  qMenuColorToAlpha.addItem(QString::fromUtf8("With Threshold"));
  qFormCfg.addRow(QString::fromUtf8("Color to Transparency:"), &qMenuColorToAlpha);
  QColorButton qBtnColor(Qt::white);
  qFormCfg.addRow(QString::fromUtf8("Pivot Color:"), &qBtnColor);
  qBoxCfg.setLayout(&qFormCfg);
  QSpinBox qEditRange;
  qEditRange.setRange(1, 255);
  qFormCfg.addRow(QString::fromUtf8("Range:"), &qEditRange);
  QFrame qHSepCfg;
  qHSepCfg.setFrameStyle(QFrame::HLine | QFrame::Plain);
  qFormCfg.addRow(&qHSepCfg);
  QHBoxLayout qHBoxFill;
  QCheckBox qTglFill;
  qTglFill.setChecked(false);
  qHBoxFill.addWidget(&qTglFill);
  QColorButton qBtnColorFill(Qt::black);
  qHBoxFill.addWidget(&qBtnColorFill, 1);
  qFormCfg.addRow(QString::fromUtf8("Fill Color:"), &qHBoxFill);
  qGrid.addWidget(&qBoxCfg, 1, 1);
  // output image
  QHBoxLayout qHBoxLblOut;
  QLabel qLblOut(QString::fromUtf8("Output Image:"));
  qHBoxLblOut.addWidget(&qLblOut);
  qHBoxLblOut.addStretch(1);
  QColorButton qBtnBgColor1(QString::fromUtf8("Color 1"), Qt::darkGray);
  qHBoxLblOut.addWidget(&qBtnBgColor1);
  QColorButton qBtnBgColor2(QString::fromUtf8("Color 2"), Qt::gray);
  qHBoxLblOut.addWidget(&qBtnBgColor2);
  qGrid.addLayout(&qHBoxLblOut, 0, 2);
  QLabel qLblImgOut;
  qLblImgOut.setPixmap(fromAlphaImage(qImgOut));
  qGrid.addWidget(&qLblImgOut, 1, 2);
  // main window
  qWin.setLayout(&qGrid);
  qWin.show();
  // helper
  auto update = [&]() {
    const int algo = qMenuColorToAlpha.currentIndex();
    switch (algo) {
      case SingleValue:
        qImgOut
          = colorToAlphaSimple(qImgIn, qBtnColor.color(),
            qTglFill.isChecked(), qBtnColorFill.color());
        break;
      case RGBRange:
        qImgOut
          = colorToAlphaThreshold(qImgIn, qBtnColor.color(), qEditRange.value(),
            qTglFill.isChecked(), qBtnColorFill.color());
        break;
    }
    qEditRange.setEnabled(algo >= RGBRange);
    qBtnColorFill.setEnabled(qTglFill.isChecked());
    qLblImgOut.setPixmap(
      fromAlphaImage(qImgOut, qBtnBgColor1.color(), qBtnBgColor2.color()));
  };
  // install signal handlers
  QObject::connect(
    &qBtnLoad, &QPushButton::clicked,
    [&]() {
      QString filePath
        = QFileDialog::getOpenFileName(
          &qWin, QString::fromUtf8("Open Image File"),
          QString(),
          QString::fromUtf8(
            "Image Files (*.png *.jpg *.jpeg);;"
            "PNG Files (*.png);;"
            "JPEG Files (*.jpg *.jpeg);;"
            "All Files (*)"));
      if (filePath.isEmpty()) return; // choice aborted
      QImage qImg;
      qImg.load(filePath);
      if (qImg.isNull()) return; // file loading failed
      qImgIn = qImg;
      qLblImgIn.setPixmap(fromImage(qImgIn));
      update();
    });
  QObject::connect(
    &qMenuColorToAlpha,
    QOverload<int>::of(&QComboBox::currentIndexChanged),
    [&](int) { update(); });
  QObject::connect(&qBtnColor, &QPushButton::clicked,
    [&]() { qBtnColor.chooseColor(); update(); });
  QObject::connect(
    &qEditRange, QOverload<int>::of(&QSpinBox::valueChanged),
    [&](int) { update(); });
  QObject::connect(&qTglFill, &QCheckBox::toggled,
    [&](bool) { update(); });
  QObject::connect(&qBtnColorFill, &QPushButton::clicked,
    [&]() { qBtnColorFill.chooseColor(); update(); });
  QObject::connect(&qBtnBgColor1, &QPushButton::clicked,
    [&]() { qBtnBgColor1.chooseColor(); update(); });
  QObject::connect(&qBtnBgColor2, &QPushButton::clicked,
    [&]() { qBtnBgColor2.chooseColor(); update(); });
  // runtime loop
  update();
  return app.exec();
}

and a helper class qColorButton.h:

// borrowed from https://stackoverflow.com/a/55889624/7478597

#ifndef Q_COLOR_BUTTON_H
#define Q_COLOR_BUTTON_H

// Qt header:
#include <QColorDialog>
#include <QPushButton>

// a Qt push button for color selection
class QColorButton: public QPushButton {
  private:
    QColor _qColor;

  public:
    explicit QColorButton(
      const QString &text = QString(), const QColor &qColor = Qt::black,
      QWidget *pQParent = nullptr):
      QPushButton(text, pQParent)
    {
      setColor(qColor);
    }
    explicit QColorButton(
      const QColor &qColor = Qt::black,
      QWidget *pQParent = nullptr):
      QColorButton(QString(), qColor, pQParent)
    { }
    virtual ~QColorButton() = default;

    QColorButton(const QColorButton&) = delete;
    QColorButton& operator=(const QColorButton&) = delete;

    const QColor& color() const { return _qColor; }
    void setColor(const QColor &qColor)
    {
      _qColor = qColor;
      QFontMetrics qFontMetrics(font());
      const int h = qFontMetrics.height();
      QPixmap qPixmap(h, h);
      qPixmap.fill(_qColor);
      setIcon(qPixmap);
    }

    QColor chooseColor()
    {
      setColor(QColorDialog::getColor(_qColor, this, text()));
      return _qColor;
    }
};

#endif // Q_COLOR_BUTTON_H

When started, a default image is loaded and the simple matching is applied:

I downloaded the sample image from jloog.com/images/.

The result looks a bit poor. The white background is matched but there appear white artefacts around black drawing. This results from sampling where pixels which covered the drawing as well as the background got respective shades of gray.

So, a better approach is to turn the distance from pivot color into a respective alpha value whereby the threshold defines the range as well as a limit upto that colors shall be considered:

That looks better.

Now, I became curious how well this works in “real” photos:

The result is better when I was afraid.

However, it shows the limits of the approach I got so far.


Update:

While I was researching the web to get the precise conversion from RGB to HSV, I learnt a lot about the different HSL and HSV models I was not aware of before. Finally, I stumbled into Color difference where I found some interesting statements:

As most definitions of color difference are distances within a color space, the standard means of determining distances is the Euclidean distance. If one presently has an RGB (Red, Green, Blue) tuple and wishes to find the color difference, computationally one of the easiest is to call R, G, B linear dimensions defining the color space.

There are a number of color distance formulae that attempt to use color spaces like HSV with the hue as a circle, placing the various colors within a three dimensional space of either a cylinder or cone, but most of these are just modifications of RGB; without accounting for differences in human color perception they will tend to be on par with a simple Euclidean metric.

So, I discarded the idea with matching in HSV space.

Instead, I made a very simple extension which IMHO provides a significant improvement concerning monochrom drawings:

The pixels with mixed drawing and background are transformed into shades of alpha but the RGB values are left untouched. This is not quite correct because it should actually become the foreground color (pencil color) blended with alpha. To fix this, I added an option to override the RGB values of the output with a color of choice.

This is the result with overridden color:

Btw. it allows a nice little extra effect – the pencil color can be modified:

(The above sample source code has been updated to reflect the last changes.)


To build the sample, either CMake can be used with this CMakeLists.txt:

project(QImageColorToAlpha)

cmake_minimum_required(VERSION 3.10.0)

set_property(GLOBAL PROPERTY USE_FOLDERS ON)
#set(CMAKE_CXX_STANDARD 17)
#set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

find_package(Qt5Widgets CONFIG REQUIRED)

include_directories(
  "${CMAKE_SOURCE_DIR}")

add_executable(testQImageColorToAlpha
  testQImageColorToAlpha.cc
  qColorButton.h # qColorButton.cc
  imageColorToAlpha.h imageColorToAlpha.cc)

target_link_libraries(testQImageColorToAlpha
  Qt5::Widgets)

# define QT_NO_KEYWORDS to prevent confusion between of Qt signal-slots and
# other signal-slot APIs
target_compile_definitions(testQImageColorToAlpha PUBLIC QT_NO_KEYWORDS)

which I used to build it in VS2017.

Alternatively, a minimal Qt project file testQImageColorToAlpha.pro:

SOURCES = testQImageColorToAlpha.cc imageColorToAlpha.cc

QT += widgets

which I tested in cygwin:

$ qmake-qt5 testQImageColorToAlpha.pro 

$ make && ./testQImageColorToAlpha
g++ -c -fno-keep-inline-dllexport -D_GNU_SOURCE -pipe -O2 -Wall -W -D_REENTRANT -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -I. -isystem /usr/include/qt5 -isystem /usr/include/qt5/QtWidgets -isystem /usr/include/qt5/QtGui -isystem /usr/include/qt5/QtCore -I. -I/usr/lib/qt5/mkspecs/cygwin-g++ -o testQImageColorToAlpha.o testQImageColorToAlpha.cc
g++ -c -fno-keep-inline-dllexport -D_GNU_SOURCE -pipe -O2 -Wall -W -D_REENTRANT -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -I. -isystem /usr/include/qt5 -isystem /usr/include/qt5/QtWidgets -isystem /usr/include/qt5/QtGui -isystem /usr/include/qt5/QtCore -I. -I/usr/lib/qt5/mkspecs/cygwin-g++ -o imageColorToAlpha.o imageColorToAlpha.cc
g++  -o testQImageColorToAlpha.exe testQImageColorToAlpha.o imageColorToAlpha.o   -lQt5Widgets -lQt5Gui -lQt5Core -lGL -lpthread 
Qt Version: 5.9.4



来源:https://stackoverflow.com/questions/59655845/how-to-automate-isolating-line-art-from-the-background-with-code-is-there-a-way

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!