JAVA图像处理系列(十)——艺术效果:水纹

艺术效果:水纹

与火焰效果一样,借鉴粒子系统的思想,我们也可以在图像中实现类似水纹的艺术效果。

粒子系统基本上都要通过循环迭代实现,下面先看看本文方法实现的水纹效果。

第一张为原始图像,第二张为循环迭代50次后的水纹效果,第三张为通过该算法生成的动态GIF图的显示效果。

cat.jpg
cat-water.jpg
cat-water.gif

算法实现

我们通过一个单独的类来保存粒子的状态,这里直接给出代码,感兴趣的朋友可直接通过阅读代码了解实现细节。

import java.awt.image.*;

public class WaterRoutine {
    java.util.Random r = new java.util.Random(System.currentTimeMillis());

    int m_iWidth;
    int m_iHeight;
    boolean m_bDrawWithLight;
    int m_iLightModifier;
    public int m_iHpage;// The current heightfield
    int m_density;// The water density - can change the density...
    // the height fields
    int[] m_iHeightField1;
    int[] m_iHeightField2;

    public int getrandom(int min, int max) {
        return r.nextInt(max - min) + min;
    }

    public int getRGB(int r, int g, int b) {
        return (0x00ff & r) << 16 | (0x00ff & g) << 8 | (0x00ff & b);
    }

    public int getBValue(int color) {
        return color & 0x00ff;
    }

    public int getGValue(int color) {
        return (color >> 8) & 0x00ff;
    }

    public int getRValue(int color) {
        return (color >> 16) & 0x00ff;
    }

    public int getRGB(BufferedImage img, int offset) {
        int x = offset % img.getWidth();
        int y = offset / img.getWidth();

        if (y >= img.getHeight())
            y = img.getHeight() - 1;

        return img.getRGB(x, y);
    }

    public void setRGB(BufferedImage img, int offset, int rgb) {
        int x = offset % img.getWidth();
        int y = offset / img.getWidth();

        if (y >= img.getHeight())
            y = img.getHeight() - 1;

        img.setRGB(x, y, rgb);
    }

    public WaterRoutine() {
        m_iHeightField1 = null;
        m_iHeightField2 = null;

        m_iWidth = 0;
        m_iHeight = 0;

        m_bDrawWithLight = true;
        m_iLightModifier = 1;
        m_iHpage = 0;
        m_density = 5;
    }

    public void create(int iWidth, int iHeight) {
        // Create our height fields
        m_iHeightField1 = new int[(iWidth * iHeight)];
        m_iHeightField2 = new int[(iWidth * iHeight)];

        // Clear our height fields
        for (int i = 0; i < iWidth * iHeight; i++) {
            m_iHeightField1[i] = 0;
            m_iHeightField2[i] = 0;
        }

        m_iWidth = iWidth;
        m_iHeight = iHeight;

        // Set our page to 0
        m_iHpage = 0;
    }

    public void flattenWater() {
        // Clear our height fields
        for (int i = 0; i < m_iWidth * m_iHeight; i++) {
            m_iHeightField1[i] = 0;
            m_iHeightField2[i] = 0;
        }
    }

    public void nullPos() {
        calcWater(m_iHpage, m_density);
        m_iHpage ^= 1;
    }

    public void render(BufferedImage pSrcImage, BufferedImage pTargetImage) {
        // Yes they have to be the same size...(for now)
        if (m_bDrawWithLight == false) {
            drawWaterNoLight(m_iHpage, pSrcImage, pTargetImage);
        } else {
            drawWaterWithLight(m_iHpage, m_iLightModifier, pSrcImage, pTargetImage);
        }
        calcWater(m_iHpage, m_density);
        m_iHpage ^= 1;
    }

