From 484af484bc6895f695f9a8f82655a9129080fc6a Mon Sep 17 00:00:00 2001 From: Matthew Cech Date: Sat, 13 Apr 2019 13:07:24 -0700 Subject: [PATCH] + Added image overlay custom class and utility logging --- src/utils/GlobalLog.java | 2 +- src/utils/ImageOverlayHelper.java | 285 ++++++++++++++++++++++++++++++ src/utils/LogFilter.java | 3 +- 3 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 src/utils/ImageOverlayHelper.java diff --git a/src/utils/GlobalLog.java b/src/utils/GlobalLog.java index f350d2d..2cf28b9 100644 --- a/src/utils/GlobalLog.java +++ b/src/utils/GlobalLog.java @@ -13,7 +13,7 @@ public class GlobalLog } public static void Log(String msg) { Write(log, LogFilter.Debug, msg); } - public static void Warn(String msg) {Write(warn, LogFilter.Debug, msg); } + public static void Warn(String msg) { Write(warn, LogFilter.Debug, msg); } public static void Error(String msg) { Write(error, LogFilter.Debug, msg); } public static void Fatal(String msg) throws Exception { Write(fatal, LogFilter.Debug, msg); throw new Exception(msg); } diff --git a/src/utils/ImageOverlayHelper.java b/src/utils/ImageOverlayHelper.java new file mode 100644 index 0000000..e1a5855 --- /dev/null +++ b/src/utils/ImageOverlayHelper.java @@ -0,0 +1,285 @@ +package utils; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; +import java.awt.image.RenderedImage; +import java.io.File; +import java.io.IOException; +import java.util.Calendar; +import java.util.Iterator; + +import javax.imageio.IIOException; +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataNode; +import javax.imageio.stream.FileImageOutputStream; +import javax.imageio.stream.ImageOutputStream; + +// A class that overlays an image based on a series of frames that will be converted into a gif. +// The class targets a pixel with max Green value and no red or blue that's fully opaque (0, 255, 0) +// and then places the center of the overlay image on the specified location. +public class ImageOverlayHelper +{ + // Lets get some trash established for the yeet + public static final boolean VERBOSE = false; + public static final int NONE = -1; + public static final int PIXEL_BYTE_LENGTH = 4; // 32-bit PNG, RGBA + public static final int FPS = 18; // This doesn't quite line up, multiples of 10 are as specific as it gets. + public static final String LOG_HEADER = "[Overlay] "; + + // Logging intermediate functions + private static void Verbose(String str) { if(VERBOSE) Log("[Verbose] " + str); } + private static void Log(String str) { GlobalLog.Log(LogFilter.Util, LOG_HEADER + str); } + private static void Warn(String str) { GlobalLog.Warn(LogFilter.Util, LOG_HEADER + str); } + private static void Error(String str) { GlobalLog.Error(LogFilter.Util, LOG_HEADER + str); } + + // Local variables + private final String basePath; // The folder location of the files + private final String baseName; // This is the consistent part of the filename of each frame, for example "image " + private final int frameCount; // The number of frames in the image + + // Constructor + public ImageOverlayHelper(String basePath, String baseName, int frameCount) + { + this.basePath = basePath; + this.baseName = baseName; + this.frameCount = frameCount; + } + + // Performs a per-frame overlay, catches IO errors as a note.) + public void Overlay(BufferedImage overlay, String outpath) + { + try + { + long start = Calendar.getInstance().getTimeInMillis(); + + ProcessOverlay(overlay, outpath); + + long end = Calendar.getInstance().getTimeInMillis(); + Log("Took " + (end - start) + "ms"); + } + catch (IOException e) + { + Error(e.getMessage()); + } + } + + // Processing the image frames to construct a gif. Relies on (0,255,0) pixel for image center specification. + public void ProcessOverlay(BufferedImage overlay, String outfileName) throws IOException + { + BufferedImage[] frames = new BufferedImage[frameCount]; + + for(int i = 0; i < frameCount; ++i) + { + BufferedImage out = CombineImages(basePath + baseName + i + ".png", overlay); + + if(out != null) + frames[i] = out; + else + Error("There was an issue with overlaying frame " + i); + } + + Verbose("Processed " + frameCount + " frames, writing..."); + ImageOutputStream output = new FileImageOutputStream(new File(outfileName)); + GifSequenceWriter writer = new GifSequenceWriter(output, frames[0].getType(), FPS, true); + + for(int i = 0; i < frameCount; ++i) + writer.writeToSequence(frames[i]); + + writer.close(); + output.close(); + } + + // Performs an overlay at a pixel location. We're making some assumptions here, mostly that there is going + // to be an RGBA-32-bit encoded PNG image read in for our parsing purposes. We then look for a 0, 255, 0 + // green pixel on the base frame to apply the overlay buffer to, centered. + public static BufferedImage CombineImages(String pathBase, BufferedImage overlay) throws IOException + { + // Acquire images + BufferedImage imageBase = null; + BufferedImage imageOverlay = overlay; + + imageBase = ImageIO.read(new File(pathBase)); + + // Grab data we know won't change. As a side note, I discovered you can get specific pixels a bit + // differently later, but it was too late, I wrote the pixel specific code already. + final byte[] pixelsBase = ((DataBufferByte) imageBase.getRaster().getDataBuffer()).getData(); + final int baseWidth = imageBase.getWidth(); + final int overlayWidth = imageOverlay.getWidth(); + final int baseHeight = imageBase.getHeight(); + final int overlayHeight = imageOverlay.getHeight(); + + // Warn about odd sizing when applicable + if(overlayHeight > baseHeight || overlayWidth > baseWidth) + Warn("Size mismatch, overlay is larger. May function, but not supported."); + + // Skim base picture for solid green pixel to use as target center + int targetX = NONE; + int targetY = NONE; + for(int i = 0; i < pixelsBase.length; i += PIXEL_BYTE_LENGTH) + { + final int x = ((i / PIXEL_BYTE_LENGTH) % baseWidth); + final int y = ((i / PIXEL_BYTE_LENGTH) / baseWidth); + + final int r = (pixelsBase[i]); + final int g = (pixelsBase[i + 1]); + final int b = (pixelsBase[i + 2]); + //final int a = (pixelsBase[i + 3]); + + // Max contrast in 32-bit PNG for green is no red no blue, max green (ratio), max alpha (implied 0 here) + if(r == -1 && g == 0 && b == -1) + { + //Log("Found target pixel at x: " + x + " y: " + y); + targetX = x; + targetY = y; + break; + } + } + + // Skip overlay if we have no target location + if(targetX == NONE || targetY == NONE) + { + Verbose("No overlay required for this frame."); + return imageBase; + } + + // Re-create a new byte array and establish overlay bounds + final int left = targetX - overlayWidth / 2; + final int right = targetX + overlayWidth / 2; + final int top = targetY - overlayHeight / 2; + final int bottom = targetY + overlayHeight / 2; + + // Apply overlay when appropriate + for(int i = 0; i < pixelsBase.length; i += PIXEL_BYTE_LENGTH) + { + final int x = ((i / PIXEL_BYTE_LENGTH) % baseWidth); + final int y = ((i / PIXEL_BYTE_LENGTH) / baseWidth); + + // If we're within the bounds of where the overlay picture should go... + if(x > left && x < right && y > top && y < bottom) + { + final int overlayX = x - left; + final int overlayY = y - top; + + // Skip out of bounds pixels + if(overlayX < 0 || overlayY < 0 || overlayX >= baseWidth || overlayY >= baseHeight) + continue; + + // Re-write bytes as overlay + imageBase.setRGB(x, y, imageOverlay.getRGB(overlayX, overlayY)); + } + } + + // Give the frame back + return imageBase; + } + + + // GifSequenceWriter.java + // + // Created by Elliot Kroo on 2009-04-25, + // (Small modifications by wisp in 2018) + // + // This work is licensed under the Creative Commons Attribution 3.0 Unported License. + // To view a copy of this license visit http://creativecommons.org/licenses/by/3.0/ + // NOTE: ^ This is Elliot's original license + public static class GifSequenceWriter + { + protected ImageWriter gifWriter; + protected ImageWriteParam imageWriteParam; + protected IIOMetadata imageMetaData; + + // NOTE: This is limited to things that divide into 1000 cleanly as multiples of 10. + public GifSequenceWriter(ImageOutputStream outputStream, int imageType, int framesPerSecond, boolean isLooping) throws IIOException, IOException + { + gifWriter = getWriter(); + imageWriteParam = gifWriter.getDefaultWriteParam(); + ImageTypeSpecifier imageTypeSpecifier = ImageTypeSpecifier.createFromBufferedImageType(imageType); + + imageMetaData = gifWriter.getDefaultImageMetadata(imageTypeSpecifier, imageWriteParam); + + String metaFormatName = imageMetaData.getNativeMetadataFormatName(); + + IIOMetadataNode root = (IIOMetadataNode) imageMetaData.getAsTree(metaFormatName); + IIOMetadataNode graphicsControlExtensionNode = getNode(root, "GraphicControlExtension"); + graphicsControlExtensionNode.setAttribute("disposalMethod", "none"); + graphicsControlExtensionNode.setAttribute("userInputFlag", "FALSE"); + graphicsControlExtensionNode.setAttribute("transparentColorFlag", "FALSE"); + graphicsControlExtensionNode.setAttribute("delayTime", "" + (int) ((1000.0 / framesPerSecond) / 10.0)); // THis line isn't super well honored + graphicsControlExtensionNode.setAttribute("transparentColorIndex","0"); + + IIOMetadataNode appEntensionsNode = getNode( root, "ApplicationExtensions"); + IIOMetadataNode child = new IIOMetadataNode("ApplicationExtension"); + child.setAttribute("applicationID", "NETSCAPE"); + child.setAttribute("authenticationCode", "2.0"); + + int loopValue = isLooping ? 0 : 1; + child.setUserObject(new byte[]{ 0x1, (byte) (loopValue & 0xFF), (byte) ((loopValue >> 8) & 0xFF)}); + appEntensionsNode.appendChild(child); + + imageMetaData.setFromTree(metaFormatName, root); + + gifWriter.setOutput(outputStream); + gifWriter.prepareWriteSequence(null); + } + + public void writeToSequence(RenderedImage img) throws IOException + { + gifWriter.writeToSequence(new IIOImage(img, null, imageMetaData), imageWriteParam); + } + + /** + * Close this GifSequenceWriter object. This does not close the underlying + * stream, just finishes off the GIF. + */ + public void close() throws IOException + { + gifWriter.endWriteSequence(); + } + + /** + * Returns the first available GIF ImageWriter using + * ImageIO.getImageWritersBySuffix("gif"). + * + * @return a GIF ImageWriter object + * @throws IIOException if no GIF image writers are returned + */ + private static ImageWriter getWriter() throws IIOException + { + Iterator iter = ImageIO.getImageWritersBySuffix("gif"); + + if(!iter.hasNext()) + throw new IIOException("No GIF Image Writers Exist"); + else + return iter.next(); + } + + /** + * Returns an existing child node, or creates and returns a new child node (if + * the requested node does not exist). + * + * @param rootNode the IIOMetadataNode to search for the child node. + * @param nodeName the name of the child node. + * + * @return the child node, if found or a new node created with the given name. + */ + private static IIOMetadataNode getNode(IIOMetadataNode rootNode, String nodeName) + { + int nodeCount = rootNode.getLength(); + for (int i = 0; i < nodeCount; i++) + { + if (rootNode.item(i).getNodeName().compareToIgnoreCase(nodeName) == 0) + return((IIOMetadataNode) rootNode.item(i)); + } + + IIOMetadataNode node = new IIOMetadataNode(nodeName); + rootNode.appendChild(node); + + return(node); + } + } +} diff --git a/src/utils/LogFilter.java b/src/utils/LogFilter.java index eeb9d3a..22742fe 100644 --- a/src/utils/LogFilter.java +++ b/src/utils/LogFilter.java @@ -2,7 +2,8 @@ package utils; public enum LogFilter { - Debug(0), Command(1), Core(2), Database(4), Response(8), Network(16), Strings(32); //8, 16, 32... (flags, so we can | together later) + // Assign numbers as flags, so we can | ('or') them together as necessary + Debug(0), Command(1), Core(2), Util(4), Database(8), Response(16), Network(32), Strings(64); private final int value; private LogFilter(int value)