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动画》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容