From: Svjatoslav Agejenko Date: Sat, 7 Jul 2012 19:41:18 +0000 (+0300) Subject: initial commit X-Git-Url: http://www2.svjatoslav.eu/gitweb/?p=imagesqueeze.git;a=commitdiff_plain;h=c7d0b8e1723045c0df086d9214a35f54db47684c initial commit --- c7d0b8e1723045c0df086d9214a35f54db47684c diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..3b25ab3 --- /dev/null +++ b/.classpath @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb60f67 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +bin/ +target/ diff --git a/.project b/.project new file mode 100755 index 0000000..cf45093 --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + imagesqueeze + ImageSqueeze - image codec optimized for photos. NO_M2ECLIPSE_SUPPORT: Project files created with the maven-eclipse-plugin are not supported in M2Eclipse. + + + + org.eclipse.jdt.core.javabuilder + + + org.maven.ide.eclipse.maven2Builder + + + + org.maven.ide.eclipse.maven2Nature + org.eclipse.jdt.core.javanature + + \ No newline at end of file diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..beb3db6 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,10 @@ +#Sun Aug 28 02:19:45 EEST 2011 +encoding//src/main/java=UTF-8 +org.eclipse.jdt.core.compiler.compliance=1.6 +encoding//src/main/resources=UTF-8 +encoding//src/test/resources=UTF-8 +encoding//src/test/java=UTF-8 +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/.settings/org.maven.ide.eclipse.prefs b/.settings/org.maven.ide.eclipse.prefs new file mode 100644 index 0000000..f3597ed --- /dev/null +++ b/.settings/org.maven.ide.eclipse.prefs @@ -0,0 +1,8 @@ +#Sun Aug 28 01:59:39 EEST 2011 +activeProfiles= +eclipse.preferences.version=1 +fullBuildGoals=process-test-resources +resolveWorkspaceProjects=true +resourceFilterGoals=process-resources resources\:testResources +skipCompilerPlugin=true +version=1 diff --git a/COPYING b/COPYING new file mode 100755 index 0000000..10828e0 --- /dev/null +++ b/COPYING @@ -0,0 +1,341 @@ + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/README.TXT b/README.TXT new file mode 100755 index 0000000..5e92643 --- /dev/null +++ b/README.TXT @@ -0,0 +1,12 @@ +Imagesqueeze -- Lossy image codec. Optimized for photos and speed. +By Svjatoslav Agejenko, svjatoslavagejenko@gmail.com, http://svjatoslav.eu + +See doc directory for more info. + +Todo: + * Javadoc + + * micro header possibility, 16 bits header version, 2x8 bits, image dimension + + * Performance test + \ No newline at end of file diff --git a/colorful photo.ImgSqz b/colorful photo.ImgSqz new file mode 100644 index 0000000..6c358b6 Binary files /dev/null and b/colorful photo.ImgSqz differ diff --git a/doc/index.html b/doc/index.html new file mode 100755 index 0000000..7ccf9f6 --- /dev/null +++ b/doc/index.html @@ -0,0 +1,47 @@ + + + + +ImageSqueeze + + +

ImageSqueeze - lossy image codec