    public void calcWater(int npage, int density) {
        int newh;
        int count = m_iWidth + 1;
        int[] newptr;
        int[] oldptr;

        // Set up the pointers
        if (npage == 0) {
            newptr = m_iHeightField1;
            oldptr = m_iHeightField2;
        } else {
            newptr = m_iHeightField2;
            oldptr = m_iHeightField1;
        }

        int x, y;

        // Sorry, this function might not be as readable as I'd like, because
        // I optimized it somewhat. (enough to make me feel satisfied with it)

        for (y = (m_iHeight - 1) * m_iWidth; count < y; count += 2) {
            for (x = count + m_iWidth - 2; count < x; count++) {

                // This does the eight-pixel method. It looks much better.

                newh = ((oldptr[count + m_iWidth] + oldptr[count - m_iWidth] + oldptr[count + 1] + oldptr[count - 1]
                        + oldptr[count - m_iWidth - 1] + oldptr[count - m_iWidth + 1] + oldptr[count + m_iWidth - 1]
                        + oldptr[count + m_iWidth + 1]) >> 2) - newptr[count];

                newptr[count] = newh - (newh >> density);
            }
        }
    }

    public void smoothWater(int npage) {
        int newh;
        int count = m_iWidth + 1;

        int[] newptr;
        int[] oldptr;

        // Set up the pointers
        if (npage == 0) {
            newptr = m_iHeightField1;
            oldptr = m_iHeightField2;
        } else {
            newptr = m_iHeightField2;
            oldptr = m_iHeightField1;
        }

        int x, y;

        // Sorry, this function might not be as readable as I'd like, because
        // I optimized it somewhat. (enough to make me feel satisfied with it)

        for (y = 1; y < m_iHeight - 1; y++) {
            for (x = 1; x < m_iWidth - 1; x++) {
                // This does the eight-pixel method. It looks much better.

                newh = ((oldptr[count + m_iWidth] + oldptr[count - m_iWidth] + oldptr[count + 1] + oldptr[count - 1]
                        + oldptr[count - m_iWidth - 1] + oldptr[count - m_iWidth + 1] + oldptr[count + m_iWidth - 1]
                        + oldptr[count + m_iWidth + 1]) >> 3) + newptr[count];

                newptr[count] = newh >> 1;
                count++;
            }
            count += 2;
        }
    }

    public void calcWaterBigFilter(int npage, int density) {
        int newh;
        int count = (2 * m_iWidth) + 2;

        int[] newptr;
        int[] oldptr;

        // Set up the pointers
        if (npage == 0) {
            newptr = m_iHeightField1;
            oldptr = m_iHeightField2;
        } else {
            newptr = m_iHeightField2;
            oldptr = m_iHeightField1;
        }

        int x, y;

        // Sorry, this function might not be as readable as I'd like, because
        // I optimized it somewhat. (enough to make me feel satisfied with it)

        for (y = 2; y < m_iHeight - 2; y++) {
            for (x = 2; x < m_iWidth - 2; x++) {
                // This does the 25-pixel method. It looks much okay.

                newh = ((((oldptr[count + m_iWidth] + oldptr[count - m_iWidth] + oldptr[count + 1]
                        + oldptr[count - 1]) << 1)
                        + ((oldptr[count - m_iWidth - 1] + oldptr[count - m_iWidth + 1] + oldptr[count + m_iWidth - 1]
                                + oldptr[count + m_iWidth + 1]))
                        + ((oldptr[count - (m_iWidth * 2)] + oldptr[count + (m_iWidth * 2)] + oldptr[count - 2]
                                + oldptr[count + 2]) >> 1)
                        + ((oldptr[count - (m_iWidth * 2) - 1] + oldptr[count - (m_iWidth * 2) + 1]
                                + oldptr[count + (m_iWidth * 2) - 1] + oldptr[count + (m_iWidth * 2) + 1]
                                + oldptr[count - 2 - m_iWidth] + oldptr[count - 2 + m_iWidth]
                                + oldptr[count + 2 - m_iWidth] + oldptr[count + 2 + m_iWidth]) >> 2)) >> 3)
                        - (newptr[count]);

                newptr[count] = newh - (newh >> density);
                count++;
            }
            count += 4;
        }
    }

