All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.alkacon.simapi.util.Quantize Maven / Gradle / Ivy

Go to download

alkacon-simapi is the JAR-Library for OpenCms. OpenCms is a Content Management System that is based on Open Source Software. Complex Intranet and Internet websites can be quickly and cost-effectively created, maintained and managed.

There is a newer version: 1.0.4
Show newest version
/*
 * File   : $Source: /alkacon/cvs/AlkaconSimapi/src/com/alkacon/simapi/util/Quantize.java,v $
 * Date   : $Date: 2007/11/20 15:59:13 $
 * Version: $Revision: 1.3 $
 *
 * Copyright (c) 2007 Alkacon Software GmbH (http://www.alkacon.com)
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * For further information about Alkacon Software GmbH, please see the
 * company website: http://www.alkacon.com
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package com.alkacon.simapi.util;

import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.awt.image.IndexColorModel;

/**
 * An efficient color quantization algorithm, adapted from the C++
 * implementation quantize.c in ImageMagick.

* * The pixels for * an image are placed into an oct tree. The oct tree is reduced in * size, and the pixels from the original image are reassigned to the * nodes in the reduced tree.

* * Here is the copyright notice from ImageMagick: * *

 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 %  Permission is hereby granted, free of charge, to any person obtaining a    %
 %  copy of this software and associated documentation files ("ImageMagick"),  %
 %  to deal in ImageMagick without restriction, including without limitation   %
 %  the rights to use, copy, modify, merge, publish, distribute, sublicense,   %
 %  and/or sell copies of ImageMagick, and to permit persons to whom the       %
 %  ImageMagick is furnished to do so, subject to the following conditions:    %
 %                                                                             %
 %  The above copyright notice and this permission notice shall be included in %
 %  all copies or substantial portions of ImageMagick.                         %
 %                                                                             %
 %  The software is provided "as is", without warranty of any kind, express or %
 %  implied, including but not limited to the warranties of merchantability,   %
 %  fitness for a particular purpose and noninfringement.  In no event shall   %
 %  E. I. du Pont de Nemours and Company be liable for any claim, damages or   %
 %  other liability, whether in an action of contract, tort or otherwise,      %
 %  arising from, out of or in connection with ImageMagick or the use or other %
 %  dealings in ImageMagick.                                                   %
 %                                                                             %
 %  Except as contained in this notice, the name of the E. I. du Pont de       %
 %  Nemours and Company shall not be used in advertising or otherwise to       %
 %  promote the sale, use or other dealings in ImageMagick without prior       %
 %  written authorization from the E. I. du Pont de Nemours and Company.       %
 %                                                                             %
 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 
* * @version 0.90 19 Sep 2000 * @author Adam Doppelt */ public class Quantize { /* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % % % % % % QQQ U U AAA N N TTTTT IIIII ZZZZZ EEEEE % % Q Q U U A A NN N T I ZZ E % % Q Q U U AAAAA N N N T I ZZZ EEEEE % % Q QQ U U A A N NN T I ZZ E % % QQQQ UUU A A N N T IIIII ZZZZZ EEEEE % % % % % % Reduce the Number of Unique Colors in an Image % % % % % % Software Design % % John Cristy % % July 1992 % % % % % % Copyright 1998 E. I. du Pont de Nemours and Company % % % % Permission is hereby granted, free of charge, to any person obtaining a % % copy of this software and associated documentation files ("ImageMagick"), % % to deal in ImageMagick without restriction, including without limitation % % the rights to use, copy, modify, merge, publish, distribute, sublicense, % % and/or sell copies of ImageMagick, and to permit persons to whom the % % ImageMagick is furnished to do so, subject to the following conditions: % % % % The above copyright notice and this permission notice shall be included in % % all copies or substantial portions of ImageMagick. % % % % The software is provided "as is", without warranty of any kind, express or % % implied, including but not limited to the warranties of merchantability, % % fitness for a particular purpose and noninfringement. In no event shall % % E. I. du Pont de Nemours and Company be liable for any claim, damages or % % other liability, whether in an action of contract, tort or otherwise, % % arising from, out of or in connection with ImageMagick or the use or other % % dealings in ImageMagick. % % % % Except as contained in this notice, the name of the E. I. du Pont de % % Nemours and Company shall not be used in advertising or otherwise to % % promote the sale, use or other dealings in ImageMagick without prior % % written authorization from the E. I. du Pont de Nemours and Company. % % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % Realism in computer graphics typically requires using 24 bits/pixel to % generate an image. Yet many graphic display devices do not contain % the amount of memory necessary to match the spatial and color % resolution of the human eye. The QUANTIZE program takes a 24 bit % image and reduces the number of colors so it can be displayed on % raster device with less bits per pixel. In most instances, the % quantized image closely resembles the original reference image. % % A reduction of colors in an image is also desirable for image % transmission and real-time animation. % % Function Quantize takes a standard RGB or monochrome images and quantizes % them down to some fixed number of colors. % % For purposes of color allocation, an image is a set of n pixels, where % each pixel is a point in RGB space. RGB space is a 3-dimensional % vector space, and each pixel, pi, is defined by an ordered triple of % red, green, and blue coordinates, (ri, gi, bi). % % Each primary color component (red, green, or blue) represents an % intensity which varies linearly from 0 to a maximum value, cmax, which % corresponds to full saturation of that color. Color allocation is % defined over a domain consisting of the cube in RGB space with % opposite vertices at (0,0,0) and (cmax,cmax,cmax). QUANTIZE requires % cmax = 255. % % The algorithm maps this domain onto a tree in which each node % represents a cube within that domain. In the following discussion % these cubes are defined by the coordinate of two opposite vertices: % The vertex nearest the origin in RGB space and the vertex farthest % from the origin. % % The tree's root node represents the the entire domain, (0,0,0) through % (cmax,cmax,cmax). Each lower level in the tree is generated by % subdividing one node's cube into eight smaller cubes of equal size. % This corresponds to bisecting the parent cube with planes passing % through the midpoints of each edge. % % The basic algorithm operates in three phases: Classification, % Reduction, and Assignment. Classification builds a color % description tree for the image. Reduction collapses the tree until % the number it represents, at most, the number of colors desired in the % output image. Assignment defines the output image's color map and % sets each pixel's color by reclassification in the reduced tree. % Our goal is to minimize the numerical discrepancies between the original % colors and quantized colors (quantization error). % % Classification begins by initializing a color description tree of % sufficient depth to represent each possible input color in a leaf. % However, it is impractical to generate a fully-formed color % description tree in the classification phase for realistic values of % cmax. If colors components in the input image are quantized to k-bit % precision, so that cmax= 2k-1, the tree would need k levels below the % root node to allow representing each possible input color in a leaf. % This becomes prohibitive because the tree's total number of nodes is % 1 + sum(i=1,k,8k). % % A complete tree would require 19,173,961 nodes for k = 8, cmax = 255. % Therefore, to avoid building a fully populated tree, QUANTIZE: (1) % Initializes data structures for nodes only as they are needed; (2) % Chooses a maximum depth for the tree as a function of the desired % number of colors in the output image (currently log2(colorMap size)). % % For each pixel in the input image, classification scans downward from % the root of the color description tree. At each level of the tree it % identifies the single node which represents a cube in RGB space % containing the pixel's color. It updates the following data for each % such node: % % n1: Number of pixels whose color is contained in the RGB cube % which this node represents; % % n2: Number of pixels whose color is not represented in a node at % lower depth in the tree; initially, n2 = 0 for all nodes except % leaves of the tree. % % Sr, Sg, Sb: Sums of the red, green, and blue component values for % all pixels not classified at a lower depth. The combination of % these sums and n2 will ultimately characterize the mean color of a % set of pixels represented by this node. % % E: The distance squared in RGB space between each pixel contained % within a node and the nodes' center. This represents the quantization % error for a node. % % Reduction repeatedly prunes the tree until the number of nodes with % n2 > 0 is less than or equal to the maximum number of colors allowed % in the output image. On any given iteration over the tree, it selects % those nodes whose E count is minimal for pruning and merges their % color statistics upward. It uses a pruning threshold, Ep, to govern % node selection as follows: % % Ep = 0 % while number of nodes with (n2 > 0) > required maximum number of colors % prune all nodes such that E <= Ep % Set Ep to minimum E in remaining nodes % % This has the effect of minimizing any quantization error when merging % two nodes together. % % When a node to be pruned has offspring, the pruning procedure invokes % itself recursively in order to prune the tree from the leaves upward. % n2, Sr, Sg, and Sb in a node being pruned are always added to the % corresponding data in that node's parent. This retains the pruned % node's color characteristics for later averaging. % % For each node, n2 pixels exist for which that node represents the % smallest volume in RGB space containing those pixel's colors. When n2 % > 0 the node will uniquely define a color in the output image. At the % beginning of reduction, n2 = 0 for all nodes except a the leaves of % the tree which represent colors present in the input image. % % The other pixel count, n1, indicates the total number of colors % within the cubic volume which the node represents. This includes n1 - % n2 pixels whose colors should be defined by nodes at a lower level in % the tree. % % Assignment generates the output image from the pruned tree. The % outpu t image consists of two parts: (1) A color map, which is an % array of color descriptions (RGB triples) for each color present in % the output image; (2) A pixel array, which represents each pixel as % an index into the color map array. % % First, the assignment phase makes one pass over the pruned color % description tree to establish the image's color map. For each node % with n2 > 0, it divides Sr, Sg, and Sb by n2 . This produces the % mean color of all pixels that classify no lower than this node. Each % of these colors becomes an entry in the color map. % % Finally, the assignment phase reclassifies each pixel in the pruned % tree to identify the deepest node containing the pixel's color. The % pixel's value in the pixel array becomes the index of this node's mean % color in the color map. % % With the permission of USC Information Sciences Institute, 4676 Admiralty % Way, Marina del Rey, California 90292, this code was adapted from module % ALCOLS written by Paul Raveling. % % The names of ISI and USC are not used in advertising or publicity % pertaining to distribution of the software without prior specific % written permission from ISI. % */ private static class Cube { /** * A single Node in the tree.

*/ static class Node { /** Children nodes. */ Node children[]; /** The parent node. */ Node m_parent; /** Alpha channel midpoint. */ int midAlpha; /** Blue color midpoint. */ int midBlue; /** Green color midpoint. */ int midGreen; /** Red color midpoint. */ int midRed; /** The pixel count for this node and all children. */ int numPixels; /** The total alpha. */ int totalAlpha; /** The total blue. */ int totalBlue; /** The total green. */ int totalGreen; /** The total red. */ int totalRed; /** The unique value. */ int unique; /** Used to build the colorMap. */ private int colorIndex; private Cube m_cube; // our index within our parent private int m_id; // our level within the tree private int m_level; private int numChildren; /** * A node based on a cube.

* * @param cube the cube to base the node on */ Node(Cube cube) { this.m_cube = cube; this.m_parent = this; this.children = new Node[MAX_CHILDREN]; this.m_id = 0; this.m_level = 0; this.numPixels = Integer.MAX_VALUE; this.midRed = (MAX_RGB + 1) >> 1; this.midGreen = (MAX_RGB + 1) >> 1; this.midBlue = (MAX_RGB + 1) >> 1; this.midAlpha = (MAX_RGB + 1) >> 1; } /** * A node based on a value set.

* * @param parent the parent * @param id the id * @param level the level */ Node(Node parent, int id, int level) { this.m_cube = parent.m_cube; this.m_parent = parent; this.children = new Node[MAX_CHILDREN]; this.m_id = id; this.m_level = level; // add to the cube ++m_cube.numNodes; if (level == m_cube.depth) { ++m_cube.m_numColors; } // add to the parent ++parent.numChildren; parent.children[id] = this; // figure out our midpoint int bi = (1 << (MAX_TREE_DEPTH - level)) >> 1; midRed = parent.midRed + ((id & 1) > 0 ? bi : -bi); midGreen = parent.midGreen + ((id & 2) > 0 ? bi : -bi); midBlue = parent.midBlue + ((id & 4) > 0 ? bi : -bi); midAlpha = parent.midAlpha + ((id & 8) > 0 ? bi : -bi); } /** * Figure out the distance between this node and som color. */ private final static int distance(int r1, int g1, int b1, int a1, int r2, int g2, int b2, int a2) { int da = a1 - a2; int dr = r1 - r2; int dg = g1 - g2; int db = b1 - b2; return da * da + dr * dr + dg * dg + db * db; } public String toString() { StringBuffer buf = new StringBuffer(); if (m_parent == this) { buf.append("root"); } else { buf.append("node"); } buf.append(' '); buf.append(m_level); buf.append(" ["); buf.append(midRed); buf.append(','); buf.append(midGreen); buf.append(','); buf.append(midBlue); buf.append(','); buf.append(midAlpha); buf.append(']'); return new String(buf); } /** * Traverses the color cube tree at a particular node * and determines which colorMap entry best represents the input * color.

* * @param red the red color value * @param green the green color value * @param blue the blue color value * @param alpha the alpha channel value * @param search the search value */ void closestColor(int red, int green, int blue, int alpha, Search search) { if (numChildren != 0) { for (int id = 0; id < MAX_CHILDREN; id++) { if (children[id] != null) { children[id].closestColor(red, green, blue, alpha, search); } } } if (unique != 0) { int distance = distance( m_cube.colorMap[0][colorIndex] & 0xff, m_cube.colorMap[1][colorIndex] & 0xff, m_cube.colorMap[2][colorIndex] & 0xff, m_cube.colorMap[3][colorIndex] & 0xff, red, green, blue, alpha); if (distance < search.distance) { search.distance = distance; search.colorIndex = colorIndex; } } } /** * Traverses the color cube tree and notes each colorMap * entry.

* * A colorMap entry is any node in the color cube tree where * the number of unique colors is not zero.

*/ void mapColors() { if (numChildren != 0) { for (int id = 0; id < MAX_CHILDREN; id++) { if (children[id] != null) { children[id].mapColors(); } } } if (unique != 0) { int add = unique >> 1; m_cube.colorMap[0][m_cube.m_numColors] = (byte)((totalRed + add) / unique); m_cube.colorMap[1][m_cube.m_numColors] = (byte)((totalGreen + add) / unique); m_cube.colorMap[2][m_cube.m_numColors] = (byte)((totalBlue + add) / unique); m_cube.colorMap[3][m_cube.m_numColors] = (byte)((totalAlpha + add) / unique); colorIndex = m_cube.m_numColors++; } } /** * Remove this children node, and make sure our parent absorbs our * pixel statistics. */ void pruneChild() { --m_parent.numChildren; m_parent.unique += unique; m_parent.totalRed += totalRed; m_parent.totalGreen += totalGreen; m_parent.totalBlue += totalBlue; m_parent.totalAlpha += totalAlpha; m_parent.children[m_id] = null; --m_cube.numNodes; m_cube = null; m_parent = null; } /** * Prune the lowest layer of the tree. */ void pruneLevel() { if (numChildren != 0) { for (int id = 0; id < MAX_CHILDREN; id++) { if (children[id] != null) { children[id].pruneLevel(); } } } if (m_level == m_cube.depth) { pruneChild(); } } /** * Remove any nodes that have fewer than threshold pixels.

* * Also, as long as we're walking the tree: - figure out the color with the * fewest pixels - recalculate the total number of colors in the * tree.

* * @param threshold the current threshold * @param nextThreshold the next threshhold * * @return the reduced nodes */ int reduce(int threshold, int nextThreshold) { if (numChildren != 0) { for (int id = 0; id < MAX_CHILDREN; id++) { if (children[id] != null) { nextThreshold = children[id].reduce(threshold, nextThreshold); } } } if (numPixels <= threshold) { pruneChild(); } else { if (unique != 0) { m_cube.m_numColors++; } if (numPixels < nextThreshold) { nextThreshold = numPixels; } } return nextThreshold; } } /** * The result of a closest color search.

*/ static class Search { /** The color index. */ int colorIndex; /** The distance. */ int distance; } /** The color map, */ byte colorMap[][]; /** The color depth. */ int depth; /** The number of colors. */ int m_numColors; /** Counter for the number of nodes in the tree. */ int numNodes; private boolean addTransparency; // firstColor is set to 1 when when addTransparency is true! private int firstColor = 0; private boolean m_alphaToBitmask; private int m_maxColors; private int[] m_pixels; private BufferedImage m_source; private Node root; /** * Creates a new cube.

* * @param source the image source * @param pixels the pixels * @param maxColors the maximum colors * @param alphaToBitmask indicates if the alpha mask should be kept */ Cube(BufferedImage source, int[] pixels, int maxColors, boolean alphaToBitmask) { this.m_source = source; this.m_pixels = pixels; this.m_maxColors = maxColors; this.m_alphaToBitmask = alphaToBitmask; int i = maxColors; // tree_depth = log maxColors // 4 for (depth = 1; i != 0; depth++) { i /= 4; } if (depth > 1) { --depth; } if (depth > MAX_TREE_DEPTH) { depth = MAX_TREE_DEPTH; } else if (depth < 2) { depth = 2; } root = new Node(this); } /** * Procedure assignment generates the output image from the pruned tree. * The output image consists of two parts: (1) A color map, which is an * array of color descriptions (RGB triples) for each color present in * the output image; (2) A pixel array, which represents each pixel as * an index into the color map array. * * First, the assignment phase makes one pass over the pruned color * description tree to establish the image's color map. For each node * with n2 > 0, it divides Sr, Sg, and Sb by n2. This produces the mean * color of all pixels that classify no lower than this node. Each of * these colors becomes an entry in the color map. * * Finally, the assignment phase reclassifies each pixel in the pruned * tree to identify the deepest node containing the pixel's color. The * pixel's value in the pixel array becomes the index of this node's * mean color in the color map. * * @return the created buffered image */ BufferedImage assignment() { colorMap = new byte[4][m_numColors]; if (addTransparency) { // if a transparency color is added, firstColor was set to 1, // so color 0 can be used for this colorMap[0][0] = 0; colorMap[1][0] = 0; colorMap[2][0] = 0; colorMap[3][0] = 0; } m_numColors = firstColor; root.mapColors(); // determine bit depth for palette int dep; for (dep = 1; dep <= 8; dep++) { if ((1 << dep) >= m_numColors) { break; } } // create the right color model, depending on transparency settings: IndexColorModel icm; if (m_alphaToBitmask) { if (addTransparency) { icm = new IndexColorModel(dep, m_numColors, colorMap[0], colorMap[1], colorMap[2], 0); } else { icm = new IndexColorModel(dep, m_numColors, colorMap[0], colorMap[1], colorMap[2]); } } else { icm = new IndexColorModel(dep, m_numColors, colorMap[0], colorMap[1], colorMap[2], colorMap[3]); } // create the indexed BufferedImage: BufferedImage dest = new BufferedImage( m_source.getWidth(), m_source.getHeight(), BufferedImage.TYPE_BYTE_INDEXED, icm); Search search = new Search(); // convert to indexed color byte[] dst = ((DataBufferByte)dest.getRaster().getDataBuffer()).getData(); for (int i = 0; i < m_pixels.length; i++) { int pixel = m_pixels[i]; int red = (pixel >> 16) & 0xff; int green = (pixel >> 8) & 0xff; int blue = (pixel >> 0) & 0xff; int alpha = (pixel >> 24) & 0xff; if (m_alphaToBitmask) { alpha = alpha < 128 ? 0 : 0xff; } // this is super weird: on some systems, transparent pixels are // not calculated correctly if the following block is taken out. // the bug is very strange, isn't related to the code (compiler error?) // but doesn't allways happen. as soon as it does, though, it doesn't // seem to want to go away. // This happened at various times on my two different debian systems // and i never found out how to really fix it. the following line seems to // prevent it from happening, but i wonder wether there's a better way // to fix it. // it looks as if the command forces alpha to take on correct values. // Until now I only knew of effects like that in quantum mechanics... if (i == 0) { String.valueOf(alpha); } if ((alpha == 0) && addTransparency) { dst[i] = 0; // transparency color is at 0 } else { // walk the tree to find the cube containing that color Node node = root; for (;;) { int id = (((red > node.midRed ? 1 : 0) << 0) | ((green > node.midGreen ? 1 : 0) << 1) | ((blue > node.midBlue ? 1 : 0) << 2) | ((alpha > node.midAlpha ? 1 : 0) << 3)); if (node.children[id] == null) { break; } node = node.children[id]; } // Find the closest color. search.distance = Integer.MAX_VALUE; node.m_parent.closestColor(red, green, blue, alpha, search); dst[i] = (byte)search.colorIndex; } } return dest; } /** * Creates the classification.

*/ void classification() { addTransparency = false; firstColor = 0; for (int i = 0; i < m_pixels.length; i++) { int pixel = m_pixels[i]; int red = (pixel >> 16) & 0xff; int green = (pixel >> 8) & 0xff; int blue = (pixel >> 0) & 0xff; int alpha = (pixel >> 24) & 0xff; if (m_alphaToBitmask) { alpha = alpha < 0x80 ? 0 : 0xff; } if (alpha > 0) { // a hard limit on the number of nodes in the tree if (numNodes > MAX_NODES) { // System.out.println("pruning"); root.pruneLevel(); --depth; } // walk the tree to depth, increasing the // numPixels count for each node Node node = root; for (int level = 1; level <= depth; ++level) { int id = (((red > node.midRed ? 1 : 0) << 0) | ((green > node.midGreen ? 1 : 0) << 1) | ((blue > node.midBlue ? 1 : 0) << 2) | ((alpha > node.midAlpha ? 1 : 0) << 3)); if (node.children[id] == null) { node = new Node(node, id, level); } else { node = node.children[id]; } node.numPixels++; } ++node.unique; node.totalRed += red; node.totalGreen += green; node.totalBlue += blue; node.totalAlpha += alpha; } else if (!addTransparency) { addTransparency = true; m_numColors++; firstColor = 1; // start at 1 as 0 will be the transparent // color } } } /** * Repeatedly prunes the tree until the number of nodes with * unique > 0 is less than or equal to the maximum number of colors * allowed in the output image.

* * When a node to be pruned has offspring, the pruning procedure invokes * itself recursively in order to prune the tree from the leaves upward. * The statistics of the node being pruned are always added to the * corresponding data in that node's parent. This retains the pruned * node's color characteristics for later averaging. */ void reduction() { int threshold = 1; while (m_numColors > m_maxColors) { m_numColors = firstColor; threshold = root.reduce(threshold, Integer.MAX_VALUE); } } } /** Maximum number of children. */ final static int MAX_CHILDREN = 16; /** Maximum number of nodes. */ final static int MAX_NODES = 266817; /** Maximum number of RGB colors. */ final static int MAX_RGB = 255; /** Maximum tree depth. */ final static int MAX_TREE_DEPTH = 8; /** * Reduce the image to the given number of colors.

* * The pixels are reduced "in place".

* * @param image the image to color reduce * @param maxColors the number of colors to reduce the image to * @param alphaToBitmask indicates if alpha information should be converted * * @return the image with the reduced color palette */ public static BufferedImage process(BufferedImage image, int maxColors, boolean alphaToBitmask) { int[] pixels; pixels = image.getRGB(0, 0, image.getWidth(), image.getHeight(), null, 0, image.getWidth()); Cube cube = new Cube(image, pixels, maxColors, alphaToBitmask); cube.classification(); cube.reduction(); return cube.assignment(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy