QPainter通过模糊实现发光效果

目录

  • 引言
  • 代码实现

引言

发光效果通常是通过在原图层后面叠加原始一层高亮的模糊图层实现,常用的模式方式则是高斯模糊。对图片的处理第一个想到的就是OpenCV,但是为了当初的模糊效果给工程中加入OpenCV似乎并不合适,当然也是因为本人的OpenCV并不熟悉。

那么Qt是否有现成的类可以去实现模糊效果呢?很庆幸,答案是有的。可以通过QGraphicsBlurEffect去实现,效果如下图所示:

原图:
QPainter通过模糊实现发光效果_第1张图片

模糊后的效果图:
QPainter通过模糊实现发光效果_第2张图片
调用代码也很简单,如下所示:

    auto blur_effect = new QGraphicsBlurEffect();
    blur_effect->setBlurRadius(5);
    ui->label->setGraphicsEffect(blur_effect);

构造QGraphicsBlurEffect,设置模糊的半径,再为需要模糊效果的组件设置效果。那么问题来了,既然模糊实现这么简单,那为什么会有这一篇博文呢?

第一个问题就是QGraphicsBlurEffect的模糊效果是对整个Widget生效,也就是如上图中展示的一样,但圆弧和背景圆写在同一个Widget中,就不能对单个圆弧进行模糊。

这里当然可以将上面的Widget拆分成两个,通过reszieEvent的方式实时调整两个组件的布局,但迫使我无法使用这个类的另一个另一个,就是Qt的封装实在是太好了,用户不用考虑什么样的Widget的都能够适配,且不需要关心什么时候去触发刷新,QGraphicsBlurEffect在设计模式上是一个装饰者,它并不能脱离QWidget存在,也就是它并不能在QPainter中调用,在需要导出的场景下它的使用是受限的。

那么如果我们现在QPainter中实现部分图形的模糊,那是不是还得使用OpenCV,倔强如我给出的答案当然是不。回到QGraphicsBlurEffect这个类,让我们相信一下Qt是怎么实现适配所有Widget的模糊的,有没有可能它是通过Widget截图的方式生成了一张模糊图,再去绘制在其装饰的组件上的。

为了确认这个猜想,我们需要去阅读Qt源码。在翻看说明文档的过程中,发现QGraphicsBlurEffect的父类QGraphicsEffect有一个纯函数draw,如下所示:

void QGraphicsEffect::draw(QPainter *painter)
This pure virtual function draws the effect and is called whenever the source needs to be drawn.
Reimplement this function in a QGraphicsEffect subclass to provide the effect’s drawing implementation, using painter.

那么顾名思义,这里必定是模糊效果的关键,那么我们调试一下源码,跟踪调用堆栈:

void QGraphicsBlurEffect::draw(QPainter *painter)
{
    // 省略
    d->filter->draw(painter, offset, pixmap);
}

看的d指针中有滤波类filter,那十有八九应该是这里实现的,进行往下跟:

void QPixmapBlurFilter::draw(QPainter *painter, const QPointF &p, const QPixmap &src, const QRectF &rect) const
{
    // 省略
    qt_blurImage(painter, srcImage, scaledRadius, (d->hints & QGraphicsBlurEffect::QualityHint), false);
    // 省略
}

这里面看到有一个模糊函数qt_blurImage,入参是painter和srcImage,那应该就是这个函数实现的模糊,源码如下:

Q_WIDGETS_EXPORT void qt_blurImage(QPainter *p, QImage &blurImage, qreal radius, bool quality, bool alphaOnly, int transposed = 0)
{
    if (blurImage.format() != QImage::Format_ARGB32_Premultiplied
        && blurImage.format() != QImage::Format_RGB32)
    {
        blurImage = blurImage.convertToFormat(QImage::Format_ARGB32_Premultiplied);
    }

    qreal scale = 1;
    if (radius >= 4 && blurImage.width() >= 2 && blurImage.height() >= 2) {
        blurImage = qt_halfScaled(blurImage);
        scale = 2;
        radius *= qreal(0.5);
    }

    if (alphaOnly)
        expblur<12, 10, true>(blurImage, radius, quality, transposed);
    else
        expblur<12, 10, false>(blurImage, radius, quality, transposed);

    if (p) {
        p->scale(scale, scale);
        p->setRenderHint(QPainter::SmoothPixmapTransform);
        p->drawImage(QRect(QPoint(0, 0), blurImage.size() / blurImage.devicePixelRatioF()), blurImage);
    }
}