    public void HeightBlob(int x, int y, int radius, int height, int page) {
        int rquad;
        int cx, cy, cyq;
        int left, top, right, bottom;

        int[] newptr;
        int[] oldptr;

        // Set up the pointers
        if (page == 0) {
            newptr = m_iHeightField1;
            oldptr = m_iHeightField2;
        } else {
            newptr = m_iHeightField2;
            oldptr = m_iHeightField1;
        }

        rquad = radius * radius;

        // Make a randomly-placed blob...
        if (x < 0)
            x = 1 + radius + r.nextInt(65536) % (m_iWidth - 2 * radius - 1);
        if (y < 0)
            y = 1 + radius + r.nextInt(65536) % (m_iHeight - 2 * radius - 1);

        left = -radius;
        right = radius;
        top = -radius;
        bottom = radius;

        // Perform edge clipping...
        if (x - radius < 1)
            left -= (x - radius - 1);
        if (y - radius < 1)
            top -= (y - radius - 1);
        if (x + radius > m_iWidth - 1)
            right -= (x + radius - m_iWidth + 1);
        if (y + radius > m_iHeight - 1)
            bottom -= (y + radius - m_iHeight + 1);

        for (cy = top; cy < bottom; cy++) {
            cyq = cy * cy;
            for (cx = left; cx < right; cx++) {
                if (cx * cx + cyq < rquad)
                    newptr[m_iWidth * (cy + y) + (cx + x)] += height;
            }
        }

    }

    public void HeightBox(int x, int y, int radius, int height, int page) {
        int cx, cy;
        int left, top, right, bottom;
        int[] newptr;
        int[] oldptr;

        // Set up the pointers
        if (page == 0) {
            newptr = m_iHeightField1;
            oldptr = m_iHeightField2;
        } else {
            newptr = m_iHeightField2;
            oldptr = m_iHeightField1;
        }

        if (x < 0)
            x = 1 + radius + r.nextInt(65536) % (m_iWidth - 2 * radius - 1);
        if (y < 0)
            y = 1 + radius + r.nextInt(65536) % (m_iHeight - 2 * radius - 1);

        left = -radius;
        right = radius;
        top = -radius;
        bottom = radius;

        // Perform edge clipping...
        if (x - radius < 1)
            left -= (x - radius - 1);
        if (y - radius < 1)
            top -= (y - radius - 1);
        if (x + radius > m_iWidth - 1)
            right -= (x + radius - m_iWidth + 1);
        if (y + radius > m_iHeight - 1)
            bottom -= (y + radius - m_iHeight + 1);

        for (cy = top; cy < bottom; cy++) {
            for (cx = left; cx < right; cx++) {
                newptr[m_iWidth * (cy + y) + (cx + x)] = height;
            }
        }

    }

    public void WarpBlob(int x, int y, int radius, int height, int page) {
        int cx, cy;
        int left, top, right, bottom;
        int square;
        int radsquare = radius * radius;
        int[] newptr;
        int[] oldptr;

        // Set up the pointers
        if (page == 0) {
            newptr = m_iHeightField1;
            oldptr = m_iHeightField2;
        } else {
            newptr = m_iHeightField2;
            oldptr = m_iHeightField1;
        }
        // radsquare = (radius*radius) << 8;
        radsquare = (radius * radius);

        height /= 64;

        left = -radius;
        right = radius;
        top = -radius;
        bottom = radius;

        // Perform edge clipping...
        if (x - radius < 1)
            left -= (x - radius - 1);
        if (y - radius < 1)
            top -= (y - radius - 1);
        if (x + radius > m_iWidth - 1)
            right -= (x + radius - m_iWidth + 1);
        if (y + radius > m_iHeight - 1)
            bottom -= (y + radius - m_iHeight + 1);

        for (cy = top; cy < bottom; cy++) {
            for (cx = left; cx < right; cx++) {
                square = cy * cy + cx * cx;
                // square <<= 8;
                if (square < radsquare) {
                    // Height[page][WATERWID*(cy+y) + cx+x]
                    // += (sqrt(radsquare)-sqrt(square))*height;
                    newptr[m_iWidth * (cy + y) + cx + x] += (int) ((radius - Math.sqrt(square)) * (float) (height));
                }
            }
        }
    }

