Commit a705628c authored by Jouni Pentikäinen's avatar Jouni Pentikäinen

Add cross-channel adjustment filter

parent 5255a67f
......@@ -401,7 +401,7 @@ public:
QList<QString> parameters() const override
{
QList<QString> list;
list << "curve" << "channel" << "lumaRed" << "lumaGreen"<< "lumaBlue";
list << "curve" << "channel" << "driverChannel" << "relative" << "lumaRed" << "lumaGreen"<< "lumaBlue";
return list;
}
......@@ -411,6 +411,10 @@ public:
return PAR_CURVE;
} else if (name == "channel") {
return PAR_CHANNEL;
} else if (name == "driverChannel") {
return PAR_DRIVER_CHANNEL;
} else if (name == "relative") {
return PAR_RELATIVE;
} else if (name == "lumaRed") {
return PAR_LUMA_R;
} else if (name == "lumaGreen") {
......@@ -423,8 +427,11 @@ public:
/**
* curve: adjustment curve as QVector<quin16>
* channel: which channel to adjust
* 0 = hue, 1 = saturation, 2 = value
* channel: which channel to adjust. See KisHSVCurve::ColorChannel.
* driverChannel: which channel to use as source for adjustments.
* relative:
* false: use curve for direct lookup.
* true: add adjustment to original. In this mode, the curve range is mapped to -1.0 to 1.0
* luma Red/Green/Blue: Used for luma calculations.
*/
void setParameter(int id, const QVariant& parameter) override
......@@ -434,11 +441,20 @@ public:
case PAR_CURVE:
m_curve = parameter.value<QVector<quint16>>();
break;
case PAR_CHANNEL: {
case PAR_CHANNEL:
case PAR_DRIVER_CHANNEL: {
int channel = parameter.toInt();
KIS_ASSERT_RECOVER_RETURN(0 <= channel && channel < KisHSVCurve::ChannelCount && "Invalid channel. Ignored!");
m_channel = channel;
if (id == PAR_CHANNEL) {
m_channel = channel;
} else {
m_driverChannel = channel;
}
} break;
case PAR_RELATIVE:
m_relative = parameter.toBool();
break;
case PAR_LUMA_R:
m_lumaRed = parameter.toDouble();
break;
......@@ -459,6 +475,8 @@ public:
RGBPixel* dst = reinterpret_cast<RGBPixel*>(dstU8);
float max = m_curve.size() - 1;
int driverChannel = m_relative ? m_driverChannel : m_channel;
float component[KisHSVCurve::ChannelCount];
// Aliases for convenience
......@@ -481,7 +499,26 @@ public:
// Normalize hue to 0.0 to 1.0 range
h /= 360.0f;
component[m_channel] = lookupComponent(component[m_channel], max);
float adjustment = lookupComponent(component[driverChannel], max);
if (m_relative) {
// Curve uses range 0.0 to 1.0, but for adjustment we need -1.0 to 1.0
adjustment = 2.0f * adjustment - 1.0f;
if (m_channel == KisHSVCurve::AllColors) {
r += adjustment;
g += adjustment;
b += adjustment;
} else {
component[m_channel] += adjustment;
}
} else {
if (m_channel == KisHSVCurve::AllColors) {
r = b = g = adjustment;
} else {
component[m_channel] = adjustment;
}
}
h *= 360.0f;
if (h > 360) h -= 360;
......@@ -532,13 +569,17 @@ private:
{
PAR_CURVE,
PAR_CHANNEL,
PAR_DRIVER_CHANNEL,
PAR_RELATIVE,
PAR_LUMA_R,
PAR_LUMA_G,
PAR_LUMA_B,
};
QVector<quint16> m_curve;
int m_channel;
int m_channel = 0;
int m_driverChannel = 0;
bool m_relative = false;
/* Note: the filter currently only supports HSV, so these are
* unused, but will be needed once HSL, etc.
......
......@@ -4,6 +4,7 @@ set(kritacolorsfilters_SOURCES
virtual_channel_info.cpp
kis_multichannel_filter_base.cpp
kis_perchannel_filter.cpp
kis_cross_channel_filter.cpp
kis_color_balance_filter.cpp
kis_desaturate_filter.cpp
)
......
......@@ -58,6 +58,7 @@
#include "kis_hsv_adjustment_filter.h"
#include "kis_perchannel_filter.h"
#include "kis_cross_channel_filter.h"
#include "kis_color_balance_filter.h"
#include "kis_desaturate_filter.h"
......@@ -69,6 +70,7 @@ ColorsFilters::ColorsFilters(QObject *parent, const QVariantList &)
KisFilterRegistry * manager = KisFilterRegistry::instance();
manager->add(new KisAutoContrast());
manager->add(new KisPerChannelFilter());
manager->add(new KisCrossChannelFilter());
manager->add(new KisDesaturateFilter());
manager->add(new KisHSVAdjustmentFilter());
manager->add(new KisColorBalanceFilter());
......
/*
* This file is part of Krita
*
* Copyright (c) 2018 Jouni Pentikainen <joupent@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "kis_cross_channel_filter.h"
#include <Qt>
#include <QLayout>
#include <QPixmap>
#include <QPainter>
#include <QLabel>
#include <QComboBox>
#include <QDomDocument>
#include <QHBoxLayout>
#include "KoChannelInfo.h"
#include "KoBasicHistogramProducers.h"
#include "KoColorModelStandardIds.h"
#include "KoColorSpace.h"
#include "KoColorTransformation.h"
#include "KoCompositeColorTransformation.h"
#include "KoCompositeOp.h"
#include "KoID.h"
#include "kis_signals_blocker.h"
#include "kis_bookmarked_configuration_manager.h"
#include "kis_config_widget.h"
#include <filter/kis_filter_configuration.h>
#include <kis_selection.h>
#include <kis_paint_device.h>
#include <kis_processing_information.h>
#include <libs/global/kis_dom_utils.h>
#include "kis_histogram.h"
#include "kis_painter.h"
#include "widgets/kis_curve_widget.h"
#include "../../color/colorspaceextensions/kis_hsv_adjustment.h"
// KisCrossChannelFilterConfiguration
KisCrossChannelFilterConfiguration::KisCrossChannelFilterConfiguration(int channelCount)
: KisMultiChannelFilterConfiguration(channelCount, "crosschannel", 1)
{
init();
m_driverChannels.resize(channelCount);
}
KisCrossChannelFilterConfiguration::~KisCrossChannelFilterConfiguration()
{}
const QVector<int> KisCrossChannelFilterConfiguration::driverChannels() const
{
return m_driverChannels;
}
void KisCrossChannelFilterConfiguration::setDriverChannels(QVector<int> driverChannels)
{
KIS_SAFE_ASSERT_RECOVER_RETURN(driverChannels.size() == m_curves.size());
m_driverChannels = driverChannels;
}
void KisCrossChannelFilterConfiguration::fromXML(const QDomElement& root)
{
KisMultiChannelFilterConfiguration::fromXML(root);
m_driverChannels.resize(m_curves.size());
QRegExp rx("driver(\\d+)");
for (QDomElement e = root.firstChildElement(); !e.isNull(); e = e.nextSiblingElement()) {
const QString attributeName = e.attribute("name");
if (rx.exactMatch(attributeName)) {
int channel = rx.cap(1).toUShort();
int driver = KisDomUtils::toInt(e.text());
if (0 <= channel && channel < m_driverChannels.size()) {
m_driverChannels[channel] = driver;
}
}
}
}
void KisCrossChannelFilterConfiguration::toXML(QDomDocument& doc, QDomElement& root) const
{
KisMultiChannelFilterConfiguration::toXML(doc, root);
for (int i = 0; i < m_driverChannels.size(); i++) {
QDomElement param = doc.createElement("param");
param.setAttribute("name", QString("driver%1").arg(i));
QDomText text = doc.createTextNode(KisDomUtils::toString(m_driverChannels[i]));
param.appendChild(text);
root.appendChild(param);
}
}
KisCubicCurve KisCrossChannelFilterConfiguration::getDefaultCurve()
{
const QList<QPointF> points { QPointF(0.0f, 0.5f), QPointF(1.0f, 0.5f) };
return KisCubicCurve(points);
}
KisCrossChannelConfigWidget::KisCrossChannelConfigWidget(QWidget * parent, KisPaintDeviceSP dev, Qt::WindowFlags f)
: KisMultiChannelConfigWidget(parent, dev, f)
{
const int virtualChannelCount = m_virtualChannels.size();
m_driverChannels.resize(virtualChannelCount);
init();
for (int i = 0; i < virtualChannelCount; i++) {
const VirtualChannelInfo &info = m_virtualChannels[i];
if (info.type() == VirtualChannelInfo::ALL_COLORS) {
continue;
}
m_page->cmbDriverChannel->addItem(info.name(), i);
}
connect(m_page->cmbDriverChannel, SIGNAL(activated(int)), this, SLOT(slotDriverChannelSelected(int)));
}
// KisCrossChannelConfigWidget
KisCrossChannelConfigWidget::~KisCrossChannelConfigWidget()
{}
void KisCrossChannelConfigWidget::setConfiguration(const KisPropertiesConfigurationSP config)
{
const auto *cfg = dynamic_cast<const KisCrossChannelFilterConfiguration*>(config.data());
m_driverChannels = cfg->driverChannels();
KisMultiChannelConfigWidget::setConfiguration(config);
}
KisPropertiesConfigurationSP KisCrossChannelConfigWidget::configuration() const
{
auto *cfg = new KisCrossChannelFilterConfiguration(m_virtualChannels.count());
KisPropertiesConfigurationSP cfgSP = cfg;
m_curves[m_activeVChannel] = m_page->curveWidget->curve();
cfg->setCurves(m_curves);
cfg->setDriverChannels(m_driverChannels);
return cfgSP;
}
void KisCrossChannelConfigWidget::updateChannelControls()
{
m_page->curveWidget->setupInOutControls(m_page->intIn, m_page->intOut, 0, 100, -100, 100);
const int index = m_page->cmbDriverChannel->findData(m_driverChannels[m_activeVChannel]);
m_page->cmbDriverChannel->setCurrentIndex(index);
}
KisPropertiesConfigurationSP KisCrossChannelConfigWidget::getDefaultConfiguration()
{
return new KisCrossChannelFilterConfiguration(m_virtualChannels.size());
}
void KisCrossChannelConfigWidget::slotDriverChannelSelected(int index)
{
const int channel = m_page->cmbDriverChannel->itemData(index).toInt();
KIS_SAFE_ASSERT_RECOVER_RETURN(0 <= channel && channel < m_virtualChannels.size());
m_driverChannels[m_activeVChannel] = channel;
updateChannelControls();
}
// KisCrossChannelFilter
KisCrossChannelFilter::KisCrossChannelFilter() : KisMultiChannelFilter(id(), i18n("&Cross-channel adjustment curves..."))
{}
KisCrossChannelFilter::~KisCrossChannelFilter()
{}
KisConfigWidget * KisCrossChannelFilter::createConfigurationWidget(QWidget *parent, const KisPaintDeviceSP dev) const
{
return new KisCrossChannelConfigWidget(parent, dev);
}
KisFilterConfigurationSP KisCrossChannelFilter::factoryConfiguration() const
{
return new KisCrossChannelFilterConfiguration(0);
}
int mapChannel(const VirtualChannelInfo &channel) {
switch (channel.type()) {
case VirtualChannelInfo::REAL: {
int pixelIndex = channel.pixelIndex();
KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(0 <= pixelIndex && pixelIndex < 4, 0);
return pixelIndex;
}
case VirtualChannelInfo::ALL_COLORS:
return KisHSVCurve::AllColors;
case VirtualChannelInfo::HUE:
return KisHSVCurve::Hue;
case VirtualChannelInfo::SATURATION:
return KisHSVCurve::Saturation;
case VirtualChannelInfo::LIGHTNESS:
return KisHSVCurve::Value;
};
KIS_SAFE_ASSERT_RECOVER_NOOP(false);
return 0;
}
KoColorTransformation* KisCrossChannelFilter::createTransformation(const KoColorSpace* cs, const KisFilterConfigurationSP config) const
{
const KisCrossChannelFilterConfiguration* configBC =
dynamic_cast<const KisCrossChannelFilterConfiguration*>(config.data());
Q_ASSERT(configBC);
const QVector<QVector<quint16> > &originalTransfers = configBC->transfers();
const QList<KisCubicCurve> &curves = configBC->curves();
const QVector<int> &drivers = configBC->driverChannels();
const QVector<VirtualChannelInfo> virtualChannels = KisMultiChannelFilter::getVirtualChannels(cs);
if (originalTransfers.size() != int(virtualChannels.size())) {
// We got an illegal number of colorchannels :(
return 0;
}
QVector<KoColorTransformation*> transforms;
// Channel order reversed in order to adjust saturation before hue. This allows mapping grays to colors.
for (int i = virtualChannels.size() - 1; i >= 0; i--) {
if (!curves[i].isConstant(0.5)) {
int channel = mapChannel(virtualChannels[i]);
int driverChannel = mapChannel(virtualChannels[drivers[i]]);
QHash<QString, QVariant> params;
params["channel"] = channel;
params["driverChannel"] = driverChannel;
params["curve"] = QVariant::fromValue(originalTransfers[i]);
params["relative"] = true;
params["lumaRed"] = cs->lumaCoefficients()[0];
params["lumaGreen"] = cs->lumaCoefficients()[1];
params["lumaBlue"] = cs->lumaCoefficients()[2];
transforms << cs->createColorTransformation("hsv_curve_adjustment", params);
}
}
return KoCompositeColorTransformation::createOptimizedCompositeTransform(transforms);
}
/*
* This file is part of Krita
*
* Copyright (c) 2018 Jouni Pentikainen <joupent@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef _KIS_CROSSCHANNEL_FILTER_H_
#define _KIS_CROSSCHANNEL_FILTER_H_
#include <QPair>
#include <QList>
#include <filter/kis_color_transformation_filter.h>
#include <filter/kis_color_transformation_configuration.h>
#include <kis_config_widget.h>
#include <kis_paint_device.h>
#include "ui_wdg_perchannel.h"
#include "virtual_channel_info.h"
#include "kis_multichannel_filter_base.h"
/**
* Filter which applies a relative adjustment to a (virtual) color channel based on the value of another.
* The amount of adjustment for a given input is controlled by a user-defined curve.
*/
class KisCrossChannelFilter : public KisMultiChannelFilter
{
public:
KisCrossChannelFilter();
~KisCrossChannelFilter() override;
KisConfigWidget * createConfigurationWidget(QWidget *parent, const KisPaintDeviceSP dev) const override;
KisFilterConfigurationSP factoryConfiguration() const override;
KoColorTransformation* createTransformation(const KoColorSpace *cs, const KisFilterConfigurationSP config) const override;
static inline KoID id() {
return KoID("crosschannel", i18n("Cross-channel color adjustment"));
}
};
class KisCrossChannelFilterConfiguration : public KisMultiChannelFilterConfiguration
{
public:
KisCrossChannelFilterConfiguration(int n);
~KisCrossChannelFilterConfiguration() override;
const QVector<int> driverChannels() const;
void setDriverChannels(QVector<int> driverChannels);
using KisFilterConfiguration::fromXML;
using KisFilterConfiguration::toXML;
void fromXML(const QDomElement& e) override;
void toXML(QDomDocument& doc, QDomElement& root) const override;
KisCubicCurve getDefaultCurve() override;
private:
QVector<int> m_driverChannels;
};
class KisCrossChannelConfigWidget : public KisMultiChannelConfigWidget
{
Q_OBJECT
public:
KisCrossChannelConfigWidget(QWidget * parent, KisPaintDeviceSP dev, Qt::WindowFlags f = 0);
~KisCrossChannelConfigWidget() override;
void setConfiguration(const KisPropertiesConfigurationSP config) override;
KisPropertiesConfigurationSP configuration() const override;
protected:
void updateChannelControls() override;
virtual KisPropertiesConfigurationSP getDefaultConfiguration() override;
private Q_SLOTS:
void slotDriverChannelSelected(int index);
private:
QVector<int> m_driverChannels;
};
#endif
......@@ -57,6 +57,11 @@ KisPerChannelConfigWidget::KisPerChannelConfigWidget(QWidget * parent, KisPaintD
: KisMultiChannelConfigWidget(parent, dev, f)
{
init();
// These are not used by this filter,
// but the dialog is shared with KisCrossChannelFilter
m_page->lblDriverChannel->hide();
m_page->cmbDriverChannel->hide();
}
KisPerChannelConfigWidget::~KisPerChannelConfigWidget()
......@@ -259,6 +264,7 @@ KoColorTransformation* KisPerChannelFilter::createTransformation(const KoColorSp
QHash<QString, QVariant> params;
params["curve"] = QVariant::fromValue(hueTransfer);
params["channel"] = KisHSVCurve::Hue;
params["relative"] = false;
params["lumaRed"] = cs->lumaCoefficients()[0];
params["lumaGreen"] = cs->lumaCoefficients()[1];
params["lumaBlue"] = cs->lumaCoefficients()[2];
......@@ -270,6 +276,7 @@ KoColorTransformation* KisPerChannelFilter::createTransformation(const KoColorSp
QHash<QString, QVariant> params;
params["curve"] = QVariant::fromValue(saturationTransfer);
params["channel"] = KisHSVCurve::Saturation;
params["relative"] = false;
params["lumaRed"] = cs->lumaCoefficients()[0];
params["lumaGreen"] = cs->lumaCoefficients()[1];
params["lumaBlue"] = cs->lumaCoefficients()[2];
......
......@@ -319,11 +319,31 @@
</layout>
</item>
<item row="0" column="1" colspan="2">
<layout class="QHBoxLayout" stretch="1,2,2,1,0">
<property name="spacing">
<number>6</number>
</property>
<item>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="2">
<widget class="QCheckBox" name="chkLogarithmic">
<property name="text">
<string>Logarithmic</string>
</property>
</widget>
</item>
<item row="0" column="1" rowspan="2">
<widget class="QComboBox" name="cmbChannel"/>
</item>
<item row="0" column="3" rowspan="2">
<widget class="QPushButton" name="resetButton">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Reset</string>
</property>
</widget>
</item>
<item row="0" column="0" rowspan="2">
<widget class="QLabel" name="textLabel1">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
......@@ -339,10 +359,7 @@
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="cmbChannel"/>
</item>
<item>
<item row="1" column="2">
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
......@@ -355,23 +372,16 @@
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="chkLogarithmic">
<property name="text">
<string>Logarithmic</string>
</property>
</widget>
<item row="2" column="1">
<widget class="QComboBox" name="cmbDriverChannel"/>
</item>
<item>
<widget class="QPushButton" name="resetButton">
<property name="sizePolicy">