可以看到,这个就是我们需要的代码,QPainter作为入参对于我们来说并不需要,我们只需要能把原始图像转换成模糊图就可以,也就是只关心QImage,需要模糊的图层通过QPainter绘制在QImage中,调用该函数进行模糊,再将模糊图通过drawImage绘制出来就完成图形的模糊效果。

代码实现

上面的原理我们已经知道,由于QPixmapBlurFilter并未对外开发,那么我们接下来要做的就是将qt_blurImage相关代码从Qt源码中摘出来,代码如下:

// qt source code, from qmemrotate.cpp & qpixmapfilter.cpp
static const int tileSize = 32;

template <class T>
static inline void qt_memrotate270_tiled_unpacked(const T *src, int w, int h, int sstride, T *dest, int dstride) {
    const int numTilesX = (w + tileSize - 1) / tileSize;
    const int numTilesY = (h + tileSize - 1) / tileSize;

    for (int tx = 0; tx < numTilesX; ++tx) {
        const int startx = tx * tileSize;
        const int stopx  = qMin(startx + tileSize, w);

        for (int ty = 0; ty < numTilesY; ++ty) {
            const int starty = h - 1 - ty * tileSize;
            const int stopy  = qMax(starty - tileSize, 0);

            for (int x = startx; x < stopx; ++x) {
                T *d          = (T *)((char *)dest + x * dstride) + h - 1 - starty;
                const char *s = (const char *)(src + x) + starty * sstride;
                for (int y = starty; y >= stopy; --y) {
                    *d++ = *(const T *)s;
                    s -= sstride;
                }
            }
        }
    }
}

template <class T>
static inline void qt_memrotate270_template(const T *src, int srcWidth, int srcHeight, int srcStride, T *dest, int dstStride) {
    //#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
    //    // packed algorithm assumes little endian and that sizeof(quint32)/sizeof(T) is an integer
    //    if (sizeof(quint32) % sizeof(T) == 0)
    //        qt_memrotate270_tiled(src, srcWidth, srcHeight, srcStride, dest, dstStride);
    //    else
    //#endif
    qt_memrotate270_tiled_unpacked<T>(src, srcWidth, srcHeight, srcStride, dest, dstStride);
}

template <>
inline void qt_memrotate270_template<quint32>(const quint32 *src, int w, int h, int sstride, quint32 *dest, int dstride) {
    // packed algorithm doesn't have any benefit for quint32
    qt_memrotate270_tiled_unpacked(src, w, h, sstride, dest, dstride);
}

template <>
inline void qt_memrotate270_template<quint64>(const quint64 *src, int w, int h, int sstride, quint64 *dest, int dstride) {
    qt_memrotate270_tiled_unpacked(src, w, h, sstride, dest, dstride);
}

template <class T>
static inline void qt_memrotate90_tiled_unpacked(const T *src, int w, int h, int sstride, T *dest, int dstride) {
    const int numTilesX = (w + tileSize - 1) / tileSize;
    const int numTilesY = (h + tileSize - 1) / tileSize;

    for (int tx = 0; tx < numTilesX; ++tx) {
        const int startx = w - tx * tileSize - 1;
        const int stopx  = qMax(startx - tileSize, 0);

        for (int ty = 0; ty < numTilesY; ++ty) {
            const int starty = ty * tileSize;
            const int stopy  = qMin(starty + tileSize, h);

            for (int x = startx; x >= stopx; --x) {
                T *d          = (T *)((char *)dest + (w - x - 1) * dstride) + starty;
                const char *s = (const char *)(src + x) + starty * sstride;
                for (int y = starty; y < stopy; ++y) {
                    *d++ = *(const T *)(s);
                    s += sstride;
                }
            }
        }
    }
}