+ Download +    + Online homepage +    + Other applications hosted on svjatoslav.eu +
+Program author:
+    Svjatoslav Agejenko
+    Homepage: http://svjatoslav.eu
+    Email: svjatoslav@svjatoslav.eu
+
+This software is distributed under GNU GENERAL PUBLIC LICENSE Version 2.
+
+
+Lossy image codec. Optimized for photos.
+I developed it to test out an image compression ideas.
+
+I believe my algorithm has following advantages:
+    * Fast. Relatively few computations per pixel. 
+    * Easy to add support for progressive image loading. (Saving is already progressive)
+
+Current limitations / to do:
+    * Documentation describing idea behind this algorithm is still missing (lack of time)
+    * Code documentation is weak.
+    * Better sample applications needed: Commandline image conversion utility. Image viewer.
+
+
+Below are original photo and the same image being compressed down to ~93 Kb and then decompressed.
+
+
+When looking very closely, slight grainyness, loss of color precision and
+blurriness (loss of detail) could be noticed as a compression artifacts.
+Still sharp edges are always preserved. Also no blocks typical to JPEG are ever seen.
+I think that is awesome result for just ~ 2.5 bits per pixel on that color photo. 
+
+ + \ No newline at end of file diff --git a/doc/originalAndCompressed.png b/doc/originalAndCompressed.png new file mode 100755 index 0000000..f45ce73 Binary files /dev/null and b/doc/originalAndCompressed.png differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..c73ed5d --- /dev/null +++ b/pom.xml @@ -0,0 +1,82 @@ + + 4.0.0 + eu.svjatoslav + imagesqueeze + 1.0-SNAPSHOT + imagesqueeze + jar + + ImageSqueeze - image codec optimized for photos + + + imagesqueeze + + + false + src/main/resources + + + true + src/main/resources + + *.xml + + + + + + true + src/test/resources + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.1 + + 1.6 + 1.6 + true + UTF-8 + + + + org.apache.maven.plugins + maven-resources-plugin + 2.4.3 + + UTF-8 + + + + + + + + + + + svjatoslav.eu + Svjatoslav repository + http://svjatoslav.eu/maven/ + + + + + + + + + + + + svjatoslav.eu + svjatoslav.eu + scp://svjatoslav.eu/opt/svjatoslav.eu/webcontent/maven + + + + diff --git a/src/main/java/eu/svjatoslav/imagesqueeze/codec/Approximator.java b/src/main/java/eu/svjatoslav/imagesqueeze/codec/Approximator.java new file mode 100755 index 0000000..622ae80 --- /dev/null +++ b/src/main/java/eu/svjatoslav/imagesqueeze/codec/Approximator.java @@ -0,0 +1,69 @@ +package eu.svjatoslav.imagesqueeze.codec; + +import java.io.IOException; + +/** + * Since it is a lossy codec, instead of storing + * exact values, approximated values are stored + * to save on bit count. + */ + +public class Approximator implements Comparable { + + public Table yTable = new Table(); + public Table uTable = new Table(); + public Table vTable = new Table(); + + public Approximator(){ + } + + public int compareTo(Approximator o) { + int result = yTable.compareTo(o.yTable); + if (result != 0) return result; + + result = uTable.compareTo(o.uTable); + if (result != 0) return result; + + result = vTable.compareTo(o.vTable); + return result; + } + + public void initialize(){ + yTable.reset(); + uTable.reset(); + vTable.reset(); + + yTable.addEntry(0, 6, 0); + yTable.addEntry(27, 30, 4); + yTable.addEntry(255, 255, 6); + + uTable.addEntry(0, 9, 0); + uTable.addEntry(27, 30, 4); + uTable.addEntry(255, 255, 6); + + vTable.addEntry(0, 9, 0); + vTable.addEntry(27, 30, 4); + vTable.addEntry(255, 255, 6); + + computeLookupTables(); + } + + public void save(BitOutputStream outputStream) throws IOException{ + yTable.save(outputStream); + uTable.save(outputStream); + vTable.save(outputStream); + } + + public void load(BitInputStream inputStream) throws IOException { + yTable.load(inputStream); + uTable.load(inputStream); + vTable.load(inputStream); + } + + public void computeLookupTables(){ + yTable.computeLookupTables(); + uTable.computeLookupTables(); + vTable.computeLookupTables(); + } + +} diff --git a/src/main/java/eu/svjatoslav/imagesqueeze/codec/BitInputStream.java b/src/main/java/eu/svjatoslav/imagesqueeze/codec/BitInputStream.java new file mode 100755 index 0000000..b5ba69a --- /dev/null +++ b/src/main/java/eu/svjatoslav/imagesqueeze/codec/BitInputStream.java @@ -0,0 +1,56 @@ +package eu.svjatoslav.imagesqueeze.codec; + +/** + * Read individual bits from the input stream. + */ + +import java.io.IOException; +import java.io.InputStream; + +public class BitInputStream { + + + int currentByte; + int currentBytePointer = -1; + + InputStream inputStream; + + public BitInputStream(InputStream inputStream){ + this.inputStream = inputStream; + } + + public int readBits(int bitCount) throws IOException { + + int readableByte = 0; + for (int i=0; i < bitCount; i++){ + + readableByte = readableByte << 1; + + if (currentBytePointer == -1){ + currentBytePointer = 7; + currentByte = inputStream.read(); + } + + int mask = 1; + mask = mask << currentBytePointer; + + int currentBit = currentByte & mask; + + if (currentBit != 0){ + readableByte = readableByte | 1; + } + + currentBytePointer--; + } + return readableByte; + } + + public int readIntegerCompressed8() throws IOException{ + if (readBits(1) == 0){ + return readBits(8); + } else { + return readBits(32); + } + } + +} diff --git a/src/main/java/eu/svjatoslav/imagesqueeze/codec/BitOutputStream.java b/src/main/java/eu/svjatoslav/imagesqueeze/codec/BitOutputStream.java new file mode 100755 index 0000000..469bb7b --- /dev/null +++ b/src/main/java/eu/svjatoslav/imagesqueeze/codec/BitOutputStream.java @@ -0,0 +1,63 @@ +package eu.svjatoslav.imagesqueeze.codec; + +/** + * Write individual bits to the output stream. + */ + +import java.io.IOException; +import java.io.OutputStream; + +public class BitOutputStream { + + int currentByte; + int currentBytePointer; + + OutputStream outputStream; + + public BitOutputStream(OutputStream outputStream){ + currentByte = 0; + currentBytePointer = 0; + this.outputStream = outputStream; + }; + + + public void storeBits(int data, int bitCount) throws IOException { + for (int i=bitCount-1; i >= 0; i--){ + + int mask = 1; + mask = mask << i; + + int currentBit = data & mask; + currentByte = currentByte << 1; + + if (currentBit != 0){ + currentByte = currentByte | 1; + } + currentBytePointer++; + + if (currentBytePointer == 8){ + currentBytePointer = 0; + outputStream.write(currentByte); + currentByte = 0; + } + } + } + + public void storeIntegerCompressed8(int data) throws IOException{ + if (data < 256){ + storeBits(0, 1); + storeBits(data, 8); + } else { + storeBits(1, 1); + storeBits(data, 32); + } + } + + public void finishByte() throws IOException { + if (currentBytePointer != 0){ + outputStream.write(currentByte); + currentBytePointer = 0; + } + } + +} diff --git a/src/main/java/eu/svjatoslav/imagesqueeze/codec/Channel.java b/src/main/java/eu/svjatoslav/imagesqueeze/codec/Channel.java new file mode 100755 index 0000000..879326e --- /dev/null +++ b/src/main/java/eu/svjatoslav/imagesqueeze/codec/Channel.java @@ -0,0 +1,52 @@ +package eu.svjatoslav.imagesqueeze.codec; + +public class Channel { + + byte [] rangeMap; + byte [] map; + + byte [] decodedRangeMap; + byte [] decodedMap; + + int bitCount; + + public Channel(int width, int height) { + rangeMap = new byte[width * height]; + + map = new byte[width * height]; + + decodedRangeMap = new byte[width * height]; + decodedRangeMap[0] = (byte)255; + + decodedMap = new byte[width * height]; + }; + + + public void reset(){ + + for (int i=0; i < decodedMap.length; i++){ + decodedMap[i] = 0; + } + + for (int i=0; i < decodedRangeMap.length; i++){ + decodedRangeMap[i] = 0; + } + decodedRangeMap[0] = (byte)255; + + for (int i=0; i < map.length; i++){ + map[i] = 0; + } + + for (int i=0; i < rangeMap.length; i++){ + rangeMap[i] = 0; + } + + bitCount = 0; + } + + public void printStatistics(){ + float bitsPerPixel = (float)bitCount / (float)rangeMap.length; + System.out.println( (bitCount/8) + " bytes. " + bitsPerPixel + " bits per pixel."); + } + +} diff --git a/src/main/java/eu/svjatoslav/imagesqueeze/codec/Color.java b/src/main/java/eu/svjatoslav/imagesqueeze/codec/Color.java new file mode 100755 index 0000000..1d8b7a6 --- /dev/null +++ b/src/main/java/eu/svjatoslav/imagesqueeze/codec/Color.java @@ -0,0 +1,49 @@ +package eu.svjatoslav.imagesqueeze.codec; + +/** + * Helper class to convert between RGB and YUV + */ +public class Color { + + int r; + int g; + int b; + + int y; + int u; + int v; + + public void YUV2RGB(){ + + b = (int)(y + 1.4075 * (v - 128)); + g = (int)(y - 0.3455 * (u - 128) - (0.7169 * (v - 128))); + r = (int)(y + 1.7790 * (u - 128)); + + if (r < 0) r = 0; + if (g < 0) g = 0; + if (b < 0) b = 0; + + if (r > 255) r = 255; + if (g > 255) g = 255; + if (b > 255) b = 255; + + } + + public void RGB2YUV(){ + + y = (int)(r * 0.299000 + g * 0.587000 + b * 0.114000); + u = (int)(r * -0.168736 + g * -0.331264 + b * 0.500000 + 128); + v = (int)(r * 0.500000 + g * -0.418688 + b * -0.081312 + 128); + + if (y < 0) y = 0; + if (u < 0) u = 0; + if (v < 0) v = 0; + + if (y > 255) y = 255; + if (u > 255) u = 255; + if (v > 255) v = 255; + + } + +}; + diff --git a/src/main/java/eu/svjatoslav/imagesqueeze/codec/ColorStats.java b/src/main/java/eu/svjatoslav/imagesqueeze/codec/ColorStats.java new file mode 100755 index 0000000..b856fe5 --- /dev/null +++ b/src/main/java/eu/svjatoslav/imagesqueeze/codec/ColorStats.java @@ -0,0 +1,37 @@ +package eu.svjatoslav.imagesqueeze.codec; + +public class ColorStats { + + int ySum; + int uSum; + int vSum; + + int pixelCount; + + + public ColorStats(){ + reset(); + } + + public void reset(){ + ySum = 0; + uSum = 0; + vSum = 0; + pixelCount = 0; + } + + public int getAverageY(){ + return ySum / pixelCount; + } + + public int getAverageU(){ + return uSum / pixelCount; + } + + public int getAverageV(){ + return vSum / pixelCount; + } + +} + + diff --git a/src/main/java/eu/svjatoslav/imagesqueeze/codec/Image.java b/src/main/java/eu/svjatoslav/imagesqueeze/codec/Image.java new file mode 100755 index 0000000..c644eaa --- /dev/null +++ b/src/main/java/eu/svjatoslav/imagesqueeze/codec/Image.java @@ -0,0 +1,120 @@ +package eu.svjatoslav.imagesqueeze.codec; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Main class representing compressed image. + */ + +public class Image { + + public ImageMetaData metaData; + + public BufferedImage bufferedImage; + + ImageEncoder encoder; + + public Image(){}; + + /** + * Initialize imagesqueeze image based on {@link BufferedImage}. {@link BufferedImage} must be of type BufferedImage.TYPE_3BYTE_BGR . + */ + public Image(BufferedImage image){ + + this.bufferedImage = image; + metaData = new ImageMetaData(); + + metaData.version = 1; + metaData.width = image.getWidth(); + metaData.height = image.getHeight(); + } + + /** + * Initialize empty imagesqueeze image. + */ + public Image(int width, int height){ + + this.bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR); + metaData = new ImageMetaData(); + + metaData.version = 1; + metaData.width = width; + metaData.height = height; + } + + /** + * Load ImgSqz image from {@link InputStream}. + */ + public void loadImage(InputStream source) throws IOException{ + BitInputStream bitInputStream = new BitInputStream(source); + + metaData = new ImageMetaData(); + metaData.load(bitInputStream); + + bufferedImage = new BufferedImage(metaData.width, metaData.height, BufferedImage.TYPE_3BYTE_BGR); + + ImageDecoder imageDecoder = new ImageDecoder(this, bitInputStream); + + imageDecoder.decode(); + } + + /** + * Load ImgSqz image from {@link File}. + */ + public void loadImage(File source) throws IOException{ + + byte [] fileContent = new byte[(int)source.length()]; + + FileInputStream fileInputStream = new FileInputStream(source); + + fileInputStream.read(fileContent); + + fileInputStream.close(); + + ByteArrayInputStream inputStream = new ByteArrayInputStream(fileContent); + + loadImage(inputStream); + } + + /** + * Save image into ImgSqz file format. + */ + public void saveImage(OutputStream outputStream) throws IOException{ + + BitOutputStream bitOutputStream = new BitOutputStream(outputStream); + + metaData.save(bitOutputStream); + + if (encoder == null){ + encoder = new ImageEncoder(this); + } + + encoder.encode(bitOutputStream); + + bitOutputStream.finishByte(); + } + + + /** + * Save image into ImgSqz file format. + */ + public void saveImage(File file) throws IOException{ + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + saveImage(outputStream); + + byte [] buffer = outputStream.toByteArray(); + FileOutputStream fileOutputStream = new FileOutputStream(file); + fileOutputStream.write(buffer); + fileOutputStream.close(); + } + + +} diff --git a/src/main/java/eu/svjatoslav/imagesqueeze/codec/ImageDecoder.java b/src/main/java/eu/svjatoslav/imagesqueeze/codec/ImageDecoder.java new file mode 100755 index 0000000..37f3c0e --- /dev/null +++ b/src/main/java/eu/svjatoslav/imagesqueeze/codec/ImageDecoder.java @@ -0,0 +1,232 @@ +package eu.svjatoslav.imagesqueeze.codec; + +/** + * Compressed image pixels decoder. + */ + +import java.awt.image.DataBufferByte; +import java.awt.image.WritableRaster; +import java.io.IOException; + +public class ImageDecoder { + + int width, height; + Image image; + + byte [] decodedYRangeMap; + byte [] decodedYMap; + + byte [] decodedURangeMap; + byte [] decodedUMap; + + byte [] decodedVRangeMap; + byte [] decodedVMap; + + Color tmpColor = new Color(); + + Approximator approximator; + BitInputStream bitInputStream; + + ColorStats colorStats = new ColorStats(); + OperatingContext context = new OperatingContext(); + + public ImageDecoder (Image image, BitInputStream bitInputStream) { + approximator = new Approximator(); + + this.image = image; + this.bitInputStream = bitInputStream; + + width = image.metaData.width; + height = image.metaData.height; + + decodedYRangeMap = new byte[width * height]; + decodedYRangeMap[0] = (byte)(255); + decodedYMap = new byte[width * height]; + + decodedURangeMap = new byte[width * height]; + decodedURangeMap[0] = (byte)(255); + decodedUMap = new byte[width * height]; + + decodedVRangeMap = new byte[width * height]; + decodedVRangeMap[0] = (byte)(255); + decodedVMap = new byte[width * height]; + + } + + + public void decode() throws IOException { + approximator.load(bitInputStream); + approximator.computeLookupTables(); + + WritableRaster raster = image.bufferedImage.getRaster(); + DataBufferByte dbi = (DataBufferByte)raster.getDataBuffer(); + byte [] pixels = dbi.getData(); + + // load top-, left-most pixel. + decodedYMap[0] = (byte)bitInputStream.readBits(8); + decodedUMap[0] = (byte)bitInputStream.readBits(8); + decodedVMap[0] = (byte)bitInputStream.readBits(8); + + Color color = new Color(); + color.y = ImageEncoder.byteToInt(decodedYMap[0]); + color.u = ImageEncoder.byteToInt(decodedUMap[0]); + color.v = ImageEncoder.byteToInt(decodedVMap[0]); + + color.YUV2RGB(); + + pixels[0] = (byte)color.r; + pixels[0+1] = (byte)color.g; + pixels[0+2] = (byte)color.b; + + + // detect initial step + int largestDimension; + int initialStep = 2; + if (width > height) { + largestDimension = width; + } else { + largestDimension = height; + } + + while (initialStep < largestDimension){ + initialStep = initialStep * 2; + } + + grid(initialStep, pixels); + } + + + public void grid(int step, byte [] pixels) throws IOException { + + gridDiagonal(step / 2, step / 2, step, pixels); + gridSquare(step / 2, 0, step, pixels); + gridSquare(0, step / 2, step, pixels); + + if (step > 2) grid(step / 2, pixels); + } + + + public void gridSquare(int offsetX, int offsetY, int step, byte [] pixels) throws IOException{ + + for (int y = offsetY; y < height; y = y + step){ + for (int x = offsetX; x < width; x = x + step){ + + + int halfStep = step / 2; + + context.initialize(image, decodedYMap, decodedUMap, decodedVMap); + context.measureNeighborEncode(x - halfStep, y); + context.measureNeighborEncode(x + halfStep, y); + context.measureNeighborEncode(x, y - halfStep); + context.measureNeighborEncode(x, y + halfStep); + + loadPixel(step, offsetX, offsetY, x, y, pixels, + context.colorStats.getAverageY(), + context.colorStats.getAverageU(), + context.colorStats.getAverageV()); + + } + } + } + + + public void gridDiagonal(int offsetX, int offsetY, int step, byte [] pixels) throws IOException{ + + for (int y = offsetY; y < height; y = y + step){ + for (int x = offsetX; x < width; x = x + step){ + + int halfStep = step / 2; + + context.initialize(image, decodedYMap, decodedUMap, decodedVMap); + context.measureNeighborEncode(x - halfStep, y - halfStep); + context.measureNeighborEncode(x + halfStep, y - halfStep); + context.measureNeighborEncode(x - halfStep, y + halfStep); + context.measureNeighborEncode(x + halfStep, y + halfStep); + + loadPixel(step, offsetX, offsetY, x, y, pixels, + context.colorStats.getAverageY(), + context.colorStats.getAverageU(), + context.colorStats.getAverageV()); + + } + } + } + + + public void loadPixel(int step, int offsetX, int offsetY, int x, int y, byte[] pixels, + int averageDecodedY, int averageDecodedU, int averageDecodedV) + throws IOException{ + + int index = (y * width) + x; + + int halfStep = step / 2; + + int parentIndex; + if (offsetX > 0){ + if (offsetY > 0){ + // diagonal approach + parentIndex = ((y - halfStep) * width) + (x - halfStep); + } else { + // take left pixel + parentIndex = (y * width) + (x - halfStep); + } + } else { + // take upper pixel + parentIndex = ((y - halfStep) * width) + x; + } + + + + int colorBufferIndex = index * 3; + + Color color = new Color(); + color.y = loadChannel(decodedYRangeMap, decodedYMap, approximator.yTable, averageDecodedY, index, parentIndex); + color.u = loadChannel(decodedURangeMap, decodedUMap, approximator.uTable, averageDecodedU, index, parentIndex); + color.v = loadChannel(decodedVRangeMap, decodedVMap, approximator.vTable, averageDecodedV, index, parentIndex); + + color.YUV2RGB(); + + pixels[colorBufferIndex] = (byte)color.r; + pixels[colorBufferIndex+1] = (byte)color.g; + pixels[colorBufferIndex+2] = (byte)color.b; + + } + + + private int loadChannel(byte [] decodedRangeMap, byte [] decodedMap, Table table, int averageDecodedValue, int index, + int parentIndex) throws IOException { + int decodedValue = averageDecodedValue; + + int inheritedRange = ImageEncoder.byteToInt(decodedRangeMap[parentIndex]); + int computedRange = inheritedRange; + + int bitCount = table.proposeBitcountForRange(inheritedRange); + int computedRangeBitCount = 0; + if ( bitCount > 0){ + + int rangeDecreases = bitInputStream.readBits(1); + if (rangeDecreases != 0){ + computedRange = table.proposeDecreasedRange(inheritedRange); + } + + decodedRangeMap[index] = (byte)computedRange; + computedRangeBitCount = table.proposeBitcountForRange(computedRange); + + if (computedRangeBitCount > 0){ + + int encodedDifference = bitInputStream.readBits(computedRangeBitCount); + + int decodedDifference = ImageEncoder.decodeValueFromGivenBits(encodedDifference, computedRange, computedRangeBitCount); + + decodedValue = averageDecodedValue - decodedDifference; + if (decodedValue > 255) decodedValue = 255; + if (decodedValue < 0) decodedValue = 0; + } + } else { + decodedRangeMap[index] = (byte)inheritedRange; + } + decodedMap[index] = (byte)decodedValue; + return decodedValue; + } + +} diff --git a/src/main/java/eu/svjatoslav/imagesqueeze/codec/ImageEncoder.java b/src/main/java/eu/svjatoslav/imagesqueeze/codec/ImageEncoder.java new file mode 100755 index 0000000..46c2135 --- /dev/null +++ b/src/main/java/eu/svjatoslav/imagesqueeze/codec/ImageEncoder.java @@ -0,0 +1,529 @@ +package eu.svjatoslav.imagesqueeze.codec; + +/** + * Compressed image pixels encoder. + */ + +import java.awt.image.DataBufferByte; +import java.awt.image.WritableRaster; +import java.io.IOException; + + +public class ImageEncoder { + + Image image; + int width, height; + + Channel yChannel; + Channel uChannel; + Channel vChannel; + + Approximator approximator; + + int bitsForY; + int bitsForU; + int bitsForV; + + //ColorStats colorStats = new ColorStats(); + OperatingContext context = new OperatingContext(); + OperatingContext context2 = new OperatingContext(); + + BitOutputStream bitOutputStream; + + public ImageEncoder(Image image){ + approximator = new Approximator(); + + //bitOutputStream = outputStream; + + this.image = image; + + } + + + public void encode(BitOutputStream bitOutputStream) throws IOException { + this.bitOutputStream = bitOutputStream; + + approximator.initialize(); + + approximator.save(bitOutputStream); + + width = image.metaData.width; + height = image.metaData.height; + + WritableRaster raster = image.bufferedImage.getRaster(); + DataBufferByte dbi = (DataBufferByte)raster.getDataBuffer(); + byte [] pixels = dbi.getData(); + + if (yChannel == null){ + yChannel = new Channel(width, height); + } else { + yChannel.reset(); + } + + if (uChannel == null){ + uChannel = new Channel(width, height); + } else { + uChannel.reset(); + } + + if (vChannel == null){ + vChannel = new Channel(width, height); + } else { + vChannel.reset(); + } + + // create YUV map out of RGB raster data + Color color = new Color(); + + for (int y=0; y < height; y++){ + for (int x=0; x < width; x++){ + + int index = (y * width) + x; + int colorBufferIndex = index * 3; + + int blue = pixels[colorBufferIndex]; + if (blue < 0) blue = blue + 256; + + int green = pixels[colorBufferIndex+1]; + if (green < 0) green = green + 256; + + int red = pixels[colorBufferIndex+2]; + if (red < 0) red = red + 256; + + color.r = red; + color.g = green; + color.b = blue; + + color.RGB2YUV(); + + yChannel.map[index] = (byte)color.y; + uChannel.map[index] = (byte)color.u; + vChannel.map[index] = (byte)color.v; + } + } + + yChannel.decodedMap[0] = yChannel.map[0]; + uChannel.decodedMap[0] = uChannel.map[0]; + vChannel.decodedMap[0] = vChannel.map[0]; + + bitOutputStream.storeBits(byteToInt(yChannel.map[0]), 8); + bitOutputStream.storeBits(byteToInt(uChannel.map[0]), 8); + bitOutputStream.storeBits(byteToInt(vChannel.map[0]), 8); + + // detect initial step + int largestDimension; + int initialStep = 2; + if (width > height) { + largestDimension = width; + } else { + largestDimension = height; + } + + while (initialStep < largestDimension){ + initialStep = initialStep * 2; + } + + rangeGrid(initialStep); + rangeRoundGrid(2); + saveGrid(initialStep); + } + + public void printStatistics(){ + System.out.println("Y channel:"); + yChannel.printStatistics(); + + System.out.println("U channel:"); + uChannel.printStatistics(); + + System.out.println("V channel:"); + vChannel.printStatistics(); + } + + public void rangeGrid(int step){ + + //gridSquare(step / 2, step / 2, step, pixels); + + rangeGridDiagonal(step / 2, step / 2, step); + rangeGridSquare(step / 2, 0, step); + rangeGridSquare(0, step / 2, step); + + if (step > 2) rangeGrid(step / 2); + } + + + public void rangeRoundGrid(int step){ + + rangeRoundGridDiagonal(step / 2, step / 2, step); + rangeRoundGridSquare(step / 2, 0, step); + rangeRoundGridSquare(0, step / 2, step); + + if (step < 1024) rangeRoundGrid(step * 2); + } + + public void saveGrid(int step) throws IOException { + + saveGridDiagonal(step / 2, step / 2, step); + saveGridSquare(step / 2, 0, step); + saveGridSquare(0, step / 2, step); + + if (step > 2) saveGrid(step / 2); + } + + + public void rangeGridSquare(int offsetX, int offsetY, int step){ + for (int y = offsetY; y < height; y = y + step){ + for (int x = offsetX; x < width; x = x + step){ + + int index = (y * width) + x; + int halfStep = step / 2; + + context.initialize(image, yChannel.map, uChannel.map, vChannel.map); + + context.measureNeighborEncode(x - halfStep, y); + context.measureNeighborEncode(x + halfStep, y); + context.measureNeighborEncode(x, y - halfStep); + context.measureNeighborEncode(x, y + halfStep); + + yChannel.rangeMap[index] = (byte)context.getYRange(index); + uChannel.rangeMap[index] = (byte)context.getURange(index); + vChannel.rangeMap[index] = (byte)context.getVRange(index); + } + } + } + + public void rangeGridDiagonal(int offsetX, int offsetY, int step){ + for (int y = offsetY; y < height; y = y + step){ + for (int x = offsetX; x < width; x = x + step){ + + int index = (y * width) + x; + int halfStep = step / 2; + + context.initialize(image, yChannel.map, uChannel.map, vChannel.map); + + context.measureNeighborEncode(x - halfStep, y - halfStep); + context.measureNeighborEncode(x + halfStep, y - halfStep); + context.measureNeighborEncode(x - halfStep, y + halfStep); + context.measureNeighborEncode(x + halfStep, y + halfStep); + + yChannel.rangeMap[index] = (byte)context.getYRange(index); + uChannel.rangeMap[index] = (byte)context.getURange(index); + vChannel.rangeMap[index] = (byte)context.getVRange(index); + } + } + } + + public void rangeRoundGridDiagonal(int offsetX, int offsetY, int step){ + for (int y = offsetY; y < height; y = y + step){ + for (int x = offsetX; x < width; x = x + step){ + + int index = (y * width) + x; + + int yRange = byteToInt(yChannel.rangeMap[index]); + int uRange = byteToInt(uChannel.rangeMap[index]); + int vRange = byteToInt(vChannel.rangeMap[index]); + + int halfStep = step / 2; + + int parentIndex = ((y - halfStep) * width) + (x - halfStep); + + int parentYRange = byteToInt(yChannel.rangeMap[parentIndex]); + + if (parentYRange < yRange){ + parentYRange = yRange; + yChannel.rangeMap[parentIndex] = (byte)parentYRange; + } + + int parentURange = byteToInt(uChannel.rangeMap[parentIndex]); + + if (parentURange < uRange){ + parentURange = uRange; + uChannel.rangeMap[parentIndex] = (byte)parentURange; + } + + int parentVRange = byteToInt(vChannel.rangeMap[parentIndex]); + + if (parentVRange < vRange){ + parentVRange = vRange; + vChannel.rangeMap[parentIndex] = (byte)parentVRange; + } + } + } + } + + public void rangeRoundGridSquare(int offsetX, int offsetY, int step){ + for (int y = offsetY; y < height; y = y + step){ + for (int x = offsetX; x < width; x = x + step){ + + int index = (y * width) + x; + + int yRange = byteToInt(yChannel.rangeMap[index]); + int uRange = byteToInt(uChannel.rangeMap[index]); + int vRange = byteToInt(vChannel.rangeMap[index]); + + int halfStep = step / 2; + + int parentIndex; + if (offsetX > 0){ + parentIndex = (y * width) + (x - halfStep); + } else { + parentIndex = ((y - halfStep) * width) + x; + } + + int parentYRange = byteToInt(yChannel.rangeMap[parentIndex]); + + if (parentYRange < yRange){ + parentYRange = yRange; + yChannel.rangeMap[parentIndex] = (byte)parentYRange; + } + + int parentURange = byteToInt(uChannel.rangeMap[parentIndex]); + + if (parentURange < uRange){ + parentURange = uRange; + uChannel.rangeMap[parentIndex] = (byte)parentURange; + } + + int parentVRange = byteToInt(vChannel.rangeMap[parentIndex]); + + if (parentVRange < vRange){ + parentVRange = vRange; + vChannel.rangeMap[parentIndex] = (byte)parentVRange; + } + + } + } + } + + public void saveGridSquare(int offsetX, int offsetY, int step) throws IOException{ + for (int y = offsetY; y < height; y = y + step){ + for (int x = offsetX; x < width; x = x + step){ + + int halfStep = step / 2; + + context2.initialize(image, yChannel.decodedMap, uChannel.decodedMap, vChannel.decodedMap); + context2.measureNeighborEncode(x - halfStep, y); + context2.measureNeighborEncode(x + halfStep, y); + context2.measureNeighborEncode(x, y - halfStep); + context2.measureNeighborEncode(x, y + halfStep); + + + savePixel(step, offsetX, offsetY, x, y, + context2.colorStats.getAverageY(), + context2.colorStats.getAverageU(), + context2.colorStats.getAverageV()); + + } + } + } + + public void saveGridDiagonal(int offsetX, int offsetY, int step) throws IOException { + for (int y = offsetY; y < height; y = y + step){ + for (int x = offsetX; x < width; x = x + step){ + + int halfStep = step / 2; + + context2.initialize(image, yChannel.decodedMap, uChannel.decodedMap, vChannel.decodedMap); + context2.measureNeighborEncode(x - halfStep, y - halfStep); + context2.measureNeighborEncode(x + halfStep, y - halfStep); + context2.measureNeighborEncode(x - halfStep, y + halfStep); + context2.measureNeighborEncode(x + halfStep, y + halfStep); + + + savePixel(step, offsetX, offsetY, x, y, + context2.colorStats.getAverageY(), + context2.colorStats.getAverageU(), + context2.colorStats.getAverageV()); + + } + } + } + + public void savePixel(int step, int offsetX, int offsetY, int x, int y, int averageDecodedY, int averageDecodedU, int averageDecodedV) throws IOException { + + int index = (y * width) + x; + + int py = byteToInt(yChannel.map[index]); + int pu = byteToInt(uChannel.map[index]); + int pv = byteToInt(vChannel.map[index]); + + int yRange = byteToInt(yChannel.rangeMap[index]); + int uRange = byteToInt(uChannel.rangeMap[index]); + int vRange = byteToInt(vChannel.rangeMap[index]); + + int halfStep = step / 2; + + int parentIndex; + if (offsetX > 0){ + if (offsetY > 0){ + // diagonal approach + parentIndex = ((y - halfStep) * width) + (x - halfStep); + } else { + // take left pixel + parentIndex = (y * width) + (x - halfStep); + } + } else { + // take upper pixel + parentIndex = ((y - halfStep) * width) + x; + } + + encodeChannel( + approximator.yTable, + yChannel, + averageDecodedY, + index, + py, + yRange, + parentIndex); + + encodeChannel( + approximator.uTable, + uChannel, + averageDecodedU, + index, + pu, + uRange, + parentIndex); + + encodeChannel( + approximator.vTable, + vChannel, + averageDecodedV, + index, + pv, + vRange, + parentIndex); + + } + + + private void encodeChannel(Table table, Channel channel, int averageDecodedValue, int index, + int value, int range, int parentIndex) + throws IOException { + + byte[] decodedRangeMap = channel.decodedRangeMap; + byte[] decodedMap = channel.decodedMap; + + int inheritedRange = byteToInt(decodedRangeMap[parentIndex]); + + int inheritedBitCount = table.proposeBitcountForRange(inheritedRange); + + if (inheritedBitCount > 0){ + int computedRange; + computedRange = table.proposeRangeForRange(range, inheritedRange); + decodedRangeMap[index] = (byte)computedRange; + + channel.bitCount++; + if (computedRange != inheritedRange){ + // brightness range shrinked + bitOutputStream.storeBits(1, 1); + } else { + // brightness range stayed the same + bitOutputStream.storeBits(0, 1); + } + + + // encode brightness into available amount of bits + int computedBitCount = table.proposeBitcountForRange(computedRange); + + if (computedBitCount > 0){ + + int differenceToEncode = -(value - averageDecodedValue); + int bitEncodedDifference = encodeValueIntoGivenBits(differenceToEncode, computedRange, computedBitCount); + + channel.bitCount = channel.bitCount + computedBitCount; + bitOutputStream.storeBits(bitEncodedDifference, computedBitCount); + + int decodedDifference = decodeValueFromGivenBits(bitEncodedDifference, computedRange, computedBitCount); + int decodedValue = averageDecodedValue - decodedDifference; + if (decodedValue > 255) decodedValue = 255; + if (decodedValue < 0) decodedValue = 0; + + decodedMap[index] = (byte)decodedValue; + } else { + decodedMap[index] = (byte)averageDecodedValue; + } + + } else { + decodedRangeMap[index] = (byte)inheritedRange; + decodedMap[index] = (byte)averageDecodedValue; + } + } + + public static int encodeValueIntoGivenBits(int value, int range, int bitCount){ + + int negativeBit = 0; + + if (value <0){ + negativeBit = 1; + value = -value; + } + + int remainingBitCount = bitCount - 1; + + if (remainingBitCount == 0){ + // no more bits remaining to encode actual value + + return negativeBit; + + } else { + // still one or more bits left, encode value as precisely as possible + + if (value > range) value = range; + + + int realvalueForThisBitcount = 1 << remainingBitCount; + // int valueMultiplier = range / realvalueForThisBitcount; + int encodedValue = value * realvalueForThisBitcount / range; + + if (encodedValue >= realvalueForThisBitcount) encodedValue = realvalueForThisBitcount - 1; + + encodedValue = (encodedValue << 1) + negativeBit; + + return encodedValue; + } + } + + + public static int decodeValueFromGivenBits(int encodedBits, int range, int bitCount){ + int negativeBit = encodedBits & 1; + + int remainingBitCount = bitCount - 1; + + if (remainingBitCount == 0){ + // no more bits remaining to encode actual value + + if (negativeBit == 0){ + return range; + } else { + return -range; + } + + } else { + // still one or more bits left, encode value as precisely as possible + + int encodedValue = (encodedBits >>> 1) + 1; + + int realvalueForThisBitcount = 1 << remainingBitCount; + + // int valueMultiplier = range / realvalueForThisBitcount; + int decodedValue = range * encodedValue / realvalueForThisBitcount; + + + if (decodedValue > range) decodedValue = range; + + if (negativeBit == 0){ + return decodedValue; + } else { + return -decodedValue; + } + + } + } + + public static int byteToInt(byte input){ + int result = input; + if (result < 0) result = result + 256; + return result; + } + +} diff --git a/src/main/java/eu/svjatoslav/imagesqueeze/codec/ImageMetaData.java b/src/main/java/eu/svjatoslav/imagesqueeze/codec/ImageMetaData.java new file mode 100755 index 0000000..68b3bea --- /dev/null +++ b/src/main/java/eu/svjatoslav/imagesqueeze/codec/ImageMetaData.java @@ -0,0 +1,34 @@ +package eu.svjatoslav.imagesqueeze.codec; + +/** + * Class to hold image metadata. + * Like image dimensions, header version, compression quality, etc.. + */ + +import java.io.IOException; + + +public class ImageMetaData { + + int version; + int width; + int height; + + + public void load(BitInputStream inputStream) throws IOException{ + + version = inputStream.readBits(16); + width = inputStream.readIntegerCompressed8(); + height = inputStream.readIntegerCompressed8(); + + } + + public void save(BitOutputStream outputStream) throws IOException{ + + outputStream.storeBits(version, 16); + outputStream.storeIntegerCompressed8(width); + outputStream.storeIntegerCompressed8(height); + + } + +} diff --git a/src/main/java/eu/svjatoslav/imagesqueeze/codec/OperatingContext.java b/src/main/java/eu/svjatoslav/imagesqueeze/codec/OperatingContext.java new file mode 100755 index 0000000..bac1630 --- /dev/null +++ b/src/main/java/eu/svjatoslav/imagesqueeze/codec/OperatingContext.java @@ -0,0 +1,51 @@ +package eu.svjatoslav.imagesqueeze.codec; + +public class OperatingContext { + + Image image; + byte[] yMap; + byte[] uMap; + byte[] vMap; + ColorStats colorStats = new ColorStats(); + + public OperatingContext(){ + } + + public void initialize(Image image, byte [] brightnessMap, byte [] colornessMap, byte [] colorMap){ + this.image = image; + this.yMap = brightnessMap; + this.uMap = colornessMap; + this.vMap = colorMap; + + colorStats.reset(); + } + + public void measureNeighborEncode(int x, int y){ + if ((y >= 0) && (y < image.metaData.height) && (x >= 0) && (x < image.metaData.width)){ + + int neighborIndex = y * image.metaData.width + x; + + colorStats.ySum = colorStats.ySum + ImageEncoder.byteToInt(yMap[neighborIndex]); + colorStats.uSum = colorStats.uSum + ImageEncoder.byteToInt(uMap[neighborIndex]); + colorStats.vSum = colorStats.vSum + ImageEncoder.byteToInt(vMap[neighborIndex]); + colorStats.pixelCount++; + } + } + + public int getYRange(int index){ + int brightness = ImageEncoder.byteToInt(yMap[index]); + return Math.abs(brightness - colorStats.getAverageY()); + } + + public int getURange(int index){ + int colorness = ImageEncoder.byteToInt(uMap[index]); + return Math.abs(colorness - colorStats.getAverageU()); + } + + public int getVRange(int index){ + int color = ImageEncoder.byteToInt(vMap[index]); + return Math.abs(color - colorStats.getAverageV()); + } + + +} diff --git a/src/main/java/eu/svjatoslav/imagesqueeze/codec/Table.java b/src/main/java/eu/svjatoslav/imagesqueeze/codec/Table.java new file mode 100755 index 0000000..4e714f4 --- /dev/null +++ b/src/main/java/eu/svjatoslav/imagesqueeze/codec/Table.java @@ -0,0 +1,187 @@ +package eu.svjatoslav.imagesqueeze.codec; + +import java.io.IOException; + +/** + * Quick lookup table. + */ + +public class Table implements Comparable{ + + int [] range = new int[100]; + int [] switchTreshold = new int[100]; + int [] bitcount = new int[100]; + + + int [] bitCountForRange = new int[256]; + int [] proposedRangeForActualRange = new int[256]; + int [] proposedRangeForActualRangeLow = new int[256]; + int [] proposedRangeForActualRangeHigh = new int[256]; + byte [] proposedDecreasedRange = new byte[256]; + + + int usedEntries = 0; + + + public void computeLookupTables(){ + int currentCheckPointer = 0; + + for (int i=0; i<256; i++){ + + if (range[currentCheckPointer] == i){ + currentCheckPointer++; + } + + if (currentCheckPointer > 0){ + bitCountForRange[i] = bitcount[currentCheckPointer-1]; + } else { + bitCountForRange[i] = 0; + } + + } + + for (int i=0; i<256; i++){ + + int seek; + seekLoop:{ + for (seek = 0; seek < usedEntries; seek ++){ + + if (switchTreshold[seek] >= i) break seekLoop; + + } + } + + proposedRangeForActualRange[i] = range[seek]; + if (seek == 0){ + proposedRangeForActualRangeLow[i] = 0; + } else { + proposedRangeForActualRangeLow[i] = switchTreshold[seek-1]+1; + } + proposedRangeForActualRangeHigh[i] = switchTreshold[seek]; + } + + + currentCheckPointer = usedEntries - 2; + for (int i=255; i >= 0; i--){ + if (range[currentCheckPointer] == i) currentCheckPointer--; + + if (currentCheckPointer < 0){ + proposedDecreasedRange[i] = 0; + } else { + proposedDecreasedRange[i] = (byte)(range[currentCheckPointer]); + } + } + + } + + /** + * @param switchTreshold - switch to this range when actual range in equal or below this treshold + */ + public void addEntry(int range, int switchTreshold, int bitcount){ + if (range < 0) range = 0; + if (range > 255) range = 255; + + if (switchTreshold < 0) switchTreshold = 0; + if (switchTreshold > 255) switchTreshold = 255; + + if (bitcount < 0) bitcount = 0; + if (bitcount > 8) bitcount = 8; + + + this.range[usedEntries] = range; + this.switchTreshold[usedEntries] = switchTreshold; + this.bitcount[usedEntries] = bitcount; + usedEntries++; + } + + + + + public int proposeRangeForRange(int actualRange, int inheritedRange){ + + if (inheritedRange > 255) inheritedRange = 255; + if (inheritedRange < 0) inheritedRange = 0; + + if (proposedRangeForActualRangeLow[inheritedRange] <= actualRange){ + return inheritedRange; + } + + return proposeDecreasedRange(inheritedRange); + } + + + public int proposeDecreasedRange(int range){ + if (range > 255) range = 255; + if (range < 0) range = 0; + + return ImageEncoder.byteToInt(proposedDecreasedRange[range]); + } + + + public int proposeBitcountForRange(int range){ + if (range > 255) range = 255; + if (range < 0) range = 0; + int proposal = bitCountForRange[range]; + return proposal; + } + + /** + * Compares two tables. + * Ignores table initialization. + */ + public int compareTo(Table o) { + if (usedEntries < o.usedEntries) return -1; + if (usedEntries > o.usedEntries) return 1; + + for (int i=0; i o.range[i]) return 1; + + if (switchTreshold[i] < o.switchTreshold[i]) return -1; + if (switchTreshold[i] > o.switchTreshold[i]) return 1; + + if (bitcount[i] < o.bitcount[i]) return -1; + if (bitcount[i] > o.bitcount[i]) return 1; + } + + return 0; + } + + public void save(BitOutputStream outputStream) throws IOException { + outputStream.storeIntegerCompressed8(usedEntries); + + for (int i=0; i < usedEntries; i++){ + outputStream.storeBits(this.range[i], 8); + outputStream.storeBits(this.switchTreshold[i], 8); + outputStream.storeBits(this.bitcount[i], 4); + } + } + + public void reset(){ + range = new int[100]; + switchTreshold = new int[100]; + bitcount = new int[100]; + + + bitCountForRange = new int[256]; + proposedRangeForActualRange = new int[256]; + proposedRangeForActualRangeLow = new int[256]; + proposedRangeForActualRangeHigh = new int[256]; + proposedDecreasedRange = new byte[256]; + + usedEntries = 0; + } + + public void load(BitInputStream inputStream) throws IOException { + reset(); + + int availableEntries = inputStream.readIntegerCompressed8(); + + for (int i=0; i < availableEntries; i++){ + addEntry(inputStream.readBits(8), inputStream.readBits(8), inputStream.readBits(4)); + } + } + + + +} diff --git a/src/main/java/eu/svjatoslav/imagesqueeze/sampleApplication/ImageFrame.java b/src/main/java/eu/svjatoslav/imagesqueeze/sampleApplication/ImageFrame.java new file mode 100755 index 0000000..3da2f77 --- /dev/null +++ b/src/main/java/eu/svjatoslav/imagesqueeze/sampleApplication/ImageFrame.java @@ -0,0 +1,49 @@ +package eu.svjatoslav.imagesqueeze.sampleApplication; +import java.awt.BorderLayout; + +import javax.swing.WindowConstants; +import javax.swing.SwingUtilities; + + +public class ImageFrame extends javax.swing.JFrame { + private ImagePanel imagePanel1; + + /** + * Auto-generated main method to display this JFrame + */ + public static void main(String[] args) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + ImageFrame inst = new ImageFrame("test"); + inst.setLocationRelativeTo(null); + inst.setVisible(true); + } + }); + } + + public ImageFrame(String title) { + super(); + setTitle(title); + initGUI(); + } + + private void initGUI() { + try { + BorderLayout thisLayout = new BorderLayout(); + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + getContentPane().setLayout(thisLayout); + { + imagePanel1 = new ImagePanel(); + getContentPane().add(getImagePanel(), BorderLayout.CENTER); + } + pack(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public ImagePanel getImagePanel() { + return imagePanel1; + } + +} diff --git a/src/main/java/eu/svjatoslav/imagesqueeze/sampleApplication/ImagePanel.java b/src/main/java/eu/svjatoslav/imagesqueeze/sampleApplication/ImagePanel.java new file mode 100755 index 0000000..749d558 --- /dev/null +++ b/src/main/java/eu/svjatoslav/imagesqueeze/sampleApplication/ImagePanel.java @@ -0,0 +1,126 @@ +package eu.svjatoslav.imagesqueeze.sampleApplication; + +import java.awt.BorderLayout; + +import java.awt.Dimension; +import java.awt.image.BufferedImage; +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +import javax.swing.JButton; + +import javax.swing.ImageIcon; +import javax.swing.JPanel; +import javax.swing.WindowConstants; +import javax.swing.JFrame; +import javax.swing.JLabel; + +import eu.svjatoslav.imagesqueeze.codec.Image; + + +public class ImagePanel extends javax.swing.JPanel { + private JLabel imageLabel; + + public BufferedImage bufferedImage; + + /** + * Auto-generated main method to display this + * JPanel inside a new JFrame. + */ + public static void main(String[] args) { + JFrame frame = new JFrame(); + frame.getContentPane().add(new ImagePanel()); + frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + frame.pack(); + frame.setVisible(true); + } + + public ImagePanel() { + super(); + initGUI(); + } + + private void initGUI() { + try { + BorderLayout thisLayout = new BorderLayout(); + this.setLayout(thisLayout); + setPreferredSize(new Dimension(660, 500)); + { + imageLabel = new JLabel(); + this.add(getImageLabel(), BorderLayout.CENTER); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public JLabel getImageLabel() { + return imageLabel; + } + + public void loadImage(File inputFile, boolean isImgSqz) throws IOException{ + FileInputStream fileInputStream = new FileInputStream(inputFile); + + loadImage(fileInputStream, isImgSqz); + } + + public void loadImage(InputStream inputStream, boolean isImgSqz) throws IOException{ + if (isImgSqz){ + // load ImageSqueeze file + + Image image = new Image(); + image.loadImage(inputStream); + + bufferedImage = image.bufferedImage; + + ImageIcon icon = new ImageIcon(bufferedImage); + //ImageIcon icon = new ImageIcon("sample data/original.png"); + + imageLabel.setIcon(icon); + + } else { + // load JPEG, PNG, GIF file + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + readLoop:{ + for (;;){ + int b = inputStream.read(); + if (b == -1) break readLoop; + outputStream.write(b); + } + } + + ImageIcon icon = new ImageIcon(outputStream.toByteArray()); + + bufferedImage = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_3BYTE_BGR); + bufferedImage.getGraphics().drawImage(icon.getImage(), 0, 0, null); + + ImageIcon displayIcon = new ImageIcon(bufferedImage); + imageLabel.setIcon(displayIcon); + } + } + + + public void createEmptyImage(Dimension dimension){ + + bufferedImage = new BufferedImage(dimension.width, dimension.height, BufferedImage.TYPE_3BYTE_BGR); + + ImageIcon icon = new ImageIcon(bufferedImage); + + imageLabel.setIcon(icon); + } + + + public void saveImage(File outputFile){ + Image image = new Image(bufferedImage); + try { + image.saveImage(outputFile); + } catch (Exception e) { + System.out.println("Error while saving image: " + e.toString()); + } + } + +} diff --git a/src/main/java/eu/svjatoslav/imagesqueeze/sampleApplication/Main.java b/src/main/java/eu/svjatoslav/imagesqueeze/sampleApplication/Main.java new file mode 100755 index 0000000..f67ff3a --- /dev/null +++ b/src/main/java/eu/svjatoslav/imagesqueeze/sampleApplication/Main.java @@ -0,0 +1,45 @@ +package eu.svjatoslav.imagesqueeze.sampleApplication; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + + + +public class Main { + + public static void main(String[] args) { + + try { + + String image = "colorful photo"; + String sourceDirectory = "/eu/svjatoslav/imagesqueeze/sampleApplication/data/"; + + // create visible frame + // load image into frame + InputStream inputStream = Main.class.getResourceAsStream(sourceDirectory + image + ".png"); + + ImageFrame frame = new ImageFrame("Original image"); + frame.getImagePanel().loadImage(inputStream, false); + frame.setVisible(true); + + + // encode image into file + frame.getImagePanel().saveImage(new File(image + ".ImgSqz")); + + + // create second frame for decoded image + ImageFrame frame2 = new ImageFrame("Encoded -> Decoded"); + + // decode image + frame2.getImagePanel().loadImage(new File(image + ".ImgSqz"), true); + frame2.setVisible(true); + + } catch (IOException exception){ + System.out.println("Error while loading an image: " + exception); + } + + + } + +} diff --git a/src/main/resources/eu/svjatoslav/imagesqueeze/sampleApplication/data/bw schematics.gif b/src/main/resources/eu/svjatoslav/imagesqueeze/sampleApplication/data/bw schematics.gif new file mode 100755 index 0000000..aab67ed Binary files /dev/null and b/src/main/resources/eu/svjatoslav/imagesqueeze/sampleApplication/data/bw schematics.gif differ diff --git a/src/main/resources/eu/svjatoslav/imagesqueeze/sampleApplication/data/clouds.png b/src/main/resources/eu/svjatoslav/imagesqueeze/sampleApplication/data/clouds.png new file mode 100755 index 0000000..42a0145 Binary files /dev/null and b/src/main/resources/eu/svjatoslav/imagesqueeze/sampleApplication/data/clouds.png differ diff --git a/src/main/resources/eu/svjatoslav/imagesqueeze/sampleApplication/data/colorful photo.png b/src/main/resources/eu/svjatoslav/imagesqueeze/sampleApplication/data/colorful photo.png new file mode 100755 index 0000000..9a063d9 Binary files /dev/null and b/src/main/resources/eu/svjatoslav/imagesqueeze/sampleApplication/data/colorful photo.png differ diff --git a/src/main/resources/eu/svjatoslav/imagesqueeze/sampleApplication/data/small logo.png b/src/main/resources/eu/svjatoslav/imagesqueeze/sampleApplication/data/small logo.png new file mode 100755 index 0000000..5914597 Binary files /dev/null and b/src/main/resources/eu/svjatoslav/imagesqueeze/sampleApplication/data/small logo.png differ