    public void sineBlob(int x, int y, int radius, int height, int page) {
        int cx, cy;
        int left, top, right, bottom;
        int square, dist;
        int radsquare = radius * radius;
        float length = (float) ((1024.0 / (float) radius) * (1024.0 / (float) radius));
        int[] newptr;
        int[] oldptr;

        // Set up the pointers
        if (page == 0) {
            newptr = m_iHeightField1;
            oldptr = m_iHeightField2;
        } else {
            newptr = m_iHeightField2;
            oldptr = m_iHeightField1;
        }

        if (x < 0)
            x = 1 + radius + r.nextInt(65536) % (m_iWidth - 2 * radius - 1);
        if (y < 0)
            y = 1 + radius + r.nextInt(65536) % (m_iHeight - 2 * radius - 1);

        // radsquare = (radius*radius) << 8;
        radsquare = (radius * radius);

        // height /= 8;

        left = -radius;
        right = radius;
        top = -radius;
        bottom = radius;

        // Perform edge clipping...
        if (x - radius < 1)
            left -= (x - radius - 1);
        if (y - radius < 1)
            top -= (y - radius - 1);
        if (x + radius > m_iWidth - 1)
            right -= (x + radius - m_iWidth + 1);
        if (y + radius > m_iHeight - 1)
            bottom -= (y + radius - m_iHeight + 1);

        for (cy = top; cy < bottom; cy++) {
            for (cx = left; cx < right; cx++) {
                square = cy * cy + cx * cx;
                if (square < radsquare) {
                    dist = (int) (Math.sqrt(square * length));
                    newptr[m_iWidth * (cy + y) + cx + x] += (int) ((Math.cos(dist) + 0xffff) * (height)) >> 19;
                }
            }
        }
    }

    public void drawWaterNoLight(int page, BufferedImage pSrcImage, BufferedImage pTargetImage) {
        int dx, dy;
        int x, y;
        int c;

        int offset = m_iWidth + 1;

        int[] ptr = m_iHeightField1;

        for (y = (m_iHeight - 1) * m_iWidth; offset < y; offset += 2) {
            for (x = offset + m_iWidth - 2; offset < x; offset++) {
                if (offset + m_iWidth >= m_iWidth * m_iHeight)
                    continue;
                dx = ptr[offset] - ptr[offset + 1];
                dy = ptr[offset] - ptr[offset + m_iWidth];

                // c = pSrcImage[offset + m_iWidth*(dy>>3) + (dx>>3)];
                c = getRGB(pSrcImage, offset + m_iWidth * (dy >> 3) + (dx >> 3));

                // pTargetImage[offset] = c;
                setRGB(pTargetImage, offset, c);

                offset++;
                if (offset + m_iWidth >= m_iWidth * m_iHeight)
                    continue;
                dx = ptr[offset] - ptr[offset + 1];
                dy = ptr[offset] - ptr[offset + m_iWidth];
                // c = pSrcImage[offset + m_iWidth*(dy>>3) + (dx>>3)];
                // pTargetImage[offset] = c;
                c = getRGB(pSrcImage, offset + m_iWidth * (dy >> 3) + (dx >> 3));
                setRGB(pTargetImage, offset, c);
            }
        }
    }

    public void drawWaterWithLight(int page, int LightModifier, BufferedImage pSrcImage, BufferedImage pTargetImage) {
        int dx, dy;
        int x, y;
        int c;

        int offset = m_iWidth + 1;
        int lIndex;
        int lBreak = m_iWidth * m_iHeight;

        int[] ptr = m_iHeightField1;

        for (y = (m_iHeight - 1) * m_iWidth; offset < y; offset += 2) {
            for (x = offset + m_iWidth - 2; offset < x; offset++) {
                if (offset + m_iWidth >= m_iWidth * m_iHeight)
                    continue;
                dx = ptr[offset] - ptr[offset + 1];
                dy = ptr[offset] - ptr[offset + m_iWidth];

                lIndex = offset + m_iWidth * (dy >> 3) + (dx >> 3);
                if (lIndex < lBreak && lIndex > 0) {
                    // c = pSrcImage[lIndex];// - (dx>>LightModifier);
                    c = getRGB(pSrcImage, lIndex);
                    // Now we shift it by the dx component...
                    //
                    c = getShiftedColor(c, dx);

                    // pTargetImage[offset] = c;
                    setRGB(pTargetImage, offset, c);
                }

                offset++;
                if (offset + m_iWidth >= m_iWidth * m_iHeight)
                    continue;
                dx = ptr[offset] - ptr[offset + 1];
                dy = ptr[offset] - ptr[offset + m_iWidth];

                lIndex = offset + m_iWidth * (dy >> 3) + (dx >> 3);
                if (lIndex < lBreak && lIndex > 0) {
                    // c = pSrcImage[lIndex];// - (dx>>LightModifier);
                    c = getRGB(pSrcImage, lIndex);
                    c = getShiftedColor(c, dx);
                    // temp[offset] = (c < 0) ? 0 : (c > 255) ? 255 : c;
                    // pTargetImage[offset] = c;
                    setRGB(pTargetImage, offset, c);
                }

            }
        }
    }