template <class T>
static inline void qt_memrotate90_template(const T *src, int srcWidth, int srcHeight, int srcStride, T *dest, int dstStride) {
    //#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
    //    // packed algorithm assumes little endian and that sizeof(quint32)/sizeof(T) is an integer
    //    if (sizeof(quint32) % sizeof(T) == 0)
    //        qt_memrotate90_tiled(src, srcWidth, srcHeight, srcStride, dest, dstStride);
    //    else
    //#endif
    qt_memrotate90_tiled_unpacked<T>(src, srcWidth, srcHeight, srcStride, dest, dstStride);
}

template <>
inline void qt_memrotate90_template<quint32>(const quint32 *src, int w, int h, int sstride, quint32 *dest, int dstride) {
    // packed algorithm doesn't have any benefit for quint32
    qt_memrotate90_tiled_unpacked(src, w, h, sstride, dest, dstride);
}

template <>
inline void qt_memrotate90_template<quint64>(const quint64 *src, int w, int h, int sstride, quint64 *dest, int dstride) {
    qt_memrotate90_tiled_unpacked(src, w, h, sstride, dest, dstride);
}

template <int shift>
inline int qt_static_shift(int value) {
    if (shift == 0)
        return value;
    else if (shift > 0)
        return value << (uint(shift) & 0x1f);
    else
        return value >> (uint(-shift) & 0x1f);
}

template <int aprec, int zprec>
inline void qt_blurinner(uchar *bptr, int &zR, int &zG, int &zB, int &zA, int alpha) {
    QRgb *pixel = (QRgb *)bptr;

#define Z_MASK (0xff << zprec)
    const int A_zprec = qt_static_shift<zprec - 24>(*pixel) & Z_MASK;
    const int R_zprec = qt_static_shift<zprec - 16>(*pixel) & Z_MASK;
    const int G_zprec = qt_static_shift<zprec - 8>(*pixel) & Z_MASK;
    const int B_zprec = qt_static_shift<zprec>(*pixel) & Z_MASK;
#undef Z_MASK

    const int zR_zprec = zR >> aprec;
    const int zG_zprec = zG >> aprec;
    const int zB_zprec = zB >> aprec;
    const int zA_zprec = zA >> aprec;

    zR += alpha * (R_zprec - zR_zprec);
    zG += alpha * (G_zprec - zG_zprec);
    zB += alpha * (B_zprec - zB_zprec);
    zA += alpha * (A_zprec - zA_zprec);

#define ZA_MASK (0xff << (zprec + aprec))
    *pixel = qt_static_shift<24 - zprec - aprec>(zA & ZA_MASK) | qt_static_shift<16 - zprec - aprec>(zR & ZA_MASK)
             | qt_static_shift<8 - zprec - aprec>(zG & ZA_MASK) | qt_static_shift<-zprec - aprec>(zB & ZA_MASK);
#undef ZA_MASK
}

const int alphaIndex = (QSysInfo::ByteOrder == QSysInfo::BigEndian ? 0 : 3);

template <int aprec, int zprec>
inline void qt_blurinner_alphaOnly(uchar *bptr, int &z, int alpha) {
    const int A_zprec = int(*(bptr)) << zprec;
    const int z_zprec = z >> aprec;
    z += alpha * (A_zprec - z_zprec);
    *(bptr) = z >> (zprec + aprec);
}

template <int aprec, int zprec, bool alphaOnly>
inline void qt_blurrow(QImage &im, int line, int alpha) {
    uchar *bptr = im.scanLine(line);

    int zR = 0, zG = 0, zB = 0, zA = 0;

    if (alphaOnly && im.format() != QImage::Format_Indexed8)
        bptr += alphaIndex;

    const int stride   = im.depth() >> 3;
    const int im_width = im.width();
    for (int index = 0; index < im_width; ++index) {
        if (alphaOnly)
            qt_blurinner_alphaOnly<aprec, zprec>(bptr, zA, alpha);
        else
            qt_blurinner<aprec, zprec>(bptr, zR, zG, zB, zA, alpha);
        bptr += stride;
    }

    bptr -= stride;

    for (int index = im_width - 2; index >= 0; --index) {
        bptr -= stride;
        if (alphaOnly)
            qt_blurinner_alphaOnly<aprec, zprec>(bptr, zA, alpha);
        else
            qt_blurinner<aprec, zprec>(bptr, zR, zG, zB, zA, alpha);
    }
}