    public int getShiftedColor(int color, int shift) {
        int R;
        int G;
        int B;
        int ir;
        int ig;
        int ib;

        R = getRValue(color) - shift;
        G = getGValue(color) - shift;
        B = getBValue(color) - shift;

        ir = (R < 0) ? 0 : (R > 255) ? 255 : R;
        ig = (G < 0) ? 0 : (G > 255) ? 255 : G;
        ib = (B < 0) ? 0 : (B > 255) ? 255 : B;

        return getRGB(ir, ig, ib);
    }
}

测试代码

下面是测试代码:

public class GifEffectWaterTest {

    public static void waterImage(int xpos, int ypos) throws IOException {
        BufferedImage img = ImageIO.read(new File("D:\\eclipse-workspace\\image-toolkit\\images\\cat.jpg"));
        
        WaterRoutine wr = new WaterRoutine();
        wr.create(img.getWidth(), img.getHeight());
        wr.HeightBlob(xpos, ypos, 50, 500, wr.m_iHpage);

        for (int i = 0; i < 50; i++) {
            wr.nullPos();
        }
        BufferedImage resultImage = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_RGB);
        wr.render(img, resultImage);

        ImageIO.write(resultImage, "jpeg", new File("D:\\eclipse-workspace\\image-toolkit\\images\\cat-water.jpg"));
    }
    
    public static Vector<BufferedImage> waterEffect(BufferedImage img, int xpos, int ypos) {
        
        Vector<BufferedImage> images = new Vector<>();
        
        WaterRoutine wr = new WaterRoutine();
        wr.create(img.getWidth(), img.getHeight());
        wr.HeightBlob(xpos, ypos, 50, 500, wr.m_iHpage);

        for (int i = 0; i < 200; i++) {
            if (i % 10 == 0) {
                BufferedImage tmpImg = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_RGB);
                wr.render(img, tmpImg);
                images.add(tmpImg);
            } else {
                wr.nullPos();
            }
        }

        return images;
    }
    
    public static void waterImageGif(int xpos, int ypos) throws IOException {
        BufferedImage img = ImageIO.read(new File("D:\\eclipse-workspace\\image-toolkit\\images\\cat.jpg"));
        OutputStream out = new FileOutputStream("D:\\eclipse-workspace\\image-toolkit\\images\\cat-water.gif");
                
        GifEncoder gif = new GifEncoder();
        if (!gif.isInit()) {
            gif.init(img.getWidth(), img.getHeight(), out, true);
            gif.setDelay(10);
        }
        
        Vector<BufferedImage> images = waterEffect(img, xpos, ypos);
        
        for (BufferedImage item : images) {
            gif.addImage(item);
        }
        
        gif.setCount(0);
        gif.encodeMultiple();
        out.close();
    }
    
    public static void main(String[] argv) throws IOException {
        waterImage(100, 300);
        waterImageGif(100, 300);
    }
}

其中waterEffect实现迭代50次后的水纹效果,waterImageGif实现每迭代10次生成一张图片,叠加或生成动态GIF图片。GIF图片生成类GifEncoder请参考文章《JAVA实现GIF动画》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355

推荐阅读更多精彩内容