template <int aprec, int zprec, bool alphaOnly>
void expblur(QImage &img, qreal radius, bool improvedQuality = false, int transposed = 0) {
    // halve the radius if we're using two passes
    if (improvedQuality)
        radius *= qreal(0.5);

    Q_ASSERT(img.format() == QImage::Format_ARGB32_Premultiplied || img.format() == QImage::Format_RGB32
             || img.format() == QImage::Format_Indexed8 || img.format() == QImage::Format_Grayscale8);

    // choose the alpha such that pixels at radius distance from a fully
    // saturated pixel will have an alpha component of no greater than
    // the cutOffIntensity
    const qreal cutOffIntensity = 2;
    int alpha =
        radius <= qreal(1e-5) ? ((1 << aprec) - 1) : qRound((1 << aprec) * (1 - qPow(cutOffIntensity * (1 / qreal(255)), 1 / radius)));

    int img_height = img.height();
    for (int row = 0; row < img_height; ++row) {
        for (int i = 0; i <= int(improvedQuality); ++i)
            qt_blurrow<aprec, zprec, alphaOnly>(img, row, alpha);
    }

    QImage temp(img.height(), img.width(), img.format());
    temp.setDevicePixelRatio(img.devicePixelRatioF());
    if (transposed >= 0) {
        if (img.depth() == 8) {
            qt_memrotate270_template(reinterpret_cast<const quint8 *>(img.bits()),
                                     img.width(),
                                     img.height(),
                                     img.bytesPerLine(),
                                     reinterpret_cast<quint8 *>(temp.bits()),
                                     temp.bytesPerLine());
        }
        else {
            qt_memrotate270_template(reinterpret_cast<const quint32 *>(img.bits()),
                                     img.width(),
                                     img.height(),
                                     img.bytesPerLine(),
                                     reinterpret_cast<quint32 *>(temp.bits()),
                                     temp.bytesPerLine());
        }
    }
    else {
        if (img.depth() == 8) {
            qt_memrotate90_template(reinterpret_cast<const quint8 *>(img.bits()),
                                    img.width(),
                                    img.height(),
                                    img.bytesPerLine(),
                                    reinterpret_cast<quint8 *>(temp.bits()),
                                    temp.bytesPerLine());
        }
        else {
            qt_memrotate90_template(reinterpret_cast<const quint32 *>(img.bits()),
                                    img.width(),
                                    img.height(),
                                    img.bytesPerLine(),
                                    reinterpret_cast<quint32 *>(temp.bits()),
                                    temp.bytesPerLine());
        }
    }

    img_height = temp.height();
    for (int row = 0; row < img_height; ++row) {
        for (int i = 0; i <= int(improvedQuality); ++i)
            qt_blurrow<aprec, zprec, alphaOnly>(temp, row, alpha);
    }

    if (transposed == 0) {
        if (img.depth() == 8) {
            qt_memrotate90_template(reinterpret_cast<const quint8 *>(temp.bits()),
                                    temp.width(),
                                    temp.height(),
                                    temp.bytesPerLine(),
                                    reinterpret_cast<quint8 *>(img.bits()),
                                    img.bytesPerLine());
        }
        else {
            qt_memrotate90_template(reinterpret_cast<const quint32 *>(temp.bits()),
                                    temp.width(),
                                    temp.height(),
                                    temp.bytesPerLine(),
                                    reinterpret_cast<quint32 *>(img.bits()),
                                    img.bytesPerLine());
        }
    }
    else {
        img = temp;
    }
}

#define AVG(a, b) (((((a) ^ (b)) & 0xfefefefeUL) >> 1) + ((a) & (b)))
#define AVG16(a, b) (((((a) ^ (b)) & 0xf7deUL) >> 1) + ((a) & (b)))

static QImage qt_halfScaled(const QImage &source) {
    if (source.width() < 2 || source.height() < 2)
        return QImage();

    QImage srcImage = source;

    if (source.format() == QImage::Format_Indexed8 || source.format() == QImage::Format_Grayscale8) {
        // assumes grayscale
        QImage dest(source.width() / 2, source.height() / 2, srcImage.format());
        dest.setDevicePixelRatio(source.devicePixelRatioF());

        const uchar *src = reinterpret_cast<const uchar *>(const_cast<const QImage &>(srcImage).bits());
        int sx           = srcImage.bytesPerLine();
        int sx2          = sx << 1;

        uchar *dst = reinterpret_cast<uchar *>(dest.bits());
        int dx     = dest.bytesPerLine();
        int ww     = dest.width();
        int hh     = dest.height();

        for (int y = hh; y; --y, dst += dx, src += sx2) {
            const uchar *p1 = src;
            const uchar *p2 = src + sx;
            uchar *q        = dst;
            for (int x = ww; x; --x, ++q, p1 += 2, p2 += 2)
                *q = ((int(p1[0]) + int(p1[1]) + int(p2[0]) + int(p2[1])) + 2) >> 2;
        }

        return dest;
    }
    else if (source.format() == QImage::Format_ARGB8565_Premultiplied) {
        QImage dest(source.width() / 2, source.height() / 2, srcImage.format());
        dest.setDevicePixelRatio(source.devicePixelRatioF());

        const uchar *src = reinterpret_cast<const uchar *>(const_cast<const QImage &>(srcImage).bits());
        int sx           = srcImage.bytesPerLine();
        int sx2          = sx << 1;

        uchar *dst = reinterpret_cast<uchar *>(dest.bits());
        int dx     = dest.bytesPerLine();
        int ww     = dest.width();
        int hh     = dest.height();

        for (int y = hh; y; --y, dst += dx, src += sx2) {
            const uchar *p1 = src;
            const uchar *p2 = src + sx;
            uchar *q        = dst;
            for (int x = ww; x; --x, q += 3, p1 += 6, p2 += 6) {
                // alpha
                q[0] = AVG(AVG(p1[0], p1[3]), AVG(p2[0], p2[3]));
                // rgb
                const quint16 p16_1  = (p1[2] << 8) | p1[1];
                const quint16 p16_2  = (p1[5] << 8) | p1[4];
                const quint16 p16_3  = (p2[2] << 8) | p2[1];
                const quint16 p16_4  = (p2[5] << 8) | p2[4];
                const quint16 result = AVG16(AVG16(p16_1, p16_2), AVG16(p16_3, p16_4));
                q[1]                 = result & 0xff;
                q[2]                 = result >> 8;
            }
        }

        return dest;
    }
    else if (source.format() != QImage::Format_ARGB32_Premultiplied && source.format() != QImage::Format_RGB32) {
        srcImage = source.convertToFormat(QImage::Format_ARGB32_Premultiplied);
    }

    QImage dest(source.width() / 2, source.height() / 2, srcImage.format());
    dest.setDevicePixelRatio(source.devicePixelRatioF());

    const quint32 *src = reinterpret_cast<const quint32 *>(const_cast<const QImage &>(srcImage).bits());
    int sx             = srcImage.bytesPerLine() >> 2;
    int sx2            = sx << 1;

    quint32 *dst = reinterpret_cast<quint32 *>(dest.bits());
    int dx       = dest.bytesPerLine() >> 2;
    int ww       = dest.width();
    int hh       = dest.height();

    for (int y = hh; y; --y, dst += dx, src += sx2) {
        const quint32 *p1 = src;
        const quint32 *p2 = src + sx;
        quint32 *q        = dst;
        for (int x = ww; x; --x, q++, p1 += 2, p2 += 2)
            *q = AVG(AVG(p1[0], p1[1]), AVG(p2[0], p2[1]));
    }

    return dest;
}

static void qt_blurImage(/*QPainter *p, */ QImage &blurImage, qreal radius, bool quality, bool alphaOnly, int transposed = 0) {
    if (blurImage.format() != QImage::Format_ARGB32_Premultiplied && blurImage.format() != QImage::Format_RGB32) {
        blurImage = blurImage.convertToFormat(QImage::Format_ARGB32_Premultiplied);
    }

    qreal scale = 1;
    if (radius >= 4 && blurImage.width() >= 2 && blurImage.height() >= 2) {
        blurImage = qt_halfScaled(blurImage);
        scale     = 2;
        radius *= qreal(0.5);
    }

    if (alphaOnly)
        expblur<12, 10, true>(blurImage, radius, quality, transposed);
    else
        expblur<12, 10, false>(blurImage, radius, quality, transposed);

    //if (p) {
    //    p->scale(scale, scale);
    //    p->setRenderHint(QPainter::SmoothPixmapTransform);
    //    p->drawImage(QRect(QPoint(0, 0), blurImage.size() / blurImage.devicePixelRatioF()), blurImage);
    //}
}

里面由图片模糊和图片翻转代码组成,qt_blurImage的QPainter参数由于并不需要使用将其屏蔽,如何绘制通过调用者处理。

实际使用的示例代码如下:

void ImageLabel::paintEvent(QPaintEvent *event)
{
    auto painter = new QPainter(this);

    //QPainter painter(this);
    painter->setRenderHint(QPainter::Antialiasing);
    painter->translate(event->rect().center());

    int diameter = qMin(event->rect().width(), event->rect().height()) / 2;
    QRect arc_rect = QRect(-diameter / 2, -diameter / 2, diameter, diameter);

    // 背景
    painter->save();
    painter->setPen(Qt::NoPen);
    painter->setBrush(QColor(32, 32, 32));
    painter->drawEllipse(arc_rect.adjusted(-50, -50, 50, 50));
    painter->setBrush(QBrush(QColor(24, 24, 24)));
    painter->drawEllipse(arc_rect.adjusted(-25, -25, 25, 25));
    painter->restore();

    // 圆弧
    qreal percent = 0.5;
    qreal startArc = (1.0 - percent) * 0.75;
    int arcHeight = 6;

    QConicalGradient gradient(0, 0, 0);
    gradient.setColorAt(0, QColor(0, 210, 242, 230));
    gradient.setColorAt(startArc, QColor(0, 210, 242, 230));
    gradient.setColorAt((0.75 - startArc) / 2, QColor(0, 129, 245, 230));
    gradient.setColorAt(0.75, QColor(69, 70, 246, 230));

    QPen pen;
    pen.setWidth(2);
    pen.setCapStyle(Qt::RoundCap);
    pen.setBrush(gradient);
    pen.setWidth(arcHeight * 3);
    pen.setBrush(gradient);

    QRect arc_rect_new = arc_rect.adjusted(-50, -50, 50, 50);
    QImage blur_image(arc_rect_new.size(), QImage::Format_ARGB32);
    blur_image.fill(QColor(0, 0, 0, 0));
    QPainter image_painter(&blur_image);
    image_painter.setRenderHint(QPainter::Antialiasing);
    image_painter.setPen(pen);
    image_painter.translate(blur_image.width() / 2, blur_image.height() / 2);
    image_painter.drawArc(arc_rect.adjusted(arcHeight, arcHeight, -arcHeight, -arcHeight), 265 * 16, - 270 * percent * 16);
    image_painter.end();

    qt_blurImage(blur_image, 40.0, false, false);
    // 模糊图绘制
    painter->drawImage(arc_rect_new, blur_image);

	// 实际图形绘制
	painter->setPen(pen);
    painter->drawArc(arc_rect.adjusted(arcHeight, arcHeight, -arcHeight, -arcHeight), 265 * 16, - 270 * percent * 16);
	
    delete painter;
}

效果如下图所示:

QPainter通过模糊实现发光效果_第3张图片

你可能感兴趣的:(